blob: 02fe3dc6365ba1c9c58a43ffec1bba8e506c5745 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.SparseArray;
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.Callback;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ActivityTabProvider.ActivityTabTabObserver;
import org.chromium.chrome.browser.GlobalDiscardableReferencePool;
import org.chromium.chrome.browser.compositor.layouts.EmptyOverviewModeObserver;
import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.image_fetcher.ImageFetcher;
import org.chromium.chrome.browser.image_fetcher.ImageFetcherConfig;
import org.chromium.chrome.browser.image_fetcher.ImageFetcherFactory;
import org.chromium.chrome.browser.init.AsyncInitializationActivity;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.StartStopWithNativeObserver;
import org.chromium.chrome.browser.omnibox.OmniboxSuggestionType;
import org.chromium.chrome.browser.omnibox.UrlBarEditingTextStateProvider;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteController.OnSuggestionsReceivedListener;
import org.chromium.chrome.browser.omnibox.suggestions.answer.AnswerSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.basic.BasicSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionHost;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewDelegate;
import org.chromium.chrome.browser.omnibox.suggestions.clipboard.ClipboardSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.editurl.EditUrlSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.tail.TailSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.tiles.TileSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.toolbar.ToolbarDataProvider;
import org.chromium.chrome.browser.ui.favicon.LargeIconBridge;
import org.chromium.components.browser_ui.util.ConversionUtils;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.query_tiles.QueryTile;
import org.chromium.content_public.browser.WebContents;
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;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.GURL;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* Handles updating the model state for the currently visible omnibox suggestions.
*/
class AutocompleteMediator implements OnSuggestionsReceivedListener, StartStopWithNativeObserver,
OmniboxSuggestionsDropdown.Observer, SuggestionHost {
/** A struct containing information about the suggestion and its view type. */
private static class SuggestionViewInfo extends MVCListAdapter.ListItem {
/** Processor managing the suggestion. */
public final SuggestionProcessor processor;
/** The suggestion this info represents. */
public final OmniboxSuggestion suggestion;
/** Whether this suggestion has been initialized. */
private boolean mIsInitialized;
public SuggestionViewInfo(
SuggestionProcessor suggestionProcessor, OmniboxSuggestion omniboxSuggestion) {
super(suggestionProcessor.getViewTypeId(), suggestionProcessor.createModel());
processor = suggestionProcessor;
suggestion = omniboxSuggestion;
}
/**
* Initialize model for the encompassed suggestion.
*
* @param suggestionIndex Target suggestion position in the suggestion list.
* @param layoutDirection View layout direction (LTR or RTL).
* @param useDarkColors Whether suggestions should be rendered using incognito or night mode
* colors.
*/
void initializeModel(int suggestionIndex, int layoutDirection, boolean useDarkColors) {
if (mIsInitialized) return;
model.set(SuggestionCommonProperties.LAYOUT_DIRECTION, layoutDirection);
model.set(SuggestionCommonProperties.USE_DARK_COLORS, useDarkColors);
processor.populateModel(suggestion, model, suggestionIndex);
mIsInitialized = true;
}
}
private static final String TAG = "Autocomplete";
private static final int SUGGESTION_NOT_FOUND = -1;
private static final int MINIMUM_NUMBER_OF_SUGGESTIONS_TO_SHOW = 5;
private static final int MAX_IMAGE_CACHE_SIZE = 500 * ConversionUtils.BYTES_PER_KILOBYTE;
// 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 static final int OMNIBOX_HISTOGRAMS_MAX_SUGGESTIONS = 10;
private final Context mContext;
private final AutocompleteDelegate mDelegate;
private final UrlBarEditingTextStateProvider mUrlBarEditingTextProvider;
private final PropertyModel mListPropertyModel;
private final List<Runnable> mDeferredNativeRunnables = new ArrayList<Runnable>();
private final Handler mHandler;
// TODO(crbug.com/982818): make EditUrlProcessor behave like all other processors and register
// it in the mSuggestionProcessors list. The processor currently cannot be combined with
// other processors because of its unique requirements.
private @Nullable EditUrlSuggestionProcessor mEditUrlProcessor;
private final TileSuggestionProcessor mTileSuggestionProcessor;
private final List<SuggestionProcessor> mSuggestionProcessors;
private final List<SuggestionViewInfo> mAvailableSuggestions;
private SparseArray<String> mGroupHeaders;
private ToolbarDataProvider mDataProvider;
private OverviewModeBehavior mOverviewModeBehavior;
private OverviewModeBehavior.OverviewModeObserver mOverviewModeObserver;
private boolean mNativeInitialized;
private AutocompleteController mAutocomplete;
private long mUrlFocusTime;
private boolean mEnableAdaptiveSuggestionsCount;
@Px
private int mMaximumSuggestionsListHeight;
private boolean mEnableDeferredKeyboardPopup;
private boolean mPendingKeyboardShowDecision;
@IntDef({SuggestionVisibilityState.DISALLOWED, SuggestionVisibilityState.PENDING_ALLOW,
SuggestionVisibilityState.ALLOWED})
@Retention(RetentionPolicy.SOURCE)
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@interface SuggestionVisibilityState {
int DISALLOWED = 0;
int PENDING_ALLOW = 1;
int ALLOWED = 2;
}
@SuggestionVisibilityState
private int mSuggestionVisibilityState;
// The timestamp (using SystemClock.elapsedRealtime()) at the point when the user started
// modifying the omnibox with new input.
private long mNewOmniboxEditSessionTimestamp = -1;
// Set to true when the user has started typing new input in the omnibox, set to false
// when the omnibox loses focus or becomes empty.
private boolean mHasStartedNewOmniboxEditSession;
/**
* 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 Runnable mRequestSuggestions;
private DeferredOnSelectionRunnable mDeferredOnSelection;
private boolean mShowCachedZeroSuggestResults;
private boolean mShouldPreventOmniboxAutocomplete;
private long mLastActionUpTimestamp;
private boolean mIgnoreOmniboxItemSelection = true;
private boolean mUseDarkColors = true;
private int mLayoutDirection;
private WindowAndroid mWindowAndroid;
private ActivityLifecycleDispatcher mLifecycleDispatcher;
private ActivityTabTabObserver mTabObserver;
private ImageFetcher mImageFetcher;
private LargeIconBridge mIconBridge;
public AutocompleteMediator(Context context, AutocompleteDelegate delegate,
UrlBarEditingTextStateProvider textProvider,
AutocompleteController autocompleteController,
Callback<List<QueryTile>> queryTileSuggestionCallback, PropertyModel listPropertyModel,
Handler handler) {
mContext = context;
mDelegate = delegate;
mUrlBarEditingTextProvider = textProvider;
mListPropertyModel = listPropertyModel;
mAutocomplete = autocompleteController;
mAutocomplete.setOnSuggestionsReceivedListener(this);
mHandler = handler;
mTileSuggestionProcessor =
new TileSuggestionProcessor(mContext, queryTileSuggestionCallback);
mSuggestionProcessors = new ArrayList<>();
mAvailableSuggestions = new ArrayList<>();
mGroupHeaders = new SparseArray<>();
mOverviewModeObserver = new EmptyOverviewModeObserver() {
@Override
public void onOverviewModeStartedShowing(boolean showToolbar) {
if (mDataProvider.shouldShowLocationBarInOverviewMode()) {
AutocompleteControllerJni.get().prefetchZeroSuggestResults();
}
}
};
}
/**
* Initialize the Mediator with default set of suggestions processors.
*/
void initDefaultProcessors() {
final Supplier<ImageFetcher> imageFetcherSupplier = createImageFetcherSupplier();
final Supplier<LargeIconBridge> iconBridgeSupplier = createIconBridgeSupplier();
mEditUrlProcessor =
new EditUrlSuggestionProcessor(mContext, this, mDelegate, iconBridgeSupplier);
registerSuggestionProcessor(new AnswerSuggestionProcessor(
mContext, this, mUrlBarEditingTextProvider, imageFetcherSupplier));
registerSuggestionProcessor(
new ClipboardSuggestionProcessor(mContext, this, iconBridgeSupplier));
registerSuggestionProcessor(
new EntitySuggestionProcessor(mContext, this, imageFetcherSupplier));
registerSuggestionProcessor(new TailSuggestionProcessor(mContext, this));
registerSuggestionProcessor(mTileSuggestionProcessor);
registerSuggestionProcessor(new BasicSuggestionProcessor(
mContext, this, mUrlBarEditingTextProvider, iconBridgeSupplier));
}
/**
* Register new processor to process OmniboxSuggestions.
* Processors will be tried in the same order as they were added.
*
* @param processor SuggestionProcessor that handles OmniboxSuggestions.
*/
void registerSuggestionProcessor(SuggestionProcessor processor) {
mSuggestionProcessors.add(processor);
}
public void destroy() {
if (mTabObserver != null) {
mTabObserver.destroy();
}
if (mImageFetcher != null) {
mImageFetcher.destroy();
mImageFetcher = null;
}
if (mOverviewModeBehavior != null) {
mOverviewModeBehavior.removeOverviewModeObserver(mOverviewModeObserver);
mOverviewModeBehavior = null;
}
}
@Override
public void onStartWithNative() {}
@Override
public void onStopWithNative() {
recordSuggestionsShown();
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
void setSuggestionVisibilityState(@SuggestionVisibilityState int state) {
mSuggestionVisibilityState = state;
}
private @SuggestionVisibilityState int getSuggestionVisibilityState() {
return mSuggestionVisibilityState;
}
private ModelList getSuggestionModelList() {
return mListPropertyModel.get(SuggestionListProperties.SUGGESTION_MODELS);
}
/**
* Create a new supplier that returns ImageFetcher instances.
* Consumers of this call:
* - should never cache the returned object, since its lifecycle is bound to external
* objects, such as Profile,
* - should always check for null ahead of using returned value. ImageFetcher may not be
* constructed if Profile is not yet initialized.
*
* @return Supplier returning ImageFetcher.
*/
private Supplier<ImageFetcher> createImageFetcherSupplier() {
return new Supplier<ImageFetcher>() {
@Override
public ImageFetcher get() {
if (getCurrentProfile() == null) {
return null;
}
if (mImageFetcher == null) {
mImageFetcher = ImageFetcherFactory.createImageFetcher(
ImageFetcherConfig.IN_MEMORY_ONLY,
GlobalDiscardableReferencePool.getReferencePool(),
MAX_IMAGE_CACHE_SIZE);
}
return mImageFetcher;
}
};
}
/**
* Create a new supplier that returns LargeIconBridge instances.
* Consumers of this call:
* - should never cache the returned object, since its lifecycle is bound to external
* objects, such as Profile,
* - should always check for null ahead of using returned value. LargeIconBridge may not be
* constructed if Profile is not yet initialized.
*
* @return Supplier returning LargeIconBridge.
*/
private Supplier<LargeIconBridge> createIconBridgeSupplier() {
return new Supplier<LargeIconBridge>() {
@Override
public LargeIconBridge get() {
if (getCurrentProfile() == null) {
return null;
}
if (mIconBridge == null) {
mIconBridge = new LargeIconBridge(getCurrentProfile());
}
return mIconBridge;
}
};
}
private Profile getCurrentProfile() {
return mDataProvider != null ? mDataProvider.getProfile() : null;
}
/**
* Check if the suggestion is created from clipboard.
*
* @param suggestion The OmniboxSuggestion to check.
* @return Whether or not the suggestion is from clipboard.
*/
private boolean isSuggestionFromClipboard(OmniboxSuggestion suggestion) {
return suggestion.getType() == OmniboxSuggestionType.CLIPBOARD_URL
|| suggestion.getType() == OmniboxSuggestionType.CLIPBOARD_TEXT
|| suggestion.getType() == OmniboxSuggestionType.CLIPBOARD_IMAGE;
}
/**
* Record histograms for presented suggestions.
*/
private void recordSuggestionsShown() {
int richEntitiesCount = 0;
ModelList currentModels = getSuggestionModelList();
for (int i = 0; i < currentModels.size(); i++) {
SuggestionViewInfo info = (SuggestionViewInfo) currentModels.get(i);
info.processor.recordSuggestionPresented(info.suggestion, info.model);
if (info.processor.getViewTypeId() == OmniboxSuggestionUiType.ENTITY_SUGGESTION) {
richEntitiesCount++;
}
}
// Note: valid range for histograms must start with (at least) 1. This does not prevent us
// from reporting 0 as a count though - values lower than 'min' fall in the 'underflow'
// bucket, while values larger than 'max' will be reported in 'overflow' bucket.
RecordHistogram.recordLinearCountHistogram("Omnibox.RichEntityShown", richEntitiesCount, 1,
OMNIBOX_HISTOGRAMS_MAX_SUGGESTIONS, OMNIBOX_HISTOGRAMS_MAX_SUGGESTIONS + 1);
}
/**
* @return The number of current autocomplete suggestions.
*/
public int getSuggestionCount() {
return getSuggestionModelList().size();
}
/**
* 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 index The index of the suggestion to fetch.
* @return The suggestion at the given index.
*/
public OmniboxSuggestion getSuggestionAt(int index) {
return ((SuggestionViewInfo) getSuggestionModelList().get(index)).suggestion;
}
/**
* Sets the data provider for the toolbar.
*/
void setToolbarDataProvider(ToolbarDataProvider provider) {
mDataProvider = provider;
}
/**
* @param overviewModeBehavior A means of accessing the current OverviewModeState and a way to
* listen to state changes.
*/
public void setOverviewModeBehavior(OverviewModeBehavior overviewModeBehavior) {
assert mOverviewModeBehavior == null;
mOverviewModeBehavior = overviewModeBehavior;
mOverviewModeBehavior.addOverviewModeObserver(mOverviewModeObserver);
}
/** Set the WindowAndroid instance associated with the containing Activity. */
void setWindowAndroid(WindowAndroid window) {
if (mLifecycleDispatcher != null) {
mLifecycleDispatcher.unregister(this);
}
mWindowAndroid = window;
if (window != null && window.getActivity().get() != null
&& window.getActivity().get() instanceof AsyncInitializationActivity) {
mLifecycleDispatcher =
((AsyncInitializationActivity) mWindowAndroid.getActivity().get())
.getLifecycleDispatcher();
}
if (mLifecycleDispatcher != null) {
mLifecycleDispatcher.register(this);
}
}
/**
* Sets the layout direction to be used for any new suggestion views.
* @see View#setLayoutDirection(int)
*/
void setLayoutDirection(int layoutDirection) {
if (mLayoutDirection == layoutDirection) return;
mLayoutDirection = layoutDirection;
ModelList currentModels = getSuggestionModelList();
for (int i = 0; i < currentModels.size(); i++) {
PropertyModel model = currentModels.get(i).model;
model.set(SuggestionCommonProperties.LAYOUT_DIRECTION, layoutDirection);
}
}
/**
* Specifies the visual state to be used by the suggestions.
* @param useDarkColors Whether dark colors should be used for fonts and icons.
* @param isIncognito Whether the UI is for incognito mode or not.
*/
void updateVisualsForState(boolean useDarkColors, boolean isIncognito) {
mUseDarkColors = useDarkColors;
mListPropertyModel.set(SuggestionListProperties.IS_INCOGNITO, isIncognito);
ModelList currentModels = getSuggestionModelList();
for (int i = 0; i < currentModels.size(); i++) {
PropertyModel model = currentModels.get(i).model;
model.set(SuggestionCommonProperties.USE_DARK_COLORS, useDarkColors);
}
}
/**
* Sets to show cached zero suggest results. This will start both caching zero suggest results
* in shared preferences and also attempt to show them when appropriate without needing native
* initialization.
* @param showCachedZeroSuggestResults Whether cached zero suggest should be shown.
*/
void setShowCachedZeroSuggestResults(boolean showCachedZeroSuggestResults) {
mShowCachedZeroSuggestResults = showCachedZeroSuggestResults;
if (mShowCachedZeroSuggestResults) mAutocomplete.startCachedZeroSuggest();
}
/** 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;
mEnableAdaptiveSuggestionsCount =
ChromeFeatureList.isEnabled(ChromeFeatureList.OMNIBOX_ADAPTIVE_SUGGESTIONS_COUNT);
mEnableDeferredKeyboardPopup =
ChromeFeatureList.isEnabled(ChromeFeatureList.OMNIBOX_DEFERRED_KEYBOARD_POPUP);
for (Runnable deferredRunnable : mDeferredNativeRunnables) {
mHandler.post(deferredRunnable);
}
mDeferredNativeRunnables.clear();
for (SuggestionProcessor processor : mSuggestionProcessors) {
processor.onNativeInitialized();
}
if (mEditUrlProcessor != null) mEditUrlProcessor.onNativeInitialized();
}
/**
* @param provider A means of accessing the activity tab.
*/
void setActivityTabProvider(ActivityTabProvider provider) {
if (mEditUrlProcessor != null) mEditUrlProcessor.setActivityTabProvider(provider);
if (mTabObserver != null) {
mTabObserver.destroy();
mTabObserver = null;
}
if (provider != null) {
mTabObserver = new ActivityTabTabObserver(provider) {
@Override
public void onPageLoadFinished(Tab tab, String url) {
maybeTriggerCacheRefresh(url);
}
@Override
protected void onObservingDifferentTab(Tab tab) {
if (tab == null) return;
maybeTriggerCacheRefresh(tab.getUrlString());
}
/**
* Trigger ZeroSuggest cache refresh in case user is accessing a new tab page.
* Avoid issuing multiple concurrent server requests for the same event to reduce
* server pressure.
*/
private void maybeTriggerCacheRefresh(String url) {
if (url == null) return;
if (!UrlConstants.NTP_URL.equals(url)) return;
AutocompleteControllerJni.get().prefetchZeroSuggestResults();
}
};
}
}
void setShareDelegateSupplier(Supplier<ShareDelegate> shareDelegateSupplier) {
mEditUrlProcessor.setShareDelegateSupplier(shareDelegateSupplier);
}
/** @see org.chromium.chrome.browser.omnibox.UrlFocusChangeListener#onUrlFocusChange(boolean) */
void onUrlFocusChange(boolean hasFocus) {
if (hasFocus) {
mUrlFocusTime = System.currentTimeMillis();
setSuggestionVisibilityState(SuggestionVisibilityState.PENDING_ALLOW);
signalPendingKeyboardShowDecision();
// For cases where we know the feature is disabled - or those where Omnibox is running
// without native code loaded - make sure we present the keyboard immediately.
if (!mEnableDeferredKeyboardPopup) {
resolvePendingKeyboardShowDecision();
}
if (mNativeInitialized) {
startZeroSuggest();
} else {
mDeferredNativeRunnables.add(() -> {
if (TextUtils.isEmpty(mUrlBarEditingTextProvider.getTextWithAutocomplete())) {
startZeroSuggest();
}
});
}
} else {
if (mNativeInitialized) recordSuggestionsShown();
setSuggestionVisibilityState(SuggestionVisibilityState.DISALLOWED);
mHasStartedNewOmniboxEditSession = false;
mNewOmniboxEditSessionTimestamp = -1;
// Prevent any upcoming omnibox suggestions from showing once a URL is loaded (and as
// a consequence the omnibox is unfocused).
hideSuggestions();
if (mImageFetcher != null) mImageFetcher.clear();
}
if (mEditUrlProcessor != null) mEditUrlProcessor.onUrlFocusChange(hasFocus);
for (SuggestionProcessor processor : mSuggestionProcessors) {
processor.onUrlFocusChange(hasFocus);
}
}
/**
* @see
* org.chromium.chrome.browser.omnibox.UrlFocusChangeListener#onUrlAnimationFinished(boolean)
*/
void onUrlAnimationFinished(boolean hasFocus) {
setSuggestionVisibilityState(hasFocus ? SuggestionVisibilityState.ALLOWED
: SuggestionVisibilityState.DISALLOWED);
updateOmniboxSuggestionsVisibility();
}
/**
* Updates the profile used for generating autocomplete suggestions.
* @param profile The profile to be used.
*/
void setAutocompleteProfile(Profile profile) {
mAutocomplete.setProfile(profile);
mIconBridge = null;
}
/**
* 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.onVoiceResults(results);
}
/**
* @return The current native pointer to the autocomplete results.
*/
// TODO(tedchoc): Figure out how to remove this.
long getCurrentNativeAutocompleteResult() {
return mAutocomplete.getCurrentNativeAutocompleteResult();
}
@Override
public SuggestionViewDelegate createSuggestionViewDelegate(
OmniboxSuggestion suggestion, int position) {
return new SuggestionViewDelegate() {
@Override
public void onSetUrlToSuggestion() {
if (mIgnoreOmniboxItemSelection) return;
mIgnoreOmniboxItemSelection = true;
AutocompleteMediator.this.onSetUrlToSuggestion(suggestion);
}
@Override
public void onSelection() {
AutocompleteMediator.this.onSelection(suggestion, position);
}
@Override
public void onLongPress() {
AutocompleteMediator.this.onLongPress(suggestion, position);
}
@Override
public void onGestureUp(long timestamp) {
mLastActionUpTimestamp = timestamp;
}
@Override
public void onGestureDown() {
stopAutocomplete(false);
}
};
}
/** Called when a query tile is selected by the user. */
void onQueryTileSelected(QueryTile queryTile) {
// For last level tile, start a search query.
if (queryTile.children.isEmpty()) {
String url = TemplateUrlServiceFactory.get().getUrlForSearchQuery(queryTile.queryText);
mDelegate.loadUrl(url, PageTransition.LINK, mLastActionUpTimestamp);
mDelegate.setKeyboardVisibility(false);
return;
}
// If the tile has sub-tiles, start a new request to the backend to get the new set
// of tiles. Also set the tile text in omnibox.
stopAutocomplete(false);
String refineText = TextUtils.concat(queryTile.queryText, " ").toString();
mDelegate.setOmniboxEditingText(refineText);
mNewOmniboxEditSessionTimestamp = SystemClock.elapsedRealtime();
mHasStartedNewOmniboxEditSession = true;
mAutocomplete.start(mDataProvider.getProfile(), mDataProvider.getCurrentUrl(),
mDataProvider.getPageClassification(mDelegate.didFocusUrlFromFakebox()),
mUrlBarEditingTextProvider.getTextWithoutAutocomplete(),
mUrlBarEditingTextProvider.getSelectionStart(),
!mUrlBarEditingTextProvider.shouldAutocomplete(), queryTile.id);
}
/**
* Triggered when the user selects one of the omnibox suggestions to navigate to.
* @param suggestion The OmniboxSuggestion which was selected.
* @param position Position of the suggestion in the drop down view.
*/
private void onSelection(OmniboxSuggestion suggestion, int position) {
if (mShowCachedZeroSuggestResults && !mNativeInitialized) {
mDeferredOnSelection = new DeferredOnSelectionRunnable(suggestion, position) {
@Override
public void run() {
onSelection(this.mSuggestion, this.mPosition);
}
};
return;
}
// Note: this call is typically scheduled for execution, rather than invoked directly.
// In some situations this means the content of mCurrentModels may change meanwhile.
int verifiedIndex = findSuggestionInModel(suggestion, position);
if (verifiedIndex != SUGGESTION_NOT_FOUND) {
SuggestionViewInfo info =
(SuggestionViewInfo) getSuggestionModelList().get(verifiedIndex);
info.processor.recordSuggestionUsed(info.suggestion, info.model);
}
loadUrlFromOmniboxMatch(position, suggestion, mLastActionUpTimestamp, true);
mDelegate.setKeyboardVisibility(false);
}
/**
* Triggered when the user selects to refine one of the omnibox suggestions.
* @param suggestion The suggestion selected.
*/
@Override
public void onRefineSuggestion(OmniboxSuggestion suggestion) {
stopAutocomplete(false);
boolean isSearchSuggestion = suggestion.isSearchSuggestion();
String refineText = suggestion.getFillIntoEdit();
if (isSearchSuggestion) refineText = TextUtils.concat(refineText, " ").toString();
mDelegate.setOmniboxEditingText(refineText);
onTextChanged(mUrlBarEditingTextProvider.getTextWithoutAutocomplete(),
mUrlBarEditingTextProvider.getTextWithAutocomplete());
if (isSearchSuggestion) {
RecordUserAction.record("MobileOmniboxRefineSuggestion.Search");
} else {
RecordUserAction.record("MobileOmniboxRefineSuggestion.Url");
}
}
/**
* Triggered when the user long presses the omnibox suggestion.
* @param suggestion The suggestion selected.
* @param position The position of the suggestion.
*/
private void onLongPress(OmniboxSuggestion suggestion, int position) {
RecordUserAction.record("MobileOmniboxDeleteGesture");
if (!suggestion.isDeletable()) return;
if (mWindowAndroid == null) return;
Activity activity = mWindowAndroid.getActivity().get();
if (activity == null || !(activity instanceof AsyncInitializationActivity)) return;
ModalDialogManager manager =
((AsyncInitializationActivity) activity).getModalDialogManager();
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");
mAutocomplete.deleteSuggestion(position, suggestion.hashCode());
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) {}
};
Resources resources = mContext.getResources();
@StringRes
int dialogMessageId = R.string.omnibox_confirm_delete;
if (isSuggestionFromClipboard(suggestion)) {
dialogMessageId = R.string.omnibox_confirm_delete_from_clipboard;
}
PropertyModel model =
new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
.with(ModalDialogProperties.CONTROLLER, dialogController)
.with(ModalDialogProperties.TITLE, suggestion.getDisplayText())
.with(ModalDialogProperties.MESSAGE, resources, 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();
// Prevent updates to the shown omnibox suggestions list while the dialog is open.
stopAutocomplete(false);
manager.showDialog(model, ModalDialogManager.ModalDialogType.APP);
}
/**
* Triggered when the user navigates to one of the suggestions without clicking on it.
* @param suggestion The suggestion that was selected.
*/
void onSetUrlToSuggestion(OmniboxSuggestion suggestion) {
mDelegate.setOmniboxEditingText(suggestion.getFillIntoEdit());
}
/**
* 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 aqs= parameter
* on regular web search URLs.
*
* @param suggestion The chosen omnibox suggestion.
* @param selectedIndex The index of the chosen omnibox suggestion.
* @param skipCheck Whether to skip an out of bounds check.
* @return The url to navigate to.
*/
private GURL updateSuggestionUrlIfNeeded(
OmniboxSuggestion suggestion, int selectedIndex, boolean skipCheck) {
// Only called once we have suggestions, and don't have a listener though which we can
// receive suggestions until the native side is ready, so this is safe
assert mNativeInitialized
: "updateSuggestionUrlIfNeeded called before native initialization";
if (suggestion.getType() == OmniboxSuggestionType.VOICE_SUGGEST
|| suggestion.getType() == OmniboxSuggestionType.TILE_SUGGESTION) {
return suggestion.getUrl();
}
int verifiedIndex = SUGGESTION_NOT_FOUND;
if (!skipCheck) {
verifiedIndex = findSuggestionInModel(suggestion, selectedIndex);
}
// If we do not have the suggestion as part of our results, skip the URL update.
if (verifiedIndex == SUGGESTION_NOT_FOUND) return suggestion.getUrl();
// TODO(mariakhomenko): Ideally we want to update match destination URL with new aqs
// for query in the omnibox and voice suggestions, but it's currently difficult to do.
long elapsedTimeSinceInputChange = mNewOmniboxEditSessionTimestamp > 0
? (SystemClock.elapsedRealtime() - mNewOmniboxEditSessionTimestamp)
: -1;
GURL updatedUrl = mAutocomplete.updateMatchDestinationUrlWithQueryFormulationTime(
verifiedIndex, suggestion.hashCode(), elapsedTimeSinceInputChange);
return updatedUrl == null ? suggestion.getUrl() : updatedUrl;
}
/**
* Check if the supplied suggestion is still in the current model and return its index.
*
* This call should be used to confirm that model has not been changed ahead of an event being
* called by all the methods that are dispatched rather than called directly.
*
* @param suggestion Suggestion to look for.
* @param index Last known position of the suggestion.
* @return Current index of the supplied suggestion, or SUGGESTION_NOT_FOUND if it is no longer
* part of the model.
*/
@SuppressWarnings("ReferenceEquality")
private int findSuggestionInModel(OmniboxSuggestion suggestion, int position) {
if (getSuggestionCount() > position && getSuggestionAt(position) == suggestion) {
return position;
}
// Underlying omnibox results may have changed since the selection was made,
// find the suggestion item, if possible.
for (int index = 0; index < getSuggestionCount(); index++) {
if (suggestion.equals(getSuggestionAt(index))) {
return index;
}
}
return SUGGESTION_NOT_FOUND;
}
/**
* Notifies the autocomplete system that the text has changed that drives autocomplete and the
* autocomplete suggestions should be updated.
*/
public void onTextChanged(String textWithoutAutocomplete, String textWithAutocomplete) {
// crbug.com/764749
Log.w(TAG, "onTextChangedForAutocomplete");
if (mShouldPreventOmniboxAutocomplete) return;
mIgnoreOmniboxItemSelection = true;
cancelPendingAutocompleteStart();
if (!mHasStartedNewOmniboxEditSession && mNativeInitialized) {
mAutocomplete.resetSession();
mNewOmniboxEditSessionTimestamp = SystemClock.elapsedRealtime();
mHasStartedNewOmniboxEditSession = true;
}
stopAutocomplete(false);
if (TextUtils.isEmpty(textWithoutAutocomplete)) {
// crbug.com/764749
Log.w(TAG, "onTextChangedForAutocomplete: url is empty");
hideSuggestions();
startZeroSuggest();
} else {
assert mRequestSuggestions == null : "Multiple omnibox requests in flight.";
mRequestSuggestions = () -> {
boolean preventAutocomplete = !mUrlBarEditingTextProvider.shouldAutocomplete();
mRequestSuggestions = null;
// There may be no tabs when searching form omnibox in overview mode. In that case,
// ToolbarDataProvider.getCurrentUrl() returns NTP url.
if (!mDataProvider.hasTab() && !mDataProvider.isInOverviewAndShowingOmnibox()) {
// crbug.com/764749
Log.w(TAG, "onTextChangedForAutocomplete: no tab");
return;
}
Profile profile = mDataProvider.getProfile();
int cursorPosition = -1;
if (mUrlBarEditingTextProvider.getSelectionStart()
== mUrlBarEditingTextProvider.getSelectionEnd()) {
// Conveniently, if there is no selection, those two functions return -1,
// exactly the same value needed to pass to start() to indicate no cursor
// position. Hence, there's no need to check for -1 here explicitly.
cursorPosition = mUrlBarEditingTextProvider.getSelectionStart();
}
int pageClassification =
mDataProvider.getPageClassification(mDelegate.didFocusUrlFromFakebox());
mAutocomplete.start(profile, mDataProvider.getCurrentUrl(), pageClassification,
textWithoutAutocomplete, cursorPosition, preventAutocomplete, null);
};
if (mNativeInitialized) {
mHandler.postDelayed(mRequestSuggestions, OMNIBOX_SUGGESTION_START_DELAY_MS);
} else {
mDeferredNativeRunnables.add(mRequestSuggestions);
}
}
mDelegate.onUrlTextChanged();
}
/**
* @param suggestion The suggestion to be processed.
* @return The appropriate suggestion processor for the provided suggestion.
*/
private SuggestionProcessor getProcessorForSuggestion(
OmniboxSuggestion suggestion, boolean isFirst) {
if (isFirst && mEditUrlProcessor != null
&& mEditUrlProcessor.doesProcessSuggestion(suggestion)) {
return mEditUrlProcessor;
}
for (SuggestionProcessor processor : mSuggestionProcessors) {
if (processor.doesProcessSuggestion(suggestion)) return processor;
}
assert false : "No default handler for suggestions";
return null;
}
/**
* Set signal indicating that the AutocompleteMediator should issue request to show or hide
* keyboard upon receiving next batch of Suggestions.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
void signalPendingKeyboardShowDecision() {
mPendingKeyboardShowDecision = true;
}
/**
* Issue request to show or hide keyboard after receiving fresh set of suggestions.
* The request is only issued if it was previously signalled as 'pending'.
*/
private void resolvePendingKeyboardShowDecision() {
if (!mPendingKeyboardShowDecision) return;
mPendingKeyboardShowDecision = false;
mDelegate.setKeyboardVisibility(shouldShowSoftKeyboard());
}
/**
* @return True if soft keyboard should be shown.
*/
private boolean shouldShowSoftKeyboard() {
return !mEnableDeferredKeyboardPopup
|| mAvailableSuggestions.size() <= MINIMUM_NUMBER_OF_SUGGESTIONS_TO_SHOW;
}
@Override
public void onSuggestionsReceived(
AutocompleteResult autocompleteResult, String inlineAutocompleteText) {
if (mShouldPreventOmniboxAutocomplete
|| getSuggestionVisibilityState() == SuggestionVisibilityState.DISALLOWED) {
resolvePendingKeyboardShowDecision();
return;
}
// This is a callback from a listener that is set up by onNativeLibraryReady,
// so can only be called once the native side is set up unless we are showing
// cached java-only suggestions.
assert mNativeInitialized
|| mShowCachedZeroSuggestResults
: "Native suggestions received before native side intialialized";
final List<OmniboxSuggestion> newSuggestions = autocompleteResult.getSuggestionsList();
if (mDeferredOnSelection != null) {
mDeferredOnSelection.setShouldLog(newSuggestions.size() > mDeferredOnSelection.mPosition
&& mDeferredOnSelection.mSuggestion.equals(
newSuggestions.get(mDeferredOnSelection.mPosition)));
mDeferredOnSelection.run();
mDeferredOnSelection = null;
}
String userText = mUrlBarEditingTextProvider.getTextWithoutAutocomplete();
mUrlTextAfterSuggestionsReceived = userText + inlineAutocompleteText;
if (setNewSuggestions(autocompleteResult)) {
// Reset all processors and clear existing suggestions if we received a new suggestions
// list.
for (SuggestionProcessor processor : mSuggestionProcessors) {
processor.onSuggestionsReceived();
}
mDelegate.onSuggestionsChanged(inlineAutocompleteText);
updateSuggestionsList(mMaximumSuggestionsListHeight);
}
resolvePendingKeyboardShowDecision();
}
/**
* Process the supplied SuggestionList to internal representation.
*
* TODO(https://crbug.com/982818): identify suggestions that have simply changed places and
* re-use them.
*
* @param autocompleteResult New autocomplete details.
* @return true, if newly supplied list was different from the previously supplied and cached
* list.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
boolean setNewSuggestions(AutocompleteResult autocompleteResult) {
final List<OmniboxSuggestion> newSuggestions = autocompleteResult.getSuggestionsList();
boolean sameSuggestions = false;
final int newSuggestionsCount = newSuggestions.size();
if (mAvailableSuggestions.size() == newSuggestionsCount) {
sameSuggestions = true;
for (int i = 0; i < newSuggestionsCount; i++) {
OmniboxSuggestion existingSuggestion = mAvailableSuggestions.get(i).suggestion;
if (!existingSuggestion.equals(newSuggestions.get(i))) {
sameSuggestions = false;
break;
}
}
}
final SparseArray<String> newGroupHeaders = autocompleteResult.getGroupHeaders();
final int newHeadersCount = newGroupHeaders.size();
sameSuggestions &= newHeadersCount == mGroupHeaders.size();
if (sameSuggestions) {
for (int i = 0; i < mGroupHeaders.size(); i++) {
final int key = mGroupHeaders.keyAt(i);
if (!TextUtils.equals(newGroupHeaders.get(key), mGroupHeaders.get(key))) {
sameSuggestions = false;
break;
}
}
}
if (sameSuggestions) return false;
mAvailableSuggestions.clear();
for (int index = 0; index < newSuggestionsCount; index++) {
final OmniboxSuggestion suggestion = newSuggestions.get(index);
final SuggestionProcessor processor = getProcessorForSuggestion(suggestion, index == 0);
final PropertyModel model = processor.createModel();
mAvailableSuggestions.add(new SuggestionViewInfo(processor, suggestion));
}
mGroupHeaders = newGroupHeaders;
return true;
}
@NonNull
SparseArray<String> getGroupHeaders() {
return mGroupHeaders;
}
/**
* Refresh list of presented suggestions.
*
* @param maximumListHeightPx Maximum height of the Suggestions list that guarantees 100%
* content visibility.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
void updateSuggestionsList(int maximumListHeightPx) {
if (mAvailableSuggestions.isEmpty()) {
hideSuggestions();
return;
}
final List<MVCListAdapter.ListItem> newSuggestionViewInfos =
new ArrayList<>(mAvailableSuggestions.size());
final ModelList prepopulatedSuggestions = getSuggestionModelList();
final int numSuggestionsToShow = mAvailableSuggestions.size();
int totalSuggestionsHeight = 0;
for (int suggestionIndex = 0; suggestionIndex < numSuggestionsToShow; suggestionIndex++) {
final SuggestionViewInfo viewInfo =
(SuggestionViewInfo) mAvailableSuggestions.get(suggestionIndex);
final SuggestionProcessor processor = viewInfo.processor;
final OmniboxSuggestion suggestion = viewInfo.suggestion;
totalSuggestionsHeight += processor.getMinimumSuggestionViewHeight();
if (mEnableAdaptiveSuggestionsCount
&& suggestionIndex >= MINIMUM_NUMBER_OF_SUGGESTIONS_TO_SHOW
&& totalSuggestionsHeight > maximumListHeightPx) {
break;
}
viewInfo.initializeModel(suggestionIndex, mLayoutDirection, mUseDarkColors);
newSuggestionViewInfos.add(viewInfo);
}
prepopulatedSuggestions.set(newSuggestionViewInfos);
if (mListPropertyModel.get(SuggestionListProperties.VISIBLE)
&& newSuggestionViewInfos.size() == 0) {
hideSuggestions();
}
updateOmniboxSuggestionsVisibility();
}
/**
* Load the url corresponding to the typed omnibox text.
* @param eventTime The timestamp the load was triggered by the user.
*/
void loadTypedOmniboxText(long eventTime) {
mDelegate.setKeyboardVisibility(false);
final String urlText = mUrlBarEditingTextProvider.getTextWithAutocomplete();
if (mNativeInitialized) {
findMatchAndLoadUrl(urlText, eventTime);
} else {
mDeferredNativeRunnables.add(() -> findMatchAndLoadUrl(urlText, eventTime));
}
}
private void findMatchAndLoadUrl(String urlText, long inputStart) {
OmniboxSuggestion suggestionMatch;
boolean inSuggestionList = true;
if (getSuggestionCount() > 0
&& urlText.trim().equals(mUrlTextAfterSuggestionsReceived.trim())) {
// Common case: the user typed something, received suggestions, then pressed enter.
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.classify(urlText, mDelegate.didFocusUrlFromFakebox());
// Classify matches don't propagate to java, so skip the OOB check.
inSuggestionList = false;
// If urlText couldn't be classified, bail.
if (suggestionMatch == null) return;
}
loadUrlFromOmniboxMatch(0, suggestionMatch, inputStart, inSuggestionList);
}
/**
* Loads the specified omnibox suggestion.
*
* @param matchPosition The position of the selected omnibox suggestion.
* @param suggestion The suggestion selected.
* @param inputStart The timestamp the input was started.
* @param inVisibleSuggestionList Whether the suggestion is in the visible suggestion list.
*/
private void loadUrlFromOmniboxMatch(int matchPosition, OmniboxSuggestion suggestion,
long inputStart, boolean inVisibleSuggestionList) {
final long activationTime = System.currentTimeMillis();
RecordHistogram.recordMediumTimesHistogram(
"Omnibox.FocusToOpenTimeAnyPopupState3", activationTime - mUrlFocusTime);
GURL url = updateSuggestionUrlIfNeeded(suggestion, matchPosition, !inVisibleSuggestionList);
// 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();
String currentPageUrl = mDataProvider.getCurrentUrl();
WebContents webContents =
mDataProvider.hasTab() ? mDataProvider.getTab().getWebContents() : null;
long elapsedTimeSinceModified = mNewOmniboxEditSessionTimestamp > 0
? (SystemClock.elapsedRealtime() - mNewOmniboxEditSessionTimestamp)
: -1;
boolean shouldSkipNativeLog = mShowCachedZeroSuggestResults
&& (mDeferredOnSelection != null) && !mDeferredOnSelection.shouldLog();
if (!shouldSkipNativeLog) {
int autocompleteLength = mUrlBarEditingTextProvider.getTextWithAutocomplete().length()
- mUrlBarEditingTextProvider.getTextWithoutAutocomplete().length();
int pageClassification =
mDataProvider.getPageClassification(mDelegate.didFocusUrlFromFakebox());
mAutocomplete.onSuggestionSelected(matchPosition, suggestion.hashCode(), type,
currentPageUrl, pageClassification, elapsedTimeSinceModified,
autocompleteLength, webContents);
}
if (((transition & PageTransition.CORE_MASK) == PageTransition.TYPED)
&& TextUtils.equals(url.getSpec(), mDataProvider.getCurrentUrl())) {
// 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 (((transition & PageTransition.CORE_MASK) == PageTransition.GENERATED)
&& TextUtils.equals(
suggestion.getFillIntoEdit(), mDataProvider.getDisplaySearchTerms())) {
// When the omnibox is displaying the default search provider search terms,
// the user focuses the omnibox, and hits Enter without refining the search
// terms, we should classify this transition 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;
}
if (suggestion.getType() == OmniboxSuggestionType.CLIPBOARD_IMAGE) {
mDelegate.loadUrlWithPostData(url.getSpec(), transition, inputStart,
suggestion.getPostContentType(), suggestion.getPostData());
return;
}
mDelegate.loadUrl(url.getSpec(), transition, inputStart);
}
/**
* Make a zero suggest request if:
* - Native is loaded.
* - The URL bar has focus.
* - The the tab/overview is not incognito.
*/
private void startZeroSuggest() {
// Reset "edited" state in the omnibox if zero suggest is triggered -- new edits
// now count as a new session.
mHasStartedNewOmniboxEditSession = false;
mNewOmniboxEditSessionTimestamp = -1;
if (mNativeInitialized && mDelegate.isUrlBarFocused()
&& (mDataProvider.hasTab() || mDataProvider.isInOverviewAndShowingOmnibox())) {
int pageClassification =
mDataProvider.getPageClassification(mDelegate.didFocusUrlFromFakebox());
mAutocomplete.startZeroSuggest(mDataProvider.getProfile(),
mUrlBarEditingTextProvider.getTextWithAutocomplete(),
mDataProvider.getCurrentUrl(), pageClassification, mDataProvider.getTitle());
}
}
/**
* Update whether the omnibox suggestions are visible.
*/
private void updateOmniboxSuggestionsVisibility() {
boolean shouldBeVisible =
getSuggestionVisibilityState() == SuggestionVisibilityState.ALLOWED
&& getSuggestionCount() > 0;
boolean wasVisible = mListPropertyModel.get(SuggestionListProperties.VISIBLE);
mListPropertyModel.set(SuggestionListProperties.VISIBLE, shouldBeVisible);
if (shouldBeVisible && !wasVisible) {
mIgnoreOmniboxItemSelection = true; // Reset to default value.
}
}
/**
* Hides the omnibox suggestion popup.
*
* <p>
* Signals the autocomplete controller to stop generating omnibox suggestions.
*
* @see AutocompleteController#stop(boolean)
*/
private void hideSuggestions() {
if (mAutocomplete == null || !mNativeInitialized) return;
stopAutocomplete(true);
getSuggestionModelList().clear();
mAvailableSuggestions.clear();
updateOmniboxSuggestionsVisibility();
}
/**
* 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.
*/
private void stopAutocomplete(boolean clear) {
if (mAutocomplete != null) mAutocomplete.stop(clear);
cancelPendingAutocompleteStart();
}
/**
* Cancels the queued task to start the autocomplete controller, if any.
*/
@VisibleForTesting
void cancelPendingAutocompleteStart() {
if (mRequestSuggestions != null) {
// There is a request for suggestions either waiting for the native side
// to start, or on the message queue. Remove it from wherever it is.
if (!mDeferredNativeRunnables.remove(mRequestSuggestions)) {
mHandler.removeCallbacks(mRequestSuggestions);
}
mRequestSuggestions = null;
}
}
/**
* Trigger autocomplete for the given query.
*/
void startAutocompleteForQuery(String query) {
stopAutocomplete(false);
if (mDataProvider.hasTab()) {
mAutocomplete.start(mDataProvider.getProfile(), mDataProvider.getCurrentUrl(),
mDataProvider.getPageClassification(false), query, -1, false, null);
}
}
/**
* Sets the autocomplete controller for the location bar.
*
* @param controller The controller that will handle autocomplete/omnibox suggestions.
* @note Only used for testing.
*/
@VisibleForTesting
public void setAutocompleteController(AutocompleteController controller) {
if (mAutocomplete != null) stopAutocomplete(true);
mAutocomplete = controller;
}
private abstract static class DeferredOnSelectionRunnable implements Runnable {
protected final OmniboxSuggestion mSuggestion;
protected final int mPosition;
protected boolean mShouldLog;
public DeferredOnSelectionRunnable(OmniboxSuggestion suggestion, int position) {
this.mSuggestion = suggestion;
this.mPosition = position;
}
/**
* Set whether the selection matches with native results for logging to make sense.
* @param log Whether the selection should be logged in native code.
*/
public void setShouldLog(boolean log) {
mShouldLog = log;
}
/**
* @return Whether the selection should be logged in native code.
*/
public boolean shouldLog() {
return mShouldLog;
}
}
/**
* Respond to Suggestion list height change and update list of presented suggestions.
*
* This typically happens as a result of soft keyboard being shown or hidden.
*
* @param newHeightPx New height of the suggestion list in pixels.
*/
@Override
public void onSuggestionDropdownHeightChanged(@Px int newHeightPx) {
if (!mEnableAdaptiveSuggestionsCount) return;
mMaximumSuggestionsListHeight = newHeightPx;
updateSuggestionsList(mMaximumSuggestionsListHeight);
}
@Override
public void onSuggestionDropdownScroll() {
if (!shouldShowSoftKeyboard()) {
mDelegate.setKeyboardVisibility(false);
}
}
}