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

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.omnibox;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.FloatProperty;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnKeyListener;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;

import org.chromium.base.CallbackController;
import org.chromium.base.CommandLine;
import org.chromium.base.ObserverList;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.metrics.TimingMetric;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.lens.LensController;
import org.chromium.chrome.browser.lens.LensEntryPoint;
import org.chromium.chrome.browser.lens.LensIntentParams;
import org.chromium.chrome.browser.lens.LensMetrics;
import org.chromium.chrome.browser.lens.LensQueryParams;
import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.omnibox.UrlBar.UrlBarDelegate;
import org.chromium.chrome.browser.omnibox.UrlBarCoordinator.SelectionState;
import org.chromium.chrome.browser.omnibox.geo.GeolocationHeader;
import org.chromium.chrome.browser.omnibox.status.StatusCoordinator;
import org.chromium.chrome.browser.omnibox.styles.OmniboxResourceProvider;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteCoordinator;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxLoadUrlParams;
import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler;
import org.chromium.chrome.browser.prefetch.settings.PreloadPagesSettingsBridge;
import org.chromium.chrome.browser.prefetch.settings.PreloadPagesState;
import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManager;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.Tab.LoadUrlResult;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.theme.ThemeUtils;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.chrome.browser.ui.theme.BrandedColorScheme;
import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
import org.chromium.chrome.browser.util.KeyNavigationUtil;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.widget.animation.CancelAwareAnimatorListener;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.OmniboxFeatures;
import org.chromium.components.search_engines.TemplateUrl;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.common.ResourceRequestBody;
import org.chromium.ui.KeyboardUtils;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.interpolators.Interpolators;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;

/**
 * Mediator for the LocationBar component. Intended location for LocationBar business logic;
 * currently, migration of this logic out of LocationBarLayout is in progress.
 */
class LocationBarMediator
        implements LocationBarDataProvider.Observer,
                OmniboxStub,
                VoiceRecognitionHandler.Delegate,
                VoiceRecognitionHandler.Observer,
                UrlBarDelegate,
                OnKeyListener,
                ComponentCallbacks,
                TemplateUrlService.TemplateUrlServiceObserver,
                BackPressHandler,
                PauseResumeWithNativeObserver {
    private static final int ICON_FADE_ANIMATION_DURATION_MS = 150;
    private static final int ICON_FADE_ANIMATION_DELAY_MS = 75;
    private static final long NTP_KEYBOARD_FOCUS_DURATION_MS = 200;
    private static final int WIDTH_CHANGE_ANIMATION_DURATION_MS = 225;
    private static final int WIDTH_CHANGE_ANIMATION_DELAY_MS = 75;
    private static Boolean sLastCachedIsLensOnOmniboxEnabled;

    /** Enabled/disabled state of 'save offline' button. */
    public interface SaveOfflineButtonState {
        /**
         * @param tab Tab displaying the page that will be saved.
         * @return {@code true} if the UI button is enabled.
         */
        boolean isEnabled(Tab tab);
    }

    /** Uma methods for omnibox. */
    public interface OmniboxUma {
        /**
         * Record the NTP navigation events on omnibox.
         *
         * @param url The URL to which the user navigated.
         * @param transition The transition type of the navigation.
         * @param isNtp Whether the current page is a NewTabPage.
         */
        void recordNavigationOnNtp(String url, int transition, boolean isNtp);
    }

    private final FloatProperty<LocationBarMediator> mUrlFocusChangeFractionProperty =
            new FloatProperty<LocationBarMediator>("") {
                @Override
                public Float get(LocationBarMediator object) {
                    return mUrlFocusChangeFraction;
                }

                @Override
                public void setValue(LocationBarMediator object, float value) {
                    setUrlFocusChangeFraction(value, value);
                }
            };

    private final FloatProperty<LocationBarMediator> mWidthChangeFractionPropertyTablet =
            new FloatProperty<LocationBarMediator>("") {
                @Override
                public Float get(LocationBarMediator object) {
                    return ((LocationBarTablet) mLocationBarLayout).getWidthChangeFraction();
                }

                @Override
                public void setValue(LocationBarMediator object, float value) {
                    ((LocationBarTablet) mLocationBarLayout).setWidthChangeAnimationFraction(value);
                    if (mUrlHasFocus) {
                        mEmbedderImpl.recalculateOmniboxAlignment();
                    }
                }
            };

    private final LocationBarLayout mLocationBarLayout;
    private VoiceRecognitionHandler mVoiceRecognitionHandler;
    private final LocationBarDataProvider mLocationBarDataProvider;
    private final LocationBarEmbedderUiOverrides mEmbedderUiOverrides;
    private StatusCoordinator mStatusCoordinator;
    private AutocompleteCoordinator mAutocompleteCoordinator;
    private OmniboxPrerender mOmniboxPrerender;
    private UrlBarCoordinator mUrlCoordinator;
    private ObservableSupplier<Profile> mProfileSupplier;
    private PrivacyPreferencesManager mPrivacyPreferencesManager;
    private CallbackController mCallbackController = new CallbackController();
    private final OverrideUrlLoadingDelegate mOverrideUrlLoadingDelegate;
    private final LocaleManager mLocaleManager;
    private final List<Runnable> mDeferredNativeRunnables = new ArrayList<>();
    private final OneshotSupplier<TemplateUrlService> mTemplateUrlServiceSupplier;
    private TemplateUrl mSearchEngine;
    private final Context mContext;
    private final BackKeyBehaviorDelegate mBackKeyBehavior;
    private final WindowAndroid mWindowAndroid;
    private GURL mOriginalUrl = GURL.emptyGURL();
    private Animator mUrlFocusChangeAnimator;
    private final ObserverList<UrlFocusChangeListener> mUrlFocusChangeListeners =
            new ObserverList<>();
    private final Rect mRootViewBounds = new Rect();
    private final SaveOfflineButtonState mSaveOfflineButtonState;
    private final OmniboxUma mOmniboxUma;
    private final OmniboxSuggestionsDropdownEmbedderImpl mEmbedderImpl;

    private boolean mNativeInitialized;
    private boolean mUrlFocusedFromFakebox;
    private boolean mUrlFocusedWithoutAnimations;
    private boolean mIsUrlFocusChangeInProgress;
    private final boolean mIsTablet;
    private boolean mShouldShowLensButtonWhenUnfocused;
    private boolean mShouldShowMicButtonWhenUnfocused;
    // Whether the microphone and bookmark buttons should be shown in the tablet location bar. These
    // buttons are hidden if the window size is < 600dp.
    private boolean mShouldShowButtonsWhenUnfocused;
    private float mUrlFocusChangeFraction;
    private boolean mUrlHasFocus;
    private LensController mLensController;
    private final BooleanSupplier mIsToolbarMicEnabledSupplier;
    // Tracks if the location bar is laid out in a focused state due to an ntp scroll.
    private boolean mIsLocationBarFocusedFromNtpScroll;
    private @BrandedColorScheme int mBrandedColorScheme = BrandedColorScheme.APP_DEFAULT;
    private ObservableSupplierImpl<Boolean> mBackPressStateSupplier =
            new ObservableSupplierImpl<>();
    private boolean mShouldClearOmniboxOnFocus = true;
    private ObservableSupplier<TabModelSelector> mTabModelSelectorSupplier;
    private SearchEngineUtils mSearchEngineUtils;

    /*package */ LocationBarMediator(
            @NonNull Context context,
            @NonNull LocationBarLayout locationBarLayout,
            @NonNull LocationBarDataProvider locationBarDataProvider,
            @NonNull LocationBarEmbedderUiOverrides embedderUiOverrides,
            @NonNull ObservableSupplier<Profile> profileSupplier,
            @NonNull PrivacyPreferencesManager privacyPreferencesManager,
            @NonNull OverrideUrlLoadingDelegate overrideUrlLoadingDelegate,
            @NonNull LocaleManager localeManager,
            @NonNull OneshotSupplier<TemplateUrlService> templateUrlServiceSupplier,
            @NonNull BackKeyBehaviorDelegate backKeyBehavior,
            @NonNull WindowAndroid windowAndroid,
            boolean isTablet,
            @NonNull LensController lensController,
            @NonNull SaveOfflineButtonState saveOfflineButtonState,
            @NonNull OmniboxUma omniboxUma,
            @NonNull BooleanSupplier isToolbarMicEnabledSupplier,
            @NonNull OmniboxSuggestionsDropdownEmbedderImpl dropdownEmbedder,
            @Nullable ObservableSupplier<TabModelSelector> tabModelSelectorSupplier) {
        mContext = context;
        mLocationBarLayout = locationBarLayout;
        mLocationBarDataProvider = locationBarDataProvider;
        mLocationBarDataProvider.addObserver(this);
        mEmbedderUiOverrides = embedderUiOverrides;
        mOverrideUrlLoadingDelegate = overrideUrlLoadingDelegate;
        mLocaleManager = localeManager;
        mVoiceRecognitionHandler = new VoiceRecognitionHandler(this, profileSupplier);
        mVoiceRecognitionHandler.addObserver(this);
        mProfileSupplier = profileSupplier;
        mProfileSupplier.addObserver(mCallbackController.makeCancelable(this::setProfile));
        mPrivacyPreferencesManager = privacyPreferencesManager;
        mTemplateUrlServiceSupplier = templateUrlServiceSupplier;
        mBackKeyBehavior = backKeyBehavior;
        mWindowAndroid = windowAndroid;
        mIsTablet = isTablet;
        mShouldShowButtonsWhenUnfocused = isTablet;
        mLensController = lensController;
        mSaveOfflineButtonState = saveOfflineButtonState;
        mOmniboxUma = omniboxUma;
        mIsToolbarMicEnabledSupplier = isToolbarMicEnabledSupplier;
        mEmbedderImpl = dropdownEmbedder;
        mTabModelSelectorSupplier = tabModelSelectorSupplier;
    }

    /**
     * Sets coordinators post-construction; they can't be set at construction time since
     * LocationBarMediator is a delegate for them, so is constructed beforehand.
     *
     * @param urlCoordinator Coordinator for the url bar.
     * @param autocompleteCoordinator Coordinator for the autocomplete component.
     * @param statusCoordinator Coordinator for the status icon.
     */
    /*package */ void setCoordinators(
            UrlBarCoordinator urlCoordinator,
            AutocompleteCoordinator autocompleteCoordinator,
            StatusCoordinator statusCoordinator) {
        mUrlCoordinator = urlCoordinator;
        mAutocompleteCoordinator = autocompleteCoordinator;
        mStatusCoordinator = statusCoordinator;
        updateShouldAnimateIconChanges();
        updateButtonVisibility();
        updateSearchEngineStatusIconShownState();
    }

    /* package */ void destroy() {
        if (mTemplateUrlServiceSupplier.hasValue()) {
            mTemplateUrlServiceSupplier.get().removeObserver(this);
        }
        mStatusCoordinator = null;
        mAutocompleteCoordinator = null;
        mUrlCoordinator = null;
        mPrivacyPreferencesManager = null;
        mVoiceRecognitionHandler.removeObserver(this);
        mVoiceRecognitionHandler.destroy();
        mVoiceRecognitionHandler = null;
        mLocationBarDataProvider.removeObserver(this);
        mDeferredNativeRunnables.clear();
        mUrlFocusChangeListeners.clear();
    }

    /*package */ void onUrlFocusChange(boolean hasFocus) {
        setUrlFocusChangeInProgress(true);
        mUrlHasFocus = hasFocus;
        // Intercept back press if it has focus.
        mBackPressStateSupplier.set(mUrlHasFocus);
        updateButtonVisibility();
        updateShouldAnimateIconChanges();
        onPrimaryColorChanged();

        if (hasFocus) {
            if (mNativeInitialized) RecordUserAction.record("FocusLocation");
            // Don't clear Omnibox if the user just pasted text to NTP Omnibox.
            if (mShouldClearOmniboxOnFocus) {
                setUrlBarText(
                        UrlBarData.EMPTY, UrlBar.ScrollType.NO_SCROLL, SelectionState.SELECT_END);
            }
        } else {
            mUrlFocusedFromFakebox = false;
            mUrlFocusedWithoutAnimations = false;
        }

        mStatusCoordinator.onUrlFocusChange(hasFocus);

        if (!mUrlFocusedWithoutAnimations) handleUrlFocusAnimation(hasFocus);

        if (hasFocus
                && mLocationBarDataProvider.hasTab()
                && !mLocationBarDataProvider.isIncognito()) {
            if (mTemplateUrlServiceSupplier.hasValue()) {
                if (mTemplateUrlServiceSupplier.get().isDefaultSearchEngineGoogle()) {
                    GeolocationHeader.primeLocationForGeoHeaderIfEnabled(
                            mProfileSupplier.get(), mTemplateUrlServiceSupplier.get());
                }
            } else {
                mTemplateUrlServiceSupplier.onAvailable(
                        (templateUrlService) -> {
                            if (templateUrlService.isDefaultSearchEngineGoogle()) {
                                GeolocationHeader.primeLocationForGeoHeaderIfEnabled(
                                        mProfileSupplier.get(), templateUrlService);
                            }
                        });
            }
        } // Focus change caused by a closed tab may result in there not being an active tab.
        if (!hasFocus && mLocationBarDataProvider.hasTab()) {
            setUrl(
                    mLocationBarDataProvider.getCurrentGurl(),
                    mLocationBarDataProvider.getUrlBarData());
        }
    }

    /*package */ void onFinishNativeInitialization() {
        mNativeInitialized = true;
        mOmniboxPrerender = new OmniboxPrerender();
        mTemplateUrlServiceSupplier.onAvailable(
                (templateUrlService) -> {
                    templateUrlService.addObserver(this);
                    if (OmniboxFeatures.sUseFusedLocationProvider.isEnabled()
                            && templateUrlService.isDefaultSearchEngineGoogle()) {
                        GeolocationHeader.primeLocationForGeoHeaderIfEnabled(
                                mProfileSupplier.get(), mTemplateUrlServiceSupplier.get());
                    }
                });

        mLocationBarLayout.onFinishNativeInitialization();
        if (mProfileSupplier.hasValue()) setProfile(mProfileSupplier.get());

        mLocationBarLayout.setMicButtonDrawable(
                AppCompatResources.getDrawable(mContext, R.drawable.ic_mic_white_24dp));
        onPrimaryColorChanged();

        for (Runnable deferredRunnable : mDeferredNativeRunnables) {
            mLocationBarLayout.post(deferredRunnable);
        }
        mDeferredNativeRunnables.clear();
        updateButtonVisibility();
    }

    /* package */ void setUrlFocusChangeFraction(
            float ntpSearchBoxScrollFraction, float urlFocusChangeFraction) {
        float fraction = Math.max(ntpSearchBoxScrollFraction, urlFocusChangeFraction);
        mUrlFocusChangeFraction = fraction;
        if (mIsTablet) {
            mLocationBarDataProvider
                    .getNewTabPageDelegate()
                    .setUrlFocusChangeAnimationPercent(fraction);
            mLocationBarLayout.setUrlFocusChangePercent(
                    fraction, fraction, mIsUrlFocusChangeInProgress);
        } else {
            // Determine when the focus state changes as a result of ntp scrolling.
            boolean isLocationBarFocusedFromNtpScroll =
                    fraction > 0f && !mIsUrlFocusChangeInProgress;
            if (isLocationBarFocusedFromNtpScroll != mIsLocationBarFocusedFromNtpScroll) {
                mIsLocationBarFocusedFromNtpScroll = isLocationBarFocusedFromNtpScroll;
                onUrlFocusedFromNtpScrollChanged();
            }

            if (fraction > 0f) {
                mLocationBarLayout.setUrlActionContainerVisibility(View.VISIBLE);
            } else if (fraction == 0f && !mIsUrlFocusChangeInProgress) {
                // If a URL focus change is in progress, then it will handle setting the visibility
                // correctly after it completes.  If done here, it would cause the URL to jump due
                // to a badly timed layout call.
                mLocationBarLayout.setUrlActionContainerVisibility(View.GONE);
            }

            // Add expansion animation for the space besides status view in location bar.
            mLocationBarLayout.setUrlFocusChangePercent(
                    ntpSearchBoxScrollFraction,
                    urlFocusChangeFraction,
                    mIsUrlFocusChangeInProgress);
            mStatusCoordinator.setUrlFocusChangePercent(fraction);
        }
    }

    /* package */ void onUrlFocusedFromNtpScrollChanged() {
        updateButtonVisibility();
    }

    /*package */ void setUnfocusedWidth(int unfocusedWidth) {
        mLocationBarLayout.setUnfocusedWidth(unfocusedWidth);
    }

    /* package */ void setVoiceRecognitionHandlerForTesting(
            VoiceRecognitionHandler voiceRecognitionHandler) {
        mVoiceRecognitionHandler = voiceRecognitionHandler;
    }

    /* package */ void setLensControllerForTesting(LensController lensController) {
        mLensController = lensController;
    }

    void resetLastCachedIsLensOnOmniboxEnabledForTesting() {
        sLastCachedIsLensOnOmniboxEnabled = null;
    }

    /* package */ void setIsUrlBarFocusedWithoutAnimationsForTesting(
            boolean isUrlBarFocusedWithoutAnimations) {
        mUrlFocusedWithoutAnimations = isUrlBarFocusedWithoutAnimations;
    }

    /*package */ void updateVisualsForState() {
        onPrimaryColorChanged();
    }

    /*package */ void setShowTitle(boolean showTitle) {
        // This method is only used in CustomTabToolbar.
    }

    /*package */ void showUrlBarCursorWithoutFocusAnimations() {
        if (mUrlHasFocus || mUrlFocusedFromFakebox) {
            return;
        }

        // Verify if Hardware keyboard still requests Software keyboard (IME) to be used.
        // If that happens, suppress early focus to take Software keyboard out of the way.
        // This is specifically relevant in Incognito mode, where Soft keyboard clobbers relevant
        // messages.
        // The setting below is not explicitly itemized in Settings.Secure, but it corresponds
        // to whether Software keyboard would be called up when Physical keyboard is in use on
        // Pixel devices.
        if (KeyboardUtils.shouldShowImeWithHardwareKeyboard(mContext)) return;

        mUrlFocusedWithoutAnimations = true;
        // This method should only be called on devices with a hardware keyboard attached, as
        // described in the documentation for LocationBar#showUrlBarCursorWithoutFocusAnimations.
        setUrlBarFocus(
                /* shouldBeFocused= */ true,
                /* pastedText= */ null,
                OmniboxFocusReason.DEFAULT_WITH_HARDWARE_KEYBOARD);
    }

    /**
     * If the URL bar was previously focused on the NTP due to a connected keyboard, an navigation
     * away from the NTP should clear this focus before filling the current tab's URL.
     */
    /*package */ void clearUrlBarCursorWithoutFocusAnimations() {
        if (mUrlCoordinator.hasFocus() && mUrlFocusedWithoutAnimations) {
            // If we did not run the focus animations, then the user has not typed any text.
            // So, clear the focus and accept whatever URL the page is currently attempting to
            // display, given that the current tab is not displaying the NTP.
            setUrlBarFocus(false, null, OmniboxFocusReason.UNFOCUS);
        }
    }

    /*package */ void revertChanges() {
        if (mUrlHasFocus) {
            GURL currentUrl = mLocationBarDataProvider.getCurrentGurl();
            if (NativePage.isChromePageUrl(currentUrl, mLocationBarDataProvider.isOffTheRecord())) {
                setUrlBarTextEmpty();
            } else {
                setUrlBarText(
                        mLocationBarDataProvider.getUrlBarData(),
                        UrlBar.ScrollType.NO_SCROLL,
                        SelectionState.SELECT_ALL);
            }
            mUrlCoordinator.setKeyboardVisibility(false, false);
        } else {
            setUrl(
                    mLocationBarDataProvider.getCurrentGurl(),
                    mLocationBarDataProvider.getUrlBarData());
        }
    }

    /* package */ void onUrlTextChanged() {
        updateButtonVisibility();
    }

    /* package */ void onSuggestionsChanged(@Nullable AutocompleteMatch defaultMatch) {
        // TODO (https://crbug.com/1152501): Refactor the LBM/LBC relationship such that LBM doesn't
        // need to communicate with other coordinators like this.
        String userText = mUrlCoordinator.getTextWithoutAutocomplete();
        mStatusCoordinator.onDefaultMatchClassified(
                // Zero suggest is always considered Search.
                TextUtils.isEmpty(userText)
                        ||
                        // Otherwise, use the default match type (if possible), or assume Search (if
                        // not).
                        (defaultMatch != null ? defaultMatch.isSearchSuggestion() : true));
        if (mUrlCoordinator.shouldAutocomplete()) {
            mUrlCoordinator.setAutocompleteText(
                    userText,
                    defaultMatch != null ? defaultMatch.getInlineAutocompletion() : null,
                    defaultMatch != null ? defaultMatch.getAdditionalText() : null);
        }

        // Handle the case where suggestions (in particular zero suggest) are received without the
        // URL focusing happening.
        if (mUrlFocusedWithoutAnimations && mUrlHasFocus) {
            handleUrlFocusAnimation(/* hasFocus= */ true);
        }

        if (mNativeInitialized
                && mProfileSupplier.hasValue()
                && !CommandLine.getInstance().hasSwitch(ChromeSwitches.DISABLE_INSTANT)
                && DeviceClassManager.enablePrerendering()
                && PreloadPagesSettingsBridge.getState(mProfileSupplier.get())
                        != PreloadPagesState.NO_PRELOADING
                && mLocationBarDataProvider.hasTab()) {
            mOmniboxPrerender.prerenderMaybe(
                    userText,
                    mOriginalUrl.getSpec(),
                    mAutocompleteCoordinator.getCurrentNativeAutocompleteResult(),
                    mProfileSupplier.get(),
                    mLocationBarDataProvider.getTab());
        }

        mUrlCoordinator.onUrlBarSuggestionsChanged(
                mAutocompleteCoordinator.getSuggestionCount() != 0);
    }

    /* package */ void loadUrl(OmniboxLoadUrlParams omniboxLoadUrlParams) {
        assert mLocationBarDataProvider != null;
        Tab currentTab = mLocationBarDataProvider.getTab();

        // The code of the rest of this class ensures that this can't be called until the native
        // side is initialized
        assert mNativeInitialized : "Loading URL before native side initialized";

        // TODO(crbug.com/40693835): Should be taking a full loaded LoadUrlParams.
        if (mOverrideUrlLoadingDelegate.willHandleLoadUrlWithPostData(
                omniboxLoadUrlParams, mLocationBarDataProvider.isIncognito())) {
            return;
        }

        String url = omniboxLoadUrlParams.url;
        if (currentTab != null) {
            boolean isCurrentTabNtpUrl = UrlUtilities.isNtpUrl(currentTab.getUrl());
            if (currentTab.isNativePage() || isCurrentTabNtpUrl) {
                mOmniboxUma.recordNavigationOnNtp(
                        omniboxLoadUrlParams.url,
                        omniboxLoadUrlParams.transitionType,
                        !currentTab.isIncognito() && isCurrentTabNtpUrl);
                // Passing in an empty string should not do anything unless the user is at the NTP.
                // Since the NTP has no url, pressing enter while clicking on the URL bar should
                // refresh the page as it does when you click and press enter on any other site.
                if (url.isEmpty()) url = currentTab.getUrl().getSpec();
            }

            if (omniboxLoadUrlParams.callback != null) {
                currentTab.addObserver(
                        new EmptyTabObserver() {
                            @Override
                            public void onLoadUrl(
                                    Tab tab, LoadUrlParams params, LoadUrlResult loadUrlResult) {
                                omniboxLoadUrlParams.callback.onLoadUrl(params, loadUrlResult);
                                tab.removeObserver(this);
                            }
                        });
            }
        }

        // Loads the |url| in a new tab or the current ContentView and gives focus to the
        // ContentView.
        if (currentTab != null && !url.isEmpty()) {
            LoadUrlParams loadUrlParams = new LoadUrlParams(url);
            try (TimingMetric record =
                    TimingMetric.shortUptime("Android.Omnibox.SetGeolocationHeadersTime")) {
                loadUrlParams.setVerbatimHeaders(GeolocationHeader.getGeoHeader(url, currentTab));
            }
            loadUrlParams.setTransitionType(
                    omniboxLoadUrlParams.transitionType | PageTransition.FROM_ADDRESS_BAR);
            if (omniboxLoadUrlParams.inputStartTimestamp != 0) {
                loadUrlParams.setInputStartTimestamp(omniboxLoadUrlParams.inputStartTimestamp);
            }

            if (!TextUtils.isEmpty(omniboxLoadUrlParams.postDataType)) {
                StringBuilder headers = new StringBuilder();
                String prevHeader = loadUrlParams.getVerbatimHeaders();
                if (prevHeader != null && !prevHeader.isEmpty()) {
                    headers.append(prevHeader);
                    headers.append("\r\n");
                }

                headers.append("Content-Type: ");
                headers.append(omniboxLoadUrlParams.postDataType);

                loadUrlParams.setVerbatimHeaders(headers.toString());
            }

            if (omniboxLoadUrlParams.postData != null
                    && omniboxLoadUrlParams.postData.length != 0) {
                loadUrlParams.setPostData(
                        ResourceRequestBody.createFromBytes(omniboxLoadUrlParams.postData));
            }

            if (omniboxLoadUrlParams.openInNewTab
                    && mTabModelSelectorSupplier != null
                    && mTabModelSelectorSupplier.get() != null) {
                mTabModelSelectorSupplier
                        .get()
                        .openNewTab(
                                loadUrlParams,
                                TabLaunchType.FROM_OMNIBOX,
                                currentTab,
                                currentTab.isIncognito());
            } else {
                currentTab.loadUrl(loadUrlParams);
            }
            RecordUserAction.record("MobileOmniboxUse");
        }
        mLocaleManager.recordLocaleBasedSearchMetrics(
                false, url, omniboxLoadUrlParams.transitionType);

        PostTask.postTask(TaskTraits.UI_USER_VISIBLE, () -> focusCurrentTab());
    }

    /* package */ boolean didFocusUrlFromFakebox() {
        return mUrlFocusedFromFakebox;
    }

    /** Recalculates the visibility of the buttons inside the location bar. */
    /* package */ void updateButtonVisibility() {
        updateDeleteButtonVisibility();
        updateMicButtonVisibility();
        updateLensButtonVisibility();
        if (mIsTablet) {
            updateTabletButtonsVisibility();
        }
    }

    /**
     * Sets the displayed URL according to the provided url string and UrlBarData.
     *
     * <p>The URL is converted to the most user friendly format (removing HTTP:// for example).
     *
     * <p>If the current tab is null, the URL text will be cleared.
     */
    /* package */ void setUrl(GURL currentUrl, UrlBarData urlBarData) {
        // If the URL is currently focused, do not replace the text they have entered with the URL.
        // Once they stop editing the URL, the current tab's URL will automatically be filled in.
        if (mUrlCoordinator.hasFocus()) {
            return;
        }

        mOriginalUrl = currentUrl;
        setUrlBarText(urlBarData, UrlBar.ScrollType.SCROLL_TO_TLD, SelectionState.SELECT_ALL);
    }

    /* package */ void deleteButtonClicked(View view) {
        if (!mNativeInitialized) return;
        RecordUserAction.record("MobileOmniboxDeleteUrl");
        setUrlBarTextEmpty();
        updateButtonVisibility();
    }

    /* package */ void micButtonClicked(View view) {
        if (!mNativeInitialized) return;
        // Hide keyboard before launch voice search to avoid keyboard action announcement in
        // TalkBack to be picked up by voice search.
        mUrlCoordinator.setKeyboardVisibility(false, false);

        RecordUserAction.record("MobileOmniboxVoiceSearch");
        mVoiceRecognitionHandler.startVoiceRecognition(
                mLocationBarLayout.getVoiceRecogintionSource());
    }

    /** package */
    void lensButtonClicked(View view) {
        if (!mNativeInitialized || mLocationBarDataProvider == null) return;
        int entryPoint = mLocationBarLayout.getLensEntryPoint();
        // Lens does not track Search Widget metrics.
        // Enable once LensMetrics#getClickedActionName includes QUICK_ACTION_SEARCH_WIDGET.
        if (entryPoint != LensEntryPoint.QUICK_ACTION_SEARCH_WIDGET) {
            LensMetrics.recordClicked(entryPoint);
        }
        startLens(entryPoint);
    }

    /* package */ void setUrlFocusChangeInProgress(boolean inProgress) {
        if (mUrlCoordinator == null) return;
        mIsUrlFocusChangeInProgress = inProgress;
        if (!inProgress) {
            updateButtonVisibility();

            // The accessibility bounding box is not properly updated when focusing the Omnibox
            // from the NTP fakebox.  Clearing/re-requesting focus triggers the bounding box to
            // be recalculated.
            if (didFocusUrlFromFakebox()
                    && mUrlHasFocus
                    && ChromeAccessibilityUtil.get().isAccessibilityEnabled()) {
                String existingText = mUrlCoordinator.getTextWithoutAutocomplete();
                mUrlCoordinator.clearFocus();
                mUrlCoordinator.requestFocus();
                // Existing text (e.g. if the user pasted via the fakebox) from the fake box
                // should be restored after toggling the focus.
                if (!TextUtils.isEmpty(existingText)) {
                    mUrlCoordinator.setUrlBarData(
                            UrlBarData.forNonUrlText(existingText),
                            UrlBar.ScrollType.NO_SCROLL,
                            UrlBarCoordinator.SelectionState.SELECT_END);
                    forceOnTextChanged();
                }
            }

            for (UrlFocusChangeListener listener : mUrlFocusChangeListeners) {
                listener.onUrlAnimationFinished(mUrlHasFocus);
            }
        }
    }

    /**
     * Handles any actions to be performed after all other actions triggered by the URL focus
     * change. This will be called after any animations are performed to transition from one focus
     * state to the other.
     *
     * @param showExpandedState Whether the url bar is expanded.
     * @param shouldShowKeyboard Whether the keyboard should be shown. This value is determined by
     *     whether url bar has got focus. Most of the time this is the same as showExpandedState,
     *     but in some cases, e.g. url bar is scrolled to the top of the screen on homepage but not
     *     focused, we set it differently.
     */
    /* package */ void finishUrlFocusChange(boolean showExpandedState, boolean shouldShowKeyboard) {
        if (mUrlCoordinator == null) return;
        mUrlCoordinator.setKeyboardVisibility(shouldShowKeyboard, true);
        setUrlFocusChangeInProgress(false);
        updateShouldAnimateIconChanges();
        if (!mIsTablet && !showExpandedState) {
            mLocationBarLayout.setUrlActionContainerVisibility(View.GONE);
        }
        if (mIsTablet) {
            float urlFocusChangeFraction = showExpandedState ? 1.0f : 0.0f;
            mLocationBarLayout.setUrlFocusChangePercent(
                    urlFocusChangeFraction, urlFocusChangeFraction, false);
            mLocationBarLayout.updateLayoutParams(
                    MeasureSpec.makeMeasureSpec(
                            mLocationBarLayout.getMeasuredWidth(), MeasureSpec.EXACTLY));
        }
        // Reset to the default value.
        mShouldClearOmniboxOnFocus = true;
    }

    /**
     * Handle and run any necessary animations that are triggered off focusing the UrlBar.
     *
     * @param hasFocus Whether focus was gained.
     */
    @VisibleForTesting
    /* package */ void handleUrlFocusAnimation(boolean hasFocus) {
        if (hasFocus) {
            mUrlFocusedWithoutAnimations = false;
        }

        for (UrlFocusChangeListener listener : mUrlFocusChangeListeners) {
            listener.onUrlFocusChange(hasFocus);
        }

        // The focus animation for phones is driven by ToolbarPhone, so we don't currently have any
        // phone-specific animation logic in this class.
        if (mIsTablet) {
            if (mUrlFocusChangeAnimator != null && mUrlFocusChangeAnimator.isRunning()) {
                mUrlFocusChangeAnimator.cancel();
                mUrlFocusChangeAnimator = null;
            }

            if (mLocationBarDataProvider.getNewTabPageDelegate().isCurrentlyVisible()) {
                finishUrlFocusChange(hasFocus, /* shouldShowKeyboard= */ hasFocus);
                return;
            }

            mLocationBarLayout.getRootView().getLocalVisibleRect(mRootViewBounds);
            float screenSizeRatio =
                    (mRootViewBounds.height()
                            / (float)
                                    (Math.max(mRootViewBounds.height(), mRootViewBounds.width())));
            mUrlFocusChangeAnimator =
                    ObjectAnimator.ofFloat(
                            this, mUrlFocusChangeFractionProperty, hasFocus ? 1f : 0f);
            mUrlFocusChangeAnimator.setDuration(
                    (long) (NTP_KEYBOARD_FOCUS_DURATION_MS * screenSizeRatio));
            mUrlFocusChangeAnimator.addListener(
                    new CancelAwareAnimatorListener() {
                        @Override
                        public void onEnd(Animator animator) {
                            finishUrlFocusChange(hasFocus, /* shouldShowKeyboard= */ hasFocus);
                        }

                        @Override
                        public void onCancel(Animator animator) {
                            setUrlFocusChangeInProgress(false);
                        }
                    });
            mUrlFocusChangeAnimator.start();
        }
    }

    /* package */ void setShouldShowMicButtonWhenUnfocusedForPhone(boolean shouldShow) {
        assert !mIsTablet;
        mShouldShowMicButtonWhenUnfocused = shouldShow;
    }

    /* package */ void setShouldShowLensButtonWhenUnfocusedForPhone(boolean shouldShow) {
        assert !mIsTablet;
        mShouldShowLensButtonWhenUnfocused = shouldShow;
    }

    /* package */ void setShouldShowMicButtonWhenUnfocusedForTesting(boolean shouldShow) {
        assert mIsTablet;
        mShouldShowMicButtonWhenUnfocused = shouldShow;
    }

    /**
     * @param shouldShow Whether buttons should be displayed in the URL bar when it's not focused.
     */
    /* package */ void setShouldShowButtonsWhenUnfocusedForTablet(boolean shouldShow) {
        assert mIsTablet;
        mShouldShowButtonsWhenUnfocused = shouldShow;
        updateButtonVisibility();
    }

    /**
     * @param button The {@link View} of the button to show. Returns An animator to run for the
     *     given view when showing buttons in the unfocused location bar. This should also be used
     *     to create animators for showing toolbar buttons.
     */
    /* package */ ObjectAnimator createShowButtonAnimatorForTablet(View button) {
        assert mIsTablet;
        if (button.getVisibility() != View.VISIBLE) {
            button.setAlpha(0.f);
        }
        ObjectAnimator buttonAnimator = ObjectAnimator.ofFloat(button, View.ALPHA, 1.f);
        buttonAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
        buttonAnimator.setStartDelay(ICON_FADE_ANIMATION_DELAY_MS);
        buttonAnimator.setDuration(ICON_FADE_ANIMATION_DURATION_MS);
        return buttonAnimator;
    }

    /**
     * @param button The {@link View} of the button to hide. Returns An animator to run for the
     *     given view when hiding buttons in the unfocused location bar. This should also be used to
     *     create animators for hiding toolbar buttons.
     */
    /* package */ ObjectAnimator createHideButtonAnimatorForTablet(View button) {
        assert mIsTablet;
        ObjectAnimator buttonAnimator = ObjectAnimator.ofFloat(button, View.ALPHA, 0.f);
        buttonAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN_INTERPOLATOR);
        buttonAnimator.setDuration(ICON_FADE_ANIMATION_DURATION_MS);
        return buttonAnimator;
    }

    /**
     * Creates animators for showing buttons in the unfocused tablet location bar. The buttons fade
     * in while the width of the location bar decreases. There are toolbar buttons that show at the
     * same time, causing the width of the location bar to change.
     *
     * @param toolbarStartPaddingDifference The difference in the toolbar's start padding between
     *     the beginning and end of the animation.
     * @return A List of animators to run.
     */
    /* package */ List<Animator> getShowButtonsWhenUnfocusedAnimatorsForTablet(
            int toolbarStartPaddingDifference) {
        assert mIsTablet;
        LocationBarTablet locationBarTablet = ((LocationBarTablet) mLocationBarLayout);

        ArrayList<Animator> animators = new ArrayList<>();

        Animator widthChangeAnimator =
                ObjectAnimator.ofFloat(this, mWidthChangeFractionPropertyTablet, 0f);
        widthChangeAnimator.setDuration(WIDTH_CHANGE_ANIMATION_DURATION_MS);
        widthChangeAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN_INTERPOLATOR);
        widthChangeAnimator.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        locationBarTablet.startAnimatingWidthChange(toolbarStartPaddingDifference);
                        setShouldShowButtonsWhenUnfocusedForTablet(true);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        // Only reset values if the animation is ending because it's completely
                        // finished and not because it was canceled.
                        if (locationBarTablet.getWidthChangeFraction() == 0.f) {
                            locationBarTablet.finishAnimatingWidthChange();
                            locationBarTablet.resetValuesAfterAnimation();
                        }
                    }
                });
        animators.add(widthChangeAnimator);

        // When buttons show in the unfocused location bar, either the delete button or bookmark
        // button will be showing. If the delete button is currently showing, the bookmark button
        // should not fade in.
        if (!locationBarTablet.isDeleteButtonVisible()) {
            animators.add(
                    createShowButtonAnimatorForTablet(
                            locationBarTablet.getBookmarkButtonForAnimation()));
        }

        if (shouldShowSaveOfflineButton()) {
            animators.add(
                    createShowButtonAnimatorForTablet(
                            locationBarTablet.getSaveOfflineButtonForAnimation()));
        } else {
            if (!locationBarTablet.isMicButtonVisible()
                    || locationBarTablet.getMicButtonAlpha() != 1.f) {
                // If the microphone button is already fully visible, don't animate its appearance.
                animators.add(
                        createShowButtonAnimatorForTablet(
                                locationBarTablet.getMicButtonForAnimation()));
            }
            if (shouldShowLensButton()
                    && (!locationBarTablet.isLensButtonVisible()
                            || locationBarTablet.getLensButtonAlpha() != 1.f)) {
                // If the Lens button is already fully visible, don't animate its appearance.
                animators.add(
                        createShowButtonAnimatorForTablet(
                                locationBarTablet.getLensButtonForAnimation()));
            }
        }

        return animators;
    }

    /**
     * Creates animators for hiding buttons in the unfocused tablet location bar. The buttons fade
     * out while the width of the location bar increases. There are toolbar buttons that hide at the
     * same time, causing the width of the location bar to change.
     *
     * @param toolbarStartPaddingDifference The difference in the toolbar's start padding between
     *     the beginning and end of the animation.
     * @return A List of animators to run.
     */
    /* package */ List<Animator> getHideButtonsWhenUnfocusedAnimatorsForTablet(
            int toolbarStartPaddingDifference) {
        LocationBarTablet locationBarTablet = ((LocationBarTablet) mLocationBarLayout);

        ArrayList<Animator> animators = new ArrayList<>();

        Animator widthChangeAnimator =
                ObjectAnimator.ofFloat(this, mWidthChangeFractionPropertyTablet, 1f);
        widthChangeAnimator.setStartDelay(WIDTH_CHANGE_ANIMATION_DELAY_MS);
        widthChangeAnimator.setDuration(WIDTH_CHANGE_ANIMATION_DURATION_MS);
        widthChangeAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN_INTERPOLATOR);
        widthChangeAnimator.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        locationBarTablet.startAnimatingWidthChange(toolbarStartPaddingDifference);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        // Only reset values if the animation is ending because it's completely
                        // finished and not because it was canceled.
                        if (locationBarTablet.getWidthChangeFraction() == 1.f) {
                            locationBarTablet.finishAnimatingWidthChange();
                            locationBarTablet.resetValuesAfterAnimation();
                            setShouldShowButtonsWhenUnfocusedForTablet(false);
                        }
                    }
                });
        animators.add(widthChangeAnimator);

        // When buttons show in the unfocused location bar, either the delete button or bookmark
        // button will be showing. If the delete button is currently showing, the bookmark button
        // should not fade out.
        if (!locationBarTablet.isDeleteButtonVisible()) {
            animators.add(
                    createHideButtonAnimatorForTablet(
                            locationBarTablet.getBookmarkButtonForAnimation()));
        }

        if (shouldShowSaveOfflineButton() && locationBarTablet.isSaveOfflineButtonVisible()) {
            animators.add(
                    createHideButtonAnimatorForTablet(
                            locationBarTablet.getSaveOfflineButtonForAnimation()));
        } else if (!(mUrlHasFocus && !locationBarTablet.isDeleteButtonVisible())) {
            // If the save offline button isn't enabled, the microphone button always shows when
            // buttons are shown in the unfocused location bar. When buttons are hidden in the
            // unfocused location bar, the microphone shows if the location bar is focused and the
            // delete button isn't showing. The microphone button should not be hidden if the
            // url bar is currently focused and the delete button isn't showing.
            animators.add(
                    createHideButtonAnimatorForTablet(
                            locationBarTablet.getMicButtonForAnimation()));
            if (shouldShowLensButton()) {
                animators.add(
                        createHideButtonAnimatorForTablet(
                                locationBarTablet.getLensButtonForAnimation()));
            }
        }

        return animators;
    }

    /**
     * Changes the text on the url bar. The text update will be applied regardless of the current
     * focus state (comparing to {@link LocationBarMediator#setUrl} which only applies text updates
     * when not focused).
     *
     * @param urlBarData The contents of the URL bar, both for editing and displaying.
     * @param scrollType Specifies how the text should be scrolled in the unfocused state.
     * @param selectionState Specifies how the text should be selected in the focused state.
     * @return Whether the URL was changed as a result of this call.
     */
    /* package */ boolean setUrlBarText(
            UrlBarData urlBarData,
            @UrlBar.ScrollType int scrollType,
            @SelectionState int selectionState) {
        return mUrlCoordinator.setUrlBarData(urlBarData, scrollType, selectionState);
    }

    /**
     * Clear any text in the URL bar.
     *
     * @return Whether this changed the existing text.
     */
    /* package */ boolean setUrlBarTextEmpty() {
        boolean textChanged =
                mUrlCoordinator.setUrlBarData(
                        UrlBarData.EMPTY,
                        UrlBar.ScrollType.SCROLL_TO_BEGINNING,
                        SelectionState.SELECT_ALL);
        forceOnTextChanged();
        return textChanged;
    }

    /* package */ void forceOnTextChanged() {
        String textWithoutAutocomplete = mUrlCoordinator.getTextWithoutAutocomplete();
        mAutocompleteCoordinator.onTextChanged(textWithoutAutocomplete);
    }

    // Private methods

    private void setProfile(Profile profile) {
        if (profile == null || !mNativeInitialized) return;
        mOmniboxPrerender.initializeForProfile(profile);
        mSearchEngineUtils = SearchEngineUtils.getForProfile(profile);
        mLocationBarLayout.setSearchEngineUtils(mSearchEngineUtils);
    }

    private void focusCurrentTab() {
        assert mLocationBarDataProvider != null;
        if (mLocationBarDataProvider.hasTab()) {
            View view = mLocationBarDataProvider.getTab().getView();
            if (view != null) view.requestFocus();
        }
    }

    /** Update visuals to use a correct color scheme depending on the primary color. */
    @VisibleForTesting
    /* package */ void updateBrandedColorScheme() {
        mBrandedColorScheme =
                OmniboxResourceProvider.getBrandedColorScheme(
                        mContext,
                        mLocationBarDataProvider.isIncognitoBranded(),
                        getPrimaryBackgroundColor());

        // The delete button only appears when the url bar has focus, so its tint is rather static,
        // and need not be assigned in updateButtonTints().
        mLocationBarLayout.setDeleteButtonTint(
                ThemeUtils.getThemedToolbarIconTint(mContext, mBrandedColorScheme));
        // If the URL changed colors and is not focused, update the URL to account for the new
        // color scheme.
        if (mUrlCoordinator.setBrandedColorScheme(mBrandedColorScheme) && !isUrlBarFocused()) {
            updateUrl();
        }
        mStatusCoordinator.setBrandedColorScheme(mBrandedColorScheme);
        if (mAutocompleteCoordinator != null) {
            mAutocompleteCoordinator.updateVisualsForState(mBrandedColorScheme);
        }
    }

    /** Returns the primary color based on the url focus, and incognito state. */
    private int getPrimaryBackgroundColor() {
        // If the url bar is focused, the toolbar background color is the default color regardless
        // of whether it is branded or not.
        if (isUrlBarFocused()) {
            return ChromeColors.getDefaultThemeColor(
                    mContext, mLocationBarDataProvider.isIncognitoBranded());
        } else {
            return mLocationBarDataProvider.getPrimaryColor();
        }
    }

    private void updateShouldAnimateIconChanges() {
        boolean shouldAnimate =
                mIsTablet ? isUrlBarFocused() : isUrlBarFocused() || mIsUrlFocusChangeInProgress;
        mStatusCoordinator.setShouldAnimateIconChanges(shouldAnimate);
    }

    private void recordOmniboxFocusReason(@OmniboxFocusReason int reason) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.OmniboxFocusReason", reason, OmniboxFocusReason.NUM_ENTRIES);
    }

    /** Updates the display of the mic button. */
    private void updateMicButtonVisibility() {
        mLocationBarLayout.setMicButtonVisibility(shouldShowMicButton());
    }

    private void updateLensButtonVisibility() {
        boolean shouldShowLensButton = shouldShowLensButton();
        LensMetrics.recordShown(LensEntryPoint.OMNIBOX, shouldShowLensButton);
        mLocationBarLayout.setLensButtonVisibility(shouldShowLensButton);
    }

    private void updateDeleteButtonVisibility() {
        mLocationBarLayout.setDeleteButtonVisibility(shouldShowDeleteButton());
    }

    private void updateTabletButtonsVisibility() {
        assert mIsTablet;
        LocationBarTablet locationBarTablet = (LocationBarTablet) mLocationBarLayout;
        boolean showBookmarkButton =
                mShouldShowButtonsWhenUnfocused && shouldShowPageActionButtons();
        locationBarTablet.setBookmarkButtonVisibility(showBookmarkButton);

        boolean showSaveOfflineButton =
                mShouldShowButtonsWhenUnfocused && shouldShowSaveOfflineButton();
        locationBarTablet.setSaveOfflineButtonVisibility(
                showSaveOfflineButton, isSaveOfflineButtonEnabled());
    }

    /**
     * @return Whether the delete button should be shown.
     */
    private boolean shouldShowDeleteButton() {
        // Show the delete button at the end when the bar has focus and has some text.
        boolean hasText =
                mUrlCoordinator != null
                        && !TextUtils.isEmpty(mUrlCoordinator.getTextWithAutocomplete());
        return hasText && (mUrlHasFocus || mIsUrlFocusChangeInProgress);
    }

    private boolean shouldShowMicButton() {
        if (shouldShowDeleteButton()) return false;
        if (!mNativeInitialized
                || mVoiceRecognitionHandler == null
                || !mVoiceRecognitionHandler.isVoiceSearchEnabled()
                || !mEmbedderUiOverrides.isVoiceEntrypointAllowed()) {
            return false;
        }
        boolean isToolbarMicEnabled = mIsToolbarMicEnabledSupplier.getAsBoolean();
        if (mIsTablet && mShouldShowButtonsWhenUnfocused) {
            return !isToolbarMicEnabled && (mUrlHasFocus || mIsUrlFocusChangeInProgress);
        } else {
            boolean canShowMicButton = !mIsTablet || !isToolbarMicEnabled;
            return canShowMicButton
                    && (mUrlHasFocus
                            || mIsUrlFocusChangeInProgress
                            || mIsLocationBarFocusedFromNtpScroll
                            || mShouldShowMicButtonWhenUnfocused);
        }
    }

    private boolean shouldShowLensButton() {
        if (shouldShowDeleteButton()) return false;

        // When this method is called on UI inflation, return false as the native is not ready.
        if (!mNativeInitialized) {
            return false;
        }

        // Never show Lens in the old search widget page context.
        // This widget must guarantee consistent feature set regardless of search engine choice or
        // other aspects that may not be met by Lens.
        if (!mEmbedderUiOverrides.isLensEntrypointAllowed()) {
            return false;
        }

        // When this method is called after native initialized, check omnibox conditions and Lens
        // eligibility.
        if (mIsTablet && mShouldShowButtonsWhenUnfocused) {
            return (mUrlHasFocus || mIsUrlFocusChangeInProgress) && isLensOnOmniboxEnabled();
        }

        return (mUrlHasFocus
                        || mIsUrlFocusChangeInProgress
                        || mIsLocationBarFocusedFromNtpScroll
                        || mShouldShowLensButtonWhenUnfocused)
                && isLensOnOmniboxEnabled();
    }

    private boolean isLensOnOmniboxEnabled() {
        if (sLastCachedIsLensOnOmniboxEnabled == null) {
            sLastCachedIsLensOnOmniboxEnabled =
                    Boolean.valueOf(isLensEnabled(LensEntryPoint.OMNIBOX));
        }

        return sLastCachedIsLensOnOmniboxEnabled.booleanValue();
    }

    private boolean shouldShowSaveOfflineButton() {
        assert mIsTablet;
        if (!mNativeInitialized || mLocationBarDataProvider == null) return false;
        Tab tab = mLocationBarDataProvider.getTab();
        if (tab == null) return false;
        // The save offline button should not be shown on native pages. Currently, trying to
        // save an offline page in incognito crashes, so don't show it on incognito either.
        return shouldShowPageActionButtons() && !tab.isOffTheRecord();
    }

    private boolean isSaveOfflineButtonEnabled() {
        if (mLocationBarDataProvider == null) return false;
        return mSaveOfflineButtonState.isEnabled(mLocationBarDataProvider.getTab());
    }

    private boolean shouldShowPageActionButtons() {
        assert mIsTablet;
        if (!mNativeInitialized) return true;

        // There are two actions, bookmark and save offline, and they should be shown if the
        // omnibox isn't focused.
        return !(mUrlHasFocus || mIsUrlFocusChangeInProgress);
    }

    private void updateUrl() {
        setUrl(mLocationBarDataProvider.getCurrentGurl(), mLocationBarDataProvider.getUrlBarData());
    }

    private void updateOmniboxPrerender() {
        if (mOmniboxPrerender == null) return;
        // Profile may be null if switching to a tab that has not yet been initialized.
        if (!mProfileSupplier.hasValue()) return;
        mOmniboxPrerender.clear(mProfileSupplier.get());
    }

    @SuppressLint("GestureBackNavigation")
    private boolean handleKeyEvent(View view, int keyCode, KeyEvent event) {
        boolean isRtl = view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
        if (mAutocompleteCoordinator.handleKeyEvent(keyCode, event)) {
            return true;
        } else if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
            if (KeyNavigationUtil.isActionDown(event) && event.getRepeatCount() == 0) {
                revertChanges();
                return true;
            }
        } else if ((!isRtl && KeyNavigationUtil.isGoRight(event))
                || (isRtl && KeyNavigationUtil.isGoLeft(event))) {
            // Ensures URL bar doesn't lose focus, when RIGHT or LEFT (RTL) key is pressed while
            // the cursor is positioned at the end of the text.
            TextView tv = (TextView) view;
            return tv.getSelectionStart() == tv.getSelectionEnd()
                    && tv.getSelectionEnd() == tv.getText().length();
        }
        return false;
    }

    private void updateSearchEngineStatusIconShownState() {
        // The search engine icon will be the first visible focused view when it's showing.
        boolean shouldShowSearchEngineLogo =
                mSearchEngineUtils == null || mSearchEngineUtils.shouldShowSearchEngineLogo();

        // This branch will be hit if the search engine logo should be shown.
        if (shouldShowSearchEngineLogo && mLocationBarLayout instanceof LocationBarPhone) {
            // When the search engine icon is enabled, icons are translations into the parent view's
            // padding area. Set clip padding to false to prevent them from getting clipped.
            mLocationBarLayout.setClipToPadding(false);
        }
    }

    // LocationBarData.Observer implementation.
    // Using the default empty onSecurityStateChanged.
    // Using the default empty onTitleChanged.

    @Override
    public void onIncognitoStateChanged() {
        sLastCachedIsLensOnOmniboxEnabled = Boolean.valueOf(isLensEnabled(LensEntryPoint.OMNIBOX));
        updateButtonVisibility();
        updateSearchEngineStatusIconShownState();
        // Update the visuals to use correct incognito colors.
        mUrlCoordinator.setIncognitoColorsEnabled(mLocationBarDataProvider.isIncognitoBranded());
    }

    @Override
    public void onNtpStartedLoading() {
        mLocationBarLayout.onNtpStartedLoading();
    }

    @Override
    public void onPrimaryColorChanged() {
        // Compute |mBrandedColorScheme| first.
        updateBrandedColorScheme();
        updateButtonTints();
    }

    @Override
    public void onUrlChanged() {
        updateUrl();
        updateOmniboxPrerender();
        updateButtonVisibility();
    }

    @Override
    public void hintZeroSuggestRefresh() {
        mAutocompleteCoordinator.prefetchZeroSuggestResults();
    }

    // TemplateUrlService.TemplateUrlServiceObserver implementation
    @Override
    public void onTemplateURLServiceChanged() {
        sLastCachedIsLensOnOmniboxEnabled = Boolean.valueOf(isLensEnabled(LensEntryPoint.OMNIBOX));
    }

    // OmniboxStub implementation.

    @Override
    public void setUrlBarFocus(boolean shouldBeFocused, @Nullable String pastedText, int reason) {
        boolean urlHasFocus = mUrlHasFocus;
        if (shouldBeFocused) {
            if (!urlHasFocus) {
                recordOmniboxFocusReason(reason);
                // Record Lens button shown when Omnibox is focused.
                if (shouldShowLensButton()) LensMetrics.recordOmniboxFocusedWhenLensShown();
            }

            if (reason == OmniboxFocusReason.FAKE_BOX_TAP
                    || reason == OmniboxFocusReason.FAKE_BOX_LONG_PRESS
                    || reason == OmniboxFocusReason.TASKS_SURFACE_FAKE_BOX_LONG_PRESS
                    || reason == OmniboxFocusReason.TASKS_SURFACE_FAKE_BOX_TAP) {
                mUrlFocusedFromFakebox = true;
            }

            mShouldClearOmniboxOnFocus = pastedText == null;

            if (urlHasFocus && mUrlFocusedWithoutAnimations) {
                handleUrlFocusAnimation(true);
            } else {
                mUrlCoordinator.requestFocus();
            }
        } else {
            assert pastedText == null;
            mUrlCoordinator.clearFocus();
        }

        if (pastedText != null) {
            // This must be happen after requestUrlFocus(), which changes the selection.
            mUrlCoordinator.setUrlBarData(
                    UrlBarData.forNonUrlText(pastedText),
                    UrlBar.ScrollType.NO_SCROLL,
                    UrlBarCoordinator.SelectionState.SELECT_END);
            /*
             When the URL bar text is programmatically set on omnibox state restoration during a
             device fold transition, {@code AutocompleteEditText#getTextWithoutAutocomplete()}
             invoked by {@code #forceOnTextChanged()} returns an empty string because {@code
             AutocompleteEditText#mModel} is not initialized. To trigger the autocomplete system in
             this case, {@code AutocompleteCoordinator#onTextChanged()} will be directly called on
             the restored omnibox text input.
            */
            if (reason == OmniboxFocusReason.FOLD_TRANSITION_RESTORATION) {
                mAutocompleteCoordinator.onTextChanged(pastedText);
            } else {
                forceOnTextChanged();
            }
        }
    }

    @Override
    public void performSearchQuery(String query, List<String> searchParams) {
        if (TextUtils.isEmpty(query)) return;

        String queryUrl =
                mTemplateUrlServiceSupplier.get().getUrlForSearchQuery(query, searchParams);

        if (!TextUtils.isEmpty(queryUrl)) {
            loadUrl(
                    new OmniboxLoadUrlParams.Builder(queryUrl, PageTransition.GENERATED)
                            .setOpenInNewTab(false)
                            .build());
        } else {
            setSearchQuery(query);
        }
    }

    @Override
    public @Nullable VoiceRecognitionHandler getVoiceRecognitionHandler() {
        return mVoiceRecognitionHandler;
    }

    @Override
    public void addUrlFocusChangeListener(UrlFocusChangeListener listener) {
        mUrlFocusChangeListeners.addObserver(listener);
    }

    @Override
    public void removeUrlFocusChangeListener(UrlFocusChangeListener listener) {
        mUrlFocusChangeListeners.removeObserver(listener);
    }

    @Override
    public boolean isUrlBarFocused() {
        return mUrlHasFocus;
    }

    @Override
    public void clearOmniboxFocus() {
        setUrlBarFocus(
                /* shouldBeFocused= */ false, /* pastedText= */ null, OmniboxFocusReason.UNFOCUS);
    }

    @Override
    public void notifyVoiceRecognitionCanceled() {}

    // VoiceRecognitionHandler.Delegate implementation.

    @Override
    public void loadUrlFromVoice(String url) {
        loadUrl(
                new OmniboxLoadUrlParams.Builder(url, PageTransition.TYPED)
                        .setOpenInNewTab(false)
                        .build());
    }

    @Override
    public void onVoiceAvailabilityImpacted() {
        updateButtonVisibility();
    }

    @Override
    public void setSearchQuery(String query) {
        if (TextUtils.isEmpty(query)) return;

        if (!mNativeInitialized) {
            mDeferredNativeRunnables.add(() -> setSearchQuery(query));
            return;
        }

        // Ensure the UrlBar has focus before entering text. If the UrlBar is not focused,
        // autocomplete text will be updated but the visible text will not.
        setUrlBarFocus(
                /* shouldBeFocused= */ true,
                /* pastedText= */ null,
                OmniboxFocusReason.SEARCH_QUERY);
        setUrlBarText(
                UrlBarData.forNonUrlText(query),
                UrlBar.ScrollType.NO_SCROLL,
                SelectionState.SELECT_ALL);
        mAutocompleteCoordinator.startAutocompleteForQuery(query);
        mUrlCoordinator.setKeyboardVisibility(true, false);
    }

    @Override
    public LocationBarDataProvider getLocationBarDataProvider() {
        return mLocationBarDataProvider;
    }

    @Override
    public AutocompleteCoordinator getAutocompleteCoordinator() {
        return mAutocompleteCoordinator;
    }

    @Override
    public WindowAndroid getWindowAndroid() {
        return mWindowAndroid;
    }

    // UrlBarDelegate implementation.

    @Nullable
    @Override
    public View getViewForUrlBackFocus() {
        assert mLocationBarDataProvider != null;
        Tab tab = mLocationBarDataProvider.getTab();
        return tab == null ? null : tab.getView();
    }

    @Override
    public boolean allowKeyboardLearning() {
        return !mLocationBarDataProvider.isOffTheRecord();
    }

    // Traditional way to intercept keycode_back, which is deprecated from T.
    @Override
    public void backKeyPressed() {
        if (mBackKeyBehavior.handleBackKeyPressed()) {
            return;
        }

        setUrlBarFocus(false, null, OmniboxFocusReason.UNFOCUS);
        // Revert the URL to match the current page.
        setUrl(mLocationBarDataProvider.getCurrentGurl(), mLocationBarDataProvider.getUrlBarData());
        focusCurrentTab();
    }

    @Override
    public void onFocusByTouch() {
        recordOmniboxFocusReason(OmniboxFocusReason.OMNIBOX_TAP);
    }

    @Override
    public void onTouchAfterFocus() {
        // The goal of this special logic is to support the following use case:
        // On opening the NTP, the URL bar gains focus with a blinking cursor and without showing
        // the zero-prefix dropdown when a hardware keyboard is connected. Subsequently, if the user
        // taps on the omnibox without typing any text into it, the zero-prefix dropdown will be
        // shown.
        //
        // A touch event will be handled after the omnibox is already focused only when the
        // following criteria are satisfied:
        // 1. |mUrlFocusedWithoutAnimations| is true, which means that the omnibox is focused on the
        // NTP without any focus animations while a hardware keyboard is connected.
        // 2. The omnibox does not contain any text. It is possible that the user has typed text
        // into the omnibox after it gains focus due to hardware keyboard availability and a
        // subsequent tap will hide the suggestions dropdown shown for the typed text, while keeping
        // the scrim on the web contents, which is not desirable.
        if (!TextUtils.isEmpty(mUrlCoordinator.getTextWithoutAutocomplete())) return;
        recordOmniboxFocusReason(OmniboxFocusReason.TAP_AFTER_FOCUS_FROM_KEYBOARD);
        completeUrlFocusAnimationAndEnableSuggestions();
    }

    /**
     * Trigger focus animations to adequately enable Autocomplete and Suggestions. This is required
     * only when the intention is to trigger the suggestions dropdown after the omnibox has gained
     * focus without animations.
     *
     * <p>This call trusts the caller has performed all necessary verifications, and will display
     * suggestions unconditionally.
     */
    /* package */ void completeUrlFocusAnimationAndEnableSuggestions() {
        if (!mUrlFocusedWithoutAnimations || mUrlCoordinator == null) return;
        handleUrlFocusAnimation(true);
    }

    // BackPressHandler implementation.
    // Modern way to intercept back press starting from T.
    @Override
    public @BackPressResult int handleBackPress() {
        int res = mUrlHasFocus ? BackPressResult.SUCCESS : BackPressResult.FAILURE;
        backKeyPressed();
        return res;
    }

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

    // OnKeyListener implementation.
    @Override
    public boolean onKey(View view, int keyCode, KeyEvent event) {
        return handleKeyEvent(view, keyCode, event);
    }

    // ComponentCallbacks implementation.

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        if (mUrlHasFocus
                && mUrlFocusedWithoutAnimations
                && newConfig.keyboard != Configuration.KEYBOARD_QWERTY) {
            // If we lose the hardware keyboard and the focus animations were not run, then the
            // user has not typed any text, so we will just clear the focus instead.
            setUrlBarFocus(
                    /* shouldBeFocused= */ false,
                    /* pastedText= */ null,
                    OmniboxFocusReason.UNFOCUS);
        }
    }

    @Override
    public void onLowMemory() {}

    @Override
    public boolean isLensEnabled(@LensEntryPoint int lensEntryPoint) {
        return mLensController.isLensEnabled(
                new LensQueryParams.Builder(
                                lensEntryPoint, mLocationBarDataProvider.isIncognito(), mIsTablet)
                        .build());
    }

    @Override
    public void startLens(@LensEntryPoint int lensEntryPoint) {
        // TODO(b/181067692): Report user action for this click.
        mLensController.startLens(
                mWindowAndroid,
                new LensIntentParams.Builder(lensEntryPoint, mLocationBarDataProvider.isIncognito())
                        .build());
    }

    // PauseResumeWithNativeObserver impl.
    @Override
    public void onResumeWithNative() {
        if (OmniboxFeatures.sUseFusedLocationProvider.isEnabled()
                && mProfileSupplier.hasValue()
                && mTemplateUrlServiceSupplier.hasValue()) {
            GeolocationHeader.primeLocationForGeoHeaderIfEnabled(
                    mProfileSupplier.get(), mTemplateUrlServiceSupplier.get());
        }
    }

    @Override
    public void onPauseWithNative() {
        if (OmniboxFeatures.sUseFusedLocationProvider.isEnabled()) {
            GeolocationHeader.stopListeningForLocationUpdates();
        }
    }

    /** Updates the tints of UI buttons. */
    private void updateButtonTints() {
        ColorStateList tint = ThemeUtils.getThemedToolbarIconTint(mContext, mBrandedColorScheme);
        mLocationBarLayout.setMicButtonTint(tint);
        mLocationBarLayout.setLensButtonTint(tint);
    }

    /**
     * Updates the color of the hint text in the search box.
     *
     * @param useDefaultUrlBarHintTextColor Whether to use the default color for the search text in
     *     the search box. If not we will use specific color for NTP's un-focus state.
     */
    public void updateUrlBarHintTextColor(boolean useDefaultUrlBarHintTextColor) {
        if (useDefaultUrlBarHintTextColor) {
            mUrlCoordinator.setUrlBarHintTextColorForDefault(mBrandedColorScheme);
        } else {
            mUrlCoordinator.setUrlBarHintTextColorForNtp();
        }
    }

    /**
     * Updates the value for the end margin of the url action container in the search box.
     *
     * @param useDefaultUrlActionContainerEndMargin Whether to use the default end margin for the
     *     url action container in the search box. If not we will use the specific end margin value
     *     for NTP's un-focus state.
     */
    public void updateUrlActionContainerEndMargin(boolean useDefaultUrlActionContainerEndMargin) {
        mLocationBarLayout.updateUrlActionContainerEndMargin(useDefaultUrlActionContainerEndMargin);
    }
}