chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBarData.java

// Copyright 2018 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.chrome.browser.omnibox;

import android.net.Uri;
import android.text.Spanned;
import android.text.TextUtils;

import androidx.annotation.Nullable;

import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.url.GURL;

import java.util.Set;

/** Encapsulates all data that is necessary for the URL bar to display its contents. */
public class UrlBarData {
    /** The URL schemes that don't need to be displayed complete with path. */
    public static final Set<String> SCHEMES_TO_SPLIT =
            Set.of(UrlConstants.HTTP_SCHEME, UrlConstants.HTTPS_SCHEME, UrlConstants.BLOB_SCHEME);

    /**
     * URI schemes that ContentView can handle.
     *
     * <p>Copied from UrlUtilities.java. UrlUtilities uses a URI to check for schemes, which is more
     * strict than Uri and causes the path stripping to fail.
     *
     * <p>The following additions have been made: "chrome", "ftp".
     */
    private static final Set<String> ACCEPTED_SCHEMES =
            Set.of(
                    ContentUrlConstants.ABOUT_SCHEME,
                    UrlConstants.DATA_SCHEME,
                    UrlConstants.FILE_SCHEME,
                    UrlConstants.FTP_SCHEME,
                    UrlConstants.HTTP_SCHEME,
                    UrlConstants.HTTPS_SCHEME,
                    UrlConstants.INLINE_SCHEME,
                    UrlConstants.JAVASCRIPT_SCHEME,
                    UrlConstants.CHROME_SCHEME);

    /** Represents an empty URL bar. */
    public static final UrlBarData EMPTY = forNonUrlText("");

    public static UrlBarData forUrl(GURL url) {
        return forUrlAndText(url, url.isValid() ? url.getSpec() : "", null);
    }

    public static UrlBarData forNonUrlText(String displayText) {
        return forNonUrlText(displayText, null);
    }

    public static UrlBarData forNonUrlText(String displayText, String editingText) {
        return create(null, displayText, 0, 0, editingText);
    }

    public static UrlBarData forUrlAndText(GURL url, String displayText) {
        return forUrlAndText(url, displayText, null);
    }

    /** Returns whether supplied URL should be shown in the Omnibox/Suggestions list. */
    public static boolean shouldShowUrl(GURL gurl, boolean isOffTheRecord) {
        return !NativePage.isChromePageUrl(gurl, isOffTheRecord) && !UrlUtilities.isNtpUrl(gurl);
    }

    public static UrlBarData forUrlAndText(
            GURL url, CharSequence displayText, @Nullable String editingText) {
        String displayTextStr = displayText == null ? "" : displayText.toString();
        if (url == null || !url.isValid() || url.isEmpty()) {
            return forNonUrlText(displayTextStr, editingText);
        }

        // The displayText may come with scheme stripped. In these cases, attempting to extract
        // scheme (e.g. via Uri.parse()) may return
        // - hostname, if the address includes port number
        //   e.g. "localhost:1234" would report scheme "localhost"
        // - path fragment, if a scheme delimiter is found later
        //   e.g. "abc.com/https://def.org" would report scheme "abc.com/https"
        // Instead of attempting to extract scheme from already modified URL, verify if the
        // displayText begins with what we know to be the current URL scheme.
        String scheme = url.getScheme();
        int pathSearchOffset = 0;
        if (!scheme.isEmpty() && displayTextStr.startsWith(scheme + ":")) {
            if (!SCHEMES_TO_SPLIT.contains(scheme)) {
                return create(url, displayTextStr, 0, displayTextStr.length(), editingText);
            }

            if (UrlConstants.BLOB_SCHEME.equals(scheme)) {
                int innerSchemeSearchOffset =
                        findFirstIndexAfterSchemeSeparator(displayTextStr, scheme.length());
                Uri innerUri = Uri.parse(displayTextStr.substring(innerSchemeSearchOffset));
                String innerScheme = innerUri.getScheme();
                // Substitute the scheme to allow for proper display of end of inner origin.
                if (!TextUtils.isEmpty(innerScheme)) {
                    scheme = innerScheme;
                }
            }

            if (ACCEPTED_SCHEMES.contains(scheme)) {
                pathSearchOffset =
                        findFirstIndexAfterSchemeSeparator(
                                displayTextStr, displayTextStr.indexOf(scheme) + scheme.length());
            }
        }
        int pathOffset = -1;
        if (pathSearchOffset < displayTextStr.length()) {
            pathOffset = displayTextStr.indexOf('/', pathSearchOffset);
        }
        if (pathOffset == -1) {
            return create(url, displayText, 0, displayTextStr.length(), editingText);
        }

        // If the '/' is the last character and the beginning of the path, then just drop
        // the path entirely.
        if (!TextUtils.isEmpty(displayText) && pathOffset == displayTextStr.length() - 1) {
            return create(url, displayText.subSequence(0, pathOffset), 0, pathOffset, editingText);
        }

        return create(url, displayText, 0, pathOffset, editingText);
    }

    public static UrlBarData create(
            @Nullable GURL url,
            CharSequence displayText,
            int originStartIndex,
            int originEndIndex,
            @Nullable String editingText) {
        return new UrlBarData(url, displayText, originStartIndex, originEndIndex, editingText);
    }

    private static int findFirstIndexAfterSchemeSeparator(
            CharSequence input, int searchStartIndex) {
        for (int index = searchStartIndex; index < input.length(); index++) {
            char c = input.charAt(index);
            if (c != ':' && c != '/') return index;
        }
        return input.length();
    }

    /** The canonical URL that is shown in the URL bar. */
    public final @Nullable GURL url;

    /**
     * The text that should be shown in the URL bar. This can be a {@link Spanned} that contains
     * formatting to highlight parts of the display text.
     */
    public final CharSequence displayText;

    /**
     * The text that should replace the display text when editing the contents of the URL bar, or
     * null to use the {@link #displayText} when editing.
     */
    public final @Nullable String editingText;

    /**
     * The character index in {@link #displayText} where the origin starts. This is required to
     * ensure that the end of the origin is not scrolled out of view for long hostnames.
     */
    public final int originStartIndex;

    /**
     * The character index in {@link #displayText} where the origin ends. This is required to ensure
     * that the end of the origin is not scrolled out of view for long hostnames.
     */
    public final int originEndIndex;

    /**
     * @return The text for editing, falling back to the display text if the former is null.
     */
    public CharSequence getEditingOrDisplayText() {
        return editingText != null ? editingText : displayText;
    }

    private UrlBarData(
            @Nullable GURL url,
            CharSequence displayText,
            int originStartIndex,
            int originEndIndex,
            @Nullable String editingText) {
        this.url = url;
        this.displayText = displayText;
        this.originStartIndex = originStartIndex;
        this.originEndIndex = originEndIndex;
        this.editingText = editingText;
    }
}