chromium/ui/android/java/src/org/chromium/ui/ElidedUrlTextView.java

// Copyright 2021 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.ui;

import android.content.Context;
import android.text.Layout;
import android.util.AttributeSet;

import androidx.appcompat.widget.AppCompatTextView;

/**
 * A TextView which truncates and displays a URL such that the origin is always visible.
 * The URL can be expanded by clicking on the it.
 */
public class ElidedUrlTextView extends AppCompatTextView {
    // The number of lines to display when the URL is truncated. This number
    // should still allow the origin to be displayed. NULL before
    // setUrlAfterLayout() is called.
    private Integer mTruncatedUrlLinesToDisplay;

    // The number of lines to display when the URL is expanded. This should be enough to display
    // at most two lines of the fragment if there is one in the URL.
    private Integer mFullLinesToDisplay;

    // If true, the text view will show the truncated text. If false, it
    // will show the full, expanded text.
    private boolean mIsShowingTruncatedText = true;

    // The length of the URL's origin in number of characters.
    private int mOriginLength = -1;

    // The maximum number of lines currently shown in the view
    private int mCurrentMaxLines = Integer.MAX_VALUE;

    /** Constructor for inflating from XML. */
    public ElidedUrlTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void setMaxLines(int maxlines) {
        super.setMaxLines(maxlines);
        mCurrentMaxLines = maxlines;
    }

    /**
     * Find the number of lines of text which must be shown in order to display the character at
     * a given index.
     */
    private int getLineForIndex(int index) {
        Layout layout = getLayout();
        int endLine = 0;
        while (endLine < layout.getLineCount() && layout.getLineEnd(endLine) < index) {
            endLine++;
        }
        // Since endLine is an index, add 1 to get the number of lines.
        return endLine + 1;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMaxLines(Integer.MAX_VALUE);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        assert mOriginLength >= 0 : "setUrl() must be called before layout.";
        String urlText = getText().toString();

        // Find the range of lines containing the origin.
        int originEndLine = getLineForIndex(mOriginLength);

        // Display an extra line so we don't accidentally hide the origin with
        // ellipses
        mTruncatedUrlLinesToDisplay = originEndLine + 1;

        // Find the line where the fragment starts. Since # is a reserved character, it is safe
        // to just search for the first # to appear in the url.
        int fragmentStartIndex = urlText.indexOf('#');
        if (fragmentStartIndex == -1) fragmentStartIndex = urlText.length();

        int fragmentStartLine = getLineForIndex(fragmentStartIndex);
        mFullLinesToDisplay = fragmentStartLine + 1;

        // If there is no origin (according to OmniboxUrlEmphasizer), make sure the fragment is
        // still hidden correctly.
        if (mFullLinesToDisplay < mTruncatedUrlLinesToDisplay) {
            mTruncatedUrlLinesToDisplay = mFullLinesToDisplay;
        }

        if (updateMaxLines()) super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * Sets the URL and the length of the URL's origin.
     * Must be called before layout.
     *
     * @param url The URL.
     * @param originLength The length of the URL's origin in number of characters.
     */
    public void setUrl(CharSequence url, int originLength) {
        assert originLength >= 0 && originLength <= url.length();
        setText(url);
        mOriginLength = originLength;
    }

    /**
     * Toggles truncating/expanding the URL text. If the URL text is not
     * truncated, has no effect.
     */
    public void toggleTruncation() {
        mIsShowingTruncatedText = !mIsShowingTruncatedText;
        if (mFullLinesToDisplay != null) {
            if (updateMaxLines()) {
                announceForAccessibilityOnToggleTruncation(mIsShowingTruncatedText);
            }
        }
    }

    private void announceForAccessibilityOnToggleTruncation(boolean isUrlTruncated) {
        announceForAccessibility(
                getResources()
                        .getString(
                                isUrlTruncated
                                        ? R.string.elided_url_text_view_url_truncated
                                        : R.string.elided_url_text_view_url_expanded));
    }

    private boolean updateMaxLines() {
        int maxLines = mFullLinesToDisplay;
        if (mIsShowingTruncatedText) {
            maxLines = mTruncatedUrlLinesToDisplay;
        }
        if (maxLines != mCurrentMaxLines) {
            setMaxLines(maxLines);
            return true;
        }
        return false;
    }
}