Bug 1271998 - Part 3 - Scroll the URL to focus the origin for overlength URLs. r=jwu
authorJan Henning <jh+bugzilla@buttercookie.de>
Thu, 24 Aug 2017 22:09:56 +0200
changeset 429521 953adb3e5e838f53df4a3a590a40b4c6f2c62949
parent 429520 c8aa9b29b27834382b0b9ce67114927c2f348925
child 429522 ecae1c71816b6d2aa21e520cb444f2cefb21ff91
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwu
bugs1271998
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1271998 - Part 3 - Scroll the URL to focus the origin for overlength URLs. r=jwu If the domain is long enough that it doesn't fully fit within the URL bar, we scroll it such that the end of the domain aligns with the right side of the URL bar, taking any possible fadingEdge effect into account. That way, we always try to show as much of the most important part of the origin as possible. Chrome uses a similar approach, although their URL bar neither fades nor allows scrolling. MozReview-Commit-ID: Ep4H4kO4MRH
mobile/android/app/src/photon/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
--- a/mobile/android/app/src/photon/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
+++ b/mobile/android/app/src/photon/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
@@ -24,25 +24,29 @@ import org.mozilla.gecko.util.HardwareUt
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ViewUtil;
 import org.mozilla.gecko.widget.themed.ThemedImageButton;
 import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
 import org.mozilla.gecko.widget.themed.ThemedTextView;
 
 import android.content.Context;
 import android.support.annotation.NonNull;
+import android.text.Editable;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
+import android.text.TextWatcher;
 import android.text.style.ForegroundColorSpan;
 import android.util.AttributeSet;
+import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
+import android.widget.HorizontalScrollView;
 
 import org.mozilla.gecko.switchboard.SwitchBoard;
 import org.mozilla.gecko.widget.themed.ThemedView;
 
 /**
 * {@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
@@ -53,18 +57,20 @@ import org.mozilla.gecko.widget.themed.T
 * 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 static final String LOGTAG = "GeckoToolbarDisplayLayout";
+    private static final int MIN_DOMAIN_SCROLL_MARGIN_DP = 10;
+
     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,
@@ -94,16 +100,18 @@ public class ToolbarDisplayLayout extend
 
     private UIMode mUiMode;
 
     private boolean mIsAttached;
 
     private final ThemedTextView mTitle;
     private final ThemedView mTitleBackground;
     private final int mTitlePadding;
+    private final HorizontalScrollView mTitleScroll;
+    private final int mMinUrlScrollMargin;
     private ToolbarPrefs mPrefs;
     private OnTitleChangeListener mTitleChangeListener;
 
     private final ThemedImageButton mSiteSecurity;
     private final ThemedImageButton mStop;
     private OnStopListener mStopListener;
 
     private final PageActionLayout mPageActionLayout;
@@ -139,16 +147,36 @@ public class ToolbarDisplayLayout extend
 
         mActivity = (BrowserApp) context;
 
         LayoutInflater.from(context).inflate(R.layout.toolbar_display_layout, this);
 
         mTitle = (ThemedTextView) findViewById(R.id.url_bar_title);
         mTitleBackground = (ThemedView) findViewById(R.id.url_bar_title_bg);
         mTitlePadding = mTitle.getPaddingRight();
+        mTitleScroll = (HorizontalScrollView) findViewById(R.id.url_bar_title_scroll_view);
+
+        final OnLayoutChangeListener resizeListener = new OnLayoutChangeListener() {
+            @Override
+            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+                final int oldWidth = oldRight - oldLeft;
+                final int newWidth = right - left;
+
+                if (newWidth != oldWidth) {
+                    scrollTitle();
+                }
+            }
+        };
+        mTitle.addTextChangedListener(new TextChangeListener());
+        mTitle.addOnLayoutChangeListener(resizeListener);
+        mTitleScroll.addOnLayoutChangeListener(resizeListener);
+
+        mMinUrlScrollMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                                                              MIN_DOMAIN_SCROLL_MARGIN_DP,
+                                                              getResources().getDisplayMetrics());
 
         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));
         mPrivateBlockedColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_blockedtext_private));
         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.url_bar_certificate_owner));
@@ -392,16 +420,63 @@ public class ToolbarDisplayLayout extend
 
         if (mSecurityImageLevel != imageLevel) {
             mSecurityImageLevel = imageLevel;
             mSiteSecurity.setImageLevel(mSecurityImageLevel);
             updatePageActions();
         }
     }
 
+    private void scrollTitle() {
+        final Editable text = mTitle.getEditableText();
+        final int textViewWidth = mTitle.getWidth();
+        final int textWidth = textViewWidth - mTitlePadding;
+        final int scrollViewWidth = mTitleScroll.getWidth();
+        if (textWidth <= scrollViewWidth) {
+            // The text fits within the ScrollView, so nothing to do here...
+            if (textViewWidth > scrollViewWidth) {
+                // ... although if the TextView is sufficiently padded on the right side, it might
+                // push the text out of view on the left side, so scroll to the beginning just to be
+                // on the safe side.
+                mTitleScroll.scrollTo(0, 0);
+            }
+            return;
+        }
+
+        final ForegroundColorSpan spanToCheck =
+                mTitle.isPrivateMode() ? mPrivateDomainColorSpan : mDomainColorSpan;
+        int domainEnd = text.getSpanEnd(spanToCheck);
+        if (domainEnd == -1) {
+            // We're not showing a domain, just scroll to the start of the text.
+            mTitleScroll.scrollTo(0, 0);
+            return;
+        }
+
+        // If we're showing an URL that is larger than the URL bar, we want to align the end of
+        // the domain part with the right side of URL bar, so as to put the focus on the base
+        // domain and avoid phishing attacks using long subdomains that have been crafted to be cut
+        // off at just the right place and then resemble a legitimate base domain.
+        final int domainTextWidth = StringUtils.getTextWidth(text.toString(), 0, domainEnd, mTitle.getPaint());
+        final int overhang = textViewWidth - domainTextWidth;
+        // For optimal alignment, we want to take the fadingEdge into account and align the domain
+        // with the start of the fade out.
+        final int maxFadingEdge = mTitleScroll.getHorizontalFadingEdgeLength();
+
+        // The width of the fadingEdge corresponds to the width of the child view that is overhanging
+        // the ScrollView, clamped by maxFadingEdge.
+        int targetMargin = overhang / 2;
+        targetMargin = Math.min(targetMargin, maxFadingEdge);
+        // Even when there is no fadingEdge, we want to keep a little margin between the domain and
+        // the end of the URL bar, so as to show the first character or so of the path part.
+        targetMargin = Math.max(targetMargin, mMinUrlScrollMargin);
+
+        final int scrollTarget = domainTextWidth + targetMargin - scrollViewWidth;
+        mTitleScroll.scrollTo(scrollTarget, 0);
+    }
+
     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();
         }
@@ -476,9 +551,22 @@ public class ToolbarDisplayLayout extend
         }
 
         return false;
     }
 
     void destroy() {
         mSiteIdentityPopup.destroy();
     }
+
+    private class TextChangeListener implements TextWatcher {
+        @Override
+        public void afterTextChanged(Editable text) {
+            scrollTitle();
+        }
+
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) { }
+    }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
@@ -1,15 +1,17 @@
 /* -*- 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.util;
 
+import android.graphics.Paint;
+import android.graphics.Rect;
 import android.net.Uri;
 import android.support.annotation.NonNull;
 import android.text.TextUtils;
 
 import java.nio.charset.Charset;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -296,9 +298,24 @@ public class StringUtils {
     }
 
     /**
      * Case-insensitive version of {@link String#startsWith(String, int)}.
      */
     public static boolean caseInsensitiveStartsWith(String text, String prefix, int start) {
         return text.regionMatches(true, start, prefix, 0, prefix.length());
     }
+
+    /**
+     * Measures the width of the given substring when rendered using the specified Paint.
+     *
+     * @param text      String to measure and return its width
+     * @param start     Index of the first char in the string to measure
+     * @param end       1 past the last char in the string measure
+     * @param textPaint the paint used to render the text
+     * @return          the width of the specified substring in screen pixels
+     */
+    public static int getTextWidth(final String text, final int start, final int end, final Paint textPaint) {
+        final Rect bounds = new Rect();
+        textPaint.getTextBounds(text, start, end, bounds);
+        return bounds.width();
+    }
 }
\ No newline at end of file