chromium/components/omnibox/browser/android/java/src/org/chromium/components/omnibox/OmniboxUrlEmphasizer.java

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.omnibox;

import android.content.Context;
import android.text.Spannable;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;

import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.VisibleForTesting;

import org.jni_zero.NativeMethods;

import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.embedder_support.util.UrlUtilities;
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 Page
 * Info popup 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;
        }

        /** @return The scheme extracted from |url|, canonicalized to lowercase. */
        public String extractScheme(String url) {
            if (!hasScheme()) return "";
            return url.subSequence(schemeStart, schemeStart + schemeLength)
                    .toString()
                    .toLowerCase(Locale.US);
        }
    }

    /**
     * Parses the |text| passed in and determines the location of the scheme and host components to
     * be emphasized.
     *
     * @param autocompleteSchemeClassifier The autocomplete scheme classifier 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(
            String text, AutocompleteSchemeClassifier autocompleteSchemeClassifier) {
        int[] emphasizeValues =
                OmniboxUrlEmphasizerJni.get()
                        .parseForEmphasizeComponents(text, autocompleteSchemeClassifier);
        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
    public interface UrlEmphasisSpan {}

    /** Used for emphasizing the URL text by changing the text color. */
    @VisibleForTesting
    public static class UrlEmphasisColorSpan extends ForegroundColorSpan
            implements UrlEmphasisSpan {
        private int mEmphasisColor;

        /** @param color The color to set the text. */
        public UrlEmphasisColorSpan(int color) {
            super(color);
            mEmphasisColor = color;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof UrlEmphasisColorSpan)) return false;
            return ((UrlEmphasisColorSpan) obj).mEmphasisColor == mEmphasisColor;
        }

        @Override
        public String toString() {
            return getClass().getName() + ", color: " + mEmphasisColor;
        }
    }

    /** Used for emphasizing the URL text by striking through the https text. */
    @VisibleForTesting
    public static class UrlEmphasisSecurityErrorSpan extends StrikethroughSpan
            implements UrlEmphasisSpan {
        @Override
        public boolean equals(Object obj) {
            return obj instanceof UrlEmphasisSecurityErrorSpan;
        }
    }

    /**
     * Modifies the given URL to emphasize the host and scheme.
     *
     * @param url The URL spannable to add emphasis to. This variable is modified.
     * @param context Context to resolve colors against.
     * @param autocompleteSchemeClassifier The autocomplete scheme classifier used to emphasize the
     *     given URL.
     * @param securityLevel A valid ConnectionSecurityLevel for the specified web contents.
     * @param useDarkForegroundColors Whether the text colors should be dark (i.e. appropriate for
     *     use on a light background).
     * @param emphasizeScheme Whether the scheme should be emphasized.
     * @deprecated in favor of {@link #emphasizeUrl(Spannable, AutocompleteSchemeClassifier, int,
     *     boolean, int, int, int, int)} because this method doesn't support dynamic colors.
     */
    @Deprecated
    public static void emphasizeUrl(
            Spannable url,
            Context context,
            AutocompleteSchemeClassifier autocompleteSchemeClassifier,
            int securityLevel,
            boolean useDarkForegroundColors,
            boolean emphasizeScheme) {
        final @ColorRes int nonEmphasizedColorId =
                useDarkForegroundColors
                        ? R.color.url_emphasis_non_emphasized_text
                        : R.color.url_emphasis_light_non_emphasized_text;
        final @ColorRes int emphasizedColorId =
                useDarkForegroundColors
                        ? R.color.url_emphasis_emphasized_text
                        : R.color.url_emphasis_light_emphasized_text;
        final @ColorRes int dangerColorId =
                useDarkForegroundColors ? R.color.default_red_dark : R.color.default_red_light;
        final @ColorRes int secureColorId =
                useDarkForegroundColors ? R.color.default_green_dark : R.color.default_green_light;

        emphasizeUrl(
                url,
                autocompleteSchemeClassifier,
                securityLevel,
                emphasizeScheme,
                context.getColor(nonEmphasizedColorId),
                context.getColor(emphasizedColorId),
                context.getColor(dangerColorId),
                context.getColor(secureColorId));
    }

    /**
     * Modifies the given URL to emphasize the host and scheme. 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 autocompleteSchemeClassifier The autocomplete scheme classifier used to emphasize the
     *     given URL.
     * @param securityLevel A valid ConnectionSecurityLevel for the specified web contents.
     * @param emphasizeScheme Whether the scheme should be emphasized.
     * @param nonEmphasizedColor Color of the non-emphasized components.
     * @param emphasizedColor Color of the emphasized components.
     * @param dangerColor Color of the scheme that denotes danger.
     * @param secureColor Color of the scheme that denotes security.
     */
    public static void emphasizeUrl(
            Spannable url,
            AutocompleteSchemeClassifier autocompleteSchemeClassifier,
            int securityLevel,
            boolean emphasizeScheme,
            @ColorInt int nonEmphasizedColor,
            @ColorInt int emphasizedColor,
            @ColorInt int dangerColor,
            @ColorInt int secureColor) {
        String urlString = url.toString();
        EmphasizeComponentsResponse emphasizeResponse =
                parseForEmphasizeComponents(urlString, autocompleteSchemeClassifier);

        int startSchemeIndex = emphasizeResponse.schemeStart;
        int endSchemeIndex = emphasizeResponse.schemeStart + emphasizeResponse.schemeLength;

        int startHostIndex = emphasizeResponse.hostStart;
        int endHostIndex = emphasizeResponse.hostStart + emphasizeResponse.hostLength;
        boolean isInternalPage =
                UrlUtilities.isInternalScheme(emphasizeResponse.extractScheme(urlString));

        // Format the scheme, if present, based on the security level.
        ForegroundColorSpan span;
        if (emphasizeResponse.hasScheme()) {
            int color = nonEmphasizedColor;
            if (!isInternalPage) {
                boolean strikeThroughScheme = false;
                switch (securityLevel) {
                    case ConnectionSecurityLevel.NONE:
                        // Intentional fall-through:
                    case ConnectionSecurityLevel.WARNING:
                        // Draw attention to the data: URI scheme for anti-spoofing reasons.
                        if (UrlConstants.DATA_SCHEME.equals(
                                emphasizeResponse.extractScheme(urlString))) {
                            color = emphasizedColor;
                        }
                        break;
                    case ConnectionSecurityLevel.DANGEROUS:
                        if (emphasizeScheme) {
                            color = dangerColor;
                        }
                        strikeThroughScheme = true;
                        break;
                    case ConnectionSecurityLevel.SECURE:
                        if (emphasizeScheme) {
                            color = secureColor;
                        }
                        break;
                    default:
                        assert false;
                }

                if (strikeThroughScheme) {
                    UrlEmphasisSecurityErrorSpan ss = new UrlEmphasisSecurityErrorSpan();
                    url.setSpan(
                            ss,
                            startSchemeIndex,
                            endSchemeIndex,
                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }
            span = new UrlEmphasisColorSpan(color);
            url.setSpan(span, startSchemeIndex, endSchemeIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

            // Highlight the portion of the URL visible between the scheme and the host,
            // typically :// or : depending on the scheme.
            if (emphasizeResponse.hasHost()) {
                span = new UrlEmphasisColorSpan(nonEmphasizedColor);
                url.setSpan(
                        span, endSchemeIndex, startHostIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }

        if (emphasizeResponse.hasHost()) {
            // Highlight the complete host.
            span = new UrlEmphasisColorSpan(emphasizedColor);
            url.setSpan(span, startHostIndex, endHostIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

            // Highlight the remainder of the URL.
            if (endHostIndex < urlString.length()) {
                span = new UrlEmphasisColorSpan(nonEmphasizedColor);
                url.setSpan(
                        span, endHostIndex, urlString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        } else if (UrlConstants.DATA_SCHEME.equals(emphasizeResponse.extractScheme(urlString))) {
            // Dim the remainder of the URL for anti-spoofing purposes.
            span = new UrlEmphasisColorSpan(nonEmphasizedColor);
            url.setSpan(
                    span,
                    emphasizeResponse.schemeStart + emphasizeResponse.schemeLength,
                    urlString.length(),
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }

    /**
     * Reset the modifications done to emphasize components 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.
     *
     * <p>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.
     *
     * <p>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
     *
     * <p>Data urls always return 0, since they do not contain an origin: data:kf94hfJEj#N => 0
     *
     * <p>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
     *
     * <p>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 autocompleteSchemeClassifier The autocomplete scheme classifier used for parsing the
     *     URL.
     * @return The index of the last character containing origin information.
     */
    public static int getOriginEndIndex(
            String url, AutocompleteSchemeClassifier autocompleteSchemeClassifier) {
        EmphasizeComponentsResponse emphasizeResponse =
                parseForEmphasizeComponents(url.toString(), autocompleteSchemeClassifier);
        if (!emphasizeResponse.hasScheme()) return url.length();

        String scheme = emphasizeResponse.extractScheme(url);

        if (scheme.equals(UrlConstants.HTTP_SCHEME) || scheme.equals(UrlConstants.HTTPS_SCHEME)) {
            return emphasizeResponse.hostStart + emphasizeResponse.hostLength;
        } else if (scheme.equals(UrlConstants.DATA_SCHEME)) {
            return 0;
        } else {
            return url.length();
        }
    }

    @NativeMethods
    public interface Natives {
        int[] parseForEmphasizeComponents(
                String text, AutocompleteSchemeClassifier autocompleteSchemeClassifier);
    }
}