1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/android/omnibox/autocomplete_controller_android.h"
7 #include "base/android/jni_android.h"
8 #include "base/android/jni_string.h"
9 #include "base/prefs/pref_service.h"
10 #include "base/strings/string16.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "base/time/time.h"
13 #include "base/timer/timer.h"
14 #include "chrome/browser/autocomplete/autocomplete_classifier.h"
15 #include "chrome/browser/autocomplete/autocomplete_classifier_factory.h"
16 #include "chrome/browser/autocomplete/autocomplete_controller.h"
17 #include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
18 #include "chrome/browser/autocomplete/shortcuts_backend_factory.h"
19 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
20 #include "chrome/browser/browser_process.h"
21 #include "chrome/browser/chrome_notification_types.h"
22 #include "chrome/browser/omnibox/omnibox_log.h"
23 #include "chrome/browser/profiles/incognito_helpers.h"
24 #include "chrome/browser/profiles/profile_android.h"
25 #include "chrome/browser/profiles/profile_manager.h"
26 #include "chrome/browser/search/search.h"
27 #include "chrome/browser/search_engines/template_url_service_factory.h"
28 #include "chrome/browser/sessions/session_tab_helper.h"
29 #include "chrome/browser/ui/search/instant_search_prerenderer.h"
30 #include "chrome/browser/ui/toolbar/toolbar_model.h"
31 #include "chrome/common/instant_types.h"
32 #include "chrome/common/pref_names.h"
33 #include "chrome/common/url_constants.h"
34 #include "components/bookmarks/browser/bookmark_model.h"
35 #include "components/keyed_service/content/browser_context_dependency_manager.h"
36 #include "components/metrics/proto/omnibox_event.pb.h"
37 #include "components/omnibox/autocomplete_input.h"
38 #include "components/omnibox/autocomplete_match.h"
39 #include "components/omnibox/autocomplete_match_type.h"
40 #include "components/omnibox/omnibox_field_trial.h"
41 #include "components/omnibox/search_provider.h"
42 #include "components/search/search.h"
43 #include "components/search_engines/template_url_service.h"
44 #include "content/public/browser/notification_details.h"
45 #include "content/public/browser/notification_service.h"
46 #include "content/public/browser/notification_source.h"
47 #include "content/public/browser/web_contents.h"
48 #include "content/public/common/url_constants.h"
49 #include "jni/AutocompleteController_jni.h"
50 #include "net/base/escape.h"
51 #include "net/base/net_util.h"
52 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
54 using base::android::AttachCurrentThread
;
55 using base::android::ConvertJavaStringToUTF16
;
56 using base::android::ConvertUTF8ToJavaString
;
57 using base::android::ConvertUTF16ToJavaString
;
58 using bookmarks::BookmarkModel
;
59 using metrics::OmniboxEventProto
;
63 const int kAndroidAutocompleteProviders
=
64 AutocompleteClassifier::kDefaultOmniboxProviders
;
67 * A prefetcher class responsible for triggering zero suggest prefetch.
68 * The prefetch occurs as a side-effect of calling OnOmniboxFocused() on
69 * the AutocompleteController object.
71 class ZeroSuggestPrefetcher
: public AutocompleteControllerDelegate
{
73 explicit ZeroSuggestPrefetcher(Profile
* profile
);
76 ~ZeroSuggestPrefetcher() override
;
79 // AutocompleteControllerDelegate:
80 void OnResultChanged(bool default_match_changed
) override
;
82 scoped_ptr
<AutocompleteController
> controller_
;
83 base::OneShotTimer
<ZeroSuggestPrefetcher
> expire_timer_
;
86 ZeroSuggestPrefetcher::ZeroSuggestPrefetcher(Profile
* profile
)
87 : controller_(new AutocompleteController(
88 profile
, TemplateURLServiceFactory::GetForProfile(profile
), this,
89 AutocompleteProvider::TYPE_ZERO_SUGGEST
)) {
90 // Creating an arbitrary fake_request_source to avoid passing in an invalid
91 // AutocompleteInput object.
92 base::string16
fake_request_source(base::ASCIIToUTF16(
93 "http://www.foobarbazblah.com"));
94 controller_
->OnOmniboxFocused(AutocompleteInput(
95 fake_request_source
, base::string16::npos
, std::string(),
96 GURL(fake_request_source
), OmniboxEventProto::INVALID_SPEC
, false, false,
97 true, true, ChromeAutocompleteSchemeClassifier(profile
)));
98 // Delete ourselves after 10s. This is enough time to cache results or
99 // give up if the results haven't been received.
100 expire_timer_
.Start(FROM_HERE
,
101 base::TimeDelta::FromMilliseconds(10000),
102 this, &ZeroSuggestPrefetcher::SelfDestruct
);
105 ZeroSuggestPrefetcher::~ZeroSuggestPrefetcher() {
108 void ZeroSuggestPrefetcher::SelfDestruct() {
112 void ZeroSuggestPrefetcher::OnResultChanged(bool default_match_changed
) {
113 // Nothing to do here, the results have been cached.
114 // We don't want to trigger deletion here because this is being called by the
115 // AutocompleteController object.
120 AutocompleteControllerAndroid::AutocompleteControllerAndroid(Profile
* profile
)
121 : autocomplete_controller_(new AutocompleteController(
122 profile
, TemplateURLServiceFactory::GetForProfile(profile
), this,
123 kAndroidAutocompleteProviders
)),
124 inside_synchronous_start_(false),
128 void AutocompleteControllerAndroid::Start(JNIEnv
* env
,
132 jstring j_desired_tld
,
133 jstring j_current_url
,
134 bool prevent_inline_autocomplete
,
136 bool allow_exact_keyword_match
,
137 bool want_asynchronous_matches
) {
138 if (!autocomplete_controller_
)
141 std::string desired_tld
;
143 if (j_current_url
!= NULL
)
144 current_url
= GURL(ConvertJavaStringToUTF16(env
, j_current_url
));
145 if (j_desired_tld
!= NULL
)
146 desired_tld
= base::android::ConvertJavaStringToUTF8(env
, j_desired_tld
);
147 base::string16 text
= ConvertJavaStringToUTF16(env
, j_text
);
148 OmniboxEventProto::PageClassification page_classification
=
149 OmniboxEventProto::OTHER
;
150 size_t cursor_pos
= j_cursor_pos
== -1 ? base::string16::npos
: j_cursor_pos
;
151 input_
= AutocompleteInput(
152 text
, cursor_pos
, desired_tld
, current_url
, page_classification
,
153 prevent_inline_autocomplete
, prefer_keyword
, allow_exact_keyword_match
,
154 want_asynchronous_matches
, ChromeAutocompleteSchemeClassifier(profile_
));
155 autocomplete_controller_
->Start(input_
);
158 ScopedJavaLocalRef
<jobject
> AutocompleteControllerAndroid::Classify(
162 return GetTopSynchronousResult(env
, obj
, j_text
, true);
165 void AutocompleteControllerAndroid::OnOmniboxFocused(
168 jstring j_omnibox_text
,
169 jstring j_current_url
,
170 jboolean is_query_in_omnibox
,
171 jboolean focused_from_fakebox
) {
172 if (!autocomplete_controller_
)
175 base::string16 url
= ConvertJavaStringToUTF16(env
, j_current_url
);
176 const GURL current_url
= GURL(url
);
177 base::string16 omnibox_text
= ConvertJavaStringToUTF16(env
, j_omnibox_text
);
179 // If omnibox text is empty, set it to the current URL for the purposes of
180 // populating the verbatim match.
181 if (omnibox_text
.empty())
184 input_
= AutocompleteInput(
185 omnibox_text
, base::string16::npos
, std::string(), current_url
,
186 ClassifyPage(current_url
, is_query_in_omnibox
, focused_from_fakebox
),
187 false, false, true, true, ChromeAutocompleteSchemeClassifier(profile_
));
188 autocomplete_controller_
->OnOmniboxFocused(input_
);
191 void AutocompleteControllerAndroid::Stop(JNIEnv
* env
,
193 bool clear_results
) {
194 if (autocomplete_controller_
!= NULL
)
195 autocomplete_controller_
->Stop(clear_results
);
198 void AutocompleteControllerAndroid::ResetSession(JNIEnv
* env
, jobject obj
) {
199 if (autocomplete_controller_
!= NULL
)
200 autocomplete_controller_
->ResetSession();
203 void AutocompleteControllerAndroid::OnSuggestionSelected(
207 jstring j_current_url
,
208 jboolean is_query_in_omnibox
,
209 jboolean focused_from_fakebox
,
210 jlong elapsed_time_since_first_modified
,
211 jobject j_web_contents
) {
212 base::string16 url
= ConvertJavaStringToUTF16(env
, j_current_url
);
213 const GURL current_url
= GURL(url
);
214 OmniboxEventProto::PageClassification current_page_classification
=
215 ClassifyPage(current_url
, is_query_in_omnibox
, focused_from_fakebox
);
216 const base::TimeTicks
& now(base::TimeTicks::Now());
217 content::WebContents
* web_contents
=
218 content::WebContents::FromJavaWebContents(j_web_contents
);
222 false, /* don't know */
227 SessionTabHelper::IdForTab(web_contents
),
228 current_page_classification
,
229 base::TimeDelta::FromMilliseconds(elapsed_time_since_first_modified
),
230 base::string16::npos
,
231 now
- autocomplete_controller_
->last_time_default_match_changed(),
232 autocomplete_controller_
->result());
233 autocomplete_controller_
->AddProvidersInfo(&log
.providers_info
);
235 content::NotificationService::current()->Notify(
236 chrome::NOTIFICATION_OMNIBOX_OPENED_URL
,
237 content::Source
<Profile
>(profile_
),
238 content::Details
<OmniboxLog
>(&log
));
241 void AutocompleteControllerAndroid::DeleteSuggestion(JNIEnv
* env
,
243 int selected_index
) {
244 const AutocompleteResult
& result
= autocomplete_controller_
->result();
245 const AutocompleteMatch
& match
= result
.match_at(selected_index
);
246 if (match
.SupportsDeletion())
247 autocomplete_controller_
->DeleteMatch(match
);
250 ScopedJavaLocalRef
<jstring
> AutocompleteControllerAndroid::
251 UpdateMatchDestinationURLWithQueryFormulationTime(
255 jlong elapsed_time_since_input_change
) {
256 // In rare cases, we navigate to cached matches and the underlying result
257 // has already been cleared, in that case ignore the URL update.
258 if (autocomplete_controller_
->result().empty())
259 return ScopedJavaLocalRef
<jstring
>();
261 AutocompleteMatch
match(
262 autocomplete_controller_
->result().match_at(selected_index
));
263 autocomplete_controller_
->UpdateMatchDestinationURLWithQueryFormulationTime(
264 base::TimeDelta::FromMilliseconds(elapsed_time_since_input_change
),
266 return ConvertUTF8ToJavaString(env
, match
.destination_url
.spec());
269 void AutocompleteControllerAndroid::Shutdown() {
270 autocomplete_controller_
.reset();
272 JNIEnv
* env
= AttachCurrentThread();
273 ScopedJavaLocalRef
<jobject
> java_bridge
=
274 weak_java_autocomplete_controller_android_
.get(env
);
275 if (java_bridge
.obj())
276 Java_AutocompleteController_notifyNativeDestroyed(env
, java_bridge
.obj());
278 weak_java_autocomplete_controller_android_
.reset();
282 AutocompleteControllerAndroid
*
283 AutocompleteControllerAndroid::Factory::GetForProfile(
284 Profile
* profile
, JNIEnv
* env
, jobject obj
) {
285 AutocompleteControllerAndroid
* bridge
=
286 static_cast<AutocompleteControllerAndroid
*>(
287 GetInstance()->GetServiceForBrowserContext(profile
, true));
288 bridge
->InitJNI(env
, obj
);
292 AutocompleteControllerAndroid::Factory
*
293 AutocompleteControllerAndroid::Factory::GetInstance() {
294 return Singleton
<AutocompleteControllerAndroid::Factory
>::get();
297 content::BrowserContext
*
298 AutocompleteControllerAndroid::Factory::GetBrowserContextToUse(
299 content::BrowserContext
* context
) const {
300 return chrome::GetBrowserContextOwnInstanceInIncognito(context
);
303 AutocompleteControllerAndroid::Factory::Factory()
304 : BrowserContextKeyedServiceFactory(
305 "AutocompleteControllerAndroid",
306 BrowserContextDependencyManager::GetInstance()) {
307 DependsOn(ShortcutsBackendFactory::GetInstance());
310 AutocompleteControllerAndroid::Factory::~Factory() {
313 KeyedService
* AutocompleteControllerAndroid::Factory::BuildServiceInstanceFor(
314 content::BrowserContext
* profile
) const {
315 return new AutocompleteControllerAndroid(static_cast<Profile
*>(profile
));
318 AutocompleteControllerAndroid::~AutocompleteControllerAndroid() {
321 void AutocompleteControllerAndroid::InitJNI(JNIEnv
* env
, jobject obj
) {
322 weak_java_autocomplete_controller_android_
=
323 JavaObjectWeakGlobalRef(env
, obj
);
326 void AutocompleteControllerAndroid::OnResultChanged(
327 bool default_match_changed
) {
328 if (!autocomplete_controller_
)
331 const AutocompleteResult
& result
= autocomplete_controller_
->result();
332 const AutocompleteResult::const_iterator
default_match(
333 result
.default_match());
334 if ((default_match
!= result
.end()) && default_match_changed
&&
335 chrome::IsInstantExtendedAPIEnabled() &&
336 chrome::ShouldPrefetchSearchResults()) {
337 InstantSuggestion prefetch_suggestion
;
338 // If the default match should be prefetched, do that.
339 if (SearchProvider::ShouldPrefetch(*default_match
)) {
340 prefetch_suggestion
.text
= default_match
->contents
;
341 prefetch_suggestion
.metadata
=
342 SearchProvider::GetSuggestMetadata(*default_match
);
344 // Send the prefetch suggestion unconditionally to the Instant search base
345 // page. If there is no suggestion to prefetch, we need to send a blank
346 // query to clear the prefetched results.
347 InstantSearchPrerenderer
* prerenderer
=
348 InstantSearchPrerenderer::GetForProfile(profile_
);
350 prerenderer
->Prerender(prefetch_suggestion
);
352 if (!inside_synchronous_start_
)
353 NotifySuggestionsReceived(autocomplete_controller_
->result());
356 void AutocompleteControllerAndroid::NotifySuggestionsReceived(
357 const AutocompleteResult
& autocomplete_result
) {
358 JNIEnv
* env
= AttachCurrentThread();
359 ScopedJavaLocalRef
<jobject
> java_bridge
=
360 weak_java_autocomplete_controller_android_
.get(env
);
361 if (!java_bridge
.obj())
364 ScopedJavaLocalRef
<jobject
> suggestion_list_obj
=
365 Java_AutocompleteController_createOmniboxSuggestionList(
366 env
, autocomplete_result
.size());
367 for (size_t i
= 0; i
< autocomplete_result
.size(); ++i
) {
368 ScopedJavaLocalRef
<jobject
> j_omnibox_suggestion
=
369 BuildOmniboxSuggestion(env
, autocomplete_result
.match_at(i
));
370 Java_AutocompleteController_addOmniboxSuggestionToList(
371 env
, suggestion_list_obj
.obj(), j_omnibox_suggestion
.obj());
374 // Get the inline-autocomplete text.
375 const AutocompleteResult::const_iterator
default_match(
376 autocomplete_result
.default_match());
377 base::string16 inline_autocomplete_text
;
378 if (default_match
!= autocomplete_result
.end()) {
379 inline_autocomplete_text
= default_match
->inline_autocompletion
;
381 ScopedJavaLocalRef
<jstring
> inline_text
=
382 ConvertUTF16ToJavaString(env
, inline_autocomplete_text
);
383 jlong j_autocomplete_result
=
384 reinterpret_cast<intptr_t>(&(autocomplete_result
));
385 Java_AutocompleteController_onSuggestionsReceived(env
,
387 suggestion_list_obj
.obj(),
389 j_autocomplete_result
);
392 OmniboxEventProto::PageClassification
393 AutocompleteControllerAndroid::ClassifyPage(const GURL
& gurl
,
394 bool is_query_in_omnibox
,
395 bool focused_from_fakebox
) const {
396 if (!gurl
.is_valid())
397 return OmniboxEventProto::INVALID_SPEC
;
399 const std::string
& url
= gurl
.spec();
401 if (gurl
.SchemeIs(content::kChromeUIScheme
) &&
402 gurl
.host() == chrome::kChromeUINewTabHost
) {
403 return OmniboxEventProto::NTP
;
406 if (url
== chrome::kChromeUINativeNewTabURL
) {
407 return focused_from_fakebox
?
408 OmniboxEventProto::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS
:
409 OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS
;
412 if (url
== url::kAboutBlankURL
)
413 return OmniboxEventProto::BLANK
;
415 if (url
== profile_
->GetPrefs()->GetString(prefs::kHomePage
))
416 return OmniboxEventProto::HOME_PAGE
;
418 if (is_query_in_omnibox
)
419 return OmniboxEventProto::SEARCH_RESULT_PAGE_DOING_SEARCH_TERM_REPLACEMENT
;
421 bool is_search_url
= TemplateURLServiceFactory::GetForProfile(profile_
)->
422 IsSearchResultsPageFromDefaultSearchProvider(gurl
);
424 return OmniboxEventProto::SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT
;
426 return OmniboxEventProto::OTHER
;
429 ScopedJavaLocalRef
<jobject
>
430 AutocompleteControllerAndroid::BuildOmniboxSuggestion(
432 const AutocompleteMatch
& match
) {
433 ScopedJavaLocalRef
<jstring
> contents
=
434 ConvertUTF16ToJavaString(env
, match
.contents
);
435 ScopedJavaLocalRef
<jstring
> description
=
436 ConvertUTF16ToJavaString(env
, match
.description
);
437 ScopedJavaLocalRef
<jstring
> answer_contents
=
438 ConvertUTF16ToJavaString(env
, match
.answer_contents
);
439 ScopedJavaLocalRef
<jstring
> answer_type
=
440 ConvertUTF16ToJavaString(env
, match
.answer_type
);
441 ScopedJavaLocalRef
<jstring
> fill_into_edit
=
442 ConvertUTF16ToJavaString(env
, match
.fill_into_edit
);
443 ScopedJavaLocalRef
<jstring
> destination_url
=
444 ConvertUTF8ToJavaString(env
, match
.destination_url
.spec());
445 // Note that we are also removing 'www' host from formatted url.
446 ScopedJavaLocalRef
<jstring
> formatted_url
= ConvertUTF16ToJavaString(env
,
447 FormatURLUsingAcceptLanguages(match
.stripped_destination_url
));
448 BookmarkModel
* bookmark_model
= BookmarkModelFactory::GetForProfile(profile_
);
449 return Java_AutocompleteController_buildOmniboxSuggestion(
456 answer_contents
.obj(),
458 fill_into_edit
.obj(),
459 destination_url
.obj(),
461 bookmark_model
&& bookmark_model
->IsBookmarked(match
.destination_url
),
462 match
.SupportsDeletion());
465 base::string16
AutocompleteControllerAndroid::FormatURLUsingAcceptLanguages(
467 if (profile_
== NULL
)
468 return base::string16();
470 std::string
languages(
471 profile_
->GetPrefs()->GetString(prefs::kAcceptLanguages
));
473 return net::FormatUrl(url
, languages
, net::kFormatUrlOmitAll
,
474 net::UnescapeRule::SPACES
, NULL
, NULL
, NULL
);
477 ScopedJavaLocalRef
<jobject
>
478 AutocompleteControllerAndroid::GetTopSynchronousResult(
482 bool prevent_inline_autocomplete
) {
483 if (!autocomplete_controller_
)
484 return ScopedJavaLocalRef
<jobject
>();
486 inside_synchronous_start_
= true;
493 prevent_inline_autocomplete
,
497 inside_synchronous_start_
= false;
498 DCHECK(autocomplete_controller_
->done());
499 const AutocompleteResult
& result
= autocomplete_controller_
->result();
501 return ScopedJavaLocalRef
<jobject
>();
503 return BuildOmniboxSuggestion(env
, *result
.begin());
506 static jlong
Init(JNIEnv
* env
, jobject obj
, jobject jprofile
) {
507 Profile
* profile
= ProfileAndroid::FromProfileAndroid(jprofile
);
511 AutocompleteControllerAndroid
* native_bridge
=
512 AutocompleteControllerAndroid::Factory::GetForProfile(profile
, env
, obj
);
513 return reinterpret_cast<intptr_t>(native_bridge
);
516 static jstring
QualifyPartialURLQuery(
517 JNIEnv
* env
, jclass clazz
, jstring jquery
) {
518 Profile
* profile
= ProfileManager::GetActiveUserProfile();
521 AutocompleteMatch match
;
522 base::string16
query_string(ConvertJavaStringToUTF16(env
, jquery
));
523 AutocompleteClassifierFactory::GetForProfile(profile
)->Classify(
527 OmniboxEventProto::INVALID_SPEC
,
530 if (!match
.destination_url
.is_valid())
533 // Only return a URL if the match is a URL type.
534 if (match
.type
!= AutocompleteMatchType::URL_WHAT_YOU_TYPED
&&
535 match
.type
!= AutocompleteMatchType::HISTORY_URL
&&
536 match
.type
!= AutocompleteMatchType::NAVSUGGEST
)
539 // As we are returning to Java, it is fine to call Release().
540 return ConvertUTF8ToJavaString(env
, match
.destination_url
.spec()).Release();
543 static void PrefetchZeroSuggestResults(JNIEnv
* env
, jclass clazz
) {
544 Profile
* profile
= ProfileManager::GetActiveUserProfile();
548 if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial())
551 // ZeroSuggestPrefetcher deletes itself after it's done prefetching.
552 new ZeroSuggestPrefetcher(profile
);
555 // Register native methods
556 bool RegisterAutocompleteControllerAndroid(JNIEnv
* env
) {
557 return RegisterNativesImpl(env
);