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
--- 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>