chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediator.java

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

package org.chromium.chrome.browser.omnibox.suggestions;

import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Handler;
import android.os.SystemClock;
import android.text.TextUtils;
import android.view.View;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ActivityState;
import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.TopResumedActivityChangedObserver;
import org.chromium.chrome.browser.omnibox.DeferredIMEWindowInsetApplicationCallback;
import org.chromium.chrome.browser.omnibox.LocationBarDataProvider;
import org.chromium.chrome.browser.omnibox.OmniboxMetrics;
import org.chromium.chrome.browser.omnibox.OmniboxMetrics.RefineActionUsage;
import org.chromium.chrome.browser.omnibox.R;
import org.chromium.chrome.browser.omnibox.UrlBarEditingTextStateProvider;
import org.chromium.chrome.browser.omnibox.styles.OmniboxResourceProvider;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteController.OnSuggestionsReceivedListener;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteDelegate.AutocompleteLoadCallback;
import org.chromium.chrome.browser.omnibox.suggestions.action.OmniboxActionFactoryImpl;
import org.chromium.chrome.browser.omnibox.suggestions.action.OmniboxAnswerAction;
import org.chromium.chrome.browser.omnibox.suggestions.basic.BasicSuggestionProcessor.BookmarkState;
import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.Tab.LoadUrlResult;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tabmodel.TabWindowManager;
import org.chromium.chrome.browser.ui.theme.BrandedColorScheme;
import org.chromium.components.metrics.OmniboxEventProtos.OmniboxEventProto.PageClassification;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.AutocompleteResult;
import org.chromium.components.omnibox.OmniboxFeatures;
import org.chromium.components.omnibox.OmniboxSuggestionType;
import org.chromium.components.omnibox.action.OmniboxAction;
import org.chromium.components.omnibox.action.OmniboxActionDelegate;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.mojom.WindowOpenDisposition;
import org.chromium.url.GURL;

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

/** Handles updating the model state for the currently visible omnibox suggestions. */
class AutocompleteMediator
        implements OnSuggestionsReceivedListener,
                OmniboxSuggestionsDropdown.GestureObserver,
                OmniboxSuggestionsDropdownScrollListener,
                TopResumedActivityChangedObserver,
                SuggestionHost {
    private static final int SCHEDULE_FOR_IMMEDIATE_EXECUTION = -1;

    // Delay triggering the omnibox results upon key press to allow the location bar to repaint
    // with the new characters.
    private static final long OMNIBOX_SUGGESTION_START_DELAY_MS = 30;

    private final @NonNull Context mContext;
    private final @NonNull AutocompleteDelegate mDelegate;
    private final @NonNull UrlBarEditingTextStateProvider mUrlBarEditingTextProvider;
    private final @NonNull PropertyModel mListPropertyModel;
    private final @NonNull ModelList mSuggestionModels;
    private final @NonNull Handler mHandler;
    private final @NonNull LocationBarDataProvider mDataProvider;
    private final @NonNull Supplier<ModalDialogManager> mModalDialogManagerSupplier;
    private final @NonNull DropdownItemViewInfoListBuilder mDropdownViewInfoListBuilder;
    private final @NonNull DropdownItemViewInfoListManager mDropdownViewInfoListManager;
    private final @NonNull Callback<Tab> mBringTabToFrontCallback;
    private final @NonNull Supplier<TabWindowManager> mTabWindowManagerSupplier;
    private final @NonNull OmniboxActionDelegate mOmniboxActionDelegate;
    private final @NonNull ActivityLifecycleDispatcher mLifecycleDispatcher;
    private final @NonNull SuggestionsListAnimationDriver mAnimationDriver;
    private final @NonNull WindowAndroid mWindowAndroid;
    private final @NonNull DeferredIMEWindowInsetApplicationCallback
            mDeferredIMEWindowInsetApplicationCallback;

    private @NonNull Optional<AutocompleteController> mAutocomplete = Optional.empty();
    private @NonNull Optional<AutocompleteResult> mAutocompleteResult = Optional.empty();
    private @NonNull Optional<Runnable> mCurrentAutocompleteRequest = Optional.empty();
    private @NonNull Optional<Runnable> mDeferredLoadAction = Optional.empty();
    private @NonNull Optional<PropertyModel> mDeleteDialogModel = Optional.empty();

    private boolean mNativeInitialized;
    private boolean mIsInZeroPrefixContext;
    private long mUrlFocusTime;
    // When set, indicates an active omnibox session.
    private boolean mIsActive;
    // When set, specifies the system time of the most recent suggestion list request.
    private Long mLastSuggestionRequestTime;
    // When set, specifies the time when the suggestion list was shown the first time.
    // Suggestions are refreshed several times per keystroke.
    private Long mFirstSuggestionListModelCreatedTime;
    private OptionalInt mPageClassification = OptionalInt.empty();

    @IntDef({
        EditSessionState.INACTIVE,
        EditSessionState.ACTIVATED_BY_USER_INPUT,
        EditSessionState.ACTIVATED_BY_QUERY_TILE
    })
    @Retention(RetentionPolicy.SOURCE)
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    @interface EditSessionState {
        int INACTIVE = 0; // Omnibox is not being edited.
        int ACTIVATED_BY_USER_INPUT = 1; // The edit session is triggered by user input.
        int ACTIVATED_BY_QUERY_TILE = 2; // The edit session is triggered from query tile.
    }

    private @EditSessionState int mEditSessionState = EditSessionState.INACTIVE;

    private @RefineActionUsage int mRefineActionUsage = RefineActionUsage.NOT_USED;

    // The timestamp (using SystemClock.elapsedRealtime()) at the point when the user started
    // modifying the omnibox with new input.
    private long mNewOmniboxEditSessionTimestamp = -1;
    // Set at the end of the Omnibox interaction to indicate whether the user selected an item
    // from the list (true) or left the Omnibox and suggestions list with no action taken (false).
    private boolean mOmniboxFocusResultedInNavigation;
    // Facilitate detection of Autocomplete actions being scheduled from an Autocomplete action.
    private boolean mIsExecutingAutocompleteAction;
    // Whether user scrolled the suggestions list.
    private boolean mSuggestionsListScrolled;

    /**
     * The text shown in the URL bar (user text + inline autocomplete) after the most recent set of
     * omnibox suggestions was received. When the user presses enter in the omnibox, this value is
     * compared to the URL bar text to determine whether the first suggestion is still valid.
     */
    private String mUrlTextAfterSuggestionsReceived;

    private boolean mShouldPreventOmniboxAutocomplete;
    private long mLastActionUpTimestamp;
    private boolean mIgnoreOmniboxItemSelection = true;

    // The number of touch down events sent to native during an omnibox session.
    private int mNumTouchDownEventForwardedInOmniboxSession;
    // The number of prefetches that were started from touch down events during an omnibox session.
    private int mNumPrefetchesStartedInOmniboxSession;
    // The suggestion that the last prefetch was started for within the current omnibox session.
    private @NonNull Optional<AutocompleteMatch> mLastPrefetchStartedSuggestion = Optional.empty();

    // Observer watching for changes to the visual state of the omnibox suggestions.
    private @NonNull Optional<AutocompleteCoordinator.OmniboxSuggestionsVisualStateObserver>
            mOmniboxSuggestionsVisualStateObserver = Optional.empty();

    public AutocompleteMediator(
            @NonNull Context context,
            @NonNull AutocompleteDelegate delegate,
            @NonNull UrlBarEditingTextStateProvider textProvider,
            @NonNull PropertyModel listPropertyModel,
            @NonNull Handler handler,
            @NonNull Supplier<ModalDialogManager> modalDialogManagerSupplier,
            @NonNull Supplier<Tab> activityTabSupplier,
            @Nullable Supplier<ShareDelegate> shareDelegateSupplier,
            @NonNull LocationBarDataProvider locationBarDataProvider,
            @NonNull Callback<Tab> bringTabToFrontCallback,
            @NonNull Supplier<TabWindowManager> tabWindowManagerSupplier,
            @NonNull BookmarkState bookmarkState,
            @NonNull OmniboxActionDelegate omniboxActionDelegate,
            @NonNull ActivityLifecycleDispatcher lifecycleDispatcher,
            @NonNull OmniboxSuggestionsDropdownEmbedder embedder,
            @NonNull WindowAndroid windowAndroid,
            @NonNull
                    DeferredIMEWindowInsetApplicationCallback
                            deferredIMEWindowInsetApplicationCallback) {
        mContext = context;
        mDelegate = delegate;
        mUrlBarEditingTextProvider = textProvider;
        mListPropertyModel = listPropertyModel;
        mModalDialogManagerSupplier = modalDialogManagerSupplier;
        mHandler = handler;
        mDataProvider = locationBarDataProvider;
        mBringTabToFrontCallback = bringTabToFrontCallback;
        mTabWindowManagerSupplier = tabWindowManagerSupplier;
        mSuggestionModels = mListPropertyModel.get(SuggestionListProperties.SUGGESTION_MODELS);
        mOmniboxActionDelegate = omniboxActionDelegate;
        mWindowAndroid = windowAndroid;
        mDropdownViewInfoListBuilder =
                new DropdownItemViewInfoListBuilder(activityTabSupplier, bookmarkState);
        mDropdownViewInfoListBuilder.setShareDelegateSupplier(shareDelegateSupplier);
        mDropdownViewInfoListManager =
                new DropdownItemViewInfoListManager(mSuggestionModels, context);
        OmniboxResourceProvider.invalidateDrawableCache();
        mLifecycleDispatcher = lifecycleDispatcher;
        mLifecycleDispatcher.register(this);
        mDeferredIMEWindowInsetApplicationCallback = deferredIMEWindowInsetApplicationCallback;

        var pm = context.getPackageManager();
        var dialIntent = new Intent(Intent.ACTION_DIAL);
        OmniboxActionFactoryImpl.get()
                .setDialerAvailable(!pm.queryIntentActivities(dialIntent, 0).isEmpty());
        mListPropertyModel.set(
                SuggestionListProperties.DRAW_OVER_ANCHOR,
                DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext));
        int addedVerticalOffset =
                context.getResources()
                        .getDimensionPixelOffset(
                                R.dimen.omnibox_suggestion_list_animation_added_vertical_offset);
        mAnimationDriver =
                new SuggestionsListAnimationDriver(
                        windowAndroid,
                        mListPropertyModel,
                        embedder::getVerticalTranslationForAnimation,
                        () -> propagateOmniboxSessionStateChange(true),
                        addedVerticalOffset);
    }

    /**
     * Sets the observer watching the state of the omnibox suggestions. This observer will be
     * notifying of visual changes to the omnibox suggestions view, such as visibility or background
     * color changes.
     */
    void setOmniboxSuggestionsVisualStateObserver(
            Optional<AutocompleteCoordinator.OmniboxSuggestionsVisualStateObserver>
                    omniboxSuggestionsVisualStateObserver) {
        assert omniboxSuggestionsVisualStateObserver != null;
        mOmniboxSuggestionsVisualStateObserver = omniboxSuggestionsVisualStateObserver;
    }

    /** Initialize the Mediator with default set of suggestion processors. */
    void initDefaultProcessors() {
        mDropdownViewInfoListBuilder.initDefaultProcessors(
                mContext, this, mUrlBarEditingTextProvider);
    }

    /**
     * @return DropdownItemViewInfoListBuilder instance used to convert OmniboxSuggestions to list
     *     of ViewInfos.
     */
    DropdownItemViewInfoListBuilder getDropdownItemViewInfoListBuilderForTest() {
        return mDropdownViewInfoListBuilder;
    }

    public void destroy() {
        stopAutocomplete(false);
        mAutocomplete.ifPresent(a -> a.removeOnSuggestionsReceivedListener(this));

        if (mNativeInitialized) {
            OmniboxActionFactoryImpl.get().destroyNativeFactory();
        }
        mHandler.removeCallbacks(null);
        mDropdownViewInfoListBuilder.destroy();
        mLifecycleDispatcher.unregister(this);
    }

    /**
     * @return The ModelList for currently shown suggestions.
     */
    ModelList getSuggestionModelListForTest() {
        return mSuggestionModels;
    }

    /**
     * Check if the suggestion is created from clipboard.
     *
     * @param suggestion The AutocompleteMatch to check.
     * @return Whether or not the suggestion is from clipboard.
     */
    private boolean isSuggestionFromClipboard(AutocompleteMatch suggestion) {
        return suggestion.getType() == OmniboxSuggestionType.CLIPBOARD_URL
                || suggestion.getType() == OmniboxSuggestionType.CLIPBOARD_TEXT
                || suggestion.getType() == OmniboxSuggestionType.CLIPBOARD_IMAGE;
    }

    /**
     * @return The number of current autocomplete suggestions.
     */
    public int getSuggestionCount() {
        return mAutocompleteResult.map(r -> r.getSuggestionsList().size()).orElse(0);
    }

    /**
     * Retrieve the omnibox suggestion at the specified index. The index represents the ordering in
     * the underlying model. The index does not represent visibility due to the current scroll
     * position of the list.
     *
     * @param matchIndex The index of the suggestion to fetch.
     * @return The suggestion at the given index.
     */
    public AutocompleteMatch getSuggestionAt(int matchIndex) {
        return mAutocompleteResult.map(r -> r.getSuggestionsList().get(matchIndex)).orElse(null);
    }

    /**
     * Sets the layout direction to be used for any new suggestion views.
     *
     * @see View#setLayoutDirection(int)
     */
    void setLayoutDirection(int layoutDirection) {
        mDropdownViewInfoListManager.setLayoutDirection(layoutDirection);
    }

    /**
     * Specifies the visual state to be used by the suggestions.
     *
     * @param brandedColorScheme The {@link @BrandedColorScheme}.
     */
    void updateVisualsForState(@BrandedColorScheme int brandedColorScheme) {
        mDropdownViewInfoListManager.setBrandedColorScheme(brandedColorScheme);
        mListPropertyModel.set(SuggestionListProperties.COLOR_SCHEME, brandedColorScheme);
        mOmniboxSuggestionsVisualStateObserver.ifPresent(
                (observer) ->
                        observer.onOmniboxSuggestionsBackgroundColorChanged(
                                OmniboxResourceProvider
                                        .getSuggestionsDropdownBackgroundColorForColorScheme(
                                                mContext, brandedColorScheme)));
    }

    /**
     * Show cached zero suggest results. Enables Autocomplete subsystem to offer most recently
     * presented suggestions in the event where Native counterpart is not yet initialized.
     *
     * <p>Note: the only supported page context right now is the ANDROID_SEARCH_WIDGET.
     */
    void startCachedZeroSuggest() {
        maybeServeCachedResult();
        postAutocompleteRequest(this::startZeroSuggest, SCHEDULE_FOR_IMMEDIATE_EXECUTION);
    }

    private void maybeCacheResult(@NonNull AutocompleteResult result) {
        if (mIsInZeroPrefixContext
                && !result.isFromCachedResult()
                && mDataProvider.getPageClassification(false)
                        == PageClassification.ANDROID_SEARCH_WIDGET_VALUE) {
            CachedZeroSuggestionsManager.saveToCache(result);
        }
    }

    private void maybeServeCachedResult() {
        int pageClass = mDataProvider.getPageClassification(false);
        if (mAutocomplete.isPresent()
                || !mIsInZeroPrefixContext
                || (pageClass != PageClassification.ANDROID_SEARCH_WIDGET_VALUE
                        && pageClass != PageClassification.ANDROID_SHORTCUTS_WIDGET_VALUE)) {
            return;
        }
        onSuggestionsReceived(CachedZeroSuggestionsManager.readFromCache(), true);
    }

    /** Notify the mediator that a item selection is pending and should be accepted. */
    void allowPendingItemSelection() {
        mIgnoreOmniboxItemSelection = false;
    }

    /** Signals that native initialization has completed. */
    void onNativeInitialized() {
        mNativeInitialized = true;
        OmniboxActionFactoryImpl.get().initNativeFactory();
        mDropdownViewInfoListManager.onNativeInitialized();
        mDropdownViewInfoListBuilder.onNativeInitialized();
        runPendingAutocompleteRequests();
    }

    /**
     * Take necessary action to update the autocomplete system state and record metrics when the
     * omnibox session state changes.
     *
     * @param activated Whether the autocomplete session should be activated when the omnibox
     *     session state changes, {@code true} if this will be activated, {@code false} otherwise.
     */
    void onOmniboxSessionStateChange(boolean activated) {
        if (mIsActive == activated) return;
        mIsActive = activated;

        // Propagate the information about omnibox session state change to all the processors first.
        // Processors need this for accounting purposes.
        // The change information should be passed before Processors receive first
        // batch of suggestions, that is:
        // - before any call to startZeroSuggest() (when first suggestions are populated), and
        // - before stopAutocomplete() (when current suggestions are erased).
        mDropdownViewInfoListBuilder.onOmniboxSessionStateChange(activated);

        if (OmniboxFeatures.shouldAnimateSuggestionsListAppearance()) {
            mAnimationDriver.onOmniboxSessionStateChange(activated);
            if (activated) {
                mDelegate.setKeyboardVisibility(true, false);
            }
        }

        if (activated) {
            mDeferredIMEWindowInsetApplicationCallback.attach(mWindowAndroid);
            dismissDeleteDialog(DialogDismissalCause.DISMISSED_BY_NATIVE);
            mRefineActionUsage = RefineActionUsage.NOT_USED;
            mOmniboxFocusResultedInNavigation = false;
            mSuggestionsListScrolled = false;
            mPageClassification = OptionalInt.of(mDataProvider.getPageClassification(false));
            mUrlFocusTime = System.currentTimeMillis();

            // Ask directly for zero-suggestions related to current input, unless the user is
            // currently visiting SearchActivity and the input is populated from the launch intent.
            // In all contexts, the input will most likely be empty, triggering the same response
            // (starting zero suggestions), but if the SearchActivity was launched with a QUERY,
            // then the query might point to a different URL than the reported Page, and the
            // suggestion would take the user to the DSE home page.
            // This is tracked by MobileStartup.LaunchCause / EXTERNAL_SEARCH_ACTION_INTENT
            // metric.
            String text = mUrlBarEditingTextProvider.getTextWithoutAutocomplete();
            onTextChanged(text);
        } else {
            mDeferredIMEWindowInsetApplicationCallback.detach();
            stopMeasuringSuggestionRequestToUiModelTime();
            cancelAutocompleteRequests();
            OmniboxMetrics.recordOmniboxFocusResultedInNavigation(
                    mOmniboxFocusResultedInNavigation);
            OmniboxMetrics.recordRefineActionUsage(mRefineActionUsage);
            OmniboxMetrics.recordSuggestionsListScrolled(
                    mPageClassification.getAsInt(), mSuggestionsListScrolled);
            mPageClassification = OptionalInt.empty();

            // Reset the per omnibox session state of touch down prefetch.
            OmniboxMetrics.recordNumPrefetchesStartedInOmniboxSession(
                    mNumPrefetchesStartedInOmniboxSession);
            mNumTouchDownEventForwardedInOmniboxSession = 0;
            mNumPrefetchesStartedInOmniboxSession = 0;
            mLastPrefetchStartedSuggestion = Optional.empty();

            mEditSessionState = EditSessionState.INACTIVE;
            mNewOmniboxEditSessionTimestamp = -1;
            // Prevent any upcoming omnibox suggestions from showing once a URL is loaded (and as
            // a consequence the omnibox is unfocused).
            clearSuggestions();
        }
    }

    /**
     * @see
     *     org.chromium.chrome.browser.omnibox.UrlFocusChangeListener#onUrlAnimationFinished(boolean)
     */
    void onUrlAnimationFinished(boolean hasFocus) {
        propagateOmniboxSessionStateChange(hasFocus);
    }

    /**
     * Updates the profile used for generating autocomplete suggestions.
     *
     * @param profile The profile to be used.
     */
    void setAutocompleteProfile(Profile profile) {
        stopAutocomplete(true);
        mAutocomplete.ifPresent(a -> a.removeOnSuggestionsReceivedListener(this));
        mAutocomplete = AutocompleteController.getForProfile(profile);
        mAutocomplete.ifPresent(a -> a.addOnSuggestionsReceivedListener(this));
        mDropdownViewInfoListBuilder.setProfile(profile);
        runPendingAutocompleteRequests();
    }

    /** Whether omnibox autocomplete should currently be prevented from generating suggestions. */
    void setShouldPreventOmniboxAutocomplete(boolean prevent) {
        mShouldPreventOmniboxAutocomplete = prevent;
    }

    /**
     * @see AutocompleteController#onVoiceResults(List)
     */
    void onVoiceResults(@Nullable List<VoiceRecognitionHandler.VoiceResult> results) {
        mAutocomplete.ifPresent(a -> a.onVoiceResults(results));
    }

    /**
     * TODO(crbug.com/40725530): Figure out how to remove this.
     *
     * @return The current native pointer to the autocomplete results.
     */
    long getCurrentNativeAutocompleteResult() {
        return mAutocompleteResult.map(r -> r.getNativeObjectRef()).orElse(0L);
    }

    /**
     * Triggered when the user selects one of the omnibox suggestions to navigate to.
     *
     * @param suggestion The AutocompleteMatch which was selected.
     * @param matchIndex Position of the suggestion in the drop down view.
     * @param url The URL associated with the suggestion.
     */
    @Override
    public void onSuggestionClicked(
            @NonNull AutocompleteMatch suggestion, int matchIndex, @NonNull GURL url) {
        mDeferredLoadAction =
                Optional.of(
                        () ->
                                loadUrlForOmniboxMatch(
                                        matchIndex,
                                        suggestion,
                                        url,
                                        mLastActionUpTimestamp,
                                        /* openInNewTab= */ false,
                                        true));

        // Note: Action will be reset when load is initiated.
        mAutocomplete.ifPresent(a -> mDeferredLoadAction.get().run());
    }

    /**
     * Triggered when the user touches down on a search suggestion.
     *
     * @param suggestion The AutocompleteMatch which was selected.
     * @param matchIndex Position of the suggestion in the drop down view.
     */
    @Override
    public void onSuggestionTouchDown(@NonNull AutocompleteMatch suggestion, int matchIndex) {
        if (mAutocomplete.isEmpty()
                || mNumTouchDownEventForwardedInOmniboxSession
                        >= OmniboxFeatures.getMaxPrefetchesPerOmniboxSession()) {
            return;
        }
        mNumTouchDownEventForwardedInOmniboxSession++;

        var tab = mDataProvider.getTab();
        WebContents webContents = tab != null ? tab.getWebContents() : null;
        boolean wasPrefetchStarted =
                mAutocomplete
                        .map(a -> a.onSuggestionTouchDown(suggestion, matchIndex, webContents))
                        .orElse(false);
        if (wasPrefetchStarted) {
            mNumPrefetchesStartedInOmniboxSession++;
            mLastPrefetchStartedSuggestion = Optional.of(suggestion);
        }
    }

    @Override
    public void onOmniboxActionClicked(@NonNull OmniboxAction action, int position) {
        if (action instanceof OmniboxAnswerAction omniboxAnswerAction) {
            Optional<AutocompleteMatch> associatedSuggestion =
                    mAutocompleteResult
                            .map(AutocompleteResult::getSuggestionsList)
                            .map((list) -> list.get(position));
            if (!associatedSuggestion.isPresent()) {
                return;
            }

            // Allow the action to record execution-related metrics before we navigate away.
            action.execute(mOmniboxActionDelegate);
            // onSuggestionClicked will post a call to finishInteraction, so we don't need to call
            // it immediately.
            loadUrlForOmniboxMatch(
                    0,
                    associatedSuggestion.get(),
                    mAutocomplete
                            .map(
                                    a ->
                                            a.getAnswerActionDestinationURL(
                                                    associatedSuggestion.get(),
                                                    mLastActionUpTimestamp,
                                                    omniboxAnswerAction))
                            .orElse(associatedSuggestion.get().getUrl()),
                    getElapsedTimeSinceInputChange(),
                    false,
                    false);
        } else {
            action.execute(mOmniboxActionDelegate);
            finishInteraction();
        }
    }

    /**
     * Triggered when the user selects to refine one of the omnibox suggestions.
     *
     * @param suggestion The suggestion selected.
     */
    @Override
    public void onRefineSuggestion(@NonNull AutocompleteMatch suggestion) {
        stopAutocomplete(false);
        boolean isSearchSuggestion = suggestion.isSearchSuggestion();
        boolean isZeroPrefix =
                TextUtils.isEmpty(mUrlBarEditingTextProvider.getTextWithoutAutocomplete());
        String refineText = suggestion.getFillIntoEdit();
        if (isSearchSuggestion) refineText = TextUtils.concat(refineText, " ").toString();

        mDelegate.setOmniboxEditingText(refineText);
        onTextChanged(mUrlBarEditingTextProvider.getTextWithoutAutocomplete());

        if (isSearchSuggestion) {
            // Note: the logic below toggles assumes individual values to be represented by
            // individual bits. This allows proper reporting of different refine button uses
            // during single interaction with the Omnibox.
            mRefineActionUsage |=
                    isZeroPrefix
                            ? RefineActionUsage.SEARCH_WITH_ZERO_PREFIX
                            : RefineActionUsage.SEARCH_WITH_PREFIX;
        }
    }

    @Override
    public void onSwitchToTab(@NonNull AutocompleteMatch match, int matchIndex) {
        if (maybeSwitchToTab(match)) {
            recordMetrics(match, matchIndex, WindowOpenDisposition.SWITCH_TO_TAB);
        } else {
            onSuggestionClicked(match, matchIndex, match.getUrl());
        }
    }

    @VisibleForTesting
    public boolean maybeSwitchToTab(AutocompleteMatch match) {
        Tab tab = mAutocomplete.map(a -> a.getMatchingTabForSuggestion(match)).orElse(null);
        if (tab == null || !mTabWindowManagerSupplier.hasValue()) return false;

        // When invoked directly from a browser, we want to trigger switch to tab animation.
        // If invoked from other activities, ex. searchActivity, we do not need to trigger the
        // animation since Android will show the animation for switching apps.
        if (tab.getWindowAndroid().getActivityState() == ActivityState.STOPPED
                || tab.getWindowAndroid().getActivityState() == ActivityState.DESTROYED) {
            mBringTabToFrontCallback.onResult(tab);
            return true;
        }

        TabModel tabModel = mTabWindowManagerSupplier.get().getTabModelForTab(tab);
        if (tabModel == null) return false;

        int tabIndex = TabModelUtils.getTabIndexById(tabModel, tab.getId());
        // In the event the user deleted the tab as part during the interaction with the
        // Omnibox, reject the switch to tab action.
        if (tabIndex == TabModel.INVALID_TAB_INDEX) return false;
        tabModel.setIndex(tabIndex, TabSelectionType.FROM_OMNIBOX);
        return true;
    }

    @Override
    public void onGesture(boolean isGestureUp, long timestamp) {
        stopAutocomplete(false);
        if (isGestureUp) {
            mLastActionUpTimestamp = timestamp;
        }
    }

    /**
     * Triggered when the user long presses the omnibox suggestion.
     *
     * @param suggestion The suggestion selected.
     * @param titleText The title to display in the delete dialog.
     */
    @Override
    public void onDeleteMatch(@NonNull AutocompleteMatch suggestion, @NonNull String titleText) {
        showDeleteDialog(
                suggestion,
                titleText,
                () -> mAutocomplete.ifPresent(a -> a.deleteMatch(suggestion)));
    }

    /**
     * Triggered when the user long presses the omnibox suggestion element (eg. a tile).
     *
     * @param suggestion The suggestion selected.
     * @param titleText The title to display in the delete dialog.
     * @param elementIndex The element of the suggestion to be deleted.
     */
    @Override
    public void onDeleteMatchElement(
            @NonNull AutocompleteMatch suggestion, @NonNull String titleText, int elementIndex) {
        showDeleteDialog(
                suggestion,
                titleText,
                () -> mAutocomplete.ifPresent(a -> a.deleteMatchElement(suggestion, elementIndex)));
    }

    /** Terminate the interaction with the Omnibox. */
    @Override
    public void finishInteraction() {
        mDelegate.clearOmniboxFocus();
    }

    public void showDeleteDialog(
            @NonNull AutocompleteMatch suggestion,
            @NonNull String titleText,
            Runnable deleteAction) {
        RecordUserAction.record("MobileOmniboxDeleteGesture");

        // Prevent updates to the shown omnibox suggestions list while the dialog is open.
        // Each update invalidates previous result set, making it impossible to perform the delete
        // action (there is no native match to delete). Calling `stopAutocomplete()` here will
        // ensure that suggestions don't change the moment the User is presented with the dialog,
        // allowing us to complete the deletion.
        stopAutocomplete(/* clear= */ false);
        if (!suggestion.isDeletable()) return;
        // Do not attempt to delete matches that have been detached from their native counterpart.
        // These matches likely come from cache, or the delete request came for a previous set of
        // matches.
        if (suggestion.getNativeObjectRef() == 0) return;

        ModalDialogManager manager = mModalDialogManagerSupplier.get();
        if (manager == null) {
            assert false : "No modal dialog manager registered for this activity.";
            return;
        }

        ModalDialogProperties.Controller dialogController =
                new ModalDialogProperties.Controller() {
                    @Override
                    public void onClick(PropertyModel model, int buttonType) {
                        if (buttonType == ModalDialogProperties.ButtonType.POSITIVE) {
                            RecordUserAction.record("MobileOmniboxDeleteRequested");
                            deleteAction.run();
                            manager.dismissDialog(
                                    model, DialogDismissalCause.POSITIVE_BUTTON_CLICKED);
                        } else if (buttonType == ModalDialogProperties.ButtonType.NEGATIVE) {
                            manager.dismissDialog(
                                    model, DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
                        }
                    }

                    @Override
                    public void onDismiss(PropertyModel model, int dismissalCause) {
                        mDeleteDialogModel = Optional.empty();
                    }
                };

        Resources resources = mContext.getResources();
        @StringRes int dialogMessageId = R.string.omnibox_confirm_delete;
        if (isSuggestionFromClipboard(suggestion)) {
            dialogMessageId = R.string.omnibox_confirm_delete_from_clipboard;
        }

        mDeleteDialogModel =
                Optional.of(
                        new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                                .with(ModalDialogProperties.CONTROLLER, dialogController)
                                .with(ModalDialogProperties.TITLE, titleText)
                                .with(ModalDialogProperties.TITLE_MAX_LINES, 1)
                                .with(
                                        ModalDialogProperties.MESSAGE_PARAGRAPH_1,
                                        resources.getString(dialogMessageId))
                                .with(
                                        ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                        resources,
                                        R.string.ok)
                                .with(
                                        ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                        resources,
                                        R.string.cancel)
                                .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true)
                                .build());

        manager.showDialog(mDeleteDialogModel.get(), ModalDialogManager.ModalDialogType.APP);
    }

    /**
     * Dismiss the delete suggestion dialog if it is showing.
     *
     * @param cause The cause of dismiss.
     */
    private void dismissDeleteDialog(@DialogDismissalCause int cause) {
        var manager = mModalDialogManagerSupplier.get();
        mDeleteDialogModel.ifPresent(model -> manager.dismissDialog(model, cause));
    }

    /**
     * Triggered when the user navigates to one of the suggestions without clicking on it.
     *
     * @param text The text to be displayed in the Omnibox.
     */
    @Override
    public void setOmniboxEditingText(@NonNull String text) {
        if (mIgnoreOmniboxItemSelection) return;
        mIgnoreOmniboxItemSelection = true;
        mDelegate.setOmniboxEditingText(text);
    }

    /**
     * Updates the URL we will navigate to from suggestion, if needed. This will update the search
     * URL to be of the corpus type if query in the omnibox is displayed and update gs_lcrp=
     * parameter on regular web search URLs.
     *
     * @param suggestion The chosen omnibox suggestion.
     * @param matchIndex The index of the chosen omnibox suggestion.
     * @param url The URL associated with the suggestion to navigate to.
     * @return The url to navigate to.
     */
    private @NonNull GURL updateSuggestionUrlIfNeeded(
            @NonNull AutocompleteMatch suggestion, int matchIndex, @NonNull GURL url) {
        if (mAutocomplete.isEmpty()) return url;
        // TODO(crbug.com/40279214): this should exclude TILE variants when horizontal render group
        // is
        // ready.
        if (suggestion.getType() == OmniboxSuggestionType.TILE_NAVSUGGEST) {
            return url;
        }

        return mAutocomplete
                .map(
                        a ->
                                a.updateMatchDestinationUrlWithQueryFormulationTime(
                                        suggestion, getElapsedTimeSinceInputChange()))
                .orElse(url);
    }

    /**
     * Notifies the autocomplete system that the text has changed that drives autocomplete and the
     * autocomplete suggestions should be updated.
     */
    public void onTextChanged(@NonNull String textWithoutAutocomplete) {
        if (mShouldPreventOmniboxAutocomplete) return;

        mIgnoreOmniboxItemSelection = true;
        cancelAutocompleteRequests();

        if (mEditSessionState == EditSessionState.INACTIVE) {
            mAutocomplete.ifPresent(a -> a.resetSession());
            mNewOmniboxEditSessionTimestamp = SystemClock.elapsedRealtime();
            mEditSessionState = EditSessionState.ACTIVATED_BY_USER_INPUT;
        }

        stopAutocomplete(false);
        mIsInZeroPrefixContext = TextUtils.isEmpty(textWithoutAutocomplete);

        if (mIsInZeroPrefixContext) {
            clearSuggestions();
            startCachedZeroSuggest();
        } else if (mDataProvider.hasTab()) {
            boolean preventAutocomplete = !mUrlBarEditingTextProvider.shouldAutocomplete();
            int cursorPosition =
                    mUrlBarEditingTextProvider.getSelectionStart()
                                    == mUrlBarEditingTextProvider.getSelectionEnd()
                            ? mUrlBarEditingTextProvider.getSelectionStart()
                            : -1;
            GURL currentUrl = mDataProvider.getCurrentGurl();

            postAutocompleteRequest(
                    () -> {
                        if (!mPageClassification.isPresent()) return;
                        startMeasuringSuggestionRequestToUiModelTime();
                        mAutocomplete.ifPresent(
                                a ->
                                        a.start(
                                                currentUrl,
                                                mPageClassification.getAsInt(),
                                                textWithoutAutocomplete,
                                                cursorPosition,
                                                preventAutocomplete));
                    },
                    OMNIBOX_SUGGESTION_START_DELAY_MS);
        }

        mDelegate.onUrlTextChanged();
    }

    @Override
    public void onSuggestionsReceived(
            @NonNull AutocompleteResult autocompleteResult, boolean isFinal) {
        // Reject results if the current session is inactive.
        if (!mIsActive) return;

        maybeCacheResult(autocompleteResult);

        @Nullable AutocompleteMatch defaultMatch = autocompleteResult.getDefaultMatch();
        String inlineAutocompleteText =
                defaultMatch != null ? defaultMatch.getInlineAutocompletion() : "";

        String userText = mUrlBarEditingTextProvider.getTextWithoutAutocomplete();
        mUrlTextAfterSuggestionsReceived = userText + inlineAutocompleteText;

        if (!mAutocompleteResult.map(r -> r.equals(autocompleteResult)).orElse(false)) {
            mAutocompleteResult = Optional.of(autocompleteResult);
            var viewInfoList =
                    mDropdownViewInfoListBuilder.buildDropdownViewInfoList(autocompleteResult);
            mDropdownViewInfoListManager.setSourceViewInfoList(viewInfoList);
            if (mIsActive) {
                mDelegate.onSuggestionsChanged(defaultMatch);
            }
        }

        mListPropertyModel.set(SuggestionListProperties.LIST_IS_FINAL, isFinal);
        measureSuggestionRequestToUiModelTime(isFinal);
    }

    /**
     * Load the url corresponding to the typed omnibox text.
     *
     * @param eventTime The timestamp the load was triggered by the user.
     * @param openInNewTab Whether the URL will be loaded in a new tab. If {@code true}, the URL
     *     will be loaded in a new tab. If {@code false}, The URL will be loaded in the current tab.
     */
    void loadTypedOmniboxText(long eventTime, boolean openInNewTab) {
        final String urlText = mUrlBarEditingTextProvider.getTextWithAutocomplete();
        cancelAutocompleteRequests();
        if (mAutocomplete.isPresent()) {
            findMatchAndLoadUrl(urlText, eventTime, openInNewTab);
        } else {
            mDeferredLoadAction =
                    Optional.of(() -> findMatchAndLoadUrl(urlText, eventTime, openInNewTab));
        }
    }

    /**
     * Search for a suggestion with the same associated URL as the supplied one.
     *
     * @param urlText The URL text to search for.
     * @param inputStart The timestamp the load was triggered by the user.
     * @param openInNewTab Whether the URL will be loaded in a new tab. If {@code true}, the URL
     *     will be loaded in a new tab. If {@code false}, The URL will be loaded in the current tab.
     */
    private void findMatchAndLoadUrl(
            @NonNull String urlText, long inputStart, boolean openInNewTab) {
        AutocompleteMatch suggestionMatch;

        if (getSuggestionCount() > 0
                && urlText.trim().equals(mUrlTextAfterSuggestionsReceived.trim())) {
            // Common case: the user typed something, received suggestions, then pressed enter.
            // This triggers the Default Match.
            suggestionMatch = getSuggestionAt(0);
        } else {
            // Less common case: there are no valid omnibox suggestions. This can happen if the
            // user tapped the URL bar to dismiss the suggestions, then pressed enter. This can
            // also happen if the user presses enter before any suggestions have been received
            // from the autocomplete controller.
            suggestionMatch = mAutocomplete.map(a -> a.classify(urlText)).orElse(null);
            // If urlText couldn't be classified, bail.
            if (suggestionMatch == null) return;
        }

        loadUrlForOmniboxMatch(
                0, suggestionMatch, suggestionMatch.getUrl(), inputStart, openInNewTab, true);
    }

    /**
     * Loads the specified omnibox suggestion.
     *
     * @param matchIndex The position of the selected omnibox suggestion.
     * @param suggestion The suggestion selected.
     * @param url The URL to load.
     * @param inputStart The timestamp the input was started.
     * @param openInNewTab Whether the suggestion will be loaded in a new tab. If {@code true}, the
     *     suggestion will be loaded in a new tab. If {@code false}, the suggestion will be loaded
     *     in the current tab.
     */
    private void loadUrlForOmniboxMatch(
            int matchIndex,
            @NonNull AutocompleteMatch suggestion,
            @NonNull GURL url,
            long inputStart,
            boolean openInNewTab,
            boolean shouldUpdateSuggestionUrl) {
        try (TraceEvent e = TraceEvent.scoped("AutocompleteMediator.loadUrlFromOmniboxMatch")) {
            OmniboxMetrics.recordFocusToOpenTime(System.currentTimeMillis() - mUrlFocusTime);

            // Clear the deferred site load action in case it executes. Reclaims a bit of memory.
            mDeferredLoadAction = Optional.empty();

            mOmniboxFocusResultedInNavigation = true;
            if (shouldUpdateSuggestionUrl) {
                url = updateSuggestionUrlIfNeeded(suggestion, matchIndex, url);
            }

            // loadUrl modifies AutocompleteController's state clearing the native
            // AutocompleteResults needed by onSuggestionsSelected. Therefore,
            // loadUrl should should be invoked last.
            int transition = suggestion.getTransition();
            int type = suggestion.getType();

            recordMetrics(suggestion, matchIndex, WindowOpenDisposition.CURRENT_TAB);
            if (((transition & PageTransition.CORE_MASK) == PageTransition.TYPED)
                    && url.equals(mDataProvider.getCurrentGurl())) {
                // When the user hit enter on the existing permanent URL, treat it like a
                // reload for scoring purposes.  We could detect this by just checking
                // user_input_in_progress_, but it seems better to treat "edits" that end
                // up leaving the URL unchanged (e.g. deleting the last character and then
                // retyping it) as reloads too.  We exclude non-TYPED transitions because if
                // the transition is GENERATED, the user input something that looked
                // different from the current URL, even if it wound up at the same place
                // (e.g. manually retyping the same search query), and it seems wrong to
                // treat this as a reload.
                transition = PageTransition.RELOAD;
            } else if (type == OmniboxSuggestionType.URL_WHAT_YOU_TYPED
                    && mUrlBarEditingTextProvider.wasLastEditPaste()) {
                // It's important to use the page transition from the suggestion or we might end
                // up saving generated URLs as typed URLs, which would then pollute the subsequent
                // omnibox results. There is one special case where the suggestion text was pasted,
                // where we want the transition type to be LINK.

                transition = PageTransition.LINK;
            }

            // Kick off an action to clear focus and dismiss the suggestions list.
            // This normally happens when the target site loads and focus is moved to the
            // webcontents. On Android T we occasionally observe focus events to be lost, resulting
            // with Suggestions list obscuring the view.
            var autocompleteLoadCallback =
                    new AutocompleteLoadCallback() {
                        @Override
                        public void onLoadUrl(LoadUrlParams params, LoadUrlResult loadUrlResult) {
                            if (loadUrlResult.navigationHandle != null) {
                                mAutocomplete.ifPresent(
                                        a ->
                                                a.createNavigationObserver(
                                                        loadUrlResult.navigationHandle,
                                                        suggestion));
                            }
                        }
                    };

            if (suggestion.getType() == OmniboxSuggestionType.CLIPBOARD_IMAGE) {
                mDelegate.loadUrl(
                        new OmniboxLoadUrlParams.Builder(url.getSpec(), transition)
                                .setInputStartTimestamp(inputStart)
                                .setpostDataAndType(
                                        suggestion.getPostData(), suggestion.getPostContentType())
                                .setAutocompleteLoadCallback(autocompleteLoadCallback)
                                .build());
            } else {
                mDelegate.loadUrl(
                        new OmniboxLoadUrlParams.Builder(url.getSpec(), transition)
                                .setInputStartTimestamp(inputStart)
                                .setOpenInNewTab(openInNewTab)
                                .setAutocompleteLoadCallback(autocompleteLoadCallback)
                                .build());
            }

            mHandler.post(this::finishInteraction);
        }
    }

    /** Sends a zero suggest request to the server in order to pre-populate the result cache. */
    /* package */ void startPrefetch() {
        int pageClassification = mDataProvider.getPageClassification(true);
        postAutocompleteRequest(
                () ->
                        mAutocomplete.ifPresent(
                                a ->
                                        a.startPrefetch(
                                                mDataProvider.getCurrentGurl(),
                                                pageClassification)),
                SCHEDULE_FOR_IMMEDIATE_EXECUTION);
    }

    /**
     * Make a zero suggest request if: - The URL bar has focus. - The the tab/overview is not
     * incognito. This method should not be called directly. Schedule execution using
     * postAutocompleteRequest.
     */
    private void startZeroSuggest() {
        // Reset "edited" state in the omnibox if zero suggest is triggered -- new edits
        // now count as a new session.
        mEditSessionState = EditSessionState.INACTIVE;
        mNewOmniboxEditSessionTimestamp = -1;
        startMeasuringSuggestionRequestToUiModelTime();

        if (mDelegate.isUrlBarFocused() && mDataProvider.hasTab()) {
            mAutocomplete.ifPresent(
                    a -> {
                        if (!mPageClassification.isPresent()) return;
                        a.startZeroSuggest(
                                mUrlBarEditingTextProvider.getTextWithAutocomplete(),
                                mDataProvider.getCurrentGurl(),
                                mPageClassification.getAsInt(),
                                mDataProvider.getTitle());
                    });
        }
    }

    /**
     * Update whether the Omnibox session is active.
     *
     * @param isActive whether session is currently active
     */
    @VisibleForTesting
    void propagateOmniboxSessionStateChange(boolean isActive) {
        boolean wasActive = mListPropertyModel.get(SuggestionListProperties.OMNIBOX_SESSION_ACTIVE);
        mListPropertyModel.set(SuggestionListProperties.OMNIBOX_SESSION_ACTIVE, isActive);

        if (isActive != wasActive) {
            mIgnoreOmniboxItemSelection |= isActive; // Reset to default value.
            mOmniboxSuggestionsVisualStateObserver.ifPresent(
                    (observer) -> observer.onOmniboxSessionStateChange(isActive));
        }
    }

    /**
     * Clear the list of suggestions.
     *
     * <p>This call is used to terminate the Autocomplete session and hide the suggestions list
     * while the Omnibox session is active.
     *
     * <p>This call *does not* terminate the Omnibox session.
     *
     * @see the {@link AutocompleteController#stop(boolean)}
     */
    @VisibleForTesting
    void clearSuggestions() {
        stopAutocomplete(true);
        dismissDeleteDialog(DialogDismissalCause.NAVIGATE_BACK_OR_TOUCH_OUTSIDE);

        mDropdownViewInfoListManager.clear();
        mAutocompleteResult = Optional.empty();
    }

    /**
     * Signals the autocomplete controller to stop generating omnibox suggestions and cancels the
     * queued task to start the autocomplete controller, if any.
     *
     * @param clear Whether to clear the most recent autocomplete results.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    void stopAutocomplete(boolean clear) {
        mAutocomplete.ifPresent(a -> a.stop(clear));
        cancelAutocompleteRequests();
    }

    /** Trigger autocomplete for the given query. */
    void startAutocompleteForQuery(@NonNull String query) {
        stopAutocomplete(false);
        if (mDataProvider.hasTab()) {
            mAutocomplete.ifPresent(
                    a ->
                            a.start(
                                    mDataProvider.getCurrentGurl(),
                                    mDataProvider.getPageClassification(false),
                                    query,
                                    -1,
                                    false));
        }
    }

    /**
     * Respond to Suggestion list height change and update list of presented suggestions.
     *
     * <p>This typically happens as a result of soft keyboard being shown or hidden.
     *
     * @param newHeightPx New height of the suggestion list in pixels.
     */
    public void onSuggestionDropdownHeightChanged(@Px int newHeight) {
        // Report the dropdown height whenever we intend to - or do show soft keyboard. This
        // addresses cases where hardware keyboard is attached to a device, or where user explicitly
        // called the keyboard back after we hid it.
        if (mDelegate.isKeyboardActive()) {
            int suggestionHeight =
                    mContext.getResources()
                            .getDimensionPixelSize(R.dimen.omnibox_suggestion_content_height);
            mAutocomplete.ifPresent(
                    a -> a.onSuggestionDropdownHeightChanged(newHeight, suggestionHeight));
        }
    }

    @Override
    public void onSuggestionDropdownScroll() {
        mSuggestionsListScrolled = true;
        mDelegate.setKeyboardVisibility(false, false);
    }

    /**
     * Called whenever a navigation happens from the omnibox to record metrics about the user's
     * interaction with the omnibox.
     *
     * @param match the selected AutocompleteMatch
     * @param suggestionLine the index of the suggestion line that holds selected match
     * @param disposition the window open disposition
     */
    private void recordMetrics(
            @NonNull AutocompleteMatch match, int suggestionLine, int disposition) {
        if (mAutocompleteResult.isEmpty()) return;

        boolean autocompleteResultIsFromCache =
                mAutocompleteResult.map(r -> r.isFromCachedResult()).orElse(true);

        OmniboxMetrics.recordUsedSuggestionFromCache(autocompleteResultIsFromCache);
        OmniboxMetrics.recordTouchDownPrefetchResult(match, mLastPrefetchStartedSuggestion);

        // Do not attempt to record other metrics for cached suggestions if the source of the list
        // is local cache. These suggestions do not have corresponding native objects and will fail
        // validation.
        if (autocompleteResultIsFromCache) return;

        GURL currentPageUrl = mDataProvider.getCurrentGurl();
        long elapsedTimeSinceModified = getElapsedTimeSinceInputChange();
        int autocompleteLength =
                mUrlBarEditingTextProvider.getTextWithAutocomplete().length()
                        - mUrlBarEditingTextProvider.getTextWithoutAutocomplete().length();
        var tab = mDataProvider.getTab();
        WebContents webContents = tab != null ? tab.getWebContents() : null;

        mAutocomplete.ifPresent(
                a ->
                        a.onSuggestionSelected(
                                match,
                                suggestionLine,
                                disposition,
                                currentPageUrl,
                                mPageClassification.getAsInt(),
                                elapsedTimeSinceModified,
                                autocompleteLength,
                                webContents));
    }

    @Override
    public void onSuggestionDropdownOverscrolledToTop() {
        mDelegate.setKeyboardVisibility(true, false);
    }

    /**
     * @return elapsed time (in milliseconds) since last input or -1 if user has chosen a
     *     zero-prefix suggestion.
     */
    private long getElapsedTimeSinceInputChange() {
        return mNewOmniboxEditSessionTimestamp > 0
                ? (SystemClock.elapsedRealtime() - mNewOmniboxEditSessionTimestamp)
                : -1;
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    @EditSessionState
    int getEditSessionStateForTest() {
        return mEditSessionState;
    }

    /**
     * Schedule Autocomplete action for execution. Each Autocomplete action posted here will cancel
     * any previously posted Autocomplete action, ensuring that the actions don't compete against
     * each other. Any action scheduled for execution before Native libraries are ready will be
     * deferred.
     *
     * <p>This call should only be used for regular suggest flows. Do not post arbitrary tasks here.
     *
     * @param action Autocomplete action to execute.
     * @param delayMillis The number of milliseconds by which the action should be delayed. Use
     *     SCHEDULE_FOR_IMMEDIATE_EXECUTION to post action at front of the message queue.
     */
    private void postAutocompleteRequest(@NonNull Runnable action, long delayMillis) {
        assert !mIsExecutingAutocompleteAction : "Can't schedule conflicting autocomplete action";
        assert ThreadUtils.runningOnUiThread() : "Detected input from a non-UI thread. Test error?";

        cancelAutocompleteRequests();
        mCurrentAutocompleteRequest =
                Optional.of(
                        new Runnable() {
                            @Override
                            public void run() {
                                mIsExecutingAutocompleteAction = true;
                                action.run();
                                mIsExecutingAutocompleteAction = false;
                                // Release completed Runnable.
                                mCurrentAutocompleteRequest = Optional.empty();
                            }
                        });

        // In the event we got Native Ready signal but no Profile yet (or the other way around),
        // delay execution of the Autocomplete request.
        if (!mNativeInitialized || mAutocomplete.isEmpty()) return;
        mCurrentAutocompleteRequest.ifPresent(
                request -> {
                    if (delayMillis == SCHEDULE_FOR_IMMEDIATE_EXECUTION) {
                        // TODO(crbug.com/40167699): Replace the following with postAtFrontOfQueue()
                        // and correct any tests that expect data instantly.
                        request.run();
                    } else {
                        mHandler.postDelayed(request, delayMillis);
                    }
                });
    }

    /** Cancel any pending autocomplete actions. */
    private void cancelAutocompleteRequests() {
        stopMeasuringSuggestionRequestToUiModelTime();
        mCurrentAutocompleteRequest.ifPresent(r -> mHandler.removeCallbacks(r));
        mCurrentAutocompleteRequest = Optional.empty();
    }

    /** Execute any pending Autocomplete requests, if the Autocomplete subsystem is ready. */
    private void runPendingAutocompleteRequests() {
        if (!mNativeInitialized || mAutocomplete.isEmpty()) return;

        mDeferredLoadAction
                // If deferred load action is present, cancel all autocomplete and load the URL.
                .map(
                        action -> {
                            cancelAutocompleteRequests();
                            return action;
                        })
                // Otherwise, run pending autocomplete action (if any).
                .or(() -> mCurrentAutocompleteRequest)
                .ifPresent(runnable -> mHandler.postAtFrontOfQueue(runnable));
    }

    /**
     * Start measuring time between - the request for suggestions and - the suggestions UI model
     * being built. This should be invoked right before we issue a request for suggestions.
     */
    private void startMeasuringSuggestionRequestToUiModelTime() {
        mLastSuggestionRequestTime = SystemClock.uptimeMillis();
        mFirstSuggestionListModelCreatedTime = null;
    }

    /**
     * Measure the time it took to build Suggestions UI model. The time is measured since the moment
     * suggestions were requested. Two histograms are recorded by this method:
     *
     * <ul>
     *   <li>Omnibox.SuggestionList.RequestToUiModel.First for the first reply associated with the
     *       request and
     *   <li>Omnibox.SuggestionList.RequestToUiModel.Last for the final reply associated with the
     *       request.
     * </ul>
     *
     * Any other replies that happen meantime are ignored and are accounted for by the last/final
     * measurement.
     *
     * @param isFinal whether the measurement is for the final suggestions repsponse
     */
    private void measureSuggestionRequestToUiModelTime(boolean isFinal) {
        if (mLastSuggestionRequestTime == null) return;

        if (mFirstSuggestionListModelCreatedTime == null) {
            mFirstSuggestionListModelCreatedTime = SystemClock.uptimeMillis();
            OmniboxMetrics.recordSuggestionRequestToModelTime(
                    /* isFirst= */ true,
                    mFirstSuggestionListModelCreatedTime - mLastSuggestionRequestTime);
        }

        if (isFinal) {
            OmniboxMetrics.recordSuggestionRequestToModelTime(
                    /* isFirst= */ false, SystemClock.uptimeMillis() - mLastSuggestionRequestTime);
            stopMeasuringSuggestionRequestToUiModelTime();
        }
    }

    /** Cancel any measurements related to the time it takes to build Suggestions UI model. */
    private void stopMeasuringSuggestionRequestToUiModelTime() {
        mLastSuggestionRequestTime = null;
        mFirstSuggestionListModelCreatedTime = null;
    }

    @Override
    public void onTopResumedActivityChanged(boolean isTopResumedActivity) {
        // TODO(crbug.com/329702834): Ensuring showing Suggestions when activity resumes.
        if (!isTopResumedActivity) {
            clearSuggestions();
            mDelegate.clearOmniboxFocus();
        }
    }
}