chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusView.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.status;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RotateDrawable;
import android.os.Build;
import android.os.Build.VERSION;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.TooltipCompat;

import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.omnibox.R;
import org.chromium.components.browser_ui.widget.ChromeTransitionDrawable;
import org.chromium.components.browser_ui.widget.CompositeTouchDelegate;
import org.chromium.components.browser_ui.widget.RoundedCornerOutlineProvider;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.interpolators.Interpolators;
import org.chromium.ui.util.TokenHolder;
import org.chromium.ui.widget.Toast;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** StatusView is a location bar's view displaying status (icons and/or text). */
public class StatusView extends LinearLayout {
    @IntDef({IconTransitionType.CROSSFADE, IconTransitionType.ROTATE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface IconTransitionType {
        int CROSSFADE = 0;
        int ROTATE = 1;
    }

    public static final int ICON_ANIMATION_DURATION_MS = 225;
    public static final int ICON_ROTATION_DURATION_MS = 250;
    private static final int ICON_ROTATION_DEGREES = 180;

    private @Nullable View mIncognitoBadge;
    // The default value is 0, which matches R.dimen.location_bar_start_padding.
    private int mTouchDelegateStartOffset;
    private int mTouchDelegateEndOffset;

    private ImageView mIconView;
    private View mIconBackground;
    private StatusIconView mStatusIconView;
    private TextView mVerboseStatusTextView;
    private View mSeparatorView;
    private View mStatusExtraSpace;

    private boolean mAnimationsEnabled;
    private boolean mAnimatingStatusIconShow;
    private boolean mAnimatingStatusIconHide;
    private boolean mIsAnimatingStatusIconChange;

    private @StringRes int mAccessibilityToast;
    private @StringRes int mAccessibilityDoubleTapDescription;

    private Drawable mStatusIconDrawable;

    private TouchDelegate mTouchDelegate;
    private CompositeTouchDelegate mCompositeTouchDelegate;

    private boolean mLastTouchDelegateRtlness;
    private Rect mLastTouchDelegateRect;

    private BrowserStateBrowserControlsVisibilityDelegate mBrowserControlsVisibilityDelegate;
    private int mShowBrowserControlsToken = TokenHolder.INVALID_TOKEN;
    private Integer mIconAnimationDurationForTests;

    public StatusView(Context context, AttributeSet attributes) {
        super(context, attributes);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mIconView = findViewById(R.id.location_bar_status_icon);
        mIconBackground = findViewById(R.id.location_bar_status_icon_bg);
        mStatusIconView = findViewById(R.id.location_bar_status_icon_view);
        mVerboseStatusTextView = findViewById(R.id.location_bar_verbose_status);
        mSeparatorView = findViewById(R.id.location_bar_verbose_status_separator);
        mStatusExtraSpace = findViewById(R.id.location_bar_verbose_status_extra_space);

        // Set onHoverListener for verbose status view to hide the divider while the verbose hover
        // highlight is showing.
        setOnHoverListener(
                new OnHoverListener() {
                    private int mSeparatorVisibility;

                    @Override
                    public boolean onHover(View v, MotionEvent event) {
                        switch (event.getAction()) {
                            case MotionEvent.ACTION_HOVER_ENTER:
                                mSeparatorVisibility = mSeparatorView.getVisibility();
                                if (getBackground() != null
                                        && mSeparatorVisibility == View.VISIBLE) {
                                    mSeparatorView.setVisibility(View.GONE);
                                }
                                return false;
                            case MotionEvent.ACTION_HOVER_EXIT:
                                if (mSeparatorView.getVisibility() != mSeparatorVisibility) {
                                    mSeparatorView.setVisibility(mSeparatorVisibility);
                                }
                                return false;
                            default:
                                return false;
                        }
                    }
                });

        // Configure icon rounding.
        mIconView.setOutlineProvider(
                new RoundedCornerOutlineProvider(
                        getResources()
                                        .getDimensionPixelSize(
                                                R.dimen.omnibox_search_engine_logo_composed_size)
                                / 2));
        mIconView.setClipToOutline(true);

        configureAccessibilityDescriptions();
    }

    /**
     * Set tooltip text resource id.
     *
     * @param tooltipTextResId tooltip text resource id.
     */
    public void setTooltipText(@StringRes int tooltipTextResId) {
        if (tooltipTextResId != Resources.ID_NULL) {
            setTooltipText(mStatusIconView.getContext().getString(tooltipTextResId));
        } else {
            setTooltipText(null);
        }
    }

    /**
     * Set hover highlight resource id.
     *
     * @param hoverHighlightResId background hover highlight resource id.
     */
    public void setHoverHighlight(@DrawableRes int hoverHighlightResId) {
        if (hoverHighlightResId != Resources.ID_NULL && isSearchEngineStatusIconVisible()) {
            setBackground(AppCompatResources.getDrawable(getContext(), hoverHighlightResId));
        } else {
            setBackground(null);
        }
    }

    /** Return whether search engine status icon is visible. */
    public boolean isSearchEngineStatusIconVisible() {
        return mStatusIconView.getIconVisibility() == VISIBLE;
    }

    /**
     * Set the composite touch delegate here to which this view's touch delegate will be added.
     *
     * @param compositeTouchDelegate The parent's CompositeTouchDelegate to be used.
     */
    public void setCompositeTouchDelegate(CompositeTouchDelegate compositeTouchDelegate) {
        mCompositeTouchDelegate = compositeTouchDelegate;
        mIconView.addOnLayoutChangeListener(
                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
                        updateTouchDelegate());
    }

    /**
     * Start animating transition of status icon.
     *
     * @param transitionType The animation transition type for the icon.
     * @param animationFinishedCallback The callback to be run after the status icon has been
     *     successfully set.
     */
    private void animateStatusIcon(
            @IconTransitionType int transitionType, @Nullable Runnable animationFinishedCallback) {
        Drawable targetIcon = mStatusIconDrawable;
        boolean wantIconHidden = mStatusIconDrawable == null;
        // We only handle additional callback after rotation.
        assert transitionType == IconTransitionType.ROTATE || animationFinishedCallback == null;

        // Ensure proper handling of animations.
        // Possible variants:
        // 1. Is: shown,           want: hidden  => animate hiding,
        // 2. Is: shown,           want: shown   => crossfade w/ChromeTransitionDrawable,
        // 3. Is: animating(show), want: hidden  => cancel animation; animate hiding,
        // 4. Is: animating(show), want: shown   => crossfade (carry on showing),
        // 5. Is: animating(hide), want: hidden  => no op,
        // 6. Is: animating(hide), want: shown   => cancel animation; animate showing; crossfade,
        // 7. Is: hidden,          want: hidden  => no op,
        // 8. Is: hidden,          want: shown   => animate showing.
        //
        // This gives 3 actions:
        // - Animate showing, if hidden or previously hiding (6 + 8); cancel previous animation (6)
        // - Animate hiding, if shown or previously showing (1 + 3); cancel previous animation (3)
        // - crossfade w/ChromeTransitionDrawable, if visible (2, 4, 6), otherwise use image
        // directly. All other options (5, 7) are no-op.
        //
        // Note: this will be compacted once we start using LayoutTransition with StatusView.
        boolean isIconHidden = mStatusIconView.getIconVisibility() == View.GONE;
        if (!wantIconHidden && (isIconHidden || mAnimatingStatusIconHide)) {
            // Action 1: animate showing, if icon was either hidden or hiding.
            if (mAnimatingStatusIconHide) mIconView.animate().cancel();
            mAnimatingStatusIconHide = false;
            mAnimatingStatusIconShow = true;
            keepControlsShownForAnimation();

            // Set StatusIcon visibility and check whether we should set hover action on StatusView.
            setStatusIconVisibility(View.VISIBLE);

            mIconView
                    .animate()
                    .alpha(1.0f)
                    .setDuration(getIconAnimationDuration())
                    .withEndAction(
                            () -> {
                                mAnimatingStatusIconShow = false;
                                allowBrowserControlsHide();
                                // Wait until the icon is visible so the bounds will be properly
                                // set.
                                updateTouchDelegate();
                            })
                    .start();
        } else if (wantIconHidden && (!isIconHidden || mAnimatingStatusIconShow)) {
            // Action 2: animate hiding, if icon was either shown or showing.
            if (mAnimatingStatusIconShow) mIconView.animate().cancel();
            mAnimatingStatusIconShow = false;
            mAnimatingStatusIconHide = true;
            keepControlsShownForAnimation();
            // Do not animate phase-out when animations are disabled.
            // While this looks nice in some cases (navigating to insecure sites),
            // it has a side-effect of briefly showing padlock (phase-out) when navigating
            // back and forth between secure and insecure sites, which seems like a glitch.
            // See bug: crbug.com/919449
            mIconView
                    .animate()
                    .setDuration(mAnimationsEnabled ? getIconAnimationDuration() : 0)
                    .alpha(0.0f)
                    .withEndAction(
                            () -> {
                                // Set StatusIcon visibility and check whether we should set hover
                                // action on StatusView.
                                setStatusIconVisibility(View.GONE);

                                mIconView.setAlpha(1f);
                                mAnimatingStatusIconHide = false;
                                allowBrowserControlsHide();
                                updateTouchDelegate();
                            })
                    .start();
        } else {
            updateTouchDelegate();
        }

        // Action 3: Specify icon content. Use ChromeTransitionDrawable whenever object is visible.
        if (targetIcon != null) {
            if (!isIconHidden) {
                Drawable existingDrawable = mIconView.getDrawable();
                if (existingDrawable instanceof ChromeTransitionDrawable) {
                    ChromeTransitionDrawable transitionDrawable =
                            (ChromeTransitionDrawable) existingDrawable;
                    // Finish any running animations in the existing drawable because we're going to
                    // reuse it. Concurrent animations could clobber each other's changes and cause
                    // inconsistent states.
                    transitionDrawable.finishTransition(true);
                    existingDrawable = transitionDrawable.getFinalDrawable();
                }

                ChromeTransitionDrawable newImage =
                        new ChromeTransitionDrawable(
                                existingDrawable,
                                transitionType == IconTransitionType.ROTATE
                                        ? getRotatedIcon(targetIcon)
                                        : targetIcon);
                mIconView.setImageDrawable(newImage);

                if (transitionType == IconTransitionType.CROSSFADE) {
                    mIsAnimatingStatusIconChange = true;
                    long duration = mAnimationsEnabled ? getIconAnimationDuration() : 0;
                    if (duration > 0) {
                        keepControlsShownForAnimation();
                    }
                    newImage.setCrossFadeEnabled(true);
                    newImage.startTransition()
                            .setDuration(duration)
                            .withEndAction(this::resetAnimationStatus);
                } else {
                    mIsAnimatingStatusIconChange = true;
                    keepControlsShownForAnimation();
                    mIconView
                            .animate()
                            .setDuration(ICON_ROTATION_DURATION_MS)
                            .rotationBy(ICON_ROTATION_DEGREES)
                            .setInterpolator(Interpolators.FAST_OUT_LINEAR_IN_INTERPOLATOR)
                            .withStartAction(
                                    () -> {
                                        newImage.startTransition()
                                                .setDuration(getIconAnimationDuration())
                                                .withEndAction(this::resetAnimationStatus);
                                    })
                            .withEndAction(
                                    () -> {
                                        mIsAnimatingStatusIconChange = false;
                                        allowBrowserControlsHide();
                                        mIconView.setRotation(0);
                                        // Only update status icon if it is still the current icon.
                                        if (mStatusIconDrawable == targetIcon) {
                                            mIconView.setImageDrawable(targetIcon);
                                            if (animationFinishedCallback != null) {
                                                animationFinishedCallback.run();
                                            }
                                        }
                                    })
                            .start();
                }

                // Update the touch delegate only if the icons are swapped without animating the
                // image view.
                if (!mAnimatingStatusIconShow) updateTouchDelegate();
            } else {
                mIconView.setImageDrawable(targetIcon);
            }
        }
    }

    private void setStatusIconVisibility(int visibility) {
        mStatusIconView.setVisibility(visibility);
    }

    /** Returns a rotated version of the icon passed in. */
    private Drawable getRotatedIcon(@NonNull Drawable icon) {
        RotateDrawable rotated = new RotateDrawable();
        rotated.setDrawable(icon);
        rotated.setToDegrees(ICON_ROTATION_DEGREES);
        // Jump drawable to its target state.
        rotated.setLevel(10000);
        return rotated;
    }

    /**
     * Specify object to receive click events.
     *
     * @param listener Instance of View.OnClickListener or null.
     */
    void setStatusClickListener(View.OnClickListener listener) {
        setOnClickListener(listener);
    }

    /** Configure accessibility descriptions. */
    void configureAccessibilityDescriptions() {
        View.OnLongClickListener listener =
                new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View view) {
                        if (mAccessibilityToast == 0) return false;
                        Context context = getContext();
                        return Toast.showAnchoredToast(
                                context,
                                view,
                                context.getResources().getString(mAccessibilityToast));
                    }
                };
        setOnLongClickListener(listener);

        setAccessibilityDelegate(
                new AccessibilityDelegate() {
                    @Override
                    public void onInitializeAccessibilityNodeInfo(
                            View host, AccessibilityNodeInfo info) {
                        super.onInitializeAccessibilityNodeInfo(host, info);

                        if (mAccessibilityDoubleTapDescription == 0) return;

                        String onTapDescription =
                                getContext()
                                        .getResources()
                                        .getString(mAccessibilityDoubleTapDescription);
                        info.addAction(
                                new AccessibilityAction(
                                        AccessibilityNodeInfo.ACTION_CLICK, onTapDescription));
                    }
                });
    }

    /** Toggle use of animations. */
    void setAnimationsEnabled(boolean enabled) {
        mAnimationsEnabled = enabled;
    }

    /**
     * Sets the Drawable to be used as the status icon.
     *
     * @param statusIconDrawable The Drawable.
     * @param transitionType The animation transition type for the icon.
     * @param animationFinishedCallback The callback to be run after the new drawable has been
     *     successfully set.
     */
    void setStatusIconResources(
            @Nullable Drawable statusIconDrawable,
            @IconTransitionType int transitionType,
            @Nullable Runnable animationFinishedCallback) {
        mStatusIconDrawable = statusIconDrawable;
        animateStatusIcon(transitionType, animationFinishedCallback);
    }

    /** Specify the status icon alpha. */
    void setStatusIconAlpha(float alpha) {
        if (mIconView == null) return;
        mIconView.setAlpha(alpha);

        if (mIconBackground != null && mIconBackground.getVisibility() == VISIBLE) {
            mIconBackground.setAlpha(alpha);
        }
    }

    /** Specify the status icon visibility. */
    public void setStatusIconShown(boolean showIcon) {
        if (mStatusIconView == null) return;
        // Check if layout was requested before changing our child view.
        boolean wasLayoutPreviouslyRequested = isLayoutRequested();

        // Set StatusIcon visibility and check whether we should set hover action on StatusView.
        setStatusIconVisibility(showIcon ? VISIBLE : GONE);

        updateTouchDelegate();
        if (mIsAnimatingStatusIconChange && !showIcon) {
            // If the icon view is hidden before it gets a chance to draw, our animation status will
            // become stale. Reset it.
            resetAnimationStatus();
        }

        // If the icon's visibility changes while layout is pending, we can end up in a bad state
        // due to a stale measurement cache. Post a task to request layout to force this visibility
        // change (crbug.com/1345552).
        if (wasLayoutPreviouslyRequested && getHandler() != null) {
            getHandler()
                    .post(
                            () ->
                                    ViewUtils.requestLayout(
                                            this, "StatusView.setStatusIconShown Runnable"));
        }
    }

    /** Specify the status icon background visibility. */
    void setStatusIconBackgroundVisibility(boolean showIconBackground) {
        if (mIconView == null || mIconBackground == null) return;

        mIconBackground.setVisibility(showIconBackground ? VISIBLE : INVISIBLE);
    }

    /** Specify accessibility string presented to user upon long click. */
    void setStatusAccessibilityToast(@StringRes int description) {
        mAccessibilityToast = description;
    }

    /** Specify accessibility string used for "Double tap to" description. */
    void setStatusAccessibilityDoubleTapDescription(@StringRes int description) {
        mAccessibilityDoubleTapDescription = description;
    }

    /** Specify content description for security icon. */
    void setStatusIconDescription(@StringRes int descriptionRes) {
        String description = null;
        int importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO;
        if (descriptionRes != 0) {
            description = getResources().getString(descriptionRes);
            importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES;
        }
        mIconView.setContentDescription(description);
        setImportantForAccessibility(importantForAccessibility);
    }

    /** Select color of Separator view. */
    void setSeparatorColor(@ColorInt int separatorColor) {
        mSeparatorView.setBackgroundColor(separatorColor);
    }

    /** Select color of verbose status text. */
    void setVerboseStatusTextColor(@ColorInt int textColor) {
        mVerboseStatusTextView.setTextColor(textColor);
    }

    /** Specify content of the verbose status text. */
    void setVerboseStatusTextContent(@StringRes int content) {
        mVerboseStatusTextView.setText(content);
    }

    /** Specify visibility of the verbose status text. */
    public void setVerboseStatusTextVisible(boolean visible) {
        int visibility = visible ? View.VISIBLE : View.GONE;
        mVerboseStatusTextView.setVisibility(visibility);
        mSeparatorView.setVisibility(visibility);
        mStatusExtraSpace.setVisibility(visibility);
    }

    /** Specify width of the verbose status text. */
    void setVerboseStatusTextWidth(int width) {
        mVerboseStatusTextView.setMaxWidth(width);
    }

    /**
     * @param incognitoBadgeVisible Whether or not the incognito badge is visible.
     */
    void setIncognitoBadgeVisibility(boolean incognitoBadgeVisible) {
        // Initialize the incognito badge on the first time it becomes visible.
        if (mIncognitoBadge == null && !incognitoBadgeVisible) return;
        if (mIncognitoBadge == null) initializeIncognitoBadge();

        mIncognitoBadge.setVisibility(incognitoBadgeVisible ? View.VISIBLE : View.GONE);
        updateTouchDelegate();
    }

    void setBrowserControlsVisibilityDelegate(
            BrowserStateBrowserControlsVisibilityDelegate browserControlsVisibilityDelegate) {
        mBrowserControlsVisibilityDelegate = browserControlsVisibilityDelegate;
    }

    private void initializeIncognitoBadge() {
        ViewStub viewStub = findViewById(R.id.location_bar_incognito_badge_stub);
        mIncognitoBadge = viewStub.inflate();
    }

    /**
     * Create a touch delegate to expand the clickable area for the padlock icon (see
     * crbug.com/970031 for motivation/info). This method will be called when the icon is animating
     * in and when layout changes. It's called on these intervals because
     *
     * <ul>
     *   <li>the layout could change and
     *   <li>the Rtl-ness of the view could change. There are checks in place to avoid doing
     *       unnecessary work, so if the rect is empty or equivalent to the one already in place, no
     *       work will be done.
     * </ul>
     */
    private void updateTouchDelegate() {
        if (mCompositeTouchDelegate == null) return;

        if (!isIconVisible()) {
            // Tear down the existing delegate if it exists.
            if (mTouchDelegate != null) {
                mCompositeTouchDelegate.removeDelegateForDescendantView(mTouchDelegate);
                mTouchDelegate = null;
                mLastTouchDelegateRect = new Rect();
            }
            return;
        }
        // Setup a touch delegate to increase the clickable area for the padlock.
        // See for more information.
        Rect touchDelegateBounds = new Rect();
        mIconView.getHitRect(touchDelegateBounds);
        if (touchDelegateBounds.equals(new Rect())) return;

        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
        if (mTouchDelegateEndOffset == 0) {
            mTouchDelegateEndOffset =
                    getResources().getDimensionPixelSize(R.dimen.location_bar_icon_margin_end);
        }
        touchDelegateBounds.left -= isRtl ? mTouchDelegateEndOffset : mTouchDelegateStartOffset;
        touchDelegateBounds.right += isRtl ? mTouchDelegateStartOffset : mTouchDelegateEndOffset;
        // Increase the delegate area height for tablets to satisfy minimum size requirements.
        // Ideally, we want to address crbug.com/1320384 to satisfy minimum size requirements.
        if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(getContext())) {
            touchDelegateBounds.top -=
                    getResources()
                            .getDimensionPixelSize(
                                    R.dimen.modern_toolbar_background_vertical_offset);
            touchDelegateBounds.bottom +=
                    getResources()
                            .getDimensionPixelSize(
                                    R.dimen.modern_toolbar_background_vertical_offset);
        }

        // If our rect and rtl-ness hasn't changed, there's no need to recreate the TouchDelegate.
        if (mTouchDelegate != null
                && touchDelegateBounds.equals(mLastTouchDelegateRect)
                && mLastTouchDelegateRtlness == isRtl) {
            return;
        }
        mLastTouchDelegateRect = touchDelegateBounds;

        // Remove the existing delegate when we recreate a new one.
        if (mTouchDelegate != null) {
            mCompositeTouchDelegate.removeDelegateForDescendantView(mTouchDelegate);
        }

        // Set the delegate on LocationBarLayout because it has available space. Setting on
        // status view itself will clip the rect.
        mTouchDelegate = new TouchDelegate(touchDelegateBounds, mIconView);
        mCompositeTouchDelegate.addDelegateForDescendantView(mTouchDelegate);
        mLastTouchDelegateRtlness = isRtl;
    }

    // TODO(ender): The final last purpose of this method is to allow
    // ToolbarButtonInProductHelpController set up help bubbles. This dependency is about to
    // change. Do not depend on this method when creating new code.
    View getSecurityView() {
        return mIconView;
    }

    /**
     * @return The width of the status icon including start/end margins.
     */
    int getStatusIconWidth() {
        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
        return lp.getMarginStart() + getMeasuredWidth() + lp.getMarginEnd();
    }

    boolean isStatusIconAnimating() {
        return mAnimatingStatusIconShow || mAnimatingStatusIconHide || mIsAnimatingStatusIconChange;
    }

    /**
     * @return True if the status icon is currently visible.
     */
    private boolean isIconVisible() {
        return mStatusIconDrawable != null
                && mStatusIconView.getIconVisibility() != GONE
                && mIconView.getAlpha() != 0;
    }

    /** Set tooltip text on StatusView for API >= 26. */
    private void setTooltipText(String tooltip) {
        if (VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            TooltipCompat.setTooltipText((View) this, tooltip);
        }
    }

    private void keepControlsShownForAnimation() {
        // isShown() being false implies that the status view isn't visible. We don't want to force
        // it back into visibility just so that we can show an animation.
        if (isShown() && mBrowserControlsVisibilityDelegate != null) {
            mShowBrowserControlsToken =
                    mBrowserControlsVisibilityDelegate.showControlsPersistentAndClearOldToken(
                            mShowBrowserControlsToken);
        }
    }

    private void allowBrowserControlsHide() {
        if (mBrowserControlsVisibilityDelegate != null) {
            mBrowserControlsVisibilityDelegate.releasePersistentShowingToken(
                    mShowBrowserControlsToken);
            mShowBrowserControlsToken = TokenHolder.INVALID_TOKEN;
        }
    }

    private void resetAnimationStatus() {
        mIsAnimatingStatusIconChange = false;
        allowBrowserControlsHide();
    }

    private int getIconAnimationDuration() {
        return mIconAnimationDurationForTests == null
                ? ICON_ANIMATION_DURATION_MS
                : mIconAnimationDurationForTests;
    }

    TouchDelegate getTouchDelegateForTesting() {
        return mTouchDelegate;
    }

    void setIconAnimationDurationForTesting(int duration) {
        mIconAnimationDurationForTests = duration;
    }
}