blob: 0bf50532fa49b60b3fd79276e71ed57c60be61d7 [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.content.Context;
import android.os.Handler;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import org.chromium.base.Callback;
import org.chromium.base.StrictModeContext;
import org.chromium.base.jank_tracker.JankTracker;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.omnibox.LocationBarDataProvider;
import org.chromium.chrome.browser.omnibox.R;
import org.chromium.chrome.browser.omnibox.UrlBar.UrlTextChangeListener;
import org.chromium.chrome.browser.omnibox.UrlBarEditingTextStateProvider;
import org.chromium.chrome.browser.omnibox.UrlFocusChangeListener;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteController.OnSuggestionsReceivedListener;
import org.chromium.chrome.browser.omnibox.suggestions.SuggestionListViewBinder.SuggestionListViewHolder;
import org.chromium.chrome.browser.omnibox.suggestions.answer.AnswerSuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.base.BaseSuggestionView;
import org.chromium.chrome.browser.omnibox.suggestions.base.BaseSuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.basic.BasicSuggestionProcessor.BookmarkState;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.carousel.BaseCarouselSuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.editurl.EditUrlSuggestionView;
import org.chromium.chrome.browser.omnibox.suggestions.editurl.EditUrlSuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.header.HeaderView;
import org.chromium.chrome.browser.omnibox.suggestions.header.HeaderViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.mostvisited.ExploreIconProvider;
import org.chromium.chrome.browser.omnibox.suggestions.mostvisited.MostVisitedTilesProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.tail.TailSuggestionView;
import org.chromium.chrome.browser.omnibox.suggestions.tail.TailSuggestionViewBinder;
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.tabmodel.TabWindowManager;
import org.chromium.chrome.browser.util.KeyNavigationUtil;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.query_tiles.QueryTile;
import org.chromium.ui.ViewProvider;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modelutil.LazyConstructionPropertyMcp;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.ArrayList;
import java.util.List;
/**
* Coordinator that handles the interactions with the autocomplete system.
*/
public class AutocompleteCoordinator implements UrlFocusChangeListener, UrlTextChangeListener {
private final @NonNull ViewGroup mParent;
private final @NonNull ObservableSupplier<Profile> mProfileSupplier;
private final @NonNull Callback<Profile> mProfileChangeCallback;
private final @NonNull OmniboxQueryTileCoordinator mQueryTileCoordinator;
private final @NonNull AutocompleteMediator mMediator;
private @Nullable OmniboxSuggestionsDropdown mDropdown;
public AutocompleteCoordinator(@NonNull ViewGroup parent,
@NonNull AutocompleteDelegate delegate,
@NonNull OmniboxSuggestionsDropdownEmbedder dropdownEmbedder,
@NonNull UrlBarEditingTextStateProvider urlBarEditingTextProvider,
@NonNull Supplier<ModalDialogManager> modalDialogManagerSupplier,
@NonNull Supplier<Tab> activityTabSupplier,
@Nullable Supplier<ShareDelegate> shareDelegateSupplier,
@NonNull LocationBarDataProvider locationBarDataProvider,
@NonNull ObservableSupplier<Profile> profileObservableSupplier,
@NonNull Callback<Tab> bringToForegroundCallback,
@NonNull Supplier<TabWindowManager> tabWindowManagerSupplier,
@NonNull BookmarkState bookmarkState, @NonNull JankTracker jankTracker,
@NonNull ExploreIconProvider exploreIconProvider) {
mParent = parent;
Context context = parent.getContext();
PropertyModel listModel = new PropertyModel(SuggestionListProperties.ALL_KEYS);
ModelList listItems = new ModelList();
listModel.set(SuggestionListProperties.EMBEDDER, dropdownEmbedder);
listModel.set(SuggestionListProperties.VISIBLE, false);
listModel.set(SuggestionListProperties.SUGGESTION_MODELS, listItems);
mQueryTileCoordinator = new OmniboxQueryTileCoordinator(context, this::onTileSelected);
mMediator = new AutocompleteMediator(context, delegate, urlBarEditingTextProvider,
listModel, new Handler(), modalDialogManagerSupplier, activityTabSupplier,
shareDelegateSupplier, locationBarDataProvider, bringToForegroundCallback,
tabWindowManagerSupplier, bookmarkState, jankTracker, exploreIconProvider);
mMediator.initDefaultProcessors(mQueryTileCoordinator::setTiles);
listModel.set(SuggestionListProperties.OBSERVER, mMediator);
ViewProvider<SuggestionListViewHolder> viewProvider =
createViewProvider(context, listItems);
viewProvider.whenLoaded((holder) -> { mDropdown = holder.dropdown; });
LazyConstructionPropertyMcp.create(listModel, SuggestionListProperties.VISIBLE,
viewProvider, SuggestionListViewBinder::bind);
mProfileSupplier = profileObservableSupplier;
mProfileChangeCallback = this::setAutocompleteProfile;
mProfileSupplier.addObserver(mProfileChangeCallback);
// https://crbug.com/966227 Set initial layout direction ahead of inflating the suggestions.
updateSuggestionListLayoutDirection();
}
/**
* Clean up resources used by this class.
*/
public void destroy() {
mProfileSupplier.removeObserver(mProfileChangeCallback);
mQueryTileCoordinator.destroy();
mMediator.destroy();
}
private ViewProvider<SuggestionListViewHolder> createViewProvider(
Context context, MVCListAdapter.ModelList modelList) {
return new ViewProvider<SuggestionListViewHolder>() {
private List<Callback<SuggestionListViewHolder>> mCallbacks = new ArrayList<>();
private SuggestionListViewHolder mHolder;
@Override
public void inflate() {
OmniboxSuggestionsDropdown dropdown;
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
dropdown = new OmniboxSuggestionsDropdown(context);
}
// Start with visibility GONE to ensure that show() is called.
// http://crbug.com/517438
dropdown.getViewGroup().setVisibility(View.GONE);
dropdown.getViewGroup().setClipToPadding(false);
OmniboxSuggestionsDropdownAdapter adapter =
new OmniboxSuggestionsDropdownAdapter(modelList);
dropdown.setAdapter(adapter);
// Note: clang-format does a bad job formatting lambdas so we turn it off here.
// clang-format off
// Register a view type for a default omnibox suggestion.
adapter.registerType(
OmniboxSuggestionUiType.DEFAULT,
parent -> new BaseSuggestionView<View>(
parent.getContext(), R.layout.omnibox_basic_suggestion),
new BaseSuggestionViewBinder<View>(SuggestionViewViewBinder::bind));
adapter.registerType(
OmniboxSuggestionUiType.EDIT_URL_SUGGESTION,
parent -> new EditUrlSuggestionView(parent.getContext()),
new EditUrlSuggestionViewBinder());
adapter.registerType(
OmniboxSuggestionUiType.ANSWER_SUGGESTION,
parent -> new BaseSuggestionView<View>(
parent.getContext(), R.layout.omnibox_answer_suggestion),
new BaseSuggestionViewBinder<View>(AnswerSuggestionViewBinder::bind));
adapter.registerType(
OmniboxSuggestionUiType.ENTITY_SUGGESTION,
parent -> new BaseSuggestionView<View>(
parent.getContext(), R.layout.omnibox_entity_suggestion),
new BaseSuggestionViewBinder<View>(EntitySuggestionViewBinder::bind));
adapter.registerType(
OmniboxSuggestionUiType.TAIL_SUGGESTION,
parent -> new BaseSuggestionView<TailSuggestionView>(
new TailSuggestionView(parent.getContext())),
new BaseSuggestionViewBinder<TailSuggestionView>(
TailSuggestionViewBinder::bind));
adapter.registerType(
OmniboxSuggestionUiType.CLIPBOARD_SUGGESTION,
parent -> new BaseSuggestionView<View>(
parent.getContext(), R.layout.omnibox_basic_suggestion),
new BaseSuggestionViewBinder<View>(SuggestionViewViewBinder::bind));
adapter.registerType(
OmniboxSuggestionUiType.TILE_SUGGESTION,
parent -> mQueryTileCoordinator.createView(parent.getContext()),
mQueryTileCoordinator::bind);
adapter.registerType(
OmniboxSuggestionUiType.TILE_NAVSUGGEST,
MostVisitedTilesProcessor::createView,
BaseCarouselSuggestionViewBinder::bind);
adapter.registerType(
OmniboxSuggestionUiType.HEADER,
parent -> new HeaderView(parent.getContext()),
HeaderViewBinder::bind);
// clang-format on
ViewGroup container = (ViewGroup) ((ViewStub) mParent.getRootView().findViewById(
R.id.omnibox_results_container_stub))
.inflate();
mHolder = new SuggestionListViewHolder(container, dropdown);
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onResult(mHolder);
}
mCallbacks = null;
}
@Override
public void whenLoaded(Callback<SuggestionListViewHolder> callback) {
if (mHolder != null) {
callback.onResult(mHolder);
return;
}
mCallbacks.add(callback);
}
};
}
@Override
public void onUrlFocusChange(boolean hasFocus) {
mMediator.onUrlFocusChange(hasFocus);
}
@Override
public void onUrlAnimationFinished(boolean hasFocus) {
mMediator.onUrlAnimationFinished(hasFocus);
}
/**
* Updates the profile used for generating autocomplete suggestions.
* @param profile The profile to be used.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public void setAutocompleteProfile(Profile profile) {
mMediator.setAutocompleteProfile(profile);
mQueryTileCoordinator.setProfile(profile);
}
/**
* Whether omnibox autocomplete should currently be prevented from generating suggestions.
*/
public void setShouldPreventOmniboxAutocomplete(boolean prevent) {
mMediator.setShouldPreventOmniboxAutocomplete(prevent);
}
/**
* @return The number of current autocomplete suggestions.
*/
public int getSuggestionCount() {
return mMediator.getSuggestionCount();
}
/**
* 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 AutocompleteMatch getSuggestionAt(int index) {
return mMediator.getSuggestionAt(index);
}
/**
* Signals that native initialization has completed.
*/
public void onNativeInitialized() {
mMediator.onNativeInitialized();
}
/**
* @see AutocompleteController#onVoiceResults(List)
*/
public void onVoiceResults(@Nullable List<VoiceRecognitionHandler.VoiceResult> results) {
mMediator.onVoiceResults(results);
}
/**
* @return The current native pointer to the autocomplete results.
* TODO(ender): Figure out how to remove this.
*/
public long getCurrentNativeAutocompleteResult() {
return mMediator.getCurrentNativeAutocompleteResult();
}
/**
* Update the layout direction of the suggestion list based on the parent layout direction.
*/
public void updateSuggestionListLayoutDirection() {
mMediator.setLayoutDirection(ViewCompat.getLayoutDirection(mParent));
}
/**
* Update the visuals of the autocomplete UI.
* @param useDarkColors Whether dark colors should be applied to the UI.
* @param isIncognito Whether the UI is for incognito mode or not.
*/
public void updateVisualsForState(boolean useDarkColors, boolean isIncognito) {
mMediator.updateVisualsForState(useDarkColors, isIncognito);
}
/**
* Show cached zero suggest results.
* Enables Autocomplete subsystem to offer most recently presented suggestions in the event
* where Native counterpart is not yet initialized.
*/
public void startCachedZeroSuggest() {
mMediator.startCachedZeroSuggest();
}
/**
* Handle the key events associated with the suggestion list.
*
* @param keyCode The keycode representing what key was interacted with.
* @param event The key event containing all meta-data associated with the event.
* @return Whether the key event was handled.
*/
public boolean handleKeyEvent(int keyCode, KeyEvent event) {
boolean isShowingList = mDropdown != null && mDropdown.getViewGroup().isShown();
boolean isAnyDirection = KeyNavigationUtil.isGoAnyDirection(event);
if (isShowingList && mMediator.getSuggestionCount() > 0 && isAnyDirection) {
mMediator.allowPendingItemSelection();
}
boolean isValidListKey = isAnyDirection || KeyNavigationUtil.isEnter(event);
if (isShowingList && isValidListKey && mDropdown.getViewGroup().onKeyDown(keyCode, event)) {
return true;
}
if (KeyNavigationUtil.isEnter(event) && mParent.getVisibility() == View.VISIBLE) {
mMediator.loadTypedOmniboxText(event.getEventTime());
return true;
}
return false;
}
@Override
public void onTextChanged(String textWithoutAutocomplete, String textWithAutocomplete) {
mMediator.onTextChanged(textWithoutAutocomplete, textWithAutocomplete);
}
/**
* Trigger autocomplete for the given query.
*/
public void startAutocompleteForQuery(String query) {
mMediator.startAutocompleteForQuery(query);
}
/**
* Given a search query, this will attempt to see if the query appears to be portion of a
* properly formed URL. If it appears to be a URL, this will return the fully qualified
* version (i.e. including the scheme, etc...). If the query does not appear to be a URL,
* this will return null.
*
* Note:
* 1) This call is expensive. Use only when it is absolutely necessary to get the exact
* information about how a given query string will be interpreted. For less restrictive
* URL vs text matching, please defer to GURL.
* 2) this updates the internal state of the autocomplete controller just as start() does.
* Future calls that reference autocomplete results by index, e.g. onSuggestionSelected(),
* should reference the returned suggestion by index 0.
*
* TODO(crbug.com/966424): Fix the dependency issue and remove this method.
* Please don't use this in any new code.
*
* @param profile The profile to expand the query for.
* @param query The query to be expanded into a fully qualified URL if appropriate.
* @return The AutocompleteMatch for a default / top match. This may be either SEARCH
* match built with the user's default search engine, or a NAVIGATION match.
*/
@Deprecated
public static AutocompleteMatch classify(Profile profile, String query) {
return AutocompleteController.getForProfile(profile).classify(query, false);
}
/**
* Sends a zero suggest request to the server in order to pre-populate the result cache.
*/
public void prefetchZeroSuggestResults() {
AutocompleteControllerJni.get().prefetchZeroSuggestResults();
}
/** @return Suggestions Dropdown view, showing the list of suggestions. */
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public OmniboxSuggestionsDropdown getSuggestionsDropdownForTest() {
return mDropdown;
}
/** @return The current receiving OnSuggestionsReceived events. */
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public OnSuggestionsReceivedListener getSuggestionsReceivedListenerForTest() {
return mMediator;
}
/** @return The ModelList for the currently shown suggestions. */
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public ModelList getSuggestionModelListForTest() {
return mMediator.getSuggestionModelListForTest();
}
private void onTileSelected(QueryTile queryTile) {
mMediator.onQueryTileSelected(queryTile);
}
}