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 metrics::OmniboxEventProto
;
62 const int kAndroidAutocompleteProviders
=
63 AutocompleteClassifier::kDefaultOmniboxProviders
;
66 * A prefetcher class responsible for triggering zero suggest prefetch.
67 * The prefetch occurs as a side-effect of calling StartZeroSuggest() on
68 * the AutocompleteController object.
70 class ZeroSuggestPrefetcher
: public AutocompleteControllerDelegate
{
72 explicit ZeroSuggestPrefetcher(Profile
* profile
);
75 virtual ~ZeroSuggestPrefetcher();
78 // AutocompleteControllerDelegate:
79 virtual void OnResultChanged(bool default_match_changed
) OVERRIDE
;
81 scoped_ptr
<AutocompleteController
> controller_
;
82 base::OneShotTimer
<ZeroSuggestPrefetcher
> expire_timer_
;
85 ZeroSuggestPrefetcher::ZeroSuggestPrefetcher(Profile
* profile
)
86 : controller_(new AutocompleteController(
87 profile
, TemplateURLServiceFactory::GetForProfile(profile
), this,
88 AutocompleteProvider::TYPE_ZERO_SUGGEST
)) {
89 // Creating an arbitrary fake_request_source to avoid passing in an invalid
90 // AutocompleteInput object.
91 base::string16
fake_request_source(base::ASCIIToUTF16(
92 "http://www.foobarbazblah.com"));
93 controller_
->StartZeroSuggest(AutocompleteInput(
94 fake_request_source
, base::string16::npos
, base::string16(),
95 GURL(fake_request_source
), OmniboxEventProto::INVALID_SPEC
, false, false,
96 true, true, ChromeAutocompleteSchemeClassifier(profile
)));
97 // Delete ourselves after 10s. This is enough time to cache results or
98 // give up if the results haven't been received.
99 expire_timer_
.Start(FROM_HERE
,
100 base::TimeDelta::FromMilliseconds(10000),
101 this, &ZeroSuggestPrefetcher::SelfDestruct
);
104 ZeroSuggestPrefetcher::~ZeroSuggestPrefetcher() {
107 void ZeroSuggestPrefetcher::SelfDestruct() {
111 void ZeroSuggestPrefetcher::OnResultChanged(bool default_match_changed
) {
112 // Nothing to do here, the results have been cached.
113 // We don't want to trigger deletion here because this is being called by the
114 // AutocompleteController object.
119 AutocompleteControllerAndroid::AutocompleteControllerAndroid(Profile
* profile
)
120 : autocomplete_controller_(new AutocompleteController(
121 profile
, TemplateURLServiceFactory::GetForProfile(profile
), this,
122 kAndroidAutocompleteProviders
)),
123 inside_synchronous_start_(false),
127 void AutocompleteControllerAndroid::Start(JNIEnv
* env
,
130 jstring j_desired_tld
,
131 jstring j_current_url
,
132 bool prevent_inline_autocomplete
,
134 bool allow_exact_keyword_match
,
135 bool want_asynchronous_matches
) {
136 if (!autocomplete_controller_
)
139 base::string16 desired_tld
;
141 if (j_current_url
!= NULL
)
142 current_url
= GURL(ConvertJavaStringToUTF16(env
, j_current_url
));
143 if (j_desired_tld
!= NULL
)
144 desired_tld
= ConvertJavaStringToUTF16(env
, j_desired_tld
);
145 base::string16 text
= ConvertJavaStringToUTF16(env
, j_text
);
146 OmniboxEventProto::PageClassification page_classification
=
147 OmniboxEventProto::OTHER
;
148 input_
= AutocompleteInput(
149 text
, base::string16::npos
, desired_tld
, current_url
, page_classification
,
150 prevent_inline_autocomplete
, prefer_keyword
, allow_exact_keyword_match
,
151 want_asynchronous_matches
, ChromeAutocompleteSchemeClassifier(profile_
));
152 autocomplete_controller_
->Start(input_
);
155 ScopedJavaLocalRef
<jobject
> AutocompleteControllerAndroid::Classify(
159 return GetTopSynchronousResult(env
, obj
, j_text
, true);
162 void AutocompleteControllerAndroid::StartZeroSuggest(
165 jstring j_omnibox_text
,
166 jstring j_current_url
,
167 jboolean is_query_in_omnibox
,
168 jboolean focused_from_fakebox
) {
169 if (!autocomplete_controller_
)
172 base::string16 url
= ConvertJavaStringToUTF16(env
, j_current_url
);
173 const GURL current_url
= GURL(url
);
174 base::string16 omnibox_text
= ConvertJavaStringToUTF16(env
, j_omnibox_text
);
176 // If omnibox text is empty, set it to the current URL for the purposes of
177 // populating the verbatim match.
178 if (omnibox_text
.empty())
181 input_
= AutocompleteInput(
182 omnibox_text
, base::string16::npos
, base::string16(), current_url
,
183 ClassifyPage(current_url
, is_query_in_omnibox
, focused_from_fakebox
),
184 false, false, true, true, ChromeAutocompleteSchemeClassifier(profile_
));
185 autocomplete_controller_
->StartZeroSuggest(input_
);
188 void AutocompleteControllerAndroid::Stop(JNIEnv
* env
,
190 bool clear_results
) {
191 if (autocomplete_controller_
!= NULL
)
192 autocomplete_controller_
->Stop(clear_results
);
195 void AutocompleteControllerAndroid::ResetSession(JNIEnv
* env
, jobject obj
) {
196 if (autocomplete_controller_
!= NULL
)
197 autocomplete_controller_
->ResetSession();
200 void AutocompleteControllerAndroid::OnSuggestionSelected(
204 jstring j_current_url
,
205 jboolean is_query_in_omnibox
,
206 jboolean focused_from_fakebox
,
207 jlong elapsed_time_since_first_modified
,
208 jobject j_web_contents
) {
209 base::string16 url
= ConvertJavaStringToUTF16(env
, j_current_url
);
210 const GURL current_url
= GURL(url
);
211 OmniboxEventProto::PageClassification current_page_classification
=
212 ClassifyPage(current_url
, is_query_in_omnibox
, focused_from_fakebox
);
213 const base::TimeTicks
& now(base::TimeTicks::Now());
214 content::WebContents
* web_contents
=
215 content::WebContents::FromJavaWebContents(j_web_contents
);
219 false, /* don't know */
224 SessionTabHelper::IdForTab(web_contents
),
225 current_page_classification
,
226 base::TimeDelta::FromMilliseconds(elapsed_time_since_first_modified
),
227 base::string16::npos
,
228 now
- autocomplete_controller_
->last_time_default_match_changed(),
229 autocomplete_controller_
->result());
230 autocomplete_controller_
->AddProvidersInfo(&log
.providers_info
);
232 content::NotificationService::current()->Notify(
233 chrome::NOTIFICATION_OMNIBOX_OPENED_URL
,
234 content::Source
<Profile
>(profile_
),
235 content::Details
<OmniboxLog
>(&log
));
238 void AutocompleteControllerAndroid::DeleteSuggestion(JNIEnv
* env
,
240 int selected_index
) {
241 const AutocompleteResult
& result
= autocomplete_controller_
->result();
242 const AutocompleteMatch
& match
= result
.match_at(selected_index
);
243 if (match
.SupportsDeletion())
244 autocomplete_controller_
->DeleteMatch(match
);
247 ScopedJavaLocalRef
<jstring
> AutocompleteControllerAndroid::
248 UpdateMatchDestinationURLWithQueryFormulationTime(
252 jlong elapsed_time_since_input_change
) {
253 // In rare cases, we navigate to cached matches and the underlying result
254 // has already been cleared, in that case ignore the URL update.
255 if (autocomplete_controller_
->result().empty())
256 return ScopedJavaLocalRef
<jstring
>();
258 AutocompleteMatch
match(
259 autocomplete_controller_
->result().match_at(selected_index
));
260 autocomplete_controller_
->UpdateMatchDestinationURLWithQueryFormulationTime(
261 base::TimeDelta::FromMilliseconds(elapsed_time_since_input_change
),
263 return ConvertUTF8ToJavaString(env
, match
.destination_url
.spec());
266 ScopedJavaLocalRef
<jobject
>
267 AutocompleteControllerAndroid::GetTopSynchronousMatch(JNIEnv
* env
,
270 return GetTopSynchronousResult(env
, obj
, query
, false);
273 void AutocompleteControllerAndroid::Shutdown() {
274 autocomplete_controller_
.reset();
276 JNIEnv
* env
= AttachCurrentThread();
277 ScopedJavaLocalRef
<jobject
> java_bridge
=
278 weak_java_autocomplete_controller_android_
.get(env
);
279 if (java_bridge
.obj())
280 Java_AutocompleteController_notifyNativeDestroyed(env
, java_bridge
.obj());
282 weak_java_autocomplete_controller_android_
.reset();
286 AutocompleteControllerAndroid
*
287 AutocompleteControllerAndroid::Factory::GetForProfile(
288 Profile
* profile
, JNIEnv
* env
, jobject obj
) {
289 AutocompleteControllerAndroid
* bridge
=
290 static_cast<AutocompleteControllerAndroid
*>(
291 GetInstance()->GetServiceForBrowserContext(profile
, true));
292 bridge
->InitJNI(env
, obj
);
296 AutocompleteControllerAndroid::Factory
*
297 AutocompleteControllerAndroid::Factory::GetInstance() {
298 return Singleton
<AutocompleteControllerAndroid::Factory
>::get();
301 content::BrowserContext
*
302 AutocompleteControllerAndroid::Factory::GetBrowserContextToUse(
303 content::BrowserContext
* context
) const {
304 return chrome::GetBrowserContextOwnInstanceInIncognito(context
);
307 AutocompleteControllerAndroid::Factory::Factory()
308 : BrowserContextKeyedServiceFactory(
309 "AutocompleteControllerAndroid",
310 BrowserContextDependencyManager::GetInstance()) {
311 DependsOn(ShortcutsBackendFactory::GetInstance());
314 AutocompleteControllerAndroid::Factory::~Factory() {
317 KeyedService
* AutocompleteControllerAndroid::Factory::BuildServiceInstanceFor(
318 content::BrowserContext
* profile
) const {
319 return new AutocompleteControllerAndroid(static_cast<Profile
*>(profile
));
322 AutocompleteControllerAndroid::~AutocompleteControllerAndroid() {
325 void AutocompleteControllerAndroid::InitJNI(JNIEnv
* env
, jobject obj
) {
326 weak_java_autocomplete_controller_android_
=
327 JavaObjectWeakGlobalRef(env
, obj
);
330 void AutocompleteControllerAndroid::OnResultChanged(
331 bool default_match_changed
) {
332 if (!autocomplete_controller_
)
335 const AutocompleteResult
& result
= autocomplete_controller_
->result();
336 const AutocompleteResult::const_iterator
default_match(
337 result
.default_match());
338 if ((default_match
!= result
.end()) && default_match_changed
&&
339 chrome::IsInstantExtendedAPIEnabled() &&
340 chrome::ShouldPrefetchSearchResults()) {
341 InstantSuggestion prefetch_suggestion
;
342 // If the default match should be prefetched, do that.
343 if (SearchProvider::ShouldPrefetch(*default_match
)) {
344 prefetch_suggestion
.text
= default_match
->contents
;
345 prefetch_suggestion
.metadata
=
346 SearchProvider::GetSuggestMetadata(*default_match
);
348 // Send the prefetch suggestion unconditionally to the Instant search base
349 // page. If there is no suggestion to prefetch, we need to send a blank
350 // query to clear the prefetched results.
351 InstantSearchPrerenderer
* prerenderer
=
352 InstantSearchPrerenderer::GetForProfile(profile_
);
354 prerenderer
->Prerender(prefetch_suggestion
);
356 if (!inside_synchronous_start_
)
357 NotifySuggestionsReceived(autocomplete_controller_
->result());
360 void AutocompleteControllerAndroid::NotifySuggestionsReceived(
361 const AutocompleteResult
& autocomplete_result
) {
362 JNIEnv
* env
= AttachCurrentThread();
363 ScopedJavaLocalRef
<jobject
> java_bridge
=
364 weak_java_autocomplete_controller_android_
.get(env
);
365 if (!java_bridge
.obj())
368 ScopedJavaLocalRef
<jobject
> suggestion_list_obj
=
369 Java_AutocompleteController_createOmniboxSuggestionList(
370 env
, autocomplete_result
.size());
371 for (size_t i
= 0; i
< autocomplete_result
.size(); ++i
) {
372 ScopedJavaLocalRef
<jobject
> j_omnibox_suggestion
=
373 BuildOmniboxSuggestion(env
, autocomplete_result
.match_at(i
));
374 Java_AutocompleteController_addOmniboxSuggestionToList(
375 env
, suggestion_list_obj
.obj(), j_omnibox_suggestion
.obj());
378 // Get the inline-autocomplete text.
379 const AutocompleteResult::const_iterator
default_match(
380 autocomplete_result
.default_match());
381 base::string16 inline_autocomplete_text
;
382 if (default_match
!= autocomplete_result
.end()) {
383 inline_autocomplete_text
= default_match
->inline_autocompletion
;
385 ScopedJavaLocalRef
<jstring
> inline_text
=
386 ConvertUTF16ToJavaString(env
, inline_autocomplete_text
);
387 jlong j_autocomplete_result
=
388 reinterpret_cast<intptr_t>(&(autocomplete_result
));
389 Java_AutocompleteController_onSuggestionsReceived(env
,
391 suggestion_list_obj
.obj(),
393 j_autocomplete_result
);
396 OmniboxEventProto::PageClassification
397 AutocompleteControllerAndroid::ClassifyPage(const GURL
& gurl
,
398 bool is_query_in_omnibox
,
399 bool focused_from_fakebox
) const {
400 if (!gurl
.is_valid())
401 return OmniboxEventProto::INVALID_SPEC
;
403 const std::string
& url
= gurl
.spec();
405 if (gurl
.SchemeIs(content::kChromeUIScheme
) &&
406 gurl
.host() == chrome::kChromeUINewTabHost
) {
407 return OmniboxEventProto::NTP
;
410 if (url
== chrome::kChromeUINativeNewTabURL
) {
411 return focused_from_fakebox
?
412 OmniboxEventProto::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS
:
413 OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS
;
416 if (url
== url::kAboutBlankURL
)
417 return OmniboxEventProto::BLANK
;
419 if (url
== profile_
->GetPrefs()->GetString(prefs::kHomePage
))
420 return OmniboxEventProto::HOME_PAGE
;
422 if (is_query_in_omnibox
)
423 return OmniboxEventProto::SEARCH_RESULT_PAGE_DOING_SEARCH_TERM_REPLACEMENT
;
425 bool is_search_url
= TemplateURLServiceFactory::GetForProfile(profile_
)->
426 IsSearchResultsPageFromDefaultSearchProvider(gurl
);
428 return OmniboxEventProto::SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT
;
430 return OmniboxEventProto::OTHER
;
433 ScopedJavaLocalRef
<jobject
>
434 AutocompleteControllerAndroid::BuildOmniboxSuggestion(
436 const AutocompleteMatch
& match
) {
437 ScopedJavaLocalRef
<jstring
> contents
=
438 ConvertUTF16ToJavaString(env
, match
.contents
);
439 ScopedJavaLocalRef
<jstring
> description
=
440 ConvertUTF16ToJavaString(env
, match
.description
);
441 ScopedJavaLocalRef
<jstring
> answer_contents
=
442 ConvertUTF16ToJavaString(env
, match
.answer_contents
);
443 ScopedJavaLocalRef
<jstring
> answer_type
=
444 ConvertUTF16ToJavaString(env
, match
.answer_type
);
445 ScopedJavaLocalRef
<jstring
> fill_into_edit
=
446 ConvertUTF16ToJavaString(env
, match
.fill_into_edit
);
447 ScopedJavaLocalRef
<jstring
> destination_url
=
448 ConvertUTF8ToJavaString(env
, match
.destination_url
.spec());
449 // Note that we are also removing 'www' host from formatted url.
450 ScopedJavaLocalRef
<jstring
> formatted_url
= ConvertUTF16ToJavaString(env
,
451 FormatURLUsingAcceptLanguages(match
.stripped_destination_url
));
452 BookmarkModel
* bookmark_model
= BookmarkModelFactory::GetForProfile(profile_
);
453 return Java_AutocompleteController_buildOmniboxSuggestion(
460 answer_contents
.obj(),
462 fill_into_edit
.obj(),
463 destination_url
.obj(),
465 bookmark_model
&& bookmark_model
->IsBookmarked(match
.destination_url
),
466 match
.SupportsDeletion());
469 base::string16
AutocompleteControllerAndroid::FormatURLUsingAcceptLanguages(
471 if (profile_
== NULL
)
472 return base::string16();
474 std::string
languages(
475 profile_
->GetPrefs()->GetString(prefs::kAcceptLanguages
));
477 return net::FormatUrl(url
, languages
, net::kFormatUrlOmitAll
,
478 net::UnescapeRule::SPACES
, NULL
, NULL
, NULL
);
481 ScopedJavaLocalRef
<jobject
>
482 AutocompleteControllerAndroid::GetTopSynchronousResult(
486 bool prevent_inline_autocomplete
) {
487 if (!autocomplete_controller_
)
488 return ScopedJavaLocalRef
<jobject
>();
490 inside_synchronous_start_
= true;
496 prevent_inline_autocomplete
,
500 inside_synchronous_start_
= false;
501 DCHECK(autocomplete_controller_
->done());
502 const AutocompleteResult
& result
= autocomplete_controller_
->result();
504 return ScopedJavaLocalRef
<jobject
>();
506 return BuildOmniboxSuggestion(env
, *result
.begin());
509 static jlong
Init(JNIEnv
* env
, jobject obj
, jobject jprofile
) {
510 Profile
* profile
= ProfileAndroid::FromProfileAndroid(jprofile
);
514 AutocompleteControllerAndroid
* native_bridge
=
515 AutocompleteControllerAndroid::Factory::GetForProfile(profile
, env
, obj
);
516 return reinterpret_cast<intptr_t>(native_bridge
);
519 static jstring
QualifyPartialURLQuery(
520 JNIEnv
* env
, jclass clazz
, jstring jquery
) {
521 Profile
* profile
= ProfileManager::GetActiveUserProfile();
524 AutocompleteMatch match
;
525 base::string16
query_string(ConvertJavaStringToUTF16(env
, jquery
));
526 AutocompleteClassifierFactory::GetForProfile(profile
)->Classify(
530 OmniboxEventProto::INVALID_SPEC
,
533 if (!match
.destination_url
.is_valid())
536 // Only return a URL if the match is a URL type.
537 if (match
.type
!= AutocompleteMatchType::URL_WHAT_YOU_TYPED
&&
538 match
.type
!= AutocompleteMatchType::HISTORY_URL
&&
539 match
.type
!= AutocompleteMatchType::NAVSUGGEST
)
542 // As we are returning to Java, it is fine to call Release().
543 return ConvertUTF8ToJavaString(env
, match
.destination_url
.spec()).Release();
546 static void PrefetchZeroSuggestResults(JNIEnv
* env
, jclass clazz
) {
547 Profile
* profile
= ProfileManager::GetActiveUserProfile();
551 if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial())
554 // ZeroSuggestPrefetcher deletes itself after it's done prefetching.
555 new ZeroSuggestPrefetcher(profile
);
558 // Register native methods
559 bool RegisterAutocompleteControllerAndroid(JNIEnv
* env
) {
560 return RegisterNativesImpl(env
);