blob: 1b8d2624aacfa611715dd53583227dbc95211560 [file] [log] [blame]
// 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.base.annotations.NativeMethods;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.util.UrlConstants;
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 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 =
OmniboxUrlEmphasizerJni.get().parseForEmphasizeComponents(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
public interface UrlEmphasisSpan {}
/**
* Used for emphasizing the URL text by changing the text color.
*/
@VisibleForTesting
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
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.
* 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 emphasizeScheme Whether the scheme should be emphasized.
*/
public static void emphasizeUrl(Spannable url, Resources resources, Profile profile,
int securityLevel, boolean isInternalPage, boolean useDarkColors,
boolean emphasizeScheme) {
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;
// Format the scheme, if present, based on the security level.
ForegroundColorSpan span;
if (emphasizeResponse.hasScheme()) {
int colorId = nonEmphasizedColorId;
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))) {
colorId = useDarkColors ? R.color.default_text_color_dark
: R.color.default_text_color_light;
}
break;
case ConnectionSecurityLevel.DANGEROUS:
if (emphasizeScheme) {
colorId = useDarkColors ? R.color.default_red_dark
: R.color.default_red_light;
}
strikeThroughScheme = true;
break;
case ConnectionSecurityLevel.EV_SECURE:
// Intentional fall-through:
case ConnectionSecurityLevel.SECURE:
if (emphasizeScheme) {
colorId = useDarkColors ? R.color.default_green_dark
: R.color.default_green_light;
}
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,
// typically :// or : depending on the scheme.
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);
}
} else if (UrlConstants.DATA_SCHEME.equals(emphasizeResponse.extractScheme(urlString))) {
// Dim the remainder of the URL for anti-spoofing purposes.
span = new UrlEmphasisColorSpan(
ApiCompatibilityUtils.getColor(resources, nonEmphasizedColorId));
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.
*
* 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();
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
interface Natives {
int[] parseForEmphasizeComponents(Profile profile, String text);
}
}