Bug 1332546 - Add CustomView to ActionBar of CustomTabsActivity draft
authorJulian_Chu <walkingice0204@gmail.com>
Thu, 09 Mar 2017 15:17:36 +0800
changeset 499672 0ce0337be2119d448b8f8a234c8255eb59b5c095
parent 499671 7a6da2e3632c70d8c3027ed1b2a1584e614af2cd
child 499673 6b6809bd53cea9c806240f4f70bc0684434b78f0
push id49471
push userbmo:walkingice0204@gmail.com
push dateThu, 16 Mar 2017 02:41:34 +0000
bugs1332546
milestone55.0a1
Bug 1332546 - Add CustomView to ActionBar of CustomTabsActivity The CustomView has three components * Icon - for site info to indicate whether the visited site is security * Title * Url Icon is for site info to indicate whether the visited site is security. Its icon type is decided by SecurityModeUtil. All of the components' color are the same as TextView color. When onTabChanged happens, it updates CustView of ActionBar to reflect current site security status. Sometimes the callback will be invoked rapidly several times in very short time. To avoid icon twinkle, to add a delay when updating CustomView. MozReview-Commit-ID: KCu3XLObmmV
mobile/android/base/java/org/mozilla/gecko/customtabs/ActionBarPresenter.java
mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
mobile/android/base/java/org/mozilla/gecko/toolbar/SecurityModeUtil.java
mobile/android/base/moz.build
mobile/android/base/resources/drawable/customtabs_site_security_icon.xml
mobile/android/base/resources/layout/customtabs_action_bar_custom_view.xml
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/ActionBarPresenter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/ActionBarPresenter.java
@@ -1,77 +1,102 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.drawable.ColorDrawable;
 import android.os.Build;
+import android.os.Handler;
 import android.support.annotation.ColorInt;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
+import android.support.v4.graphics.drawable.DrawableCompat;
 import android.support.v7.app.ActionBar;
-import android.support.v7.widget.Toolbar;
 import android.text.TextUtils;
-import android.util.Log;
+import android.view.View;
 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.R;
+import org.mozilla.gecko.SiteIdentity;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.toolbar.SecurityModeUtil;
 import org.mozilla.gecko.util.ColorUtil;
 
-import java.lang.reflect.Field;
-
 /**
  * This class is used to maintain appearance of ActionBar of CustomTabsActivity, includes background
  * color, custom-view and so on.
  */
 public class ActionBarPresenter {
 
-    private static final String LOGTAG = "CustomTabsActionBar";
-    private final ActionBar mActionBar;
-    private boolean useDomainTitle = true;
+    @ColorInt
+    private static final int DEFAULT_TEXT_PRIMARY_COLOR = 0xFFFFFFFF;
+    private static final long CUSTOM_VIEW_UPDATE_DELAY = 1000;
 
-    ActionBarPresenter(@NonNull final ActionBar actionBar, @NonNull Toolbar toolbar) {
-        mActionBar = actionBar;
-        initActionBar(toolbar);
-    }
+    private final ActionBar mActionBar;
+    private final ImageButton mIconView;
+    private final TextView mTitleView;
+    private final TextView mUrlView;
+    private final Handler mHandler = new Handler();
+
+    private Runnable mUpdateAction;
 
-    private void initActionBar(@NonNull final Toolbar toolbar) {
-        try {
-            // Since we don't create the Toolbar's TextView ourselves, this seems
-            // to be the only way of changing the ellipsize setting.
-            final Field f = toolbar.getClass().getDeclaredField("mTitleTextView");
-            f.setAccessible(true);
-            final TextView textView = (TextView) f.get(toolbar);
-            textView.setEllipsize(TextUtils.TruncateAt.START);
-        } catch (Exception e) {
-            // If we can't ellipsize at the start of the title, we shouldn't display the host
-            // so as to avoid displaying a misleadingly truncated host.
-            Log.w(LOGTAG, "Failed to get Toolbar TextView, using default title.");
-            useDomainTitle = false;
-        }
+    @ColorInt
+    private int mTextPrimaryColor = DEFAULT_TEXT_PRIMARY_COLOR;
+
+    ActionBarPresenter(@NonNull final ActionBar actionBar) {
+        mActionBar = actionBar;
+        mActionBar.setDisplayShowCustomEnabled(true);
+        mActionBar.setDisplayShowTitleEnabled(false);
+
+        mActionBar.setCustomView(R.layout.customtabs_action_bar_custom_view);
+        final View customView = mActionBar.getCustomView();
+        mIconView = (ImageButton) customView.findViewById(R.id.custom_tabs_action_bar_icon);
+        mTitleView = (TextView) customView.findViewById(R.id.custom_tabs_action_bar_title);
+        mUrlView = (TextView) customView.findViewById(R.id.custom_tabs_action_bar_url);
+
+        onThemeChanged(mActionBar.getThemedContext().getTheme());
     }
 
     /**
-     * Update appearance of ActionBar, includes its Title.
+     * To display Url in CustomView only and immediately.
      *
-     * @param title A string to be used as Title in Actionbar
+     * @param url Url String to display
+     */
+    public void displayUrlOnly(@NonNull final String url) {
+        updateCustomView(null, null, url);
+    }
+
+    /**
+     * Update appearance of CustomView of ActionBar.
+     *
+     * @param tab a Tab instance of current web page to provide information to render ActionBar.
      */
-    @UiThread
-    public void update(@Nullable final String title) {
-        if (useDomainTitle || TextUtils.isEmpty(title)) {
-            mActionBar.setTitle(AppConstants.MOZ_APP_BASENAME);
-        } else {
-            mActionBar.setTitle(title);
-        }
+    public void update(@NonNull final Tab tab) {
+        final String title = tab.getTitle();
+        final String url = tab.getBaseDomain();
+
+        // Do not update CustomView immediately. If this method be invoked rapidly several times,
+        // only apply last one.
+        mHandler.removeCallbacks(mUpdateAction);
+        mUpdateAction = new Runnable() {
+            @Override
+            public void run() {
+                updateCustomView(tab.getSiteIdentity(), title, url);
+            }
+        };
+        mHandler.postDelayed(mUpdateAction, CUSTOM_VIEW_UPDATE_DELAY);
     }
 
     /**
      * Set background color to ActionBar, as well as Status bar.
      *
      * @param color  the color to apply to ActionBar
      * @param window Window instance for changing color status bar, or null if won't change it.
      */
@@ -82,9 +107,58 @@ public class ActionBarPresenter {
 
         if (window != null) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                 window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
                 window.setStatusBarColor(ColorUtil.darken(color, 0.25));
             }
         }
     }
+
+    /**
+     * To update appearance of CustomView of ActionBar, includes its icon, title and url text.
+     *
+     * @param identity SiteIdentity for current website. Could be null if don't want to show icon.
+     * @param title    Title for current website. Could be null if don't want to show title.
+     * @param url      URL for current website. At least Custom will show this url.
+     */
+    @UiThread
+    private void updateCustomView(@Nullable SiteIdentity identity,
+                                  @Nullable String title,
+                                  @NonNull String url) {
+        // update site-info icon
+        if (identity == null) {
+            mIconView.setVisibility(View.INVISIBLE);
+        } else {
+            final SecurityModeUtil.Mode mode = SecurityModeUtil.resolve(identity);
+            mIconView.setVisibility(View.VISIBLE);
+            mIconView.setImageLevel(mode.ordinal());
+
+            if (mode == SecurityModeUtil.Mode.LOCK_SECURE) {
+                // Lock-Secure is special case. Keep its original green color.
+                DrawableCompat.setTintList(mIconView.getDrawable(), null);
+            } else {
+                // Icon use same color as TextView.
+                DrawableCompat.setTint(mIconView.getDrawable(), mTextPrimaryColor);
+            }
+        }
+
+        // If no title to use, use Url as title
+        if (TextUtils.isEmpty(title)) {
+            mTitleView.setText(url);
+            mUrlView.setText(null);
+            mUrlView.setVisibility(View.GONE);
+        } else {
+            mTitleView.setText(title);
+            mUrlView.setText(url);
+            mUrlView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private void onThemeChanged(@NonNull final Resources.Theme currentTheme) {
+        // Theme might be light or dark. To get text color for custom-view.
+        final TypedArray themeArray = currentTheme.obtainStyledAttributes(
+                new int[]{android.R.attr.textColorPrimary});
+
+        mTextPrimaryColor = themeArray.getColor(0, DEFAULT_TEXT_PRIMARY_COLOR);
+        themeArray.recycle();
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -26,75 +26,69 @@ 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.widget.ImageButton;
 
-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.util.List;
 
 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";
 
     @ColorInt
     private static final int DEFAULT_ACTION_BAR_COLOR = 0xFF363b40; // default color to match design
 
     private final SparseArrayCompat<PendingIntent> menuItemsIntent = new SparseArrayCompat<>();
     private GeckoPopupMenu popupMenu;
-    private int tabId = -1;
     private ActionBarPresenter actionBarPresenter;
-    private String toolbarTitle;
     // A state to indicate whether this activity is finishing with customize animation
     private boolean usingCustomAnimation = false;
 
     @ColorInt
     private int toolbarColor = DEFAULT_ACTION_BAR_COLOR;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
         if (savedInstanceState != null) {
             toolbarColor = savedInstanceState.getInt(SAVED_TOOLBAR_COLOR, DEFAULT_ACTION_BAR_COLOR);
-            toolbarTitle = savedInstanceState.getString(SAVED_TOOLBAR_TITLE, AppConstants.MOZ_APP_BASENAME);
         } else {
             toolbarColor = getIntent().getIntExtra(EXTRA_TOOLBAR_COLOR, DEFAULT_ACTION_BAR_COLOR);
-            toolbarTitle = AppConstants.MOZ_APP_BASENAME;
         }
 
         // Translucent color does not make sense for toolbar color. Ensure it is 0xFF.
         toolbarColor = 0xFF000000 | toolbarColor;
 
         setThemeFromToolbarColor();
 
         final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
         setSupportActionBar(toolbar);
         final ActionBar actionBar = getSupportActionBar();
-        actionBar.setDisplayHomeAsUpEnabled(true);
         bindNavigationCallback(toolbar);
 
-        actionBarPresenter = new ActionBarPresenter(actionBar, toolbar);
+        actionBarPresenter = new ActionBarPresenter(actionBar);
+        actionBarPresenter.displayUrlOnly(getIntent().getDataString());
         actionBarPresenter.setBackgroundColor(toolbarColor, getWindow());
-        actionBarPresenter.update(toolbarTitle);
+        actionBar.setDisplayHomeAsUpEnabled(true);
 
         Tabs.registerOnTabsChangedListener(this);
     }
 
     private void setThemeFromToolbarColor() {
         @StyleRes
         int styleRes = (ColorUtil.getReadableTextColor(toolbarColor) == Color.BLACK)
                 ? R.style.GeckoCustomTabs_Light
@@ -147,48 +141,34 @@ public class CustomTabsActivity extends 
 
     @Override
     protected void onDone() {
         finish();
     }
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
-        if (tab == null) {
-            return;
-        }
-
-        if (tabId >= 0 && tab.getId() != tabId) {
+        if (!Tabs.getInstance().isSelectedTab(tab)) {
             return;
         }
 
-        if (msg == Tabs.TabEvents.LOCATION_CHANGE) {
-            tabId = tab.getId();
-            final Uri uri = Uri.parse(tab.getURL());
-            String title = null;
-            if (uri != null) {
-                title = uri.getHost();
-            }
-            if (title == null || title.isEmpty()) {
-                toolbarTitle = AppConstants.MOZ_APP_BASENAME;
-            } else {
-                toolbarTitle = title;
-            }
-            actionBarPresenter.update(toolbarTitle);
+        if (msg == Tabs.TabEvents.LOCATION_CHANGE
+                || msg == Tabs.TabEvents.SECURITY_CHANGE
+                || msg == Tabs.TabEvents.TITLE) {
+            actionBarPresenter.update(tab);
         }
 
         updateMenuItemForward();
     }
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
 
         outState.putInt(SAVED_TOOLBAR_COLOR, toolbarColor);
-        outState.putString(SAVED_TOOLBAR_TITLE, toolbarTitle);
     }
 
     @Override
     public void onResume() {
         if (lastSelectedTabId >= 0) {
             final Tabs tabs = Tabs.getInstance();
             final Tab tab = tabs.getTab(lastSelectedTabId);
             if (tab == null) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/SecurityModeUtil.java
@@ -0,0 +1,84 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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.toolbar;
+
+import android.support.annotation.Nullable;
+
+import org.mozilla.gecko.SiteIdentity;
+import org.mozilla.gecko.SiteIdentity.MixedMode;
+import org.mozilla.gecko.SiteIdentity.SecurityMode;
+import org.mozilla.gecko.SiteIdentity.TrackingMode;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Util class which encapsulate logic of how CustomTabsActivity treats SiteIdentity.
+ * TODO: Bug 1347037 - This class should be reusable for other components
+ */
+public class SecurityModeUtil {
+
+    // defined basic mapping between SecurityMode and SecurityModeUtil.Mode
+    private static final Map<SecurityMode, Mode> securityModeMap;
+
+    static {
+        securityModeMap = new HashMap<>();
+        securityModeMap.put(SecurityMode.UNKNOWN, Mode.UNKNOWN);
+        securityModeMap.put(SecurityMode.IDENTIFIED, Mode.LOCK_SECURE);
+        securityModeMap.put(SecurityMode.VERIFIED, Mode.LOCK_SECURE);
+        securityModeMap.put(SecurityMode.CHROMEUI, Mode.UNKNOWN);
+    }
+
+    /**
+     * To resolve which mode to be used for given SiteIdentity. Its logic is similar to
+     * ToolbarDisplayLayout.updateSiteIdentity
+     *
+     * @param identity An identity of a site to be resolved
+     * @return Corresponding mode for resolved SiteIdentity, UNKNOWN as default.
+     */
+    public static Mode resolve(final @Nullable SiteIdentity identity) {
+        if (identity == null) {
+            return Mode.UNKNOWN;
+        }
+
+        final SecurityMode securityMode = identity.getSecurityMode();
+        final MixedMode activeMixedMode = identity.getMixedModeActive();
+        final MixedMode displayMixedMode = identity.getMixedModeDisplay();
+        final TrackingMode trackingMode = identity.getTrackingMode();
+        final boolean securityException = identity.isSecurityException();
+
+        if (securityMode == SiteIdentity.SecurityMode.CHROMEUI) {
+            return Mode.UNKNOWN;
+        }
+
+        if (securityException) {
+            return Mode.MIXED_MODE;
+        } else if (trackingMode == TrackingMode.TRACKING_CONTENT_LOADED) {
+            return Mode.TRACKING_CONTENT_LOADED;
+        } else if (trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED) {
+            return Mode.TRACKING_CONTENT_BLOCKED;
+        } else if (activeMixedMode == MixedMode.LOADED) {
+            return Mode.MIXED_MODE;
+        } else if (displayMixedMode == MixedMode.LOADED) {
+            return Mode.WARNING;
+        }
+
+        return securityModeMap.containsKey(securityMode)
+                ? securityModeMap.get(securityMode)
+                : Mode.UNKNOWN;
+    }
+
+    // Security mode constants, which map to the icons / levels defined in:
+    // http://dxr.mozilla.org/mozilla-central/source/mobile/android/base/java/org/mozilla/gecko/resources/drawable/customtabs_site_security_level.xml
+    public enum Mode {
+        UNKNOWN,
+        LOCK_SECURE,
+        WARNING,
+        MIXED_MODE,
+        TRACKING_CONTENT_BLOCKED,
+        TRACKING_CONTENT_LOADED
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -766,16 +766,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'toolbar/BrowserToolbarPhoneBase.java',
     'toolbar/BrowserToolbarTablet.java',
     'toolbar/BrowserToolbarTabletBase.java',
     'toolbar/CanvasDelegate.java',
     'toolbar/ForwardButton.java',
     'toolbar/NavButton.java',
     'toolbar/PageActionLayout.java',
     'toolbar/PhoneTabsButton.java',
+    'toolbar/SecurityModeUtil.java',
     'toolbar/ShapedButton.java',
     'toolbar/ShapedButtonFrameLayout.java',
     'toolbar/SiteIdentityPopup.java',
     'toolbar/TabCounter.java',
     'toolbar/ToolbarDisplayLayout.java',
     'toolbar/ToolbarEditLayout.java',
     'toolbar/ToolbarEditText.java',
     'toolbar/ToolbarPrefs.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/customtabs_site_security_icon.xml
@@ -0,0 +1,27 @@
+<?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/. -->
+
+<level-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:drawable="@drawable/site_security_unknown"
+        android:maxLevel="0"/>
+    <item
+        android:drawable="@drawable/lock_secure"
+        android:maxLevel="1"/>
+    <item
+        android:drawable="@drawable/warning_minor"
+        android:maxLevel="2"/>
+    <item
+        android:drawable="@drawable/lock_disabled"
+        android:maxLevel="3"/>
+    <item
+        android:drawable="@drawable/shield_enabled"
+        android:maxLevel="4"/>
+    <item
+        android:drawable="@drawable/shield_disabled"
+        android:maxLevel="5"/>
+
+</level-list>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/customtabs_action_bar_custom_view.xml
@@ -0,0 +1,59 @@
+<?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/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:tools="http://schemas.android.com/tools"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="horizontal">
+
+    <FrameLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:paddingEnd="5dp"
+        android:paddingRight="5dp">
+
+        <ImageButton
+            android:id="@+id/custom_tabs_action_bar_icon"
+            style="@style/UrlBar.ImageButton"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_gravity="center_vertical"
+            android:contentDescription="@string/site_security"
+            android:padding="3dp"
+            android:scaleType="fitCenter"
+            android:src="@drawable/customtabs_site_security_icon"
+            android:visibility="invisible"/>
+
+    </FrameLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:animateLayoutChanges="true"
+        android:gravity="center_vertical"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/custom_tabs_action_bar_title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:maxLines="1"
+            android:textColor="?android:textColorPrimary"
+            android:textSize="14sp"
+            tools:text="Mozilla.org"/>
+
+        <TextView
+            android:id="@+id/custom_tabs_action_bar_url"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:maxLines="1"
+            android:textColor="?android:textColorPrimary"
+            android:textSize="10sp"
+            tools:text="https://mozilla.org"/>
+
+    </LinearLayout>
+
+</LinearLayout>