| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/autocomplete/autocomplete_controller.h" |
| |
| #include <set> |
| #include <string> |
| |
| #include "base/command_line.h" |
| #include "base/format_macros.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram.h" |
| #include "base/string_number_conversions.h" |
| #include "base/stringprintf.h" |
| #include "base/time.h" |
| #include "chrome/browser/autocomplete/autocomplete_controller_delegate.h" |
| #include "chrome/browser/autocomplete/builtin_provider.h" |
| #include "chrome/browser/autocomplete/extension_app_provider.h" |
| #include "chrome/browser/autocomplete/history_contents_provider.h" |
| #include "chrome/browser/autocomplete/history_quick_provider.h" |
| #include "chrome/browser/autocomplete/history_url_provider.h" |
| #include "chrome/browser/autocomplete/keyword_provider.h" |
| #include "chrome/browser/autocomplete/search_provider.h" |
| #include "chrome/browser/autocomplete/shortcuts_provider.h" |
| #include "chrome/browser/autocomplete/zero_suggest_provider.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/search_engines/template_url.h" |
| #include "chrome/common/chrome_notification_types.h" |
| #include "content/public/browser/notification_service.h" |
| #include "grit/generated_resources.h" |
| #include "grit/theme_resources.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| #if defined(OS_CHROMEOS) |
| #include "chrome/browser/autocomplete/contact_provider_chromeos.h" |
| #include "chrome/browser/chromeos/contacts/contact_manager.h" |
| #endif |
| |
| namespace { |
| |
| // Converts the given type to an integer based on the AQS specification. |
| // For more details, See http://goto.google.com/binary-clients-logging . |
| int AutocompleteMatchToAssistedQueryType(const AutocompleteMatch::Type& type) { |
| switch (type) { |
| case AutocompleteMatch::SEARCH_SUGGEST: return 0; |
| case AutocompleteMatch::NAVSUGGEST: return 5; |
| case AutocompleteMatch::SEARCH_WHAT_YOU_TYPED: return 57; |
| case AutocompleteMatch::URL_WHAT_YOU_TYPED: return 58; |
| case AutocompleteMatch::SEARCH_HISTORY: return 59; |
| case AutocompleteMatch::HISTORY_URL: return 60; |
| case AutocompleteMatch::HISTORY_TITLE: return 61; |
| case AutocompleteMatch::HISTORY_BODY: return 62; |
| case AutocompleteMatch::HISTORY_KEYWORD: return 63; |
| default: return 64; |
| } |
| } |
| |
| // Appends available autocompletion of the given type and number to the existing |
| // available autocompletions string, encoding according to the spec. |
| void AppendAvailableAutocompletion(int type, |
| int count, |
| std::string* autocompletions) { |
| if (!autocompletions->empty()) |
| autocompletions->append("j"); |
| base::StringAppendF(autocompletions, "%d", type); |
| if (count > 1) |
| base::StringAppendF(autocompletions, "l%d", count); |
| } |
| |
| // Amount of time (in ms) between when the user stops typing and when we remove |
| // any copied entries. We do this from the time the user stopped typing as some |
| // providers (such as SearchProvider) wait for the user to stop typing before |
| // they initiate a query. |
| const int kExpireTimeMS = 500; |
| |
| } // namespace |
| |
| const int AutocompleteController::kNoItemSelected = -1; |
| |
| AutocompleteController::AutocompleteController( |
| Profile* profile, |
| AutocompleteControllerDelegate* delegate, |
| int provider_types) |
| : delegate_(delegate), |
| keyword_provider_(NULL), |
| search_provider_(NULL), |
| zero_suggest_provider_(NULL), |
| done_(true), |
| in_start_(false), |
| in_zero_suggest_(false), |
| profile_(profile) { |
| bool use_hqp = !!(provider_types & AutocompleteProvider::TYPE_HISTORY_QUICK); |
| // TODO(mrossetti): Permanently modify the HistoryURLProvider to not search |
| // titles once HQP is turned on permanently. |
| // History quick provider can be used on all platforms other than Android. |
| // TODO(jcivelli): Enable the History Quick Provider and figure out why it |
| // reports the wrong results for some pages. |
| #if defined(OS_ANDROID) |
| use_hqp = false; |
| #endif |
| |
| if (provider_types & AutocompleteProvider::TYPE_BUILTIN) |
| providers_.push_back(new BuiltinProvider(this, profile)); |
| #if defined(OS_CHROMEOS) |
| if (provider_types & AutocompleteProvider::TYPE_CONTACT) |
| providers_.push_back(new ContactProvider(this, profile, |
| contacts::ContactManager::GetInstance()->GetWeakPtr())); |
| #endif |
| if (provider_types & AutocompleteProvider::TYPE_EXTENSION_APP) |
| providers_.push_back(new ExtensionAppProvider(this, profile)); |
| if (provider_types & AutocompleteProvider::TYPE_HISTORY_CONTENTS) |
| providers_.push_back(new HistoryContentsProvider(this, profile, use_hqp)); |
| if (use_hqp) |
| providers_.push_back(new HistoryQuickProvider(this, profile)); |
| if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL) |
| providers_.push_back(new HistoryURLProvider(this, profile)); |
| // Search provider/"tab to search" can be used on all platforms other than |
| // Android. |
| #if !defined(OS_ANDROID) |
| if (provider_types & AutocompleteProvider::TYPE_KEYWORD) { |
| keyword_provider_ = new KeywordProvider(this, profile); |
| providers_.push_back(keyword_provider_); |
| } |
| #endif |
| if (provider_types & AutocompleteProvider::TYPE_SEARCH) { |
| search_provider_ = new SearchProvider(this, profile); |
| providers_.push_back(search_provider_); |
| } |
| if (provider_types & AutocompleteProvider::TYPE_SHORTCUTS) |
| providers_.push_back(new ShortcutsProvider(this, profile)); |
| |
| // Create ZeroSuggest if it is enabled. |
| if (provider_types & AutocompleteProvider::TYPE_ZERO_SUGGEST) { |
| zero_suggest_provider_ = ZeroSuggestProvider::Create(this, profile); |
| if (zero_suggest_provider_) |
| providers_.push_back(zero_suggest_provider_); |
| } |
| |
| for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i) |
| (*i)->AddRef(); |
| } |
| |
| AutocompleteController::~AutocompleteController() { |
| // The providers may have tasks outstanding that hold refs to them. We need |
| // to ensure they won't call us back if they outlive us. (Practically, |
| // calling Stop() should also cancel those tasks and make it so that we hold |
| // the only refs.) We also don't want to bother notifying anyone of our |
| // result changes here, because the notification observer is in the midst of |
| // shutdown too, so we don't ask Stop() to clear |result_| (and notify). |
| result_.Reset(); // Not really necessary. |
| Stop(false); |
| |
| for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i) |
| (*i)->Release(); |
| |
| providers_.clear(); // Not really necessary. |
| } |
| |
| void AutocompleteController::Start( |
| const string16& text, |
| const string16& desired_tld, |
| bool prevent_inline_autocomplete, |
| bool prefer_keyword, |
| bool allow_exact_keyword_match, |
| AutocompleteInput::MatchesRequested matches_requested) { |
| const string16 old_input_text(input_.text()); |
| const AutocompleteInput::MatchesRequested old_matches_requested = |
| input_.matches_requested(); |
| input_ = AutocompleteInput(text, desired_tld, prevent_inline_autocomplete, |
| prefer_keyword, allow_exact_keyword_match, matches_requested); |
| |
| // See if we can avoid rerunning autocomplete when the query hasn't changed |
| // much. When the user presses or releases the ctrl key, the desired_tld |
| // changes, and when the user finishes an IME composition, inline autocomplete |
| // may no longer be prevented. In both these cases the text itself hasn't |
| // changed since the last query, and some providers can do much less work (and |
| // get matches back more quickly). Taking advantage of this reduces flicker. |
| // |
| // NOTE: This comes after constructing |input_| above since that construction |
| // can change the text string (e.g. by stripping off a leading '?'). |
| const bool minimal_changes = (input_.text() == old_input_text) && |
| (input_.matches_requested() == old_matches_requested); |
| |
| expire_timer_.Stop(); |
| |
| // Start the new query. |
| in_zero_suggest_ = false; |
| in_start_ = true; |
| base::TimeTicks start_time = base::TimeTicks::Now(); |
| for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); |
| ++i) { |
| (*i)->Start(input_, minimal_changes); |
| if (matches_requested != AutocompleteInput::ALL_MATCHES) |
| DCHECK((*i)->done()); |
| } |
| if (matches_requested == AutocompleteInput::ALL_MATCHES && |
| (text.length() < 6)) { |
| base::TimeTicks end_time = base::TimeTicks::Now(); |
| std::string name = "Omnibox.QueryTime." + base::IntToString(text.length()); |
| base::Histogram* counter = base::Histogram::FactoryGet( |
| name, 1, 1000, 50, base::Histogram::kUmaTargetedHistogramFlag); |
| counter->Add(static_cast<int>((end_time - start_time).InMilliseconds())); |
| } |
| in_start_ = false; |
| CheckIfDone(); |
| UpdateResult(true); |
| |
| if (!done_) |
| StartExpireTimer(); |
| } |
| |
| void AutocompleteController::Stop(bool clear_result) { |
| for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end(); |
| ++i) { |
| (*i)->Stop(clear_result); |
| } |
| |
| expire_timer_.Stop(); |
| done_ = true; |
| if (clear_result && !result_.empty()) { |
| result_.Reset(); |
| // NOTE: We pass in false since we're trying to only clear the popup, not |
| // touch the edit... this is all a mess and should be cleaned up :( |
| NotifyChanged(false); |
| } |
| } |
| |
| void AutocompleteController::StartZeroSuggest( |
| const GURL& url, |
| const string16& user_text) { |
| if (zero_suggest_provider_ != NULL) { |
| DCHECK(!in_start_); // We should not be already running a query. |
| in_zero_suggest_ = true; |
| zero_suggest_provider_->StartZeroSuggest(url, user_text); |
| } |
| } |
| |
| void AutocompleteController::StopZeroSuggest() { |
| if (zero_suggest_provider_ != NULL) { |
| DCHECK(!in_start_); // We should not be already running a query. |
| zero_suggest_provider_->Stop(false); |
| } |
| } |
| |
| void AutocompleteController::DeleteMatch(const AutocompleteMatch& match) { |
| DCHECK(match.deletable); |
| match.provider->DeleteMatch(match); // This may synchronously call back to |
| // OnProviderUpdate(). |
| // If DeleteMatch resulted in a callback to OnProviderUpdate and we're |
| // not done, we might attempt to redisplay the deleted match. Make sure |
| // we aren't displaying it by removing any old entries. |
| ExpireCopiedEntries(); |
| } |
| |
| void AutocompleteController::ExpireCopiedEntries() { |
| // Clear out the results. This ensures no results from the previous result set |
| // are copied over. |
| result_.Reset(); |
| // We allow matches from the previous result set to starve out matches from |
| // the new result set. This means in order to expire matches we have to query |
| // the providers again. |
| UpdateResult(false); |
| } |
| |
| void AutocompleteController::OnProviderUpdate(bool updated_matches) { |
| if (in_zero_suggest_) { |
| // We got ZeroSuggest results before Start(). Show only those results, |
| // because results from other providers are stale. |
| result_.Reset(); |
| result_.AppendMatches(zero_suggest_provider_->matches()); |
| result_.SortAndCull(input_); |
| NotifyChanged(true); |
| } else { |
| CheckIfDone(); |
| // Multiple providers may provide synchronous results, so we only update the |
| // results if we're not in Start(). |
| if (!in_start_ && (updated_matches || done_)) |
| UpdateResult(false); |
| } |
| } |
| |
| void AutocompleteController::AddProvidersInfo( |
| ProvidersInfo* provider_info) const { |
| provider_info->clear(); |
| for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end(); |
| ++i) { |
| // Add per-provider info, if any. |
| (*i)->AddProviderInfo(provider_info); |
| |
| // This is also a good place to put code to add info that you want to |
| // add for every provider. |
| } |
| } |
| |
| void AutocompleteController::UpdateResult(bool is_synchronous_pass) { |
| AutocompleteResult last_result; |
| last_result.Swap(&result_); |
| |
| for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end(); |
| ++i) |
| result_.AppendMatches((*i)->matches()); |
| |
| // Sort the matches and trim to a small number of "best" matches. |
| result_.SortAndCull(input_); |
| |
| // Need to validate before invoking CopyOldMatches as the old matches are not |
| // valid against the current input. |
| #ifndef NDEBUG |
| result_.Validate(); |
| #endif |
| |
| if (!done_) { |
| // This conditional needs to match the conditional in Start that invokes |
| // StartExpireTimer. |
| result_.CopyOldMatches(input_, last_result); |
| } |
| |
| UpdateKeywordDescriptions(&result_); |
| UpdateAssociatedKeywords(&result_); |
| UpdateAssistedQueryStats(&result_); |
| |
| bool notify_default_match = is_synchronous_pass; |
| if (!is_synchronous_pass) { |
| const bool last_default_was_valid = |
| last_result.default_match() != last_result.end(); |
| const bool default_is_valid = result_.default_match() != result_.end(); |
| // We've gotten async results. Send notification that the default match |
| // updated if fill_into_edit differs or associated_keyword differ. (The |
| // latter can change if we've just started Chrome and the keyword database |
| // finishes loading while processing this request.) We don't check the URL |
| // as that may change for the default match even though the fill into edit |
| // hasn't changed (see SearchProvider for one case of this). |
| notify_default_match = |
| (last_default_was_valid != default_is_valid) || |
| (default_is_valid && |
| ((result_.default_match()->fill_into_edit != |
| last_result.default_match()->fill_into_edit) || |
| (result_.default_match()->associated_keyword.get() != |
| last_result.default_match()->associated_keyword.get()))); |
| } |
| |
| NotifyChanged(notify_default_match); |
| } |
| |
| void AutocompleteController::UpdateAssociatedKeywords( |
| AutocompleteResult* result) { |
| if (!keyword_provider_) |
| return; |
| |
| std::set<string16> keywords; |
| for (ACMatches::iterator match(result->begin()); match != result->end(); |
| ++match) { |
| string16 keyword(match->GetSubstitutingExplicitlyInvokedKeyword(profile_)); |
| if (!keyword.empty()) { |
| keywords.insert(keyword); |
| } else { |
| string16 keyword = match->associated_keyword.get() ? |
| match->associated_keyword->keyword : |
| keyword_provider_->GetKeywordForText(match->fill_into_edit); |
| |
| // Only add the keyword if the match does not have a duplicate keyword |
| // with a more relevant match. |
| if (!keyword.empty() && !keywords.count(keyword)) { |
| keywords.insert(keyword); |
| |
| if (!match->associated_keyword.get()) |
| match->associated_keyword.reset(new AutocompleteMatch( |
| keyword_provider_->CreateAutocompleteMatch(match->fill_into_edit, |
| keyword, input_))); |
| } else { |
| match->associated_keyword.reset(); |
| } |
| } |
| } |
| } |
| |
| void AutocompleteController::UpdateAssistedQueryStats( |
| AutocompleteResult* result) { |
| if (result->empty()) |
| return; |
| |
| // Build the impressions string (the AQS part after "."). |
| std::string autocompletions; |
| int count = 0; |
| int last_type = -1; |
| for (ACMatches::iterator match(result->begin()); match != result->end(); |
| ++match) { |
| int type = AutocompleteMatchToAssistedQueryType(match->type); |
| if (last_type != -1 && type != last_type) { |
| AppendAvailableAutocompletion(last_type, count, &autocompletions); |
| count = 1; |
| } else { |
| count++; |
| } |
| last_type = type; |
| } |
| AppendAvailableAutocompletion(last_type, count, &autocompletions); |
| |
| // Go over all matches and set AQS if the match supports it. |
| for (size_t index = 0; index < result->size(); ++index) { |
| AutocompleteMatch* match = result->match_at(index); |
| const TemplateURL* template_url = match->GetTemplateURL(profile_); |
| if (!template_url || !match->search_terms_args.get()) |
| continue; |
| match->search_terms_args->assisted_query_stats = |
| base::StringPrintf("chrome.%" PRIuS ".%s", |
| index, |
| autocompletions.c_str()); |
| match->destination_url = GURL(template_url->url_ref().ReplaceSearchTerms( |
| *match->search_terms_args)); |
| } |
| } |
| |
| void AutocompleteController::UpdateKeywordDescriptions( |
| AutocompleteResult* result) { |
| string16 last_keyword; |
| for (AutocompleteResult::iterator i(result->begin()); i != result->end(); |
| ++i) { |
| if ((i->provider->type() == AutocompleteProvider::TYPE_KEYWORD && |
| !i->keyword.empty()) || |
| (i->provider->type() == AutocompleteProvider::TYPE_SEARCH && |
| AutocompleteMatch::IsSearchType(i->type))) { |
| i->description.clear(); |
| i->description_class.clear(); |
| DCHECK(!i->keyword.empty()); |
| if (i->keyword != last_keyword) { |
| const TemplateURL* template_url = i->GetTemplateURL(profile_); |
| if (template_url) { |
| i->description = l10n_util::GetStringFUTF16( |
| IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION, |
| template_url->AdjustedShortNameForLocaleDirection()); |
| i->description_class.push_back( |
| ACMatchClassification(0, ACMatchClassification::DIM)); |
| } |
| last_keyword = i->keyword; |
| } |
| } else { |
| last_keyword.clear(); |
| } |
| } |
| } |
| |
| void AutocompleteController::NotifyChanged(bool notify_default_match) { |
| if (delegate_) |
| delegate_->OnResultChanged(notify_default_match); |
| if (done_) { |
| content::NotificationService::current()->Notify( |
| chrome::NOTIFICATION_AUTOCOMPLETE_CONTROLLER_RESULT_READY, |
| content::Source<AutocompleteController>(this), |
| content::NotificationService::NoDetails()); |
| } |
| } |
| |
| void AutocompleteController::CheckIfDone() { |
| for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end(); |
| ++i) { |
| if (!(*i)->done()) { |
| done_ = false; |
| return; |
| } |
| } |
| done_ = true; |
| } |
| |
| void AutocompleteController::StartExpireTimer() { |
| if (result_.HasCopiedMatches()) |
| expire_timer_.Start(FROM_HERE, |
| base::TimeDelta::FromMilliseconds(kExpireTimeMS), |
| this, &AutocompleteController::ExpireCopiedEntries); |
| } |