// Copyright 2014 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; import android.os.Bundle; import android.text.TextUtils; import org.chromium.base.VisibleForTesting; import org.chromium.base.annotations.CalledByNative; import org.chromium.chrome.browser.omnibox.OmniboxSuggestion.MatchClassification; import org.chromium.chrome.browser.omnibox.VoiceSuggestionProvider.VoiceResult; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.content_public.browser.WebContents; import java.util.ArrayList; import java.util.List; /** * Bridge to the native AutocompleteControllerAndroid. */ public class AutocompleteController { // Maximum number of search/history suggestions to show. private static final int MAX_DEFAULT_SUGGESTION_COUNT = 5; // Maximum number of voice suggestions to show. private static final int MAX_VOICE_SUGGESTION_COUNT = 3; private long mNativeAutocompleteControllerAndroid; private long mCurrentNativeAutocompleteResult; private final OnSuggestionsReceivedListener mListener; private final VoiceSuggestionProvider mVoiceSuggestionProvider = new VoiceSuggestionProvider(); /** * Listener for receiving OmniboxSuggestions. */ public static interface OnSuggestionsReceivedListener { void onSuggestionsReceived(List<OmniboxSuggestion> suggestions, String inlineAutocompleteText); } public AutocompleteController(OnSuggestionsReceivedListener listener) { this(null, listener); } public AutocompleteController(Profile profile, OnSuggestionsReceivedListener listener) { if (profile != null) { mNativeAutocompleteControllerAndroid = nativeInit(profile); } mListener = listener; } /** * Resets the underlying autocomplete controller based on the specified profile. * * <p>This will implicitly stop the autocomplete suggestions, so * {@link #start(Profile, String, String, boolean)} must be called again to start them flowing * again. This should not be an issue as changing profiles should not normally occur while * waiting on omnibox suggestions. * * @param profile The profile to reset the AutocompleteController with. */ public void setProfile(Profile profile) { stop(true); if (profile == null) { mNativeAutocompleteControllerAndroid = 0; return; } mNativeAutocompleteControllerAndroid = nativeInit(profile); } /** * Starts querying for omnibox suggestions for a given text. * * @param profile The profile to use for starting the AutocompleteController * @param url The URL of the current tab, used to suggest query refinements. * @param text The text to query autocomplete suggestions for. * @param preventInlineAutocomplete Whether autocomplete suggestions should be prevented. */ public void start(Profile profile, String url, String text, boolean preventInlineAutocomplete) { start(profile, url, text, -1, preventInlineAutocomplete); } /** * Starts querying for omnibox suggestions for a given text. * * @param profile The profile to use for starting the AutocompleteController * @param url The URL of the current tab, used to suggest query refinements. * @param text The text to query autocomplete suggestions for. * @param cursorPosition The position of the cursor within the text. * @param preventInlineAutocomplete Whether autocomplete suggestions should be prevented. */ public void start(Profile profile, String url, String text, int cursorPosition, boolean preventInlineAutocomplete) { if (profile == null || TextUtils.isEmpty(url)) return; mNativeAutocompleteControllerAndroid = nativeInit(profile); // Initializing the native counterpart might still fail. if (mNativeAutocompleteControllerAndroid != 0) { nativeStart(mNativeAutocompleteControllerAndroid, text, cursorPosition, null, url, preventInlineAutocomplete, false, false, true); } } /** * Given some string |text| that the user wants to use for navigation, determines how it should * be interpreted. This is a fallback in case the user didn't select a visible suggestion (e.g. * the user pressed enter before omnibox suggestions had been shown). * * Note: 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. * * @param text The user's input text to classify (i.e. what they typed in the omnibox) * @return The OmniboxSuggestion specifying where to navigate, the transition type, etc. May * be null if the input is invalid. */ public OmniboxSuggestion classify(String text) { if (mNativeAutocompleteControllerAndroid != 0) { return nativeClassify(mNativeAutocompleteControllerAndroid, text); } return null; } /** * Starts a query for suggestions before any input is available from the user. * * @param profile The profile to use for starting the AutocompleteController. * @param omniboxText The text displayed in the omnibox. * @param url The url of the currently loaded web page. * @param focusedFromFakebox Whether the user entered the omnibox by tapping the fakebox on the * native NTP. This should be false on all other pages. */ public void startZeroSuggest(Profile profile, String omniboxText, String url, boolean focusedFromFakebox) { if (profile == null || TextUtils.isEmpty(url)) return; mNativeAutocompleteControllerAndroid = nativeInit(profile); if (mNativeAutocompleteControllerAndroid != 0) { nativeOnOmniboxFocused(mNativeAutocompleteControllerAndroid, omniboxText, url, focusedFromFakebox); } } /** * Stops generating autocomplete suggestions for the currently specified text from * {@link #start(Profile,String, String, boolean)}. * * <p> * Calling this method with {@code false}, will result in * {@link #onSuggestionsReceived(List, String, long)} being called with an empty * result set. * * @param clear Whether to clear the most recent autocomplete results. */ public void stop(boolean clear) { if (clear) mVoiceSuggestionProvider.clearVoiceSearchResults(); mCurrentNativeAutocompleteResult = 0; if (mNativeAutocompleteControllerAndroid != 0) { nativeStop(mNativeAutocompleteControllerAndroid, clear); } } /** * Resets session for autocomplete controller. This happens every time we start typing * new input into the omnibox. */ public void resetSession() { if (mNativeAutocompleteControllerAndroid != 0) { nativeResetSession(mNativeAutocompleteControllerAndroid); } } /** * Deletes an omnibox suggestion, if possible. * @param position The position at which the suggestion is located. */ public void deleteSuggestion(int position) { if (mNativeAutocompleteControllerAndroid != 0) { nativeDeleteSuggestion(mNativeAutocompleteControllerAndroid, position); } } /** * @return Native pointer to current autocomplete results. */ @VisibleForTesting public long getCurrentNativeAutocompleteResult() { return mCurrentNativeAutocompleteResult; } @CalledByNative protected void onSuggestionsReceived( List<OmniboxSuggestion> suggestions, String inlineAutocompleteText, long currentNativeAutocompleteResult) { if (suggestions.size() > MAX_DEFAULT_SUGGESTION_COUNT) { // Trim to the default amount of normal suggestions we can have. suggestions.subList(MAX_DEFAULT_SUGGESTION_COUNT, suggestions.size()).clear(); } // Run through new providers to get an updated list of suggestions. suggestions = mVoiceSuggestionProvider.addVoiceSuggestions( suggestions, MAX_VOICE_SUGGESTION_COUNT); mCurrentNativeAutocompleteResult = currentNativeAutocompleteResult; // Notify callbacks of suggestions. mListener.onSuggestionsReceived(suggestions, inlineAutocompleteText); } @CalledByNative private void notifyNativeDestroyed() { mNativeAutocompleteControllerAndroid = 0; } /** * Called whenever a navigation happens from the omnibox to record metrics about the user's * interaction with the omnibox. * * @param selectedIndex The index of the suggestion that was selected. * @param type The type of the selected suggestion. * @param currentPageUrl The URL of the current page. * @param focusedFromFakebox Whether the user entered the omnibox by tapping the fakebox on the * native NTP. This should be false on all other pages. * @param elapsedTimeSinceModified The number of ms that passed between the user first * modifying text in the omnibox and selecting a suggestion. * @param completedLength The length of the default match's inline autocompletion if any. * @param webContents The web contents for the tab where the selected suggestion will be shown. */ public void onSuggestionSelected(int selectedIndex, int type, String currentPageUrl, boolean focusedFromFakebox, long elapsedTimeSinceModified, int completedLength, WebContents webContents) { // Don't natively log voice suggestion results as we add them in Java. if (type == OmniboxSuggestionType.VOICE_SUGGEST) return; nativeOnSuggestionSelected(mNativeAutocompleteControllerAndroid, selectedIndex, currentPageUrl, focusedFromFakebox, elapsedTimeSinceModified, completedLength, webContents); } /** * Pass the autocomplete controller a {@link Bundle} representing the results of a voice * recognition. * @param data A {@link Bundle} containing the results of a voice recognition. * @return The top voice match if one exists, {@code null} otherwise. */ public VoiceResult onVoiceResults(Bundle data) { mVoiceSuggestionProvider.setVoiceResultsFromIntentBundle(data); List<VoiceResult> results = mVoiceSuggestionProvider.getResults(); return (results != null && results.size() > 0) ? results.get(0) : null; } @CalledByNative private static List<OmniboxSuggestion> createOmniboxSuggestionList(int size) { return new ArrayList<OmniboxSuggestion>(size); } @CalledByNative private static void addOmniboxSuggestionToList(List<OmniboxSuggestion> suggestionList, OmniboxSuggestion suggestion) { suggestionList.add(suggestion); } @CalledByNative private static OmniboxSuggestion buildOmniboxSuggestion( int nativeType, boolean isSearchType, int relevance, int transition, String contents, int[] contentClassificationOffsets, int[] contentClassificationStyles, String description, int[] descriptionClassificationOffsets, int[] descriptionClassificationStyles, String answerContents, String answerType, String fillIntoEdit, String url, boolean isStarred, boolean isDeletable) { assert contentClassificationOffsets.length == contentClassificationStyles.length; List<MatchClassification> contentClassifications = new ArrayList<>(); for (int i = 0; i < contentClassificationOffsets.length; i++) { contentClassifications.add(new MatchClassification( contentClassificationOffsets[i], contentClassificationStyles[i])); } assert descriptionClassificationOffsets.length == descriptionClassificationStyles.length; List<MatchClassification> descriptionClassifications = new ArrayList<>(); for (int i = 0; i < descriptionClassificationOffsets.length; i++) { descriptionClassifications.add(new MatchClassification( descriptionClassificationOffsets[i], descriptionClassificationStyles[i])); } return new OmniboxSuggestion(nativeType, isSearchType, relevance, transition, contents, contentClassifications, description, descriptionClassifications, answerContents, answerType, fillIntoEdit, url, isStarred, isDeletable); } /** * Updates aqs parameters on the selected match that we will navigate to and returns the * updated URL. |selected_index| is the position of the selected match and * |elapsed_time_since_input_change| is the time in ms between the first typed input and match * selection. * * @param selectedIndex The index of the autocomplete entry selected. * @param elapsedTimeSinceInputChange The number of ms between the time the user started * typing in the omnibox and the time the user has selected * a suggestion. * @return The url to navigate to for this match with aqs parameter updated, if we are * making a Google search query. */ public String updateMatchDestinationUrlWithQueryFormulationTime(int selectedIndex, long elapsedTimeSinceInputChange) { return nativeUpdateMatchDestinationURLWithQueryFormulationTime( mNativeAutocompleteControllerAndroid, selectedIndex, elapsedTimeSinceInputChange); } @VisibleForTesting protected native long nativeInit(Profile profile); private native void nativeStart(long nativeAutocompleteControllerAndroid, String text, int cursorPosition, String desiredTld, String currentUrl, boolean preventInlineAutocomplete, boolean preferKeyword, boolean allowExactKeywordMatch, boolean wantAsynchronousMatches); private native OmniboxSuggestion nativeClassify(long nativeAutocompleteControllerAndroid, String text); private native void nativeStop(long nativeAutocompleteControllerAndroid, boolean clearResults); private native void nativeResetSession(long nativeAutocompleteControllerAndroid); private native void nativeOnSuggestionSelected(long nativeAutocompleteControllerAndroid, int selectedIndex, String currentPageUrl, boolean focusedFromFakebox, long elapsedTimeSinceModified, int completedLength, WebContents webContents); private native void nativeOnOmniboxFocused(long nativeAutocompleteControllerAndroid, String omniboxText, String currentUrl, boolean focusedFromFakebox); private native void nativeDeleteSuggestion(long nativeAutocompleteControllerAndroid, int selectedIndex); private native String nativeUpdateMatchDestinationURLWithQueryFormulationTime( long nativeAutocompleteControllerAndroid, int selectedIndex, long elapsedTimeSinceInputChange); /** * 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. * * @param query The query to be expanded into a fully qualified URL if appropriate. * @return The fully qualified URL or null. */ public static native String nativeQualifyPartialURLQuery(String query); /** * Sends a zero suggest request to the server in order to pre-populate the result cache. */ public static native void nativePrefetchZeroSuggestResults(); }