// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.omnibox; import android.content.res.Resources; import android.text.Spannable; import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.R; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.components.security_state.ConnectionSecurityLevel; import java.util.Locale; /** * A helper class that emphasizes the various components of a URL. Useful in the * Omnibox and Origin Info dialog where different parts of the URL should appear * in different colours depending on the scheme, host and connection. */ public class OmniboxUrlEmphasizer { /** * Describes the components of a URL that should be emphasized. */ public static class EmphasizeComponentsResponse { /** The start index of the scheme. */ public final int schemeStart; /** The length of the scheme. */ public final int schemeLength; /** The start index of the host. */ public final int hostStart; /** The length of the host. */ public final int hostLength; EmphasizeComponentsResponse( int schemeStart, int schemeLength, int hostStart, int hostLength) { this.schemeStart = schemeStart; this.schemeLength = schemeLength; this.hostStart = hostStart; this.hostLength = hostLength; } /** * @return Whether the URL has a scheme to be emphasized. */ public boolean hasScheme() { return schemeLength > 0; } /** * @return Whether the URL has a host to be emphasized. */ public boolean hasHost() { return hostLength > 0; } } /** * Parses the |text| passed in and determines the location of the scheme and * host components to be emphasized. * * @param profile The profile to be used for parsing. * @param text The text to be parsed for emphasis components. * @return The response object containing the locations of the emphasis * components. */ public static EmphasizeComponentsResponse parseForEmphasizeComponents( Profile profile, String text) { int[] emphasizeValues = nativeParseForEmphasizeComponents(profile, text); assert emphasizeValues != null; assert emphasizeValues.length == 4; return new EmphasizeComponentsResponse( emphasizeValues[0], emphasizeValues[1], emphasizeValues[2], emphasizeValues[3]); } /** * Denotes that a span is used for emphasizing the URL. */ @VisibleForTesting interface UrlEmphasisSpan { } /** * Used for emphasizing the URL text by changing the text color. */ @VisibleForTesting static class UrlEmphasisColorSpan extends ForegroundColorSpan implements UrlEmphasisSpan { /** * @param color The color to set the text. */ public UrlEmphasisColorSpan(int color) { super(color); } } /** * Used for emphasizing the URL text by striking through the https text. */ @VisibleForTesting static class UrlEmphasisSecurityErrorSpan extends StrikethroughSpan implements UrlEmphasisSpan { } /** * Modifies the given URL to emphasize the TLD and second domain. * TODO(sashab): Make this take an EmphasizeComponentsResponse object to * prevent calling parseForEmphasizeComponents() again. * * @param url The URL spannable to add emphasis to. This variable is * modified. * @param resources Resources for the given application context. * @param profile The profile viewing the given URL. * @param securityLevel A valid ConnectionSecurityLevel for the specified * web contents. * @param isInternalPage Whether this page is an internal Chrome page. * @param useDarkColors Whether the text colors should be dark (i.e. * appropriate for use on a light background). * @param emphasizeHttpsScheme Whether the https scheme should be emphasized. */ public static void emphasizeUrl(Spannable url, Resources resources, Profile profile, int securityLevel, boolean isInternalPage, boolean useDarkColors, boolean emphasizeHttpsScheme) { String urlString = url.toString(); EmphasizeComponentsResponse emphasizeResponse = parseForEmphasizeComponents(profile, urlString); int nonEmphasizedColorId = R.color.url_emphasis_non_emphasized_text; if (!useDarkColors) { nonEmphasizedColorId = R.color.url_emphasis_light_non_emphasized_text; } int startSchemeIndex = emphasizeResponse.schemeStart; int endSchemeIndex = emphasizeResponse.schemeStart + emphasizeResponse.schemeLength; int startHostIndex = emphasizeResponse.hostStart; int endHostIndex = emphasizeResponse.hostStart + emphasizeResponse.hostLength; // Color the HTTPS scheme. ForegroundColorSpan span; if (emphasizeResponse.hasScheme()) { int colorId = nonEmphasizedColorId; if (!isInternalPage) { boolean strikeThroughScheme = false; switch (securityLevel) { case ConnectionSecurityLevel.NONE: // Intentional fall-through: case ConnectionSecurityLevel.SECURITY_WARNING: break; case ConnectionSecurityLevel.DANGEROUS: if (emphasizeHttpsScheme) colorId = R.color.google_red_700; strikeThroughScheme = true; break; case ConnectionSecurityLevel.EV_SECURE: // Intentional fall-through: case ConnectionSecurityLevel.SECURE: if (emphasizeHttpsScheme) colorId = R.color.google_green_700; break; default: assert false; } if (strikeThroughScheme) { UrlEmphasisSecurityErrorSpan ss = new UrlEmphasisSecurityErrorSpan(); url.setSpan(ss, startSchemeIndex, endSchemeIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } span = new UrlEmphasisColorSpan(ApiCompatibilityUtils.getColor(resources, colorId)); url.setSpan( span, startSchemeIndex, endSchemeIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // Highlight the portion of the URL visible between the scheme and the host. For // https, this will be ://. For normal pages, this will be empty as we trim off // http://. if (emphasizeResponse.hasHost()) { span = new UrlEmphasisColorSpan( ApiCompatibilityUtils.getColor(resources, nonEmphasizedColorId)); url.setSpan(span, endSchemeIndex, startHostIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } if (emphasizeResponse.hasHost()) { // Highlight the complete host. int hostColorId = R.color.url_emphasis_domain_and_registry; if (!useDarkColors) { hostColorId = R.color.url_emphasis_light_domain_and_registry; } span = new UrlEmphasisColorSpan(ApiCompatibilityUtils.getColor(resources, hostColorId)); url.setSpan(span, startHostIndex, endHostIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // Highlight the remainder of the URL. if (endHostIndex < urlString.length()) { span = new UrlEmphasisColorSpan( ApiCompatibilityUtils.getColor(resources, nonEmphasizedColorId)); url.setSpan(span, endHostIndex, urlString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } /** * Reset the modifications done to emphasize the TLD and second domain of the URL. * * @param url The URL spannable to remove emphasis from. This variable is * modified. */ public static void deEmphasizeUrl(Spannable url) { UrlEmphasisSpan[] emphasisSpans = getEmphasisSpans(url); if (emphasisSpans.length == 0) return; for (UrlEmphasisSpan span : emphasisSpans) { url.removeSpan(span); } } /** * Returns whether the given URL has any emphasis spans applied. * * @param url The URL spannable to check emphasis on. * @return True if the URL has emphasis spans, false if not. */ public static boolean hasEmphasisSpans(Spannable url) { return getEmphasisSpans(url).length != 0; } /** * Returns the emphasis spans applied to the URL. * * @param url The URL spannable to get spans for. * @return The spans applied to the URL with emphasizeUrl(). */ public static UrlEmphasisSpan[] getEmphasisSpans(Spannable url) { return url.getSpans(0, url.length(), UrlEmphasisSpan.class); } /** * Returns the index of the first character containing non-origin * information, or 0 if the URL does not contain an origin. * * For "data" URLs, the URL is not considered to contain an origin. * For non-http and https URLs, the whole URL is considered the origin. * * For example, HTTP and HTTPS urls return the index of the first character * after the domain: * http://www.google.com/?q=foo => 21 (up to the 'm' in google.com) * https://www.google.com/?q=foo => 22 * * Data urls always return 0, since they do not contain an origin: * data:kf94hfJEj#N => 0 * * Other URLs treat the whole URL as an origin: * file://my/pc/somewhere/foo.html => 31 * about:blank => 11 * chrome://version => 18 * chrome-native://bookmarks => 25 * invalidurl => 10 * * TODO(sashab): Make this take an EmphasizeComponentsResponse object to * prevent calling parseForEmphasizeComponents() again. * * @param url The URL to find the last origin character in. * @param profile The profile visiting this URL (used for parsing the URL). * @return The index of the last character containing origin information. */ public static int getOriginEndIndex(String url, Profile profile) { EmphasizeComponentsResponse emphasizeResponse = parseForEmphasizeComponents(profile, url.toString()); if (!emphasizeResponse.hasScheme()) return url.length(); int startSchemeIndex = emphasizeResponse.schemeStart; int endSchemeIndex = emphasizeResponse.schemeStart + emphasizeResponse.schemeLength; String scheme = url.subSequence(startSchemeIndex, endSchemeIndex).toString().toLowerCase( Locale.US); if (scheme.equals("http") || scheme.equals("https")) { return emphasizeResponse.hostStart + emphasizeResponse.hostLength; } else if (scheme.equals("data")) { return 0; } else { return url.length(); } } private static native int[] nativeParseForEmphasizeComponents(Profile profile, String text); }