autocomplete_controller.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
1// Copyright (c) 2012 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/autocomplete/autocomplete_controller.h"
6
7#include <set>
8#include <string>
9
10#include "base/command_line.h"
11#include "base/format_macros.h"
12#include "base/logging.h"
13#include "base/metrics/histogram.h"
14#include "base/stringprintf.h"
15#include "base/strings/string_number_conversions.h"
16#include "base/time.h"
17#include "chrome/browser/autocomplete/autocomplete_controller_delegate.h"
18#include "chrome/browser/autocomplete/bookmark_provider.h"
19#include "chrome/browser/autocomplete/builtin_provider.h"
20#include "chrome/browser/autocomplete/extension_app_provider.h"
21#include "chrome/browser/autocomplete/history_contents_provider.h"
22#include "chrome/browser/autocomplete/history_quick_provider.h"
23#include "chrome/browser/autocomplete/history_url_provider.h"
24#include "chrome/browser/autocomplete/keyword_provider.h"
25#include "chrome/browser/autocomplete/search_provider.h"
26#include "chrome/browser/autocomplete/shortcuts_provider.h"
27#include "chrome/browser/autocomplete/zero_suggest_provider.h"
28#include "chrome/browser/omnibox/omnibox_field_trial.h"
29#include "chrome/browser/profiles/profile.h"
30#include "chrome/browser/search_engines/template_url.h"
31#include "chrome/common/chrome_notification_types.h"
32#include "chrome/common/chrome_switches.h"
33#include "content/public/browser/notification_service.h"
34#include "grit/generated_resources.h"
35#include "grit/theme_resources.h"
36#include "ui/base/l10n/l10n_util.h"
37
38#if defined(OS_CHROMEOS)
39#include "chrome/browser/autocomplete/contact_provider_chromeos.h"
40#include "chrome/browser/chromeos/contacts/contact_manager.h"
41#endif
42
43namespace {
44
45// Converts the given type to an integer based on the AQS specification.
46// For more details, See http://goto.google.com/binary-clients-logging .
47int AutocompleteMatchToAssistedQueryType(const AutocompleteMatch::Type& type) {
48  switch (type) {
49    case AutocompleteMatch::SEARCH_SUGGEST:        return 0;
50    case AutocompleteMatch::NAVSUGGEST:            return 5;
51    case AutocompleteMatch::SEARCH_WHAT_YOU_TYPED: return 57;
52    case AutocompleteMatch::URL_WHAT_YOU_TYPED:    return 58;
53    case AutocompleteMatch::SEARCH_HISTORY:        return 59;
54    case AutocompleteMatch::HISTORY_URL:           return 60;
55    case AutocompleteMatch::HISTORY_TITLE:         return 61;
56    case AutocompleteMatch::HISTORY_BODY:          return 62;
57    case AutocompleteMatch::HISTORY_KEYWORD:       return 63;
58    case AutocompleteMatch::BOOKMARK_TITLE:        return 65;
59    // NOTE: Default must remain 64 for server-side compatability.
60    default:                                       return 64;
61  }
62}
63
64// Appends available autocompletion of the given type and number to the existing
65// available autocompletions string, encoding according to the spec.
66void AppendAvailableAutocompletion(int type,
67                                   int count,
68                                   std::string* autocompletions) {
69  if (!autocompletions->empty())
70    autocompletions->append("j");
71  base::StringAppendF(autocompletions, "%d", type);
72  if (count > 1)
73    base::StringAppendF(autocompletions, "l%d", count);
74}
75
76// Amount of time (in ms) between when the user stops typing and when we remove
77// any copied entries. We do this from the time the user stopped typing as some
78// providers (such as SearchProvider) wait for the user to stop typing before
79// they initiate a query.
80const int kExpireTimeMS = 500;
81
82}  // namespace
83
84const int AutocompleteController::kNoItemSelected = -1;
85
86AutocompleteController::AutocompleteController(
87    Profile* profile,
88    AutocompleteControllerDelegate* delegate,
89    int provider_types)
90    : delegate_(delegate),
91      keyword_provider_(NULL),
92      search_provider_(NULL),
93      zero_suggest_provider_(NULL),
94      done_(true),
95      in_start_(false),
96      in_zero_suggest_(false),
97      profile_(profile) {
98  // AND with the disabled providers, if any.
99  provider_types &= ~OmniboxFieldTrial::GetDisabledProviderTypes();
100  bool use_hqp = !!(provider_types & AutocompleteProvider::TYPE_HISTORY_QUICK);
101  // TODO(mrossetti): Permanently modify the HistoryURLProvider to not search
102  // titles once HQP is turned on permanently.
103  // History quick provider can be used on all platforms other than Android.
104  // TODO(jcivelli): Enable the History Quick Provider and figure out why it
105  // reports the wrong results for some pages.
106#if defined(OS_ANDROID)
107  use_hqp = false;
108#endif
109
110  if (provider_types & AutocompleteProvider::TYPE_BUILTIN)
111    providers_.push_back(new BuiltinProvider(this, profile));
112#if defined(OS_CHROMEOS)
113  if (provider_types & AutocompleteProvider::TYPE_CONTACT)
114    providers_.push_back(new ContactProvider(this, profile,
115        contacts::ContactManager::GetInstance()->GetWeakPtr()));
116#endif
117  if (provider_types & AutocompleteProvider::TYPE_EXTENSION_APP)
118    providers_.push_back(new ExtensionAppProvider(this, profile));
119  if (provider_types & AutocompleteProvider::TYPE_HISTORY_CONTENTS)
120    providers_.push_back(new HistoryContentsProvider(this, profile, use_hqp));
121  if (use_hqp)
122    providers_.push_back(new HistoryQuickProvider(this, profile));
123  if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL)
124    providers_.push_back(new HistoryURLProvider(this, profile));
125  // Search provider/"tab to search" can be used on all platforms other than
126  // Android.
127#if !defined(OS_ANDROID)
128  if (provider_types & AutocompleteProvider::TYPE_KEYWORD) {
129    keyword_provider_ = new KeywordProvider(this, profile);
130    providers_.push_back(keyword_provider_);
131  }
132#endif
133  if (provider_types & AutocompleteProvider::TYPE_SEARCH) {
134    search_provider_ = new SearchProvider(this, profile);
135    providers_.push_back(search_provider_);
136  }
137  if (provider_types & AutocompleteProvider::TYPE_SHORTCUTS)
138    providers_.push_back(new ShortcutsProvider(this, profile));
139
140  // Create ZeroSuggest if it is enabled.
141  if (provider_types & AutocompleteProvider::TYPE_ZERO_SUGGEST) {
142    zero_suggest_provider_ = ZeroSuggestProvider::Create(this, profile);
143    if (zero_suggest_provider_)
144      providers_.push_back(zero_suggest_provider_);
145  }
146
147  if ((provider_types & AutocompleteProvider::TYPE_BOOKMARK) &&
148      !CommandLine::ForCurrentProcess()->HasSwitch(
149          switches::kDisableBookmarkAutocompleteProvider))
150    providers_.push_back(new BookmarkProvider(this, profile));
151
152  for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i)
153    (*i)->AddRef();
154}
155
156AutocompleteController::~AutocompleteController() {
157  // The providers may have tasks outstanding that hold refs to them.  We need
158  // to ensure they won't call us back if they outlive us.  (Practically,
159  // calling Stop() should also cancel those tasks and make it so that we hold
160  // the only refs.)  We also don't want to bother notifying anyone of our
161  // result changes here, because the notification observer is in the midst of
162  // shutdown too, so we don't ask Stop() to clear |result_| (and notify).
163  result_.Reset();  // Not really necessary.
164  Stop(false);
165
166  for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i)
167    (*i)->Release();
168
169  providers_.clear();  // Not really necessary.
170}
171
172void AutocompleteController::Start(const AutocompleteInput& input) {
173  const string16 old_input_text(input_.text());
174  const AutocompleteInput::MatchesRequested old_matches_requested =
175      input_.matches_requested();
176  input_ = input;
177
178  // See if we can avoid rerunning autocomplete when the query hasn't changed
179  // much.  When the user presses or releases the ctrl key, the desired_tld
180  // changes, and when the user finishes an IME composition, inline autocomplete
181  // may no longer be prevented.  In both these cases the text itself hasn't
182  // changed since the last query, and some providers can do much less work (and
183  // get matches back more quickly).  Taking advantage of this reduces flicker.
184  //
185  // NOTE: This comes after constructing |input_| above since that construction
186  // can change the text string (e.g. by stripping off a leading '?').
187  const bool minimal_changes = (input_.text() == old_input_text) &&
188      (input_.matches_requested() == old_matches_requested);
189
190  expire_timer_.Stop();
191
192  // Start the new query.
193  in_zero_suggest_ = false;
194  in_start_ = true;
195  base::TimeTicks start_time = base::TimeTicks::Now();
196  for (ACProviders::iterator i(providers_.begin()); i != providers_.end();
197       ++i) {
198    (*i)->Start(input_, minimal_changes);
199    if (input.matches_requested() != AutocompleteInput::ALL_MATCHES)
200      DCHECK((*i)->done());
201  }
202  if (input.matches_requested() == AutocompleteInput::ALL_MATCHES &&
203      (input.text().length() < 6)) {
204    base::TimeTicks end_time = base::TimeTicks::Now();
205    std::string name = "Omnibox.QueryTime." + base::IntToString(
206        input.text().length());
207    base::HistogramBase* counter = base::Histogram::FactoryGet(
208        name, 1, 1000, 50, base::Histogram::kUmaTargetedHistogramFlag);
209    counter->Add(static_cast<int>((end_time - start_time).InMilliseconds()));
210  }
211  in_start_ = false;
212  CheckIfDone();
213  // The second true forces saying the default match has changed.
214  // This triggers the edit model to update things such as the inline
215  // autocomplete state.  In particular, if the user has typed a key
216  // since the last notification, and we're now re-running
217  // autocomplete, then we need to update the inline autocompletion
218  // even if the current match is for the same URL as the last run's
219  // default match.  Likewise, the controller doesn't know what's
220  // happened in the edit since the last time it ran autocomplete.
221  // The user might have selected all the text and hit delete, then
222  // typed a new character.  The selection and delete won't send any
223  // signals to the controller so it doesn't realize that anything was
224  // cleared or changed.  Even if the default match hasn't changed, we
225  // need the edit model to update the display.
226  UpdateResult(false, true);
227
228  if (!done_)
229    StartExpireTimer();
230}
231
232void AutocompleteController::Stop(bool clear_result) {
233  for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end();
234       ++i) {
235    (*i)->Stop(clear_result);
236  }
237
238  expire_timer_.Stop();
239  done_ = true;
240  if (clear_result && !result_.empty()) {
241    result_.Reset();
242    // NOTE: We pass in false since we're trying to only clear the popup, not
243    // touch the edit... this is all a mess and should be cleaned up :(
244    NotifyChanged(false);
245  }
246}
247
248void AutocompleteController::StartZeroSuggest(
249    const GURL& url,
250    const string16& user_text) {
251  if (zero_suggest_provider_ != NULL) {
252    DCHECK(!in_start_);  // We should not be already running a query.
253    in_zero_suggest_ = true;
254    zero_suggest_provider_->StartZeroSuggest(url, user_text);
255  }
256}
257
258void AutocompleteController::StopZeroSuggest() {
259  if (zero_suggest_provider_ != NULL) {
260    DCHECK(!in_start_);  // We should not be already running a query.
261    zero_suggest_provider_->Stop(false);
262  }
263}
264
265void AutocompleteController::DeleteMatch(const AutocompleteMatch& match) {
266  DCHECK(match.deletable);
267  match.provider->DeleteMatch(match);  // This may synchronously call back to
268                                       // OnProviderUpdate().
269  // If DeleteMatch resulted in a callback to OnProviderUpdate and we're
270  // not done, we might attempt to redisplay the deleted match. Make sure
271  // we aren't displaying it by removing any old entries.
272  ExpireCopiedEntries();
273}
274
275void AutocompleteController::ExpireCopiedEntries() {
276  // The first true makes UpdateResult() clear out the results and
277  // regenerate them, thus ensuring that no results from the previous
278  // result set remain.
279  UpdateResult(true, false);
280}
281
282void AutocompleteController::OnProviderUpdate(bool updated_matches) {
283  if (in_zero_suggest_) {
284    // We got ZeroSuggest results before Start(). Show only those results,
285    // because results from other providers are stale.
286    result_.Reset();
287    result_.AppendMatches(zero_suggest_provider_->matches());
288    result_.SortAndCull(input_, profile_);
289    NotifyChanged(true);
290  } else {
291    CheckIfDone();
292    // Multiple providers may provide synchronous results, so we only update the
293    // results if we're not in Start().
294    if (!in_start_ && (updated_matches || done_))
295      UpdateResult(false, false);
296  }
297}
298
299void AutocompleteController::AddProvidersInfo(
300    ProvidersInfo* provider_info) const {
301  provider_info->clear();
302  for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end();
303       ++i) {
304    // Add per-provider info, if any.
305    (*i)->AddProviderInfo(provider_info);
306
307    // This is also a good place to put code to add info that you want to
308    // add for every provider.
309  }
310}
311
312void AutocompleteController::ResetSession() {
313  for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end();
314       ++i)
315    (*i)->ResetSession();
316}
317
318void AutocompleteController::UpdateResult(
319    bool regenerate_result,
320    bool force_notify_default_match_changed) {
321  const bool last_default_was_valid = result_.default_match() != result_.end();
322  // The following two variables are only set and used if
323  // |last_default_was_valid|.
324  string16 last_default_fill_into_edit, last_default_associated_keyword;
325  if (last_default_was_valid) {
326    last_default_fill_into_edit = result_.default_match()->fill_into_edit;
327    if (result_.default_match()->associated_keyword != NULL)
328      last_default_associated_keyword =
329          result_.default_match()->associated_keyword->keyword;
330  }
331
332  if (regenerate_result)
333    result_.Reset();
334
335  AutocompleteResult last_result;
336  last_result.Swap(&result_);
337
338  for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end();
339       ++i)
340    result_.AppendMatches((*i)->matches());
341
342  // Sort the matches and trim to a small number of "best" matches.
343  result_.SortAndCull(input_, profile_);
344
345  // Need to validate before invoking CopyOldMatches as the old matches are not
346  // valid against the current input.
347#ifndef NDEBUG
348  result_.Validate();
349#endif
350
351  if (!done_) {
352    // This conditional needs to match the conditional in Start that invokes
353    // StartExpireTimer.
354    result_.CopyOldMatches(input_, last_result, profile_);
355  }
356
357  UpdateKeywordDescriptions(&result_);
358  UpdateAssociatedKeywords(&result_);
359  UpdateAssistedQueryStats(&result_);
360
361  const bool default_is_valid = result_.default_match() != result_.end();
362  string16 default_associated_keyword;
363  if (default_is_valid &&
364      (result_.default_match()->associated_keyword != NULL)) {
365    default_associated_keyword =
366        result_.default_match()->associated_keyword->keyword;
367  }
368  // We've gotten async results. Send notification that the default match
369  // updated if fill_into_edit differs or associated_keyword differ.  (The
370  // latter can change if we've just started Chrome and the keyword database
371  // finishes loading while processing this request.) We don't check the URL
372  // as that may change for the default match even though the fill into edit
373  // hasn't changed (see SearchProvider for one case of this).
374  const bool notify_default_match =
375      (last_default_was_valid != default_is_valid) ||
376      (last_default_was_valid &&
377       ((result_.default_match()->fill_into_edit !=
378          last_default_fill_into_edit) ||
379         (default_associated_keyword != last_default_associated_keyword)));
380  if (notify_default_match)
381    last_time_default_match_changed_ = base::TimeTicks::Now();
382
383  NotifyChanged(force_notify_default_match_changed || notify_default_match);
384}
385
386void AutocompleteController::UpdateAssociatedKeywords(
387    AutocompleteResult* result) {
388  if (!keyword_provider_)
389    return;
390
391  std::set<string16> keywords;
392  for (ACMatches::iterator match(result->begin()); match != result->end();
393       ++match) {
394    string16 keyword(match->GetSubstitutingExplicitlyInvokedKeyword(profile_));
395    if (!keyword.empty()) {
396      keywords.insert(keyword);
397    } else {
398      string16 keyword = match->associated_keyword.get() ?
399          match->associated_keyword->keyword :
400          keyword_provider_->GetKeywordForText(match->fill_into_edit);
401
402      // Only add the keyword if the match does not have a duplicate keyword
403      // with a more relevant match.
404      if (!keyword.empty() && !keywords.count(keyword)) {
405        keywords.insert(keyword);
406
407        if (!match->associated_keyword.get())
408          match->associated_keyword.reset(new AutocompleteMatch(
409              keyword_provider_->CreateAutocompleteMatch(match->fill_into_edit,
410                  keyword, input_)));
411      } else {
412        match->associated_keyword.reset();
413      }
414    }
415  }
416}
417
418void AutocompleteController::UpdateAssistedQueryStats(
419    AutocompleteResult* result) {
420  if (result->empty())
421    return;
422
423  // Build the impressions string (the AQS part after ".").
424  std::string autocompletions;
425  int count = 0;
426  int last_type = -1;
427  for (ACMatches::iterator match(result->begin()); match != result->end();
428       ++match) {
429    int type = AutocompleteMatchToAssistedQueryType(match->type);
430    if (last_type != -1 && type != last_type) {
431      AppendAvailableAutocompletion(last_type, count, &autocompletions);
432      count = 1;
433    } else {
434      count++;
435    }
436    last_type = type;
437  }
438  AppendAvailableAutocompletion(last_type, count, &autocompletions);
439
440  // Go over all matches and set AQS if the match supports it.
441  for (size_t index = 0; index < result->size(); ++index) {
442    AutocompleteMatch* match = result->match_at(index);
443    const TemplateURL* template_url = match->GetTemplateURL(profile_, false);
444    if (!template_url || !match->search_terms_args.get())
445      continue;
446    match->search_terms_args->assisted_query_stats =
447        base::StringPrintf("chrome.%" PRIuS ".%s",
448                           index,
449                           autocompletions.c_str());
450    match->destination_url = GURL(template_url->url_ref().ReplaceSearchTerms(
451        *match->search_terms_args));
452  }
453}
454
455GURL AutocompleteController::GetDestinationURL(
456    const AutocompleteMatch& match,
457    base::TimeDelta query_formulation_time) const {
458  GURL destination_url(match.destination_url);
459  TemplateURL* template_url = match.GetTemplateURL(profile_, false);
460
461  // Append the query formulation time (time from when the user first typed a
462  // character into the omnibox to when the user selected a query) and whether
463  // a field trial has triggered to the AQS parameter, if other AQS parameters
464  // were already populated.
465  if (template_url && match.search_terms_args.get() &&
466      !match.search_terms_args->assisted_query_stats.empty()) {
467    TemplateURLRef::SearchTermsArgs search_terms_args(*match.search_terms_args);
468    search_terms_args.assisted_query_stats += base::StringPrintf(
469        ".%" PRId64 "j%d",
470        query_formulation_time.InMilliseconds(),
471        search_provider_ &&
472        search_provider_->field_trial_triggered_in_session());
473    destination_url = GURL(template_url->url_ref().
474                           ReplaceSearchTerms(search_terms_args));
475  }
476  return destination_url;
477}
478
479void AutocompleteController::UpdateKeywordDescriptions(
480    AutocompleteResult* result) {
481  string16 last_keyword;
482  for (AutocompleteResult::iterator i(result->begin()); i != result->end();
483       ++i) {
484    if ((i->provider->type() == AutocompleteProvider::TYPE_KEYWORD &&
485         !i->keyword.empty()) ||
486        (i->provider->type() == AutocompleteProvider::TYPE_SEARCH &&
487         AutocompleteMatch::IsSearchType(i->type))) {
488      i->description.clear();
489      i->description_class.clear();
490      DCHECK(!i->keyword.empty());
491      if (i->keyword != last_keyword) {
492        const TemplateURL* template_url = i->GetTemplateURL(profile_, false);
493        if (template_url) {
494          i->description = l10n_util::GetStringFUTF16(
495              IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
496              template_url->AdjustedShortNameForLocaleDirection());
497          i->description_class.push_back(
498              ACMatchClassification(0, ACMatchClassification::DIM));
499        }
500        last_keyword = i->keyword;
501      }
502    } else {
503      last_keyword.clear();
504    }
505  }
506}
507
508void AutocompleteController::NotifyChanged(bool notify_default_match) {
509  if (delegate_)
510    delegate_->OnResultChanged(notify_default_match);
511  if (done_) {
512    content::NotificationService::current()->Notify(
513        chrome::NOTIFICATION_AUTOCOMPLETE_CONTROLLER_RESULT_READY,
514        content::Source<AutocompleteController>(this),
515        content::NotificationService::NoDetails());
516  }
517}
518
519void AutocompleteController::CheckIfDone() {
520  for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end();
521       ++i) {
522    if (!(*i)->done()) {
523      done_ = false;
524      return;
525    }
526  }
527  done_ = true;
528}
529
530void AutocompleteController::StartExpireTimer() {
531  if (result_.HasCopiedMatches())
532    expire_timer_.Start(FROM_HERE,
533                        base::TimeDelta::FromMilliseconds(kExpireTimeMS),
534                        this, &AutocompleteController::ExpireCopiedEntries);
535}
536