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
54using base::android::AttachCurrentThread;
55using base::android::ConvertJavaStringToUTF16;
56using base::android::ConvertUTF8ToJavaString;
57using base::android::ConvertUTF16ToJavaString;
58using metrics::OmniboxEventProto;
59
60namespace {
61
62const 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 */
70class 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
85ZeroSuggestPrefetcher::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
104ZeroSuggestPrefetcher::~ZeroSuggestPrefetcher() {
105}
106
107void ZeroSuggestPrefetcher::SelfDestruct() {
108  delete this;
109}
110
111void 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
119AutocompleteControllerAndroid::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
127void 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
155ScopedJavaLocalRef<jobject> AutocompleteControllerAndroid::Classify(
156    JNIEnv* env,
157    jobject obj,
158    jstring j_text) {
159  return GetTopSynchronousResult(env, obj, j_text, true);
160}
161
162void 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
188void AutocompleteControllerAndroid::Stop(JNIEnv* env,
189                                         jobject obj,
190                                         bool clear_results) {
191  if (autocomplete_controller_ != NULL)
192    autocomplete_controller_->Stop(clear_results);
193}
194
195void AutocompleteControllerAndroid::ResetSession(JNIEnv* env, jobject obj) {
196  if (autocomplete_controller_ != NULL)
197    autocomplete_controller_->ResetSession();
198}
199
200void 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
238void 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
247ScopedJavaLocalRef<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
266ScopedJavaLocalRef<jobject>
267AutocompleteControllerAndroid::GetTopSynchronousMatch(JNIEnv* env,
268                                                      jobject obj,
269                                                      jstring query) {
270  return GetTopSynchronousResult(env, obj, query, false);
271}
272
273void 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
286AutocompleteControllerAndroid*
287AutocompleteControllerAndroid::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
296AutocompleteControllerAndroid::Factory*
297AutocompleteControllerAndroid::Factory::GetInstance() {
298  return Singleton<AutocompleteControllerAndroid::Factory>::get();
299}
300
301content::BrowserContext*
302AutocompleteControllerAndroid::Factory::GetBrowserContextToUse(
303    content::BrowserContext* context) const {
304  return chrome::GetBrowserContextOwnInstanceInIncognito(context);
305}
306
307AutocompleteControllerAndroid::Factory::Factory()
308    : BrowserContextKeyedServiceFactory(
309          "AutocompleteControllerAndroid",
310          BrowserContextDependencyManager::GetInstance()) {
311  DependsOn(ShortcutsBackendFactory::GetInstance());
312}
313
314AutocompleteControllerAndroid::Factory::~Factory() {
315}
316
317KeyedService* AutocompleteControllerAndroid::Factory::BuildServiceInstanceFor(
318    content::BrowserContext* profile) const {
319  return new AutocompleteControllerAndroid(static_cast<Profile*>(profile));
320}
321
322AutocompleteControllerAndroid::~AutocompleteControllerAndroid() {
323}
324
325void AutocompleteControllerAndroid::InitJNI(JNIEnv* env, jobject obj) {
326  weak_java_autocomplete_controller_android_ =
327      JavaObjectWeakGlobalRef(env, obj);
328}
329
330void 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
360void 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
396OmniboxEventProto::PageClassification
397AutocompleteControllerAndroid::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
433ScopedJavaLocalRef<jobject>
434AutocompleteControllerAndroid::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
469base::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
481ScopedJavaLocalRef<jobject>
482AutocompleteControllerAndroid::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
509static 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
519static 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
546static 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
559bool RegisterAutocompleteControllerAndroid(JNIEnv* env) {
560  return RegisterNativesImpl(env);
561}
562