Bug 1271998 - Part 3 - Scroll the URL to focus the origin for overlength URLs. r?walkingice,jwu draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Thu, 24 Aug 2017 22:09:56 +0200
changeset 659242 b89f3e004db350d5b740f36c708482bb8ce52c18
parent 659241 9e97442cdf91a20486f5d09841a90b6e45eab83b
child 659243 74cd7cbcea5e263658a034a21d2e4c536cc81ef4
push id78062
push usermozilla@buttercookie.de
push dateTue, 05 Sep 2017 18:20:38 +0000
reviewerswalkingice, jwu
bugs1271998
milestone57.0a1
Bug 1271998 - Part 3 - Scroll the URL to focus the origin for overlength URLs. r?walkingice,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