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. 4 5 #include "chrome/browser/android/omnibox/autocomplete_controller_android.h" 6 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" 53 54 using base::android::AttachCurrentThread; 55 using base::android::ConvertJavaStringToUTF16; 56 using base::android::ConvertUTF8ToJavaString; 57 using base::android::ConvertUTF16ToJavaString; 58 using metrics::OmniboxEventProto; 59 60 namespace { 61 62 const int kAndroidAutocompleteProviders = 63 AutocompleteClassifier::kDefaultOmniboxProviders; 64 65 /** 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. 69 */ 70 class ZeroSuggestPrefetcher : public AutocompleteControllerDelegate { 71 public: 72 explicit ZeroSuggestPrefetcher(Profile* profile); 73 74 private: 75 virtual ~ZeroSuggestPrefetcher(); 76 void SelfDestruct(); 77 78 // AutocompleteControllerDelegate: 79 virtual void OnResultChanged(bool default_match_changed) OVERRIDE; 80 81 scoped_ptr<AutocompleteController> controller_; 82 base::OneShotTimer<ZeroSuggestPrefetcher> expire_timer_; 83 }; 84 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); 102 } 103 104 ZeroSuggestPrefetcher::~ZeroSuggestPrefetcher() { 105 } 106 107 void ZeroSuggestPrefetcher::SelfDestruct() { 108 delete this; 109 } 110 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. 115 } 116 117 } // namespace 118 119 AutocompleteControllerAndroid::AutocompleteControllerAndroid(Profile* profile) 120 : autocomplete_controller_(new AutocompleteController( 121 profile, TemplateURLServiceFactory::GetForProfile(profile), this, 122 kAndroidAutocompleteProviders)), 123 inside_synchronous_start_(false), 124 profile_(profile) { 125 } 126 127 void AutocompleteControllerAndroid::Start(JNIEnv* env, 128 jobject obj, 129 jstring j_text, 130 jstring j_desired_tld, 131 jstring j_current_url, 132 bool prevent_inline_autocomplete, 133 bool prefer_keyword, 134 bool allow_exact_keyword_match, 135 bool want_asynchronous_matches) { 136 if (!autocomplete_controller_) 137 return; 138 139 base::string16 desired_tld; 140 GURL current_url; 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_); 153 } 154 155 ScopedJavaLocalRef<jobject> AutocompleteControllerAndroid::Classify( 156 JNIEnv* env, 157 jobject obj, 158 jstring j_text) { 159 return GetTopSynchronousResult(env, obj, j_text, true); 160 } 161 162 void AutocompleteControllerAndroid::StartZeroSuggest( 163 JNIEnv* env, 164 jobject obj, 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_) 170 return; 171 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); 175 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()) 179 omnibox_text = url; 180 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_); 186 } 187 188 void AutocompleteControllerAndroid::Stop(JNIEnv* env, 189 jobject obj, 190 bool clear_results) { 191 if (autocomplete_controller_ != NULL) 192 autocomplete_controller_->Stop(clear_results); 193 } 194 195 void AutocompleteControllerAndroid::ResetSession(JNIEnv* env, jobject obj) { 196 if (autocomplete_controller_ != NULL) 197 autocomplete_controller_->ResetSession(); 198 } 199 200 void AutocompleteControllerAndroid::OnSuggestionSelected( 201 JNIEnv* env, 202 jobject obj, 203 jint selected_index, 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); 216 217 OmniboxLog log( 218 input_.text(), 219 false, /* don't know */ 220 input_.type(), 221 true, 222 selected_index, 223 false, 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); 231 232 content::NotificationService::current()->Notify( 233 chrome::NOTIFICATION_OMNIBOX_OPENED_URL, 234 content::Source<Profile>(profile_), 235 content::Details<OmniboxLog>(&log)); 236 } 237 238 void AutocompleteControllerAndroid::DeleteSuggestion(JNIEnv* env, 239 jobject obj, 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); 245 } 246 247 ScopedJavaLocalRef<jstring> AutocompleteControllerAndroid:: 248 UpdateMatchDestinationURLWithQueryFormulationTime( 249 JNIEnv* env, 250 jobject obj, 251 jint selected_index, 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>(); 257 258 AutocompleteMatch match( 259 autocomplete_controller_->result().match_at(selected_index)); 260 autocomplete_controller_->UpdateMatchDestinationURLWithQueryFormulationTime( 261 base::TimeDelta::FromMilliseconds(elapsed_time_since_input_change), 262 &match); 263 return ConvertUTF8ToJavaString(env, match.destination_url.spec()); 264 } 265 266 ScopedJavaLocalRef<jobject> 267 AutocompleteControllerAndroid::GetTopSynchronousMatch(JNIEnv* env, 268 jobject obj, 269 jstring query) { 270 return GetTopSynchronousResult(env, obj, query, false); 271 } 272 273 void AutocompleteControllerAndroid::Shutdown() { 274 autocomplete_controller_.reset(); 275 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()); 281 282 weak_java_autocomplete_controller_android_.reset(); 283 } 284 285 // static 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); 293 return bridge; 294 } 295 296 AutocompleteControllerAndroid::Factory* 297 AutocompleteControllerAndroid::Factory::GetInstance() { 298 return Singleton<AutocompleteControllerAndroid::Factory>::get(); 299 } 300 301 content::BrowserContext* 302 AutocompleteControllerAndroid::Factory::GetBrowserContextToUse( 303 content::BrowserContext* context) const { 304 return chrome::GetBrowserContextOwnInstanceInIncognito(context); 305 } 306 307 AutocompleteControllerAndroid::Factory::Factory() 308 : BrowserContextKeyedServiceFactory( 309 "AutocompleteControllerAndroid", 310 BrowserContextDependencyManager::GetInstance()) { 311 DependsOn(ShortcutsBackendFactory::GetInstance()); 312 } 313 314 AutocompleteControllerAndroid::Factory::~Factory() { 315 } 316 317 KeyedService* AutocompleteControllerAndroid::Factory::BuildServiceInstanceFor( 318 content::BrowserContext* profile) const { 319 return new AutocompleteControllerAndroid(static_cast<Profile*>(profile)); 320 } 321 322 AutocompleteControllerAndroid::~AutocompleteControllerAndroid() { 323 } 324 325 void AutocompleteControllerAndroid::InitJNI(JNIEnv* env, jobject obj) { 326 weak_java_autocomplete_controller_android_ = 327 JavaObjectWeakGlobalRef(env, obj); 328 } 329 330 void AutocompleteControllerAndroid::OnResultChanged( 331 bool default_match_changed) { 332 if (!autocomplete_controller_) 333 return; 334 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); 347 } 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_); 353 if (prerenderer) 354 prerenderer->Prerender(prefetch_suggestion); 355 } 356 if (!inside_synchronous_start_) 357 NotifySuggestionsReceived(autocomplete_controller_->result()); 358 } 359 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()) 366 return; 367 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()); 376 } 377 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; 384 } 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, 390 java_bridge.obj(), 391 suggestion_list_obj.obj(), 392 inline_text.obj(), 393 j_autocomplete_result); 394 } 395 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; 402 403 const std::string& url = gurl.spec(); 404 405 if (gurl.SchemeIs(content::kChromeUIScheme) && 406 gurl.host() == chrome::kChromeUINewTabHost) { 407 return OmniboxEventProto::NTP; 408 } 409 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; 414 } 415 416 if (url == url::kAboutBlankURL) 417 return OmniboxEventProto::BLANK; 418 419 if (url == profile_->GetPrefs()->GetString(prefs::kHomePage)) 420 return OmniboxEventProto::HOME_PAGE; 421 422 if (is_query_in_omnibox) 423 return OmniboxEventProto::SEARCH_RESULT_PAGE_DOING_SEARCH_TERM_REPLACEMENT; 424 425 bool is_search_url = TemplateURLServiceFactory::GetForProfile(profile_)-> 426 IsSearchResultsPageFromDefaultSearchProvider(gurl); 427 if (is_search_url) 428 return OmniboxEventProto::SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT; 429 430 return OmniboxEventProto::OTHER; 431 } 432 433 ScopedJavaLocalRef<jobject> 434 AutocompleteControllerAndroid::BuildOmniboxSuggestion( 435 JNIEnv* env, 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( 454 env, 455 match.type, 456 match.relevance, 457 match.transition, 458 contents.obj(), 459 description.obj(), 460 answer_contents.obj(), 461 answer_type.obj(), 462 fill_into_edit.obj(), 463 destination_url.obj(), 464 formatted_url.obj(), 465 bookmark_model && bookmark_model->IsBookmarked(match.destination_url), 466 match.SupportsDeletion()); 467 } 468 469 base::string16 AutocompleteControllerAndroid::FormatURLUsingAcceptLanguages( 470 GURL url) { 471 if (profile_ == NULL) 472 return base::string16(); 473 474 std::string languages( 475 profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)); 476 477 return net::FormatUrl(url, languages, net::kFormatUrlOmitAll, 478 net::UnescapeRule::SPACES, NULL, NULL, NULL); 479 } 480 481 ScopedJavaLocalRef<jobject> 482 AutocompleteControllerAndroid::GetTopSynchronousResult( 483 JNIEnv* env, 484 jobject obj, 485 jstring j_text, 486 bool prevent_inline_autocomplete) { 487 if (!autocomplete_controller_) 488 return ScopedJavaLocalRef<jobject>(); 489 490 inside_synchronous_start_ = true; 491 Start(env, 492 obj, 493 j_text, 494 NULL, 495 NULL, 496 prevent_inline_autocomplete, 497 false, 498 false, 499 false); 500 inside_synchronous_start_ = false; 501 DCHECK(autocomplete_controller_->done()); 502 const AutocompleteResult& result = autocomplete_controller_->result(); 503 if (result.empty()) 504 return ScopedJavaLocalRef<jobject>(); 505 506 return BuildOmniboxSuggestion(env, *result.begin()); 507 } 508 509 static jlong Init(JNIEnv* env, jobject obj, jobject jprofile) { 510 Profile* profile = ProfileAndroid::FromProfileAndroid(jprofile); 511 if (!profile) 512 return 0; 513 514 AutocompleteControllerAndroid* native_bridge = 515 AutocompleteControllerAndroid::Factory::GetForProfile(profile, env, obj); 516 return reinterpret_cast<intptr_t>(native_bridge); 517 } 518 519 static jstring QualifyPartialURLQuery( 520 JNIEnv* env, jclass clazz, jstring jquery) { 521 Profile* profile = ProfileManager::GetActiveUserProfile(); 522 if (!profile) 523 return NULL; 524 AutocompleteMatch match; 525 base::string16 query_string(ConvertJavaStringToUTF16(env, jquery)); 526 AutocompleteClassifierFactory::GetForProfile(profile)->Classify( 527 query_string, 528 false, 529 false, 530 OmniboxEventProto::INVALID_SPEC, 531 &match, 532 NULL); 533 if (!match.destination_url.is_valid()) 534 return NULL; 535 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) 540 return NULL; 541 542 // As we are returning to Java, it is fine to call Release(). 543 return ConvertUTF8ToJavaString(env, match.destination_url.spec()).Release(); 544 } 545 546 static void PrefetchZeroSuggestResults(JNIEnv* env, jclass clazz) { 547 Profile* profile = ProfileManager::GetActiveUserProfile(); 548 if (!profile) 549 return; 550 551 if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial()) 552 return; 553 554 // ZeroSuggestPrefetcher deletes itself after it's done prefetching. 555 new ZeroSuggestPrefetcher(profile); 556 } 557 558 // Register native methods 559 bool RegisterAutocompleteControllerAndroid(JNIEnv* env) { 560 return RegisterNativesImpl(env); 561 } 562