chromium/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/selectable_list/SelectableListToolbar.java

// Copyright 2017 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.browser_ui.widget.selectable_list;

import android.app.Activity;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;

import androidx.annotation.CallSuper;
import androidx.annotation.ColorInt;
import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
import androidx.annotation.MenuRes;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.core.graphics.drawable.DrawableCompat;

import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.NumberRollView;
import org.chromium.components.browser_ui.widget.R;
import org.chromium.components.browser_ui.widget.TintedDrawable;
import org.chromium.components.browser_ui.widget.displaystyle.DisplayStyleObserver;
import org.chromium.components.browser_ui.widget.displaystyle.HorizontalDisplayStyle;
import org.chromium.components.browser_ui.widget.displaystyle.UiConfig;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate.SelectionObserver;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.UiUtils;
import org.chromium.ui.text.EmptyTextWatcher;
import org.chromium.ui.util.ColorUtils;

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

/**
 * A toolbar that changes its view depending on whether a selection is established. The toolbar
 * also optionally shows a search view depending on whether {@link #initializeSearchView()} has
 * been called.
 *
 * @param <E> The type of the selectable items this toolbar interacts with.
 */
public class SelectableListToolbar<E> extends Toolbar
        implements SelectionObserver<E>,
                OnClickListener,
                OnEditorActionListener,
                DisplayStyleObserver {
    /** A delegate that handles searching the list of selectable items associated with this toolbar. */
    public interface SearchDelegate {
        /**
         * Called when the text in the search EditText box has changed.
         * @param query The text in the search EditText box.
         */
        void onSearchTextChanged(String query);

        /** Called when a search is ended. */
        void onEndSearch();
    }

    @IntDef({ViewType.NORMAL_VIEW, ViewType.SELECTION_VIEW, ViewType.SEARCH_VIEW})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ViewType {
        int NORMAL_VIEW = 0;
        int SELECTION_VIEW = 1;
        int SEARCH_VIEW = 2;
    }

    @IntDef({
        NavigationButton.NONE,
        NavigationButton.SEARCH_BACK,
        NavigationButton.SELECTION_BACK,
        NavigationButton.NORMAL_VIEW_BACK
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface NavigationButton {
        int NONE = 0;
        int SEARCH_BACK = 1;
        int SELECTION_BACK = 2;
        int NORMAL_VIEW_BACK = 3;
    }

    protected boolean mIsSelectionEnabled;
    protected SelectionDelegate<E> mSelectionDelegate;

    private final ObservableSupplierImpl<Boolean> mIsSearchingSupplier =
            new ObservableSupplierImpl<>();
    private boolean mHasSearchView;
    private LinearLayout mSearchView;
    private EditText mSearchEditText;
    private ImageButton mClearTextButton;
    private SearchDelegate mSearchDelegate;
    private boolean mSearchEnabled;
    private boolean mUpdateStatusBarColor;
    private boolean mShowBackInNormalView;

    protected NumberRollView mNumberRollView;
    private Drawable mMenuButton;
    private Drawable mNavigationIconDrawable;

    private @NavigationButton int mNavigationButton;
    private @StringRes int mTitleResId;
    private @IdRes int mSearchMenuItemId;
    private @IdRes int mInfoMenuItemId;
    private @IdRes int mNormalGroupResId;
    private @IdRes int mSelectedGroupResId;

    private @ColorInt int mNormalBackgroundColor;
    private @ColorInt int mSearchBackgroundColor;
    private ColorStateList mIconColorList;

    private UiConfig mUiConfig;
    private int mWideDisplayStartOffsetPx;
    private int mModernNavButtonStartOffsetPx;
    private int mModernToolbarActionMenuEndOffsetPx;
    private int mModernToolbarSearchIconOffsetPx;

    private boolean mIsDestroyed;

    private boolean mShowInfoIcon;
    private int mShowInfoStringId;
    private int mHideInfoStringId;

    // current view type that SelectableListToolbar is showing
    private int mViewType;

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

    /** Destroys and cleans up itself. */
    @CallSuper
    public void destroy() {
        mIsDestroyed = true;
        if (mSelectionDelegate != null) mSelectionDelegate.removeObserver(this);
        if (mSearchEditText != null) hideKeyboard();
    }

    /**
     * @see {@link #initialize(SelectionDelegate, int, int, int, boolean, int, boolean)}.
     */
    public void initialize(
            SelectionDelegate<E> delegate,
            int titleResId,
            int normalGroupResId,
            int selectedGroupResId,
            boolean updateStatusBarColor) {
        initialize(
                delegate,
                titleResId,
                normalGroupResId,
                selectedGroupResId,
                updateStatusBarColor,
                /* menuResId= */ 0,
                /* showBackInNormalView= */ false);
    }

    /**
     * Initializes the SelectionToolbar.
     *
     * @param delegate The SelectionDelegate that will inform the toolbar of selection changes.
     * @param titleResId The resource id of the title string. May be 0 if this class shouldn't set
     *     set a title when the selection is cleared.
     * @param normalGroupResId The resource id of the menu group to show when a selection isn't
     *     established.
     * @param selectedGroupResId The resource id of the menu item to show when a selection is
     *     established.
     * @param updateStatusBarColor Whether the status bar color should be updated to match the
     *     toolbar color. If true, the status bar will only be updated if the current device fully
     *     supports theming and is on Android M+.
     * @param menuResId The resource id of the menu. {@code 0} if not required.
     * @param showBackInNormalView Whether the back button should be shown in normal view.
     */
    public void initialize(
            SelectionDelegate<E> delegate,
            @StringRes int titleResId,
            @IdRes int normalGroupResId,
            @IdRes int selectedGroupResId,
            boolean updateStatusBarColor,
            @MenuRes int menuResId,
            boolean showBackInNormalView) {
        mTitleResId = titleResId;
        if (menuResId != Resources.ID_NULL) inflateMenu(menuResId);
        mNormalGroupResId = normalGroupResId;
        mSelectedGroupResId = selectedGroupResId;
        // TODO(twellington): Setting the status bar color crashes on Nokia devices. Re-enable
        // after a Nokia test device is procured and the crash can be debugged.
        // See https://crbug.com/880694.
        mUpdateStatusBarColor = false;

        mSelectionDelegate = delegate;
        mSelectionDelegate.addObserver(this);

        mModernNavButtonStartOffsetPx =
                getResources()
                        .getDimensionPixelSize(
                                R.dimen.selectable_list_toolbar_nav_button_start_offset);
        mModernToolbarActionMenuEndOffsetPx =
                getResources()
                        .getDimensionPixelSize(R.dimen.selectable_list_action_bar_end_padding);
        mModernToolbarSearchIconOffsetPx =
                getResources()
                        .getDimensionPixelSize(R.dimen.selectable_list_search_icon_end_padding);

        mNormalBackgroundColor = SemanticColorUtils.getDefaultBgColor(getContext());
        setBackgroundColor(mNormalBackgroundColor);

        mIconColorList =
                AppCompatResources.getColorStateList(
                        getContext(), R.color.default_icon_color_tint_list);

        setTitleTextAppearance(getContext(), R.style.TextAppearance_Headline_Primary);
        if (mTitleResId != 0) setTitle(mTitleResId);

        mMenuButton =
                UiUtils.getTintedDrawable(
                        getContext(),
                        R.drawable.ic_more_vert_24dp,
                        R.color.default_icon_color_secondary_tint_list);
        setOverflowIcon(mMenuButton);
        mNavigationIconDrawable =
                UiUtils.getTintedDrawable(
                        getContext(),
                        R.drawable.ic_arrow_back_white_24dp,
                        R.color.default_icon_color_tint_list);

        mShowInfoIcon = true;
        mShowInfoStringId = R.string.show_info;
        mHideInfoStringId = R.string.hide_info;

        if (showBackInNormalView) {
            mShowBackInNormalView = true;
            setNavigationButton(NavigationButton.NORMAL_VIEW_BACK);
        }
    }

    /**
     * Inflates and initializes the search view.
     *
     * @param searchDelegate The delegate that will handle performing searches.
     * @param hintStringResId The hint text to show in the search view's EditText box.
     * @param searchMenuItemId The menu item used to activate the search view. This item will be
     *     hidden when selection is enabled or if the list of selectable items associated with this
     *     toolbar is empty.
     */
    public void initializeSearchView(
            SearchDelegate searchDelegate,
            @StringRes int hintStringResId,
            @IdRes int searchMenuItemId) {
        mHasSearchView = true;
        mSearchDelegate = searchDelegate;
        mSearchMenuItemId = searchMenuItemId;
        mSearchBackgroundColor = Color.WHITE;

        LayoutInflater.from(getContext()).inflate(R.layout.search_toolbar, this);

        mSearchView = findViewById(R.id.search_view);
        mSearchEditText = mSearchView.findViewById(R.id.search_text);
        mSearchEditText.setHint(hintStringResId);
        mSearchEditText.setOnEditorActionListener(this);
        mSearchEditText.addTextChangedListener(
                new EmptyTextWatcher() {
                    @Override
                    public void onTextChanged(CharSequence s, int start, int before, int count) {
                        mClearTextButton.setVisibility(
                                TextUtils.isEmpty(s) ? View.INVISIBLE : View.VISIBLE);
                        if (isSearching()) mSearchDelegate.onSearchTextChanged(s.toString());
                    }
                });

        mClearTextButton = findViewById(R.id.clear_text_button);
        mClearTextButton.setOnClickListener(v -> mSearchEditText.setText(""));
    }

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

        LayoutInflater.from(getContext()).inflate(R.layout.number_roll_view, this);
        mNumberRollView = findViewById(R.id.selection_mode_number);
        mNumberRollView.setString(R.plurals.selected_items);
        mNumberRollView.setStringForZero(R.string.select_items);
    }

    @Override
    @CallSuper
    public void onSelectionStateChange(List<E> selectedItems) {
        boolean wasSelectionEnabled = mIsSelectionEnabled;
        mIsSelectionEnabled = mSelectionDelegate.isSelectionEnabled();

        // If onSelectionStateChange() gets called before onFinishInflate(), mNumberRollView
        // will be uninitialized. See crbug.com/637948.
        if (mNumberRollView == null) {
            mNumberRollView = findViewById(R.id.selection_mode_number);
        }

        if (mIsSelectionEnabled) {
            showSelectionView(selectedItems, wasSelectionEnabled);
        } else if (isSearching()) {
            showSearchViewInternal();
        } else {
            showNormalView();
        }

        if (mIsSelectionEnabled) {
            @StringRes
            int resId =
                    wasSelectionEnabled
                            ? R.string.accessibility_toolbar_multi_select
                            : R.string.accessibility_toolbar_screen_position;
            announceForAccessibility(
                    getContext().getString(resId, Integer.toString(selectedItems.size())));
        }
    }

    @Override
    public void onClick(View view) {
        if (mIsDestroyed) return;

        switch (mNavigationButton) {
            case NavigationButton.NONE:
                break;
            case NavigationButton.SEARCH_BACK:
                onSearchNavigationBack();
                break;
            case NavigationButton.SELECTION_BACK:
                mSelectionDelegate.clearSelection();
                break;
            case NavigationButton.NORMAL_VIEW_BACK:
                onNavigationBack();
                break;
            default:
                assert false : "Incorrect navigation button state";
        }
    }

    /**
     * Handle a click on the search navigation back button. If this toolbar has a search view, the
     * search view will be hidden.
     */
    public void onSearchNavigationBack() {
        if (!mHasSearchView || !isSearching()) return;

        hideSearchView();
    }

    /**
     * Handle a click on the normal view back button. Can be overridden to give behavior to the back
     * button on the normal view.
     */
    protected void onNavigationBack() {}

    /**
     * Update the current navigation button (the top-left icon on LTR)
     *
     * @param navigationButton one of NAVIGATION_BUTTON_* constants.
     */
    protected void setNavigationButton(@NavigationButton int navigationButton) {
        @StringRes int contentDescriptionId = Resources.ID_NULL;

        mNavigationButton = navigationButton;
        setNavigationOnClickListener(this);

        switch (mNavigationButton) {
            case NavigationButton.NONE:
                break;
            case NavigationButton.SEARCH_BACK:
                DrawableCompat.setTintList(mNavigationIconDrawable, mIconColorList);
                contentDescriptionId = R.string.accessibility_toolbar_btn_back;
                break;
            case NavigationButton.SELECTION_BACK:
                DrawableCompat.setTintList(mNavigationIconDrawable, mIconColorList);
                contentDescriptionId = R.string.accessibility_cancel_selection;
                break;
            case NavigationButton.NORMAL_VIEW_BACK:
                DrawableCompat.setTintList(
                        mNavigationIconDrawable,
                        AppCompatResources.getColorStateList(
                                getContext(), R.color.default_icon_color_secondary_tint_list));
                contentDescriptionId = R.string.accessibility_toolbar_btn_back;
                break;
            default:
                assert false : "Incorrect navigationButton argument";
        }

        setNavigationIcon(
                contentDescriptionId == Resources.ID_NULL ? null : mNavigationIconDrawable);
        setNavigationContentDescription(contentDescriptionId);

        updateDisplayStyleIfNecessary();
    }

    /** Shows the search edit text box and related views. */
    public void showSearchView(boolean showKeyboard) {
        assert mHasSearchView;

        mIsSearchingSupplier.set(true);
        mSelectionDelegate.clearSelection();

        showSearchViewInternal();

        mSearchEditText.requestFocus();
        if (showKeyboard) {
            KeyboardVisibilityDelegate.getInstance().showKeyboard(mSearchEditText);
        }

        setTitle(null);
    }

    /** Hides the search edit text box and related views. Notifies delegate of the change. */
    public void hideSearchView() {
        hideSearchView(/* notifyDelegate= */ true);
    }

    /**
     * Hides the search edit text box and related views.
     * @param notifyDelegate Whether to notify the delegate of this change.
     */
    public void hideSearchView(boolean notifyDelegate) {
        assert mHasSearchView;

        if (!isSearching()) return;

        mIsSearchingSupplier.set(false);
        mSearchEditText.setText("");
        hideKeyboard();
        showNormalView();

        if (notifyDelegate) mSearchDelegate.onEndSearch();
    }

    /**
     * Called to enable/disable search menu button.
     * @param searchEnabled Whether the search button should be enabled.
     */
    public void setSearchEnabled(boolean searchEnabled) {
        if (mHasSearchView) {
            mSearchEnabled = searchEnabled;
            updateSearchMenuItem();
        }
    }

    @Override
    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
        if (actionId == EditorInfo.IME_ACTION_SEARCH) {
            KeyboardVisibilityDelegate.getInstance().hideKeyboard(v);
        }
        return false;
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        if (mIsDestroyed) return;

        if (mSelectionDelegate != null) mSelectionDelegate.clearSelection();
        if (isSearching()) hideSearchView();
    }

    /**
     * When the toolbar has a wide display style, its contents will be width constrained to
     * {@link UiConfig#WIDE_DISPLAY_STYLE_MIN_WIDTH_DP}. If the current screen width is greater than
     * UiConfig#WIDE_DISPLAY_STYLE_MIN_WIDTH_DP, the toolbar contents will be visually centered by
     * adding padding to both sides.
     *
     * @param uiConfig The UiConfig used to observe display style changes.
     */
    public void configureWideDisplayStyle(UiConfig uiConfig) {
        mWideDisplayStartOffsetPx =
                getResources().getDimensionPixelSize(R.dimen.toolbar_wide_display_start_offset);

        mUiConfig = uiConfig;
        mUiConfig.addObserver(this);
    }

    @Override
    public void onDisplayStyleChanged(UiConfig.DisplayStyle newDisplayStyle) {
        int padding =
                SelectableListLayout.getPaddingForDisplayStyle(newDisplayStyle, getResources());
        int paddingStartOffset = 0;
        boolean isSearchViewShowing = isSearching() && !mIsSelectionEnabled;
        MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();

        if (newDisplayStyle.horizontal == HorizontalDisplayStyle.WIDE
                && !(isSearching()
                        || mIsSelectionEnabled
                        || mNavigationButton != NavigationButton.NONE)) {
            // The title in the wide display should be aligned with the texts of the list elements.
            paddingStartOffset = mWideDisplayStartOffsetPx;
        }

        // The margin instead of padding will be set to adjust the modern search view background
        // in search mode.
        if (newDisplayStyle.horizontal == HorizontalDisplayStyle.WIDE && isSearchViewShowing) {
            params.setMargins(padding, params.topMargin, padding, params.bottomMargin);
            padding = 0;
        } else {
            params.setMargins(0, params.topMargin, 0, params.bottomMargin);
        }
        setLayoutParams(params);

        // Navigation button should have more start padding in order to keep the navigation icon
        // and the list item icon aligned.
        int navigationButtonStartOffsetPx =
                mNavigationButton != NavigationButton.NONE ? mModernNavButtonStartOffsetPx : 0;

        int actionMenuBarEndOffsetPx =
                mIsSelectionEnabled
                        ? mModernToolbarActionMenuEndOffsetPx
                        : mModernToolbarSearchIconOffsetPx;

        this.setPaddingRelative(
                padding + paddingStartOffset + navigationButtonStartOffsetPx,
                this.getPaddingTop(),
                padding + actionMenuBarEndOffsetPx,
                this.getPaddingBottom());
    }

    /**
     * @return Whether search mode is currently active. Once a search is started, this method will
     *         return true until the search is ended regardless of whether the toolbar view changes
     *         dues to a selection.
     */
    public boolean isSearching() {
        assert mIsSearchingSupplier.get() != null : "Supplier is not correctly initialized.";
        return mIsSearchingSupplier.get();
    }

    public ObservableSupplier<Boolean> isSearchingSupplier() {
        return mIsSearchingSupplier;
    }

    SelectionDelegate<E> getSelectionDelegate() {
        return mSelectionDelegate;
    }

    protected void showNormalView() {
        // hide overflow menu explicitly: crbug.com/999269
        hideOverflowMenu();

        mViewType = ViewType.NORMAL_VIEW;
        getMenu().setGroupVisible(mNormalGroupResId, true);
        getMenu().setGroupVisible(mSelectedGroupResId, false);
        if (mHasSearchView) {
            mSearchView.setVisibility(View.GONE);
            updateSearchMenuItem();
        }
        if (mShowBackInNormalView) {
            setNavigationButton(NavigationButton.NORMAL_VIEW_BACK);
        } else {
            setNavigationButton(NavigationButton.NONE);
        }
        setBackgroundColor(mNormalBackgroundColor);
        if (mTitleResId != 0) setTitle(mTitleResId);

        mNumberRollView.setVisibility(View.GONE);
        mNumberRollView.setNumber(0, false);

        updateDisplayStyleIfNecessary();
    }

    protected void showSelectionView(List<E> selectedItems, boolean wasSelectionEnabled) {
        mViewType = ViewType.SELECTION_VIEW;

        getMenu().setGroupVisible(mNormalGroupResId, false);
        getMenu().setGroupVisible(mSelectedGroupResId, true);
        getMenu().setGroupEnabled(mSelectedGroupResId, !selectedItems.isEmpty());
        if (mHasSearchView) mSearchView.setVisibility(View.GONE);

        setNavigationButton(NavigationButton.SELECTION_BACK);
        setBackgroundColor(mNormalBackgroundColor);

        switchToNumberRollView(selectedItems, wasSelectionEnabled);

        if (isSearching()) hideKeyboard();

        updateDisplayStyleIfNecessary();
    }

    private void showSearchViewInternal() {
        mViewType = ViewType.SEARCH_VIEW;

        getMenu().setGroupVisible(mNormalGroupResId, false);
        getMenu().setGroupVisible(mSelectedGroupResId, false);
        mNumberRollView.setVisibility(View.GONE);
        mSearchView.setVisibility(View.VISIBLE);

        setNavigationButton(NavigationButton.SEARCH_BACK);
        setBackgroundResource(R.drawable.search_toolbar_modern_bg);
        updateStatusBarColor(mSearchBackgroundColor);

        updateDisplayStyleIfNecessary();
    }

    private void updateSearchMenuItem() {
        if (!mHasSearchView) return;
        MenuItem searchMenuItem = getMenu().findItem(mSearchMenuItemId);
        if (searchMenuItem != null) {
            searchMenuItem.setVisible(mSearchEnabled && !mIsSelectionEnabled && !isSearching());
        }
    }

    protected void switchToNumberRollView(List<E> selectedItems, boolean wasSelectionEnabled) {
        setTitle(null);
        mNumberRollView.setVisibility(View.VISIBLE);
        if (!wasSelectionEnabled) mNumberRollView.setNumber(0, false);
        mNumberRollView.setNumber(selectedItems.size(), true);
    }

    private void updateDisplayStyleIfNecessary() {
        if (mUiConfig != null) onDisplayStyleChanged(mUiConfig.getCurrentDisplayStyle());
    }

    /**
     * Set info menu item used to toggle info header.
     *
     * @param infoMenuItemId The menu item to show or hide information.
     */
    public void setInfoMenuItem(@IdRes int infoMenuItemId) {
        mInfoMenuItemId = infoMenuItemId;
    }

    /**
     * Update icon, title, and visibility of info menu item.
     *
     * @param showItem Whether or not info menu item should show.
     * @param infoShowing Whether or not info header is currently showing.
     */
    public void updateInfoMenuItem(boolean showItem, boolean infoShowing) {
        MenuItem infoMenuItem = getMenu().findItem(mInfoMenuItemId);
        if (infoMenuItem != null) {
            if (mShowInfoIcon) {
                Drawable iconDrawable =
                        TintedDrawable.constructTintedDrawable(
                                getContext(),
                                R.drawable.btn_info,
                                infoShowing
                                        ? R.color.default_icon_color_accent1_tint_list
                                        : R.color.default_icon_color_secondary_tint_list);

                infoMenuItem.setIcon(iconDrawable);
            }

            infoMenuItem.setTitle(infoShowing ? mHideInfoStringId : mShowInfoStringId);
            infoMenuItem.setVisible(showItem);
        }
    }

    /** Hides the keyboard. */
    public void hideKeyboard() {
        KeyboardVisibilityDelegate.getInstance().hideKeyboard(mSearchEditText);
    }

    @Override
    public void setTitle(CharSequence title) {
        super.setTitle(title);

        // The super class adds an AppCompatTextView for the title which not focusable by default.
        // Set TextView children to focusable so the title can gain focus in accessibility mode.
        makeTextViewChildrenAccessible();
    }

    @Override
    public void setBackgroundColor(@ColorInt int color) {
        super.setBackgroundColor(color);

        updateStatusBarColor(color);
    }

    private void updateStatusBarColor(@ColorInt int color) {
        if (!mUpdateStatusBarColor) return;

        Context context = getContext();
        if (!(context instanceof Activity)) return;

        Window window = ((Activity) context).getWindow();
        UiUtils.setStatusBarColor(window, color);
        UiUtils.setStatusBarIconColor(
                window.getDecorView().getRootView(),
                !ColorUtils.shouldUseLightForegroundOnBackground(color));
    }

    public View getSearchViewForTests() {
        return mSearchView;
    }

    public @NavigationButton int getNavigationButtonForTests() {
        return mNavigationButton;
    }

    /** Ends any in-progress animations. */
    public void endAnimationsForTesting() {
        mNumberRollView.endAnimationsForTesting();
    }

    private void makeTextViewChildrenAccessible() {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (!(child instanceof TextView)) continue;
            child.setFocusable(true);

            // setFocusableInTouchMode is problematic for buttons, see
            // https://crbug.com/813422.
            if ((child instanceof Button)) continue;
            child.setFocusableInTouchMode(true);
        }
    }

    @VisibleForTesting
    public @ViewType int getCurrentViewType() {
        return mViewType;
    }
}