| // 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. |
| |
| #include "chrome/browser/android/omnibox/autocomplete_controller_android.h" |
| |
| #include "base/android/jni_android.h" |
| #include "base/android/jni_string.h" |
| #include "base/prefs/pref_service.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "chrome/browser/autocomplete/autocomplete_classifier.h" |
| #include "chrome/browser/autocomplete/autocomplete_classifier_factory.h" |
| #include "chrome/browser/autocomplete/autocomplete_controller.h" |
| #include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h" |
| #include "chrome/browser/autocomplete/shortcuts_backend_factory.h" |
| #include "chrome/browser/bookmarks/bookmark_model_factory.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/omnibox/omnibox_log.h" |
| #include "chrome/browser/profiles/incognito_helpers.h" |
| #include "chrome/browser/profiles/profile_android.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/search/search.h" |
| #include "chrome/browser/search_engines/template_url_service_factory.h" |
| #include "chrome/browser/sessions/session_tab_helper.h" |
| #include "chrome/browser/ui/search/instant_search_prerenderer.h" |
| #include "chrome/browser/ui/toolbar/toolbar_model.h" |
| #include "chrome/common/instant_types.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/bookmarks/browser/bookmark_model.h" |
| #include "components/keyed_service/content/browser_context_dependency_manager.h" |
| #include "components/metrics/proto/omnibox_event.pb.h" |
| #include "components/omnibox/autocomplete_input.h" |
| #include "components/omnibox/autocomplete_match.h" |
| #include "components/omnibox/autocomplete_match_type.h" |
| #include "components/omnibox/omnibox_field_trial.h" |
| #include "components/omnibox/search_provider.h" |
| #include "components/search/search.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "content/public/browser/notification_details.h" |
| #include "content/public/browser/notification_service.h" |
| #include "content/public/browser/notification_source.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/url_constants.h" |
| #include "jni/AutocompleteController_jni.h" |
| #include "net/base/escape.h" |
| #include "net/base/net_util.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| |
| using base::android::AttachCurrentThread; |
| using base::android::ConvertJavaStringToUTF16; |
| using base::android::ConvertUTF8ToJavaString; |
| using base::android::ConvertUTF16ToJavaString; |
| using bookmarks::BookmarkModel; |
| using metrics::OmniboxEventProto; |
| |
| namespace { |
| |
| const int kAndroidAutocompleteProviders = |
| AutocompleteClassifier::kDefaultOmniboxProviders; |
| |
| /** |
| * A prefetcher class responsible for triggering zero suggest prefetch. |
| * The prefetch occurs as a side-effect of calling OnOmniboxFocused() on |
| * the AutocompleteController object. |
| */ |
| class ZeroSuggestPrefetcher : public AutocompleteControllerDelegate { |
| public: |
| explicit ZeroSuggestPrefetcher(Profile* profile); |
| |
| private: |
| ~ZeroSuggestPrefetcher() override; |
| void SelfDestruct(); |
| |
| // AutocompleteControllerDelegate: |
| void OnResultChanged(bool default_match_changed) override; |
| |
| scoped_ptr<AutocompleteController> controller_; |
| base::OneShotTimer<ZeroSuggestPrefetcher> expire_timer_; |
| }; |
| |
| ZeroSuggestPrefetcher::ZeroSuggestPrefetcher(Profile* profile) |
| : controller_(new AutocompleteController( |
| profile, TemplateURLServiceFactory::GetForProfile(profile), this, |
| AutocompleteProvider::TYPE_ZERO_SUGGEST)) { |
| // Creating an arbitrary fake_request_source to avoid passing in an invalid |
| // AutocompleteInput object. |
| base::string16 fake_request_source(base::ASCIIToUTF16( |
| "http://www.foobarbazblah.com")); |
| controller_->OnOmniboxFocused(AutocompleteInput( |
| fake_request_source, base::string16::npos, std::string(), |
| GURL(fake_request_source), OmniboxEventProto::INVALID_SPEC, false, false, |
| true, true, ChromeAutocompleteSchemeClassifier(profile))); |
| // Delete ourselves after 10s. This is enough time to cache results or |
| // give up if the results haven't been received. |
| expire_timer_.Start(FROM_HERE, |
| base::TimeDelta::FromMilliseconds(10000), |
| this, &ZeroSuggestPrefetcher::SelfDestruct); |
| } |
| |
| ZeroSuggestPrefetcher::~ZeroSuggestPrefetcher() { |
| } |
| |
| void ZeroSuggestPrefetcher::SelfDestruct() { |
| delete this; |
| } |
| |
| void ZeroSuggestPrefetcher::OnResultChanged(bool default_match_changed) { |
| // Nothing to do here, the results have been cached. |
| // We don't want to trigger deletion here because this is being called by the |
| // AutocompleteController object. |
| } |
| |
| } // namespace |
| |
| AutocompleteControllerAndroid::AutocompleteControllerAndroid(Profile* profile) |
| : autocomplete_controller_(new AutocompleteController( |
| profile, TemplateURLServiceFactory::GetForProfile(profile), this, |
| kAndroidAutocompleteProviders)), |
| inside_synchronous_start_(false), |
| profile_(profile) { |
| } |
| |
| void AutocompleteControllerAndroid::Start(JNIEnv* env, |
| jobject obj, |
| jstring j_text, |
| jint j_cursor_pos, |
| jstring j_desired_tld, |
| jstring j_current_url, |
| bool prevent_inline_autocomplete, |
| bool prefer_keyword, |
| bool allow_exact_keyword_match, |
| bool want_asynchronous_matches) { |
| if (!autocomplete_controller_) |
| return; |
| |
| std::string desired_tld; |
| GURL current_url; |
| if (j_current_url != NULL) |
| current_url = GURL(ConvertJavaStringToUTF16(env, j_current_url)); |
| if (j_desired_tld != NULL) |
| desired_tld = base::android::ConvertJavaStringToUTF8(env, j_desired_tld); |
| base::string16 text = ConvertJavaStringToUTF16(env, j_text); |
| OmniboxEventProto::PageClassification page_classification = |
| OmniboxEventProto::OTHER; |
| size_t cursor_pos = j_cursor_pos == -1 ? base::string16::npos : j_cursor_pos; |
| input_ = AutocompleteInput( |
| text, cursor_pos, desired_tld, current_url, page_classification, |
| prevent_inline_autocomplete, prefer_keyword, allow_exact_keyword_match, |
| want_asynchronous_matches, ChromeAutocompleteSchemeClassifier(profile_)); |
| autocomplete_controller_->Start(input_); |
| } |
| |
| ScopedJavaLocalRef<jobject> AutocompleteControllerAndroid::Classify( |
| JNIEnv* env, |
| jobject obj, |
| jstring j_text) { |
| return GetTopSynchronousResult(env, obj, j_text, true); |
| } |
| |
| void AutocompleteControllerAndroid::OnOmniboxFocused( |
| JNIEnv* env, |
| jobject obj, |
| jstring j_omnibox_text, |
| jstring j_current_url, |
| jboolean is_query_in_omnibox, |
| jboolean focused_from_fakebox) { |
| if (!autocomplete_controller_) |
| return; |
| |
| base::string16 url = ConvertJavaStringToUTF16(env, j_current_url); |
| const GURL current_url = GURL(url); |
| base::string16 omnibox_text = ConvertJavaStringToUTF16(env, j_omnibox_text); |
| |
| // If omnibox text is empty, set it to the current URL for the purposes of |
| // populating the verbatim match. |
| if (omnibox_text.empty()) |
| omnibox_text = url; |
| |
| input_ = AutocompleteInput( |
| omnibox_text, base::string16::npos, std::string(), current_url, |
| ClassifyPage(current_url, is_query_in_omnibox, focused_from_fakebox), |
| false, false, true, true, ChromeAutocompleteSchemeClassifier(profile_)); |
| autocomplete_controller_->OnOmniboxFocused(input_); |
| } |
| |
| void AutocompleteControllerAndroid::Stop(JNIEnv* env, |
| jobject obj, |
| bool clear_results) { |
| if (autocomplete_controller_ != NULL) |
| autocomplete_controller_->Stop(clear_results); |
| } |
| |
| void AutocompleteControllerAndroid::ResetSession(JNIEnv* env, jobject obj) { |
| if (autocomplete_controller_ != NULL) |
| autocomplete_controller_->ResetSession(); |
| } |
| |
| void AutocompleteControllerAndroid::OnSuggestionSelected( |
| JNIEnv* env, |
| jobject obj, |
| jint selected_index, |
| jstring j_current_url, |
| jboolean is_query_in_omnibox, |
| jboolean focused_from_fakebox, |
| jlong elapsed_time_since_first_modified, |
| jobject j_web_contents) { |
| base::string16 url = ConvertJavaStringToUTF16(env, j_current_url); |
| const GURL current_url = GURL(url); |
| OmniboxEventProto::PageClassification current_page_classification = |
| ClassifyPage(current_url, is_query_in_omnibox, focused_from_fakebox); |
| const base::TimeTicks& now(base::TimeTicks::Now()); |
| content::WebContents* web_contents = |
| content::WebContents::FromJavaWebContents(j_web_contents); |
| |
| OmniboxLog log( |
| input_.text(), |
| false, /* don't know */ |
| input_.type(), |
| true, |
| selected_index, |
| false, |
| SessionTabHelper::IdForTab(web_contents), |
| current_page_classification, |
| base::TimeDelta::FromMilliseconds(elapsed_time_since_first_modified), |
| base::string16::npos, |
| now - autocomplete_controller_->last_time_default_match_changed(), |
| autocomplete_controller_->result()); |
| autocomplete_controller_->AddProvidersInfo(&log.providers_info); |
| |
| content::NotificationService::current()->Notify( |
| chrome::NOTIFICATION_OMNIBOX_OPENED_URL, |
| content::Source<Profile>(profile_), |
| content::Details<OmniboxLog>(&log)); |
| } |
| |
| void AutocompleteControllerAndroid::DeleteSuggestion(JNIEnv* env, |
| jobject obj, |
| int selected_index) { |
| const AutocompleteResult& result = autocomplete_controller_->result(); |
| const AutocompleteMatch& match = result.match_at(selected_index); |
| if (match.SupportsDeletion()) |
| autocomplete_controller_->DeleteMatch(match); |
| } |
| |
| ScopedJavaLocalRef<jstring> AutocompleteControllerAndroid:: |
| UpdateMatchDestinationURLWithQueryFormulationTime( |
| JNIEnv* env, |
| jobject obj, |
| jint selected_index, |
| jlong elapsed_time_since_input_change) { |
| // In rare cases, we navigate to cached matches and the underlying result |
| // has already been cleared, in that case ignore the URL update. |
| if (autocomplete_controller_->result().empty()) |
| return ScopedJavaLocalRef<jstring>(); |
| |
| AutocompleteMatch match( |
| autocomplete_controller_->result().match_at(selected_index)); |
| autocomplete_controller_->UpdateMatchDestinationURLWithQueryFormulationTime( |
| base::TimeDelta::FromMilliseconds(elapsed_time_since_input_change), |
| &match); |
| return ConvertUTF8ToJavaString(env, match.destination_url.spec()); |
| } |
| |
| void AutocompleteControllerAndroid::Shutdown() { |
| autocomplete_controller_.reset(); |
| |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> java_bridge = |
| weak_java_autocomplete_controller_android_.get(env); |
| if (java_bridge.obj()) |
| Java_AutocompleteController_notifyNativeDestroyed(env, java_bridge.obj()); |
| |
| weak_java_autocomplete_controller_android_.reset(); |
| } |
| |
| // static |
| AutocompleteControllerAndroid* |
| AutocompleteControllerAndroid::Factory::GetForProfile( |
| Profile* profile, JNIEnv* env, jobject obj) { |
| AutocompleteControllerAndroid* bridge = |
| static_cast<AutocompleteControllerAndroid*>( |
| GetInstance()->GetServiceForBrowserContext(profile, true)); |
| bridge->InitJNI(env, obj); |
| return bridge; |
| } |
| |
| AutocompleteControllerAndroid::Factory* |
| AutocompleteControllerAndroid::Factory::GetInstance() { |
| return Singleton<AutocompleteControllerAndroid::Factory>::get(); |
| } |
| |
| content::BrowserContext* |
| AutocompleteControllerAndroid::Factory::GetBrowserContextToUse( |
| content::BrowserContext* context) const { |
| return chrome::GetBrowserContextOwnInstanceInIncognito(context); |
| } |
| |
| AutocompleteControllerAndroid::Factory::Factory() |
| : BrowserContextKeyedServiceFactory( |
| "AutocompleteControllerAndroid", |
| BrowserContextDependencyManager::GetInstance()) { |
| DependsOn(ShortcutsBackendFactory::GetInstance()); |
| } |
| |
| AutocompleteControllerAndroid::Factory::~Factory() { |
| } |
| |
| KeyedService* AutocompleteControllerAndroid::Factory::BuildServiceInstanceFor( |
| content::BrowserContext* profile) const { |
| return new AutocompleteControllerAndroid(static_cast<Profile*>(profile)); |
| } |
| |
| AutocompleteControllerAndroid::~AutocompleteControllerAndroid() { |
| } |
| |
| void AutocompleteControllerAndroid::InitJNI(JNIEnv* env, jobject obj) { |
| weak_java_autocomplete_controller_android_ = |
| JavaObjectWeakGlobalRef(env, obj); |
| } |
| |
| void AutocompleteControllerAndroid::OnResultChanged( |
| bool default_match_changed) { |
| if (!autocomplete_controller_) |
| return; |
| |
| const AutocompleteResult& result = autocomplete_controller_->result(); |
| const AutocompleteResult::const_iterator default_match( |
| result.default_match()); |
| if ((default_match != result.end()) && default_match_changed && |
| chrome::IsInstantExtendedAPIEnabled() && |
| chrome::ShouldPrefetchSearchResults()) { |
| InstantSuggestion prefetch_suggestion; |
| // If the default match should be prefetched, do that. |
| if (SearchProvider::ShouldPrefetch(*default_match)) { |
| prefetch_suggestion.text = default_match->contents; |
| prefetch_suggestion.metadata = |
| SearchProvider::GetSuggestMetadata(*default_match); |
| } |
| // Send the prefetch suggestion unconditionally to the Instant search base |
| // page. If there is no suggestion to prefetch, we need to send a blank |
| // query to clear the prefetched results. |
| InstantSearchPrerenderer* prerenderer = |
| InstantSearchPrerenderer::GetForProfile(profile_); |
| if (prerenderer) |
| prerenderer->Prerender(prefetch_suggestion); |
| } |
| if (!inside_synchronous_start_) |
| NotifySuggestionsReceived(autocomplete_controller_->result()); |
| } |
| |
| void AutocompleteControllerAndroid::NotifySuggestionsReceived( |
| const AutocompleteResult& autocomplete_result) { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> java_bridge = |
| weak_java_autocomplete_controller_android_.get(env); |
| if (!java_bridge.obj()) |
| return; |
| |
| ScopedJavaLocalRef<jobject> suggestion_list_obj = |
| Java_AutocompleteController_createOmniboxSuggestionList( |
| env, autocomplete_result.size()); |
| for (size_t i = 0; i < autocomplete_result.size(); ++i) { |
| ScopedJavaLocalRef<jobject> j_omnibox_suggestion = |
| BuildOmniboxSuggestion(env, autocomplete_result.match_at(i)); |
| Java_AutocompleteController_addOmniboxSuggestionToList( |
| env, suggestion_list_obj.obj(), j_omnibox_suggestion.obj()); |
| } |
| |
| // Get the inline-autocomplete text. |
| const AutocompleteResult::const_iterator default_match( |
| autocomplete_result.default_match()); |
| base::string16 inline_autocomplete_text; |
| if (default_match != autocomplete_result.end()) { |
| inline_autocomplete_text = default_match->inline_autocompletion; |
| } |
| ScopedJavaLocalRef<jstring> inline_text = |
| ConvertUTF16ToJavaString(env, inline_autocomplete_text); |
| jlong j_autocomplete_result = |
| reinterpret_cast<intptr_t>(&(autocomplete_result)); |
| Java_AutocompleteController_onSuggestionsReceived(env, |
| java_bridge.obj(), |
| suggestion_list_obj.obj(), |
| inline_text.obj(), |
| j_autocomplete_result); |
| } |
| |
| OmniboxEventProto::PageClassification |
| AutocompleteControllerAndroid::ClassifyPage(const GURL& gurl, |
| bool is_query_in_omnibox, |
| bool focused_from_fakebox) const { |
| if (!gurl.is_valid()) |
| return OmniboxEventProto::INVALID_SPEC; |
| |
| const std::string& url = gurl.spec(); |
| |
| if (gurl.SchemeIs(content::kChromeUIScheme) && |
| gurl.host() == chrome::kChromeUINewTabHost) { |
| return OmniboxEventProto::NTP; |
| } |
| |
| if (url == chrome::kChromeUINativeNewTabURL) { |
| return focused_from_fakebox ? |
| OmniboxEventProto::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS : |
| OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS; |
| } |
| |
| if (url == url::kAboutBlankURL) |
| return OmniboxEventProto::BLANK; |
| |
| if (url == profile_->GetPrefs()->GetString(prefs::kHomePage)) |
| return OmniboxEventProto::HOME_PAGE; |
| |
| if (is_query_in_omnibox) |
| return OmniboxEventProto::SEARCH_RESULT_PAGE_DOING_SEARCH_TERM_REPLACEMENT; |
| |
| bool is_search_url = TemplateURLServiceFactory::GetForProfile(profile_)-> |
| IsSearchResultsPageFromDefaultSearchProvider(gurl); |
| if (is_search_url) |
| return OmniboxEventProto::SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT; |
| |
| return OmniboxEventProto::OTHER; |
| } |
| |
| ScopedJavaLocalRef<jobject> |
| AutocompleteControllerAndroid::BuildOmniboxSuggestion( |
| JNIEnv* env, |
| const AutocompleteMatch& match) { |
| ScopedJavaLocalRef<jstring> contents = |
| ConvertUTF16ToJavaString(env, match.contents); |
| ScopedJavaLocalRef<jstring> description = |
| ConvertUTF16ToJavaString(env, match.description); |
| ScopedJavaLocalRef<jstring> answer_contents = |
| ConvertUTF16ToJavaString(env, match.answer_contents); |
| ScopedJavaLocalRef<jstring> answer_type = |
| ConvertUTF16ToJavaString(env, match.answer_type); |
| ScopedJavaLocalRef<jstring> fill_into_edit = |
| ConvertUTF16ToJavaString(env, match.fill_into_edit); |
| ScopedJavaLocalRef<jstring> destination_url = |
| ConvertUTF8ToJavaString(env, match.destination_url.spec()); |
| // Note that we are also removing 'www' host from formatted url. |
| ScopedJavaLocalRef<jstring> formatted_url = ConvertUTF16ToJavaString(env, |
| FormatURLUsingAcceptLanguages(match.stripped_destination_url)); |
| BookmarkModel* bookmark_model = BookmarkModelFactory::GetForProfile(profile_); |
| return Java_AutocompleteController_buildOmniboxSuggestion( |
| env, |
| match.type, |
| match.relevance, |
| match.transition, |
| contents.obj(), |
| description.obj(), |
| answer_contents.obj(), |
| answer_type.obj(), |
| fill_into_edit.obj(), |
| destination_url.obj(), |
| formatted_url.obj(), |
| bookmark_model && bookmark_model->IsBookmarked(match.destination_url), |
| match.SupportsDeletion()); |
| } |
| |
| base::string16 AutocompleteControllerAndroid::FormatURLUsingAcceptLanguages( |
| GURL url) { |
| if (profile_ == NULL) |
| return base::string16(); |
| |
| std::string languages( |
| profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)); |
| |
| return net::FormatUrl(url, languages, net::kFormatUrlOmitAll, |
| net::UnescapeRule::SPACES, NULL, NULL, NULL); |
| } |
| |
| ScopedJavaLocalRef<jobject> |
| AutocompleteControllerAndroid::GetTopSynchronousResult( |
| JNIEnv* env, |
| jobject obj, |
| jstring j_text, |
| bool prevent_inline_autocomplete) { |
| if (!autocomplete_controller_) |
| return ScopedJavaLocalRef<jobject>(); |
| |
| inside_synchronous_start_ = true; |
| Start(env, |
| obj, |
| j_text, |
| -1, |
| NULL, |
| NULL, |
| prevent_inline_autocomplete, |
| false, |
| false, |
| false); |
| inside_synchronous_start_ = false; |
| DCHECK(autocomplete_controller_->done()); |
| const AutocompleteResult& result = autocomplete_controller_->result(); |
| if (result.empty()) |
| return ScopedJavaLocalRef<jobject>(); |
| |
| return BuildOmniboxSuggestion(env, *result.begin()); |
| } |
| |
| static jlong Init(JNIEnv* env, jobject obj, jobject jprofile) { |
| Profile* profile = ProfileAndroid::FromProfileAndroid(jprofile); |
| if (!profile) |
| return 0; |
| |
| AutocompleteControllerAndroid* native_bridge = |
| AutocompleteControllerAndroid::Factory::GetForProfile(profile, env, obj); |
| return reinterpret_cast<intptr_t>(native_bridge); |
| } |
| |
| static jstring QualifyPartialURLQuery( |
| JNIEnv* env, jclass clazz, jstring jquery) { |
| Profile* profile = ProfileManager::GetActiveUserProfile(); |
| if (!profile) |
| return NULL; |
| AutocompleteMatch match; |
| base::string16 query_string(ConvertJavaStringToUTF16(env, jquery)); |
| AutocompleteClassifierFactory::GetForProfile(profile)->Classify( |
| query_string, |
| false, |
| false, |
| OmniboxEventProto::INVALID_SPEC, |
| &match, |
| NULL); |
| if (!match.destination_url.is_valid()) |
| return NULL; |
| |
| // Only return a URL if the match is a URL type. |
| if (match.type != AutocompleteMatchType::URL_WHAT_YOU_TYPED && |
| match.type != AutocompleteMatchType::HISTORY_URL && |
| match.type != AutocompleteMatchType::NAVSUGGEST) |
| return NULL; |
| |
| // As we are returning to Java, it is fine to call Release(). |
| return ConvertUTF8ToJavaString(env, match.destination_url.spec()).Release(); |
| } |
| |
| static void PrefetchZeroSuggestResults(JNIEnv* env, jclass clazz) { |
| Profile* profile = ProfileManager::GetActiveUserProfile(); |
| if (!profile) |
| return; |
| |
| if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial()) |
| return; |
| |
| // ZeroSuggestPrefetcher deletes itself after it's done prefetching. |
| new ZeroSuggestPrefetcher(profile); |
| } |
| |
| // Register native methods |
| bool RegisterAutocompleteControllerAndroid(JNIEnv* env) { |
| return RegisterNativesImpl(env); |
| } |