chromium/chrome/android/java/src/org/chromium/chrome/browser/findinpage/FindToolbar.java

// Copyright 2015 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.findinpage;

import android.animation.Animator;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Vibrator;
import android.provider.Settings;
import android.text.InputType;
import android.text.Selection;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.core.view.inputmethod.EditorInfoCompat;

import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.browser_ui.widget.text.VerticallyFixedEditText;
import org.chromium.components.find_in_page.FindInPageBridge;
import org.chromium.components.find_in_page.FindMatchRectsDetails;
import org.chromium.components.find_in_page.FindNotificationDetails;
import org.chromium.components.find_in_page.FindResultBar;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.text.EmptyTextWatcher;
import org.chromium.url.GURL;

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

/** A toolbar providing find in page functionality. */
public class FindToolbar extends LinearLayout implements BackPressHandler {
    private static final long ACCESSIBLE_ANNOUNCEMENT_DELAY_MILLIS = 500;

    @IntDef({
        FindLocationBarState.SHOWN,
        FindLocationBarState.SHOWING,
        FindLocationBarState.HIDDEN,
        FindLocationBarState.HIDING
    })
    @Retention(RetentionPolicy.SOURCE)
    private @interface FindLocationBarState {
        int SHOWN = 0;
        int SHOWING = 1;
        int HIDDEN = 2;
        int HIDING = 3;
    }

    // Toolbar UI
    private TextView mFindStatus;
    protected FindQuery mFindQuery;
    protected ImageButton mCloseFindButton;
    protected ImageButton mFindPrevButton;
    protected ImageButton mFindNextButton;
    protected View mDivider;

    private FindResultBar mResultBar;

    private TabModelSelector mTabModelSelector;
    private final TabModelSelectorObserver mTabModelSelectorObserver;
    private final TabModelObserver mTabModelObserver;
    private Tab mCurrentTab;
    private final TabObserver mTabObserver;
    private WindowAndroid mWindowAndroid;
    private FindInPageBridge mFindInPageBridge;
    private FindToolbarObserver mObserver;

    /** Most recently entered search text (globally, in non-incognito tabs). */
    private String mLastUserSearch = "";

    /** Whether toolbar text is being set automatically (not typed by user). */
    private boolean mSettingFindTextProgrammatically;

    /** Whether the search key should trigger a new search. */
    private boolean mSearchKeyShouldTriggerSearch;

    private @FindLocationBarState int mCurrentState = FindLocationBarState.HIDDEN;
    private @FindLocationBarState int mDesiredState = FindLocationBarState.HIDDEN;

    private Handler mHandler = new Handler();
    private Runnable mAccessibleAnnouncementRunnable;
    private boolean mAccessibilityDidActivateResult;
    private final ObservableSupplierImpl<Boolean> mBackPressStateSupplier =
            new ObservableSupplierImpl<>();

    /** Subclasses EditText in order to intercept BACK key presses. */
    @SuppressLint("Instantiatable")
    static class FindQuery extends VerticallyFixedEditText {
        private FindToolbar mFindToolbar;

        public FindQuery(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        void setFindToolbar(FindToolbar findToolbar) {
            mFindToolbar = findToolbar;
        }

        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_ENTER
                    || keyCode == KeyEvent.KEYCODE_F3
                    || (keyCode == KeyEvent.KEYCODE_G && event.isCtrlPressed())) {
                mFindToolbar.hideKeyboardAndStartFinding(!event.isShiftPressed());
                return true;
            }
            if (keyCode == KeyEvent.KEYCODE_ESCAPE && event.hasNoModifiers()) {
                mFindToolbar.deactivate();
                return true;
            }
            return super.onKeyDown(keyCode, event);
        }

        @Override
        public boolean onTextContextMenuItem(int id) {
            if (id == android.R.id.paste) {
                ClipboardManager clipboard =
                        (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
                ClipData clipData = clipboard.getPrimaryClip();
                if (clipData != null) {
                    // Convert the clip data to a simple string
                    StringBuilder builder = new StringBuilder();
                    for (int i = 0; i < clipData.getItemCount(); i++) {
                        builder.append(clipData.getItemAt(i).coerceToText(getContext()));
                    }

                    // Identify how much of the original text should be replaced
                    int min = 0;
                    int max = getText().length();

                    if (isFocused()) {
                        final int selStart = getSelectionStart();
                        final int selEnd = getSelectionEnd();

                        min = Math.max(0, Math.min(selStart, selEnd));
                        max = Math.max(0, Math.max(selStart, selEnd));
                    }

                    Selection.setSelection(getText(), max);
                    getText().replace(min, max, builder.toString());
                    return true;
                }
            }
            return super.onTextContextMenuItem(id);
        }

        @Override
        public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
            InputConnection connection = super.onCreateInputConnection(outAttrs);
            if (mFindToolbar.isIncognito()) {
                outAttrs.imeOptions |= EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING;
            }
            return connection;
        }
    }

    public FindToolbar(Context context, AttributeSet attrs) {
        super(context, attrs);

        mTabObserver =
                new EmptyTabObserver() {
                    @Override
                    public void onActivityAttachmentChanged(
                            Tab tab, @Nullable WindowAndroid window) {
                        if (window == null && getVisibility() == View.VISIBLE) {
                            deactivate(/* clearSelection= */ true);
                        }
                    }

                    @Override
                    public void onPageLoadStarted(Tab tab, GURL url) {
                        deactivate();
                    }

                    @Override
                    public void onContentChanged(Tab tab) {
                        deactivate();
                    }

                    @Override
                    public void onClosingStateChanged(Tab tab, boolean closing) {
                        if (closing) deactivate();
                    }

                    @Override
                    public void onFindResultAvailable(FindNotificationDetails result) {
                        onFindResult(result);
                    }

                    @Override
                    public void onFindMatchRectsAvailable(FindMatchRectsDetails result) {
                        onFindMatchRects(result);
                    }
                };

        mTabModelSelectorObserver =
                new TabModelSelectorObserver() {
                    @Override
                    public void onTabModelSelected(TabModel newModel, TabModel oldModel) {
                        deactivate();
                        updateVisualsForTabModel(isIncognito());
                    }
                };

        mTabModelObserver =
                new TabModelObserver() {
                    @Override
                    public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) {
                        if (tab != mCurrentTab) deactivate();
                    }

                    @Override
                    public void tabRemoved(Tab tab) {
                        if (tab != mCurrentTab) return;
                        deactivate();
                    }
                };
    }

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

        setOrientation(HORIZONTAL);
        setGravity(Gravity.CENTER_VERTICAL);

        mFindQuery = findViewById(R.id.find_query);
        mFindQuery.setFindToolbar(this);
        mFindQuery.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_FILTER);
        mFindQuery.setSelectAllOnFocus(true);
        mFindQuery.setOnFocusChangeListener(
                new View.OnFocusChangeListener() {
                    @Override
                    public void onFocusChange(View v, boolean hasFocus) {
                        mAccessibilityDidActivateResult = false;
                        if (!hasFocus) {
                            if (mFindQuery.getText().length() > 0) {
                                mSearchKeyShouldTriggerSearch = true;
                            }
                            mWindowAndroid.getKeyboardDelegate().hideKeyboard(mFindQuery);
                        }
                    }
                });
        mFindQuery.addTextChangedListener(
                new EmptyTextWatcher() {
                    @Override
                    public void onTextChanged(CharSequence s, int start, int before, int count) {
                        if (mFindInPageBridge == null) return;

                        mAccessibilityDidActivateResult = false;

                        if (mSettingFindTextProgrammatically) return;

                        // If we're called during onRestoreInstanceState() the current
                        // view won't have been set yet. TODO(husky): Find a better fix.
                        assert mCurrentTab != null;
                        assert mCurrentTab.getWebContents() != null;
                        if (mCurrentTab.getWebContents() == null) return;

                        if (s.length() > 0) {
                            // Don't clearResults() as that would cause flicker.
                            // Just wait until onFindResultReceived updates it.
                            mSearchKeyShouldTriggerSearch = false;
                            mFindInPageBridge.startFinding(s.toString(), true, false);
                        } else {
                            clearResults();
                            mFindInPageBridge.stopFinding(true);
                            setPrevNextEnabled(false);
                        }

                        if (!isIncognito()) {
                            mLastUserSearch = s.toString();
                        }
                    }
                });
        mFindQuery.setOnEditorActionListener(
                new TextView.OnEditorActionListener() {
                    @Override
                    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                        if (event != null && event.getAction() == KeyEvent.ACTION_UP) return false;

                        if (mFindInPageBridge == null) return false;

                        // Only trigger a new find if the text was set programmatically.
                        // Otherwise just revisit the current active match.
                        if (mSearchKeyShouldTriggerSearch) {
                            mSearchKeyShouldTriggerSearch = false;
                            hideKeyboardAndStartFinding(true);
                        } else {
                            mWindowAndroid.getKeyboardDelegate().hideKeyboard(mFindQuery);
                            mFindInPageBridge.activateFindInPageResultForAccessibility();
                            mAccessibilityDidActivateResult = true;
                        }
                        return true;
                    }
                });

        mFindStatus = findViewById(R.id.find_status);
        setStatus("", false);

        mFindPrevButton = findViewById(R.id.find_prev_button);
        mFindPrevButton.setOnClickListener(
                new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        hideKeyboardAndStartFinding(false);
                    }
                });

        mFindNextButton = findViewById(R.id.find_next_button);
        mFindNextButton.setOnClickListener(
                new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        hideKeyboardAndStartFinding(true);
                    }
                });

        setPrevNextEnabled(false);

        mCloseFindButton = findViewById(R.id.close_find_button);
        mCloseFindButton.setOnClickListener(
                new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        deactivate();
                    }
                });

        mDivider = findViewById(R.id.find_separator);
    }

    @Override
    public @BackPressResult int handleBackPress() {
        int result =
                shouldDeactivateByBackPress() ? BackPressResult.SUCCESS : BackPressResult.FAILURE;
        deactivate();
        return result;
    }

    @Override
    public ObservableSupplier<Boolean> getHandleBackPressChangedSupplier() {
        return mBackPressStateSupplier;
    }

    // Overridden by subclasses.
    protected void findResultSelected(Rect rect) {}

    private void hideKeyboardAndStartFinding(boolean forward) {
        if (mFindInPageBridge == null) return;

        final String findQuery = mFindQuery.getText().toString();
        if (findQuery.length() == 0) return;

        mWindowAndroid.getKeyboardDelegate().hideKeyboard(mFindQuery);
        mFindInPageBridge.startFinding(findQuery, forward, false);
        mFindInPageBridge.activateFindInPageResultForAccessibility();
        mAccessibilityDidActivateResult = true;
    }

    private boolean mShowKeyboardOnceWindowIsFocused;

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        if (mShowKeyboardOnceWindowIsFocused) {
            mShowKeyboardOnceWindowIsFocused = false;
            // See showKeyboard() for explanation.
            // By this point we've already waited till the window regains focus
            // from the options menu, but we still need to use postDelayed with
            // a zero wait time to delay until all the side-effects are complete
            // (e.g. becoming the target of the Input Method).
            mHandler.postDelayed(
                    new Runnable() {
                        @Override
                        public void run() {
                            showKeyboard();

                            // This is also a great time to set accessibility focus to the query box
                            // -
                            // this also fails if we don't wait until the window regains focus.
                            // Sending a HOVER_ENTER event before the ACCESSIBILITY_FOCUSED event
                            // is a widely-used hack to force TalkBack to move accessibility focus
                            // to a view, which is discouraged in general but reasonable in this
                            // case.
                            mFindQuery.sendAccessibilityEvent(
                                    AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
                            mFindQuery.sendAccessibilityEvent(
                                    AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                        }
                    },
                    0);
        }
    }

    private void onFindMatchRects(FindMatchRectsDetails matchRects) {
        if (mResultBar == null) return;
        if (mFindQuery.getText().length() > 0) {
            mResultBar.setMatchRects(matchRects.version, matchRects.rects, matchRects.activeRect);
        } else {
            // Since we don't issue a request for an empty string we never get a 'no rects' response
            // in that case. This could cause us to display stale state if the user is deleting the
            // search string. If the response for the last character comes in after we've issued a
            // clearReslts in TextChangedListener that response will be accepted and we will end up
            // showing stale results for an empty query.
            // Sending an empty string message seems a bit wasteful, so instead we simply ignore all
            // results that come in if the query is empty.
            mResultBar.clearMatchRects();
        }
    }

    private void onFindResult(FindNotificationDetails result) {
        if (mResultBar != null) mResultBar.onFindResult();

        assert mFindInPageBridge != null;

        if ((result.activeMatchOrdinal == -1 || result.numberOfMatches == 1)
                && !result.finalUpdate) {
            // Wait until activeMatchOrdinal has been determined (is no longer
            // -1) before showing counts. Additionally, to reduce flicker,
            // ignore short-lived interim notifications with numberOfMatches set
            // to 1, which are sent as soon as something has been found (see bug
            // 894389 and FindBarController::UpdateFindBarForCurrentResult).
            // Instead wait until the scoping effort starts returning real
            // match counts (or the search actually finishes with 1 result).
            // This also protects against receiving bogus rendererSelectionRects
            // at the start (see below for why we can't filter them out).
            return;
        }

        if (result.finalUpdate) {
            if (result.numberOfMatches > 0) {
                // TODO(johnme): Don't wait till end of find, stream rects live!
                mFindInPageBridge.requestFindMatchRects(
                        mResultBar != null ? mResultBar.getRectsVersion() : -1);
            } else {
                clearResults();
            }

            findResultSelected(result.rendererSelectionRect);
        }

        // Even though we wait above until activeMatchOrdinal is no longer -1,
        // it's possible for it to still be -1 (unknown) in the final find
        // notification. This happens very rarely, e.g. if the m_activeMatch
        // found by WebFrameImpl::find has been removed from the DOM by the time
        // WebFrameImpl::scopeStringMatches tries to find the ordinal of the
        // active match (while counting the matches), as in b/4147049. In such
        // cases it looks less broken to show 0 instead of -1 (as desktop does).
        Context context = getContext();
        String text =
                context.getResources()
                        .getString(
                                R.string.find_in_page_count,
                                Math.max(result.activeMatchOrdinal, 0),
                                result.numberOfMatches);
        setStatus(text, result.numberOfMatches == 0);

        setPrevNextEnabled(result.numberOfMatches > 0);

        // The accessible version will be something like "Result 1 of 9".
        String accessibleText =
                getAccessibleStatusText(
                        Math.max(result.activeMatchOrdinal, 0), result.numberOfMatches);
        mFindStatus.setContentDescription(accessibleText);
        announceStatusForAccessibility(accessibleText);

        // Vibrate when no results are found, unless you're just deleting chars.
        if (result.numberOfMatches == 0
                && result.finalUpdate
                && !mFindInPageBridge
                        .getPreviousFindText()
                        .startsWith(mFindQuery.getText().toString())) {
            final boolean hapticFeedbackEnabled =
                    Settings.System.getInt(
                                    context.getContentResolver(),
                                    Settings.System.HAPTIC_FEEDBACK_ENABLED,
                                    1)
                            == 1;
            if (hapticFeedbackEnabled) {
                Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
                final long noResultsVibrateDurationMs = 50;
                v.vibrate(noResultsVibrateDurationMs);
            }
        }
    }

    private String getAccessibleStatusText(int activeMatchOrdinal, int numberOfMatches) {
        Context context = getContext();
        return (numberOfMatches > 0)
                ? context.getResources()
                        .getString(
                                R.string.accessible_find_in_page_count,
                                activeMatchOrdinal,
                                numberOfMatches)
                : context.getResources().getString(R.string.accessible_find_in_page_no_results);
    }

    private void announceStatusForAccessibility(final String announcementText) {
        // Don't announce if the user has already activated a result by pressing Enter/Search
        // or clicking on the Next/Previous buttons.
        if (mAccessibilityDidActivateResult) return;

        // Delay the announcement briefly, and if any additional announcements come in,
        // have them preempt the previous queued one. That makes for a better user experience
        // than speaking instantly as you're typing and constantly interrupting itself.

        if (mAccessibleAnnouncementRunnable != null) {
            mHandler.removeCallbacks(mAccessibleAnnouncementRunnable);
        }

        mAccessibleAnnouncementRunnable =
                new Runnable() {
                    @Override
                    public void run() {
                        mFindQuery.announceForAccessibility(announcementText);
                    }
                };
        mHandler.postDelayed(mAccessibleAnnouncementRunnable, ACCESSIBLE_ANNOUNCEMENT_DELAY_MILLIS);
    }

    /** The find toolbar's container must provide access to its TabModel. */
    public void setTabModelSelector(TabModelSelector modelSelector) {
        mTabModelSelector = modelSelector;
        updateVisualsForTabModel(isIncognito());
    }

    /** Sets the WindowAndroid in which the find toolbar will be shown. Needed for animations. */
    public void setWindowAndroid(WindowAndroid windowAndroid) {
        mWindowAndroid = windowAndroid;
    }

    /**
     * Handles updating any visual elements of the find toolbar based on changes to the tab model.
     * @param isIncognito Whether the current tab model is incognito or not.
     */
    protected void updateVisualsForTabModel(boolean isIncognito) {}

    /**
     * Sets a custom ActionMode.Callback instance to the FindQuery.  This lets us
     * get notified when the user tries to do copy, paste, etc. on the FindQuery.
     * @param callback The ActionMode.Callback instance to be notified when selection ActionMode
     * is triggered.
     */
    public void setActionModeCallbackForTextEdit(ActionMode.Callback callback) {
        mFindQuery.setCustomSelectionActionModeCallback(callback);
    }

    /** Sets the observer to be notified of changes to the find toolbar. */
    protected void setObserver(FindToolbarObserver observer) {
        mObserver = observer;
    }

    /** Checks to see if a WebContents is available to hook into. */
    protected boolean isWebContentAvailable() {
        Tab currentTab = mTabModelSelector.getCurrentTab();
        return currentTab != null
                && currentTab.getWebContents() != null
                && !currentTab.isNativePage();
    }

    /**
     * Initializes the find toolbar. Should be called just after the find toolbar is shown.
     * If the toolbar is already showing, this just focuses the toolbar.
     */
    public final void activate() {
        ThreadUtils.checkUiThread();
        if (!isWebContentAvailable()) return;

        if (mCurrentState == FindLocationBarState.SHOWN) {
            requestQueryFocus();
            return;
        }

        mDesiredState = FindLocationBarState.SHOWN;
        if (mCurrentState != FindLocationBarState.HIDDEN) return;
        setCurrentState(FindLocationBarState.SHOWING);
        handleActivate();
    }

    /** Logic for handling the activation of the find toolbar. */
    protected void handleActivate() {
        mTabModelSelector.addObserver(mTabModelSelectorObserver);
        for (TabModel model : mTabModelSelector.getModels()) {
            model.addObserver(mTabModelObserver);
        }
        mCurrentTab = mTabModelSelector.getCurrentTab();
        mCurrentTab.addObserver(mTabObserver);
        mFindInPageBridge = new FindInPageBridge(mCurrentTab.getWebContents());
        initializeFindText();
        mFindQuery.requestFocus();
        // The keyboard doesn't show itself automatically.
        showKeyboard();
        // Always show the bar to make the FindToolbar more distinct from the Omnibox.
        setResultsBarVisibility(true);
        updateVisualsForTabModel(isIncognito());

        setCurrentState(FindLocationBarState.SHOWN);
    }

    /**
     * Call this just before closing the find toolbar. The selection on the page will be cleared.
     */
    public final void deactivate() {
        deactivate(true);
    }

    /**
     * Call this just before closing the find toolbar.
     * @param clearSelection Whether the selection on the page should be cleared.
     */
    public final void deactivate(boolean clearSelection) {
        ThreadUtils.checkUiThread();

        mDesiredState = FindLocationBarState.HIDDEN;
        if (mCurrentState != FindLocationBarState.SHOWN) return;
        setCurrentState(FindLocationBarState.HIDING);
        handleDeactivation(clearSelection);
    }

    /** Logic for handling deactivating the find toolbar. */
    protected void handleDeactivation(boolean clearSelection) {
        setResultsBarVisibility(false);

        mTabModelSelector.removeObserver(mTabModelSelectorObserver);
        for (TabModel model : mTabModelSelector.getModels()) {
            model.removeObserver(mTabModelObserver);
        }

        mCurrentTab.removeObserver(mTabObserver);

        mWindowAndroid.getKeyboardDelegate().hideKeyboard(mFindQuery);
        if (mFindQuery.getText().length() > 0) {
            clearResults();
            mFindInPageBridge.stopFinding(clearSelection);
        }

        mFindInPageBridge.destroy();
        mFindInPageBridge = null;
        mCurrentTab = null;

        setCurrentState(FindLocationBarState.HIDDEN);
    }

    private void setCurrentState(@FindLocationBarState int state) {
        mCurrentState = state;
        mBackPressStateSupplier.set(shouldDeactivateByBackPress());

        // Notify the observers if we hit the transition states.
        if (mObserver != null) {
            if (mCurrentState == FindLocationBarState.HIDDEN) {
                mObserver.onFindToolbarHidden();
            } else if (mCurrentState == FindLocationBarState.SHOWN) {
                mObserver.onFindToolbarShown();
            }
        }

        // Ensure the current state reflects the desired state if the state change happened while
        // processing the previous state change.
        assert mDesiredState == FindLocationBarState.HIDDEN
                || mDesiredState == FindLocationBarState.SHOWN;
        if (mCurrentState == FindLocationBarState.HIDDEN
                && mDesiredState == FindLocationBarState.SHOWN) {
            activate();
        } else if (mCurrentState == FindLocationBarState.SHOWN
                && mDesiredState == FindLocationBarState.HIDDEN) {
            deactivate();
        }
    }

    // Whether the find toolbar should be deactivated by back press.
    private boolean shouldDeactivateByBackPress() {
        return mCurrentState == FindLocationBarState.SHOWN;
    }

    /** Requests focus for the query input field and shows the keyboard. */
    public void requestQueryFocus() {
        mFindQuery.requestFocus();
        showKeyboard();
    }

    /** Called by the tablet-specific implementation when the hide animation is about to begin. */
    protected void onHideAnimationStart() {
        // We do this because hiding the bar after the animation ends doesn't look good.
        setResultsBarVisibility(false);
    }

    /**
     * @see WindowAndroid#startAnimationOverContent(Animator)
     */
    protected void startAnimationOverContent(Animator animation) {
        mWindowAndroid.startAnimationOverContent(animation);
    }

    @VisibleForTesting
    public FindResultBar getFindResultBar() {
        return mResultBar;
    }

    /** Returns whether an animation to show/hide the FindToolbar is currently running. */
    @VisibleForTesting
    public boolean isAnimating() {
        return false;
    }

    /** Restores the last text searched in this tab, or the global last search. */
    private void initializeFindText() {
        assert mFindInPageBridge != null;

        mSettingFindTextProgrammatically = true;
        String findText = null;
        if (mSettingFindTextProgrammatically) {
            findText = mFindInPageBridge.getPreviousFindText();
            if (findText.isEmpty() && !isIncognito()) {
                findText = mLastUserSearch;
            }
            mSearchKeyShouldTriggerSearch = true;
        } else {
            mSearchKeyShouldTriggerSearch = false;
        }
        mFindQuery.setText(findText);
        mSettingFindTextProgrammatically = false;
    }

    /** Sets the find query text string. */
    void setFindQuery(String findText) {
        mFindQuery.setText(findText);
    }

    /** Clears the result displays (except in-page match highlighting). */
    protected void clearResults() {
        setStatus("", false);
        if (mResultBar != null) {
            mResultBar.clearMatchRects();
        }
    }

    private void setResultsBarVisibility(boolean visibility) {
        if (visibility
                && mResultBar == null
                && mCurrentTab != null
                && mCurrentTab.getWebContents() != null) {
            assert mFindInPageBridge != null;

            mResultBar =
                    new FindResultBar(
                            getContext(),
                            mCurrentTab.getContentView(),
                            mWindowAndroid,
                            mFindInPageBridge);
        } else if (!visibility) {
            if (mResultBar != null) {
                mResultBar.dismiss();
                mResultBar = null;
            }
        }
    }

    private void setStatus(String text, boolean failed) {
        mFindStatus.setText(text);
        mFindStatus.setContentDescription(null);
        mFindStatus.setTextColor(getStatusColor(failed, isIncognito()));
        mFindStatus.setVisibility(TextUtils.isEmpty(text) ? GONE : VISIBLE);
    }

    /**
     * @param failed    Whether or not the find query had any matching results.
     * @param incognito Whether or not the current tab is incognito.
     * @return          The color of the status text.
     */
    protected int getStatusColor(boolean failed, boolean incognito) {
        return failed
                ? getContext().getColor(R.color.find_in_page_failed_results_status_color)
                : SemanticColorUtils.getDefaultTextColorSecondary(getContext());
    }

    protected void setPrevNextEnabled(boolean enable) {
        mFindPrevButton.setEnabled(enable);
        mFindNextButton.setEnabled(enable);
    }

    private void showKeyboard() {
        if (!mFindQuery.hasWindowFocus()) {
            // HACK: showKeyboard() is normally called from activate() which is
            // triggered by an options menu item. Unfortunately, because the
            // options menu is still focused at this point, that means our
            // window doesn't actually have focus when this first gets called,
            // and hence it isn't the target of the Input Method, and in
            // practice that means the soft keyboard never shows up (whatever
            // flags you pass). So as a workaround we postpone asking for the
            // keyboard to be shown until just after the window gets refocused.
            // See onWindowFocusChanged(boolean hasFocus).
            mShowKeyboardOnceWindowIsFocused = true;
            return;
        }
        mWindowAndroid.getKeyboardDelegate().showKeyboard(mFindQuery);
    }

    protected boolean isIncognito() {
        return mTabModelSelector != null && mTabModelSelector.isIncognitoSelected();
    }
}