Bug 1315937 - Add basic custom menu to CustomTabsActivity draft
authorJulian_Chu <walkingice0204@gmail.com>
Wed, 01 Mar 2017 13:42:10 +0800
changeset 497289 5071e2e45489726fd51b8efb3b850df1776635e5
parent 494469 316c27bc21488c727ac3e429b6bcc2cb390442cd
child 497290 f309c59a70ae4f2498eb24c5e2b9402d6ef18d85
push id48857
push userbmo:walkingice0204@gmail.com
push dateMon, 13 Mar 2017 04:50:52 +0000
bugs1315937
milestone54.0a1
Bug 1315937 - Add basic custom menu to CustomTabsActivity Besides custom-menu-items from 3-rd party apps, we designed a custom menu which always has * buttons for Forward, Reload * One menu item for 'Open by Firefox' * a footer 'Powered by Firefox' This patch adds a button as an anchor to standard menu. Once user click it, to show a custom menu (GeckoPopupMenu) base on the anchor. In current design, there is only one style for menu, regardless of Dark or Light theme. MozReview-Commit-ID: 5RMiGDlxLTU
mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/resources/layout/customtabs_options_menu_footer.xml
mobile/android/base/resources/menu/customtabs_menu.xml
mobile/android/base/resources/values/styles.xml
mobile/android/base/resources/values/themes.xml
mobile/android/base/strings.xml.in
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -2,55 +2,64 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.customtabs;
 
 import android.app.PendingIntent;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.StyleRes;
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.view.MenuItemCompat;
 import android.support.v7.app.ActionBar;
 import android.support.v7.widget.Toolbar;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.Menu;
+import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.ViewGroup.LayoutParams;
 import android.view.Window;
 import android.view.WindowManager;
+import android.widget.ImageButton;
 import android.widget.TextView;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.menu.GeckoMenu;
+import org.mozilla.gecko.menu.GeckoMenuInflater;
 import org.mozilla.gecko.util.ColorUtil;
+import org.mozilla.gecko.widget.GeckoPopupMenu;
 
 import java.lang.reflect.Field;
 
 import static android.support.customtabs.CustomTabsIntent.EXTRA_TOOLBAR_COLOR;
 
 public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedListener {
     private static final String LOGTAG = "CustomTabsActivity";
     private static final String SAVED_TOOLBAR_COLOR = "SavedToolbarColor";
     private static final String SAVED_TOOLBAR_TITLE = "SavedToolbarTitle";
     private static final int NO_COLOR = -1;
 
     private ActionBar actionBar;
+    private GeckoPopupMenu popupMenu;
     private int tabId = -1;
     private boolean useDomainTitle = true;
 
     private int toolbarColor;
     private String toolbarTitle;
 
     // A state to indicate whether this activity is finishing with customize animation
     private boolean usingCustomAnimation = false;
@@ -180,16 +189,18 @@ public class CustomTabsActivity extends 
             }
             if (!useDomainTitle || title == null || title.isEmpty()) {
                 toolbarTitle = AppConstants.MOZ_APP_BASENAME;
             } else {
                 toolbarTitle = title;
             }
             actionBar.setTitle(toolbarTitle);
         }
+
+        updateMenuItemForward();
     }
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
 
         outState.putInt(SAVED_TOOLBAR_COLOR, toolbarColor);
         outState.putString(SAVED_TOOLBAR_TITLE, toolbarTitle);
@@ -209,27 +220,58 @@ public class CustomTabsActivity extends 
 
     // Usually should use onCreateOptionsMenu() to initialize menu items. But GeckoApp overwrite
     // it to support custom menu(Bug 739412). Then the parameter *menu* in this.onCreateOptionsMenu()
     // and this.onPrepareOptionsMenu() are different instances - GeckoApp.onCreatePanelMenu() changed it.
     // CustomTabsActivity only use standard menu in ActionBar, so initialize menu here.
     @Override
     public boolean onCreatePanelMenu(final int id, final Menu menu) {
         insertActionButton(menu, getIntent());
+
+        popupMenu = createCustomPopupMenu();
+
+        // Create a ImageButton manually, and use it as an anchor for PopupMenu.
+        final ImageButton btn = new ImageButton(getContext(),
+                null, 0, R.style.Widget_MenuButtonCustomTabs);
+        btn.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+        btn.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View anchor) {
+                popupMenu.setAnchor(anchor);
+                popupMenu.show();
+            }
+        });
+
+        // Insert the anchor-button to Menu
+        final MenuItem item = menu.add(Menu.NONE, R.id.menu, Menu.NONE, "Menu Button");
+        item.setActionView(btn);
+        MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
+
+        updateMenuItemForward();
         return true;
     }
 
+    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case android.R.id.home:
                 finish();
                 return true;
             case R.id.action_button:
                 onActionButtonClicked();
                 return true;
+            case R.id.custom_tabs_menu_forward:
+                onForwardClicked();
+                return true;
+            case R.id.custom_tabs_menu_reload:
+                onReloadClicked();
+                return true;
+            case R.id.custom_tabs_menu_open_in:
+                onOpenInClicked();
+                return true;
         }
         return super.onOptionsItemSelected(item);
     }
 
     /**
      * To insert a MenuItem (as an ActionButton) into Menu.
      *
      * @param menu   The options menu in which to place items.
@@ -283,13 +325,97 @@ public class CustomTabsActivity extends 
         additional.setData(Uri.parse(tab.getURL()));
         try {
             pendingIntent.send(this, 0, additional);
         } catch (PendingIntent.CanceledException e) {
             Log.w(LOGTAG, "Performing a canceled pending intent", e);
         }
     }
 
+    /**
+     * To generate a popup menu which looks like an ordinary option menu, but have extra elements
+     * such as footer.
+     *
+     * @return a GeckoPopupMenu which can be placed on any view.
+     */
+    private GeckoPopupMenu createCustomPopupMenu() {
+        final GeckoPopupMenu popupMenu = new GeckoPopupMenu(this);
+        final GeckoMenu geckoMenu = popupMenu.getMenu();
+
+        // pass to to Activity.onMenuItemClick for consistency.
+        popupMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                return CustomTabsActivity.this.onMenuItemClick(item);
+            }
+        });
+
+        // to add Fennec default menu
+        final MenuInflater inflater = new GeckoMenuInflater(this);
+        inflater.inflate(R.menu.customtabs_menu, geckoMenu);
+
+        // insert default browser name to title of menu-item-Open-In
+        final MenuItem openItem = geckoMenu.findItem(R.id.custom_tabs_menu_open_in);
+        if (openItem != null) {
+            final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://"));
+            final ResolveInfo info = getPackageManager()
+                    .resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY);
+            final String name = info.loadLabel(getPackageManager()).toString();
+            openItem.setTitle(getString(R.string.custom_tabs_menu_item_open_in, name));
+        }
+
+        geckoMenu.addFooterView(
+                getLayoutInflater().inflate(R.layout.customtabs_options_menu_footer, geckoMenu, false),
+                null,
+                false);
+
+        return popupMenu;
+    }
+
+    /**
+     * Update state of Forward button in Popup Menu. It is clickable only if current tab can do forward.
+     */
+    private void updateMenuItemForward() {
+        if ((popupMenu == null)
+                || (popupMenu.getMenu() == null)
+                || (popupMenu.getMenu().findItem(R.id.custom_tabs_menu_forward) == null)) {
+            return;
+        }
+
+        final MenuItem forwardMenuItem = popupMenu.getMenu().findItem(R.id.custom_tabs_menu_forward);
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        final boolean enabled = (tab != null && tab.canDoForward());
+        forwardMenuItem.setEnabled(enabled);
+    }
+
+    private void onReloadClicked() {
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        if (tab != null) {
+            tab.doReload(true);
+        }
+    }
+
+    private void onForwardClicked() {
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        if ((tab != null) && tab.canDoForward()) {
+            tab.doForward();
+        }
+    }
+
+    /**
+     * Callback for Open-in menu item.
+     */
+    private void onOpenInClicked() {
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+        if (tab != null) {
+            // To launch default browser with url of current tab.
+            final Intent intent = new Intent();
+            intent.setData(Uri.parse(tab.getURL()));
+            intent.setAction(Intent.ACTION_VIEW);
+            startActivity(intent);
+        }
+    }
+
     private void onActionButtonClicked() {
         PendingIntent pendingIntent = IntentUtil.getActionButtonPendingIntent(getIntent());
         performPendingIntent(pendingIntent);
     }
 }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -277,16 +277,20 @@
      for experimental features. -->
 <!ENTITY pref_category_experimental "Experimental features">
 
 <!-- Custom Tabs is an Android API for allowing third-party apps to open URLs in a customized UI.
      Instead of switching to the browser it appears as if the user stays in the third-party app.
      For more see: https://developer.chrome.com/multidevice/android/customtabs -->
 <!ENTITY pref_custom_tabs "Custom Tabs">
 <!ENTITY pref_custom_tabs_summary3 "Allow apps to open websites using a customized version of &brandShortName;">
+<!-- Localization note (custom_tabs_menu_item_open_in): The variable is replaced by the name of
+     default browser from user's preference, such as "Open in Firefox" -->
+<!ENTITY custom_tabs_menu_item_open_in "Open in &formatS;">
+<!ENTITY custom_tabs_menu_footer "Powered by &brandShortName;">
 
 <!-- Localization note (pref_activity_stream): Experimental feature, see https://testpilot.firefox.com/experiments/activity-stream  -->
 <!ENTITY pref_activity_stream "Activity Stream">
 <!ENTITY pref_activity_stream_summary "A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you\'re looking for in &brandShortName;.">
 
 <!ENTITY tracking_protection_prompt_title "Now with Tracking Protection">
 <!ENTITY tracking_protection_prompt_text "Actively block tracking elements so you don\'t have to worry.">
 <!ENTITY tracking_protection_prompt_tip_text "Visit Privacy settings to learn more">
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/customtabs_options_menu_footer.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <!-- padding is consistent with Widget.MenuItemDefault -->
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="30dp"
+        android:background="@color/toolbar_menu_dark_grey"
+        android:gravity="center_vertical"
+        android:paddingEnd="10dp"
+        android:paddingLeft="15dp"
+        android:paddingRight="10dp"
+        android:paddingStart="15dp"
+        android:text="@string/custom_tabs_menu_footer"
+        android:textColor="#8f9195"
+        android:textSize="12sp"
+        tools:ignore="RtlHardcoded"/>
+</FrameLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/menu/customtabs_menu.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- We disable AlwaysShowAction because we interpret the menu
+     attributes ourselves and thus the warning isn't relevant to us. -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:tools="http://schemas.android.com/tools"
+      tools:ignore="AlwaysShowAction">
+
+    <item
+        android:id="@+id/custom_tabs_menu_forward"
+        android:icon="@drawable/ic_menu_forward"
+        android:showAsAction="always"
+        android:title="@string/forward"/>
+
+    <item
+        android:id="@+id/custom_tabs_menu_reload"
+        android:icon="@drawable/ic_menu_reload"
+        android:showAsAction="always"
+        android:title="@string/share"/>
+
+    <item
+        android:id="@+id/custom_tabs_menu_open_in"
+        android:title="@string/custom_tabs_menu_item_open_in"/>
+
+</menu>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -114,16 +114,28 @@
         <item name="android:drawablePadding">6dip</item>
         <item name="android:gravity">center_vertical</item>
         <item name="android:textAppearance">@style/TextAppearance</item>
         <item name="android:singleLine">true</item>
         <item name="android:ellipsize">middle</item>
         <item name="android:textSize">@dimen/menu_item_textsize</item>
     </style>
 
+    <style name="Widget.MenuButtonCustomTabs" parent="GeckoActionBar.Button.MenuButton">
+        <item name="android:tint">?android:textColorPrimary</item>
+    </style>
+
+    <style name="Widget.MenuItemCustomTabs" parent="Widget.MenuItemDefault">
+        <!-- In current design, there is only one style for Popup-menu, regardless of Dark/Light theme -->
+        <item name="android:textColor">@android:color/black</item>
+        <item name="android:textColorHighlight">?android:textColorHighlight</item>
+        <item name="android:textColorHint">?android:attr/textColorHint</item>
+        <item name="android:textColorLink">?android:attr/textColorLink</item>
+    </style>
+
     <style name="Widget.FolderTitle">
         <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemTitle</item>
     </style>
 
     <style name="Widget.FolderTitle.OneLine">
         <item name="android:textSize">@dimen/home_folder_title_oneline_textsize</item>
     </style>
 
--- a/mobile/android/base/resources/values/themes.xml
+++ b/mobile/android/base/resources/values/themes.xml
@@ -119,15 +119,20 @@
         <item name="android:statusBarColor">@android:color/transparent</item>
     </style>
 
     <!--
         Themes for CustomTabsActivity. Since CustomTabsActivity usually be used by 3-rd party apps,
         to create separated themes to keep look and feel be consistent with ordinary Android app.
         And ensure changes to CustomTabsActivity won't effect GeckoApp.
     -->
+
     <style name="GeckoCustomTabs" parent="Theme.AppCompat.NoActionBar">
+        <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
+        <item name="menuItemDefaultStyle">@style/Widget.MenuItemCustomTabs</item>
     </style>
 
     <style name="GeckoCustomTabs.Light" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
+        <item name="menuItemDefaultStyle">@style/Widget.MenuItemCustomTabs</item>
     </style>
 
 </resources>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -234,16 +234,18 @@
 
   <string name="pref_whats_new_notification">&pref_whats_new_notification;</string>
   <string name="pref_whats_new_notification_summary">&pref_whats_new_notification_summary;</string>
 
   <string name="pref_category_experimental">&pref_category_experimental;</string>
 
   <string name="pref_custom_tabs">&pref_custom_tabs;</string>
   <string name="pref_custom_tabs_summary">&pref_custom_tabs_summary3;</string>
+  <string name="custom_tabs_menu_item_open_in">&custom_tabs_menu_item_open_in;</string>
+  <string name="custom_tabs_menu_footer">&custom_tabs_menu_footer;</string>
 
   <string name="pref_activity_stream">&pref_activity_stream;</string>
   <string name="pref_activity_stream_summary">&pref_activity_stream_summary;</string>
 
   <string name="pref_char_encoding">&pref_char_encoding;</string>
   <string name="pref_char_encoding_on">&pref_char_encoding_on;</string>
   <string name="pref_char_encoding_off">&pref_char_encoding_off;</string>
   <string name="pref_clear_private_data_now">&pref_clear_private_data2;</string>