summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java530
1 files changed, 530 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
new file mode 100644
index 000000000..163ed4a51
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
@@ -0,0 +1,530 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+
+import android.support.v4.content.ContextCompat;
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.reader.ReaderModeUtils;
+import org.mozilla.gecko.SiteIdentity;
+import org.mozilla.gecko.SiteIdentity.MixedMode;
+import org.mozilla.gecko.SiteIdentity.SecurityMode;
+import org.mozilla.gecko.SiteIdentity.TrackingMode;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.animation.PropertyAnimator;
+import org.mozilla.gecko.animation.ViewHelper;
+import org.mozilla.gecko.toolbar.BrowserToolbarTabletBase.ForwardButtonAnimation;
+import org.mozilla.gecko.Experiments;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.StringUtils;
+import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
+import org.mozilla.gecko.widget.themed.ThemedTextView;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+
+import com.keepsafe.switchboard.SwitchBoard;
+
+/**
+* {@code ToolbarDisplayLayout} is the UI for when the toolbar is in
+* display state. It's used to display the state of the currently selected
+* tab. It should always be updated through a single entry point
+* (updateFromTab) and should never track any tab events or gecko messages
+* on its own to keep it as dumb as possible.
+*
+* The UI has two possible modes: progress and display which are triggered
+* when UpdateFlags.PROGRESS is used depending on the current tab state.
+* The progress mode is triggered when the tab is loading a page. Display mode
+* is used otherwise.
+*
+* {@code ToolbarDisplayLayout} is meant to be owned by {@code BrowserToolbar}
+* which is the main event bus for the toolbar subsystem.
+*/
+public class ToolbarDisplayLayout extends ThemedLinearLayout {
+
+ private static final String LOGTAG = "GeckoToolbarDisplayLayout";
+ private boolean mTrackingProtectionEnabled;
+
+ // To be used with updateFromTab() to allow the caller
+ // to give enough context for the requested state change.
+ enum UpdateFlags {
+ TITLE,
+ FAVICON,
+ PROGRESS,
+ SITE_IDENTITY,
+ PRIVATE_MODE,
+
+ // Disable any animation that might be
+ // triggered from this state change. Mostly
+ // used on tab switches, see BrowserToolbar.
+ DISABLE_ANIMATIONS
+ }
+
+ private enum UIMode {
+ PROGRESS,
+ DISPLAY
+ }
+
+ interface OnStopListener {
+ Tab onStop();
+ }
+
+ interface OnTitleChangeListener {
+ void onTitleChange(CharSequence title);
+ }
+
+ private final BrowserApp mActivity;
+
+ private UIMode mUiMode;
+
+ private boolean mIsAttached;
+
+ private final ThemedTextView mTitle;
+ private final int mTitlePadding;
+ private ToolbarPrefs mPrefs;
+ private OnTitleChangeListener mTitleChangeListener;
+
+ private final ImageButton mSiteSecurity;
+
+ private final ImageButton mStop;
+ private OnStopListener mStopListener;
+
+ private final PageActionLayout mPageActionLayout;
+
+ private final SiteIdentityPopup mSiteIdentityPopup;
+ private int mSecurityImageLevel;
+
+ // Security level 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/site_security_level.xml
+ // Default level (unverified pages) - globe icon:
+ private static final int LEVEL_DEFAULT_GLOBE = 0;
+ // Levels for displaying Mixed Content state icons.
+ private static final int LEVEL_WARNING_MINOR = 3;
+ private static final int LEVEL_LOCK_DISABLED = 4;
+ // Levels for displaying Tracking Protection state icons.
+ private static final int LEVEL_SHIELD_ENABLED = 5;
+ private static final int LEVEL_SHIELD_DISABLED = 6;
+ // Icon used for about:home
+ private static final int LEVEL_SEARCH_ICON = 999;
+
+ private final ForegroundColorSpan mUrlColorSpan;
+ private final ForegroundColorSpan mPrivateUrlColorSpan;
+ private final ForegroundColorSpan mBlockedColorSpan;
+ private final ForegroundColorSpan mDomainColorSpan;
+ private final ForegroundColorSpan mPrivateDomainColorSpan;
+ private final ForegroundColorSpan mCertificateOwnerColorSpan;
+
+ public ToolbarDisplayLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setOrientation(HORIZONTAL);
+
+ mActivity = (BrowserApp) context;
+
+ LayoutInflater.from(context).inflate(R.layout.toolbar_display_layout, this);
+
+ mTitle = (ThemedTextView) findViewById(R.id.url_bar_title);
+ mTitlePadding = mTitle.getPaddingRight();
+
+ mUrlColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_urltext));
+ mPrivateUrlColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_urltext_private));
+ mBlockedColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_blockedtext));
+ mDomainColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_domaintext));
+ mPrivateDomainColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_domaintext_private));
+ mCertificateOwnerColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.affirmative_green));
+
+ mSiteSecurity = (ImageButton) findViewById(R.id.site_security);
+
+ mSiteIdentityPopup = new SiteIdentityPopup(mActivity);
+ mSiteIdentityPopup.setAnchor(this);
+ mSiteIdentityPopup.setOnVisibilityChangeListener(mActivity);
+
+ mStop = (ImageButton) findViewById(R.id.stop);
+ mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mIsAttached = true;
+
+ mSiteSecurity.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mSiteIdentityPopup.show();
+ }
+ });
+
+ mStop.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mStopListener != null) {
+ // Force toolbar to switch to Display mode
+ // immediately based on the stopped tab.
+ final Tab tab = mStopListener.onStop();
+ if (tab != null) {
+ updateUiMode(UIMode.DISPLAY);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mIsAttached = false;
+ }
+
+ @Override
+ public void setNextFocusDownId(int nextId) {
+ mStop.setNextFocusDownId(nextId);
+ mSiteSecurity.setNextFocusDownId(nextId);
+ mPageActionLayout.setNextFocusDownId(nextId);
+ }
+
+ void setToolbarPrefs(final ToolbarPrefs prefs) {
+ mPrefs = prefs;
+ }
+
+ void updateFromTab(@NonNull Tab tab, EnumSet<UpdateFlags> flags) {
+ // Several parts of ToolbarDisplayLayout's state depends
+ // on the views being attached to the view tree.
+ if (!mIsAttached) {
+ return;
+ }
+
+ if (flags.contains(UpdateFlags.TITLE)) {
+ updateTitle(tab);
+ }
+
+ if (flags.contains(UpdateFlags.SITE_IDENTITY)) {
+ updateSiteIdentity(tab);
+ }
+
+ if (flags.contains(UpdateFlags.PROGRESS)) {
+ updateProgress(tab);
+ }
+
+ if (flags.contains(UpdateFlags.PRIVATE_MODE)) {
+ mTitle.setPrivateMode(tab.isPrivate());
+ }
+ }
+
+ void setTitle(CharSequence title) {
+ mTitle.setText(title);
+
+ if (mTitleChangeListener != null) {
+ mTitleChangeListener.onTitleChange(title);
+ }
+ }
+
+ private void updateTitle(@NonNull Tab tab) {
+ // Keep the title unchanged if there's no selected tab,
+ // or if the tab is entering reader mode.
+ if (tab.isEnteringReaderMode()) {
+ return;
+ }
+
+ final String url = tab.getURL();
+
+ // Setting a null title will ensure we just see the
+ // "Enter Search or Address" placeholder text.
+ if (AboutPages.isTitlelessAboutPage(url)) {
+ setTitle(null);
+ setContentDescription(null);
+ return;
+ }
+
+ // Show the about:blocked page title in red, regardless of prefs
+ if (tab.getErrorType() == Tab.ErrorType.BLOCKED) {
+ final String title = tab.getDisplayTitle();
+
+ final SpannableStringBuilder builder = new SpannableStringBuilder(title);
+ builder.setSpan(mBlockedColorSpan, 0, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+
+ setTitle(builder);
+ setContentDescription(null);
+ return;
+ }
+
+ final String baseDomain = tab.getBaseDomain();
+
+ String strippedURL = stripAboutReaderURL(url);
+
+ final boolean isHttpOrHttps = StringUtils.isHttpOrHttps(strippedURL);
+
+ if (mPrefs.shouldTrimUrls()) {
+ strippedURL = StringUtils.stripCommonSubdomains(StringUtils.stripScheme(strippedURL));
+ }
+
+ // The URL bar does not support RTL currently (See bug 928688 and meta bug 702845).
+ // Displaying a URL using RTL (or mixed) characters can lead to an undesired reordering
+ // of elements of the URL. That's why we are forcing the URL to use LTR (bug 1284372).
+ strippedURL = StringUtils.forceLTR(strippedURL);
+
+ // This value is not visible to screen readers but we rely on it when running UI tests. Screen
+ // readers will instead focus BrowserToolbar and read the "base domain" from there. UI tests
+ // will read the content description to obtain the full URL for performing assertions.
+ setContentDescription(strippedURL);
+
+ final SiteIdentity siteIdentity = tab.getSiteIdentity();
+ if (siteIdentity.hasOwner() && SwitchBoard.isInExperiment(mActivity, Experiments.URLBAR_SHOW_EV_CERT_OWNER)) {
+ // Show Owner of EV certificate as title
+ updateTitleFromSiteIdentity(siteIdentity);
+ } else if (isHttpOrHttps && !HardwareUtils.isTablet() && !TextUtils.isEmpty(baseDomain)
+ && SwitchBoard.isInExperiment(mActivity, Experiments.URLBAR_SHOW_ORIGIN_ONLY)) {
+ // Show just the base domain as title
+ setTitle(baseDomain);
+ } else {
+ // Display full URL with base domain highlighted as title
+ updateAndColorTitleFromFullURL(strippedURL, baseDomain, tab.isPrivate());
+ }
+ }
+
+ private void updateTitleFromSiteIdentity(SiteIdentity siteIdentity) {
+ final String title;
+
+ if (siteIdentity.hasCountry()) {
+ title = String.format("%s (%s)", siteIdentity.getOwner(), siteIdentity.getCountry());
+ } else {
+ title = siteIdentity.getOwner();
+ }
+
+ final SpannableString spannable = new SpannableString(title);
+ spannable.setSpan(mCertificateOwnerColorSpan, 0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ setTitle(spannable);
+ }
+
+ private void updateAndColorTitleFromFullURL(String url, String baseDomain, boolean isPrivate) {
+ if (TextUtils.isEmpty(baseDomain)) {
+ setTitle(url);
+ return;
+ }
+
+ int index = url.indexOf(baseDomain);
+ if (index == -1) {
+ setTitle(url);
+ return;
+ }
+
+ final SpannableStringBuilder builder = new SpannableStringBuilder(url);
+
+ builder.setSpan(isPrivate ? mPrivateUrlColorSpan : mUrlColorSpan, 0, url.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ builder.setSpan(isPrivate ? mPrivateDomainColorSpan : mDomainColorSpan,
+ index, index + baseDomain.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+
+ setTitle(builder);
+ }
+
+ private String stripAboutReaderURL(final String url) {
+ if (!AboutPages.isAboutReader(url)) {
+ return url;
+ }
+
+ return ReaderModeUtils.stripAboutReaderUrl(url);
+ }
+
+ private void updateSiteIdentity(@NonNull Tab tab) {
+ final SiteIdentity siteIdentity = tab.getSiteIdentity();
+
+ mSiteIdentityPopup.setSiteIdentity(siteIdentity);
+
+ final SecurityMode securityMode;
+ final MixedMode activeMixedMode;
+ final MixedMode displayMixedMode;
+ final TrackingMode trackingMode;
+ if (siteIdentity == null) {
+ securityMode = SecurityMode.UNKNOWN;
+ activeMixedMode = MixedMode.UNKNOWN;
+ displayMixedMode = MixedMode.UNKNOWN;
+ trackingMode = TrackingMode.UNKNOWN;
+ } else {
+ securityMode = siteIdentity.getSecurityMode();
+ activeMixedMode = siteIdentity.getMixedModeActive();
+ displayMixedMode = siteIdentity.getMixedModeDisplay();
+ trackingMode = siteIdentity.getTrackingMode();
+ }
+
+ // This is a bit tricky, but we have one icon and three potential indicators.
+ // Default to the identity level
+ int imageLevel = securityMode.ordinal();
+
+ // about: pages should default to having no icon too (the same as SecurityMode.UNKNOWN), however
+ // SecurityMode.CHROMEUI has a different ordinal - hence we need to manually reset it here.
+ // (We then continue and process the tracking / mixed content icons as usual, even for about: pages, as they
+ // can still load external sites.)
+ if (securityMode == SecurityMode.CHROMEUI) {
+ imageLevel = LEVEL_DEFAULT_GLOBE; // == SecurityMode.UNKNOWN.ordinal()
+ }
+
+ // Check to see if any protection was overridden first
+ if (AboutPages.isTitlelessAboutPage(tab.getURL())) {
+ // We always want to just show a search icon on about:home
+ imageLevel = LEVEL_SEARCH_ICON;
+ } else if (trackingMode == TrackingMode.TRACKING_CONTENT_LOADED) {
+ imageLevel = LEVEL_SHIELD_DISABLED;
+ } else if (trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED) {
+ imageLevel = LEVEL_SHIELD_ENABLED;
+ } else if (activeMixedMode == MixedMode.MIXED_CONTENT_LOADED) {
+ imageLevel = LEVEL_LOCK_DISABLED;
+ } else if (displayMixedMode == MixedMode.MIXED_CONTENT_LOADED) {
+ imageLevel = LEVEL_WARNING_MINOR;
+ }
+
+ if (mSecurityImageLevel != imageLevel) {
+ mSecurityImageLevel = imageLevel;
+ mSiteSecurity.setImageLevel(mSecurityImageLevel);
+ updatePageActions();
+ }
+
+ mTrackingProtectionEnabled = trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED;
+ }
+
+ private void updateProgress(@NonNull Tab tab) {
+ final boolean shouldShowThrobber = tab.getState() == Tab.STATE_LOADING;
+
+ updateUiMode(shouldShowThrobber ? UIMode.PROGRESS : UIMode.DISPLAY);
+
+ if (Tab.STATE_SUCCESS == tab.getState() && mTrackingProtectionEnabled) {
+ mActivity.showTrackingProtectionPromptIfApplicable();
+ }
+ }
+
+ private void updateUiMode(UIMode uiMode) {
+ if (mUiMode == uiMode) {
+ return;
+ }
+
+ mUiMode = uiMode;
+
+ // The "Throbber start" and "Throbber stop" log messages in this method
+ // are needed by S1/S2 tests (http://mrcote.info/phonedash/#).
+ // See discussion in Bug 804457. Bug 805124 tracks paring these down.
+ if (mUiMode == UIMode.PROGRESS) {
+ Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber start");
+ } else {
+ Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber stop");
+ }
+
+ updatePageActions();
+ }
+
+ private void updatePageActions() {
+ final boolean isShowingProgress = (mUiMode == UIMode.PROGRESS);
+
+ mStop.setVisibility(isShowingProgress ? View.VISIBLE : View.GONE);
+ mPageActionLayout.setVisibility(!isShowingProgress ? View.VISIBLE : View.GONE);
+
+ // We want title to fill the whole space available for it when there are icons
+ // being shown on the right side of the toolbar as the icons already have some
+ // padding in them. This is just to avoid wasting space when icons are shown.
+ mTitle.setPadding(0, 0, (!isShowingProgress ? mTitlePadding : 0), 0);
+ }
+
+ List<View> getFocusOrder() {
+ return Arrays.asList(mSiteSecurity, mPageActionLayout, mStop);
+ }
+
+ void setOnStopListener(OnStopListener listener) {
+ mStopListener = listener;
+ }
+
+ void setOnTitleChangeListener(OnTitleChangeListener listener) {
+ mTitleChangeListener = listener;
+ }
+
+ /**
+ * Update the Site Identity popup anchor.
+ *
+ * Tablet UI has a tablet-specific doorhanger anchor, so update it after all the views
+ * are inflated.
+ * @param view View to use as the anchor for the Site Identity popup.
+ */
+ void updateSiteIdentityAnchor(View view) {
+ mSiteIdentityPopup.setAnchor(view);
+ }
+
+ void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) {
+ if (animation == ForwardButtonAnimation.HIDE) {
+ // We animate these items individually, rather than this entire view,
+ // so that we don't animate certain views, e.g. the stop button.
+ anim.attach(mTitle,
+ PropertyAnimator.Property.TRANSLATION_X,
+ 0);
+ anim.attach(mSiteSecurity,
+ PropertyAnimator.Property.TRANSLATION_X,
+ 0);
+
+ // We're hiding the forward button. We're going to reset the margin before
+ // the animation starts, so we shift these items to the right so that they don't
+ // appear to move initially.
+ ViewHelper.setTranslationX(mTitle, width);
+ ViewHelper.setTranslationX(mSiteSecurity, width);
+ } else {
+ anim.attach(mTitle,
+ PropertyAnimator.Property.TRANSLATION_X,
+ width);
+ anim.attach(mSiteSecurity,
+ PropertyAnimator.Property.TRANSLATION_X,
+ width);
+ }
+ }
+
+ void finishForwardAnimation() {
+ ViewHelper.setTranslationX(mTitle, 0);
+ ViewHelper.setTranslationX(mSiteSecurity, 0);
+ }
+
+ void prepareStartEditingAnimation() {
+ // Hide page actions/stop buttons immediately
+ ViewHelper.setAlpha(mPageActionLayout, 0);
+ ViewHelper.setAlpha(mStop, 0);
+ }
+
+ void prepareStopEditingAnimation(PropertyAnimator anim) {
+ // Fade toolbar buttons (page actions, stop) after the entry
+ // is shrunk back to its original size.
+ anim.attach(mPageActionLayout,
+ PropertyAnimator.Property.ALPHA,
+ 1);
+
+ anim.attach(mStop,
+ PropertyAnimator.Property.ALPHA,
+ 1);
+ }
+
+ boolean dismissSiteIdentityPopup() {
+ if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) {
+ mSiteIdentityPopup.dismiss();
+ return true;
+ }
+
+ return false;
+ }
+
+ void destroy() {
+ mSiteIdentityPopup.destroy();
+ }
+}