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/zero_suggest_provider.h" 6 7#include "base/callback.h" 8#include "base/i18n/case_conversion.h" 9#include "base/json/json_string_value_serializer.h" 10#include "base/metrics/histogram.h" 11#include "base/prefs/pref_service.h" 12#include "base/strings/string16.h" 13#include "base/strings/string_util.h" 14#include "base/strings/utf_string_conversions.h" 15#include "base/time/time.h" 16#include "chrome/browser/autocomplete/autocomplete_classifier.h" 17#include "chrome/browser/autocomplete/autocomplete_classifier_factory.h" 18#include "chrome/browser/autocomplete/autocomplete_input.h" 19#include "chrome/browser/autocomplete/autocomplete_match.h" 20#include "chrome/browser/autocomplete/autocomplete_provider_listener.h" 21#include "chrome/browser/autocomplete/history_url_provider.h" 22#include "chrome/browser/autocomplete/search_provider.h" 23#include "chrome/browser/history/history_types.h" 24#include "chrome/browser/history/top_sites.h" 25#include "chrome/browser/metrics/variations/variations_http_header_provider.h" 26#include "chrome/browser/omnibox/omnibox_field_trial.h" 27#include "chrome/browser/profiles/profile.h" 28#include "chrome/browser/search/search.h" 29#include "chrome/browser/search_engines/template_url_service.h" 30#include "chrome/browser/search_engines/template_url_service_factory.h" 31#include "chrome/common/pref_names.h" 32#include "chrome/common/url_constants.h" 33#include "components/metrics/proto/omnibox_input_type.pb.h" 34#include "components/pref_registry/pref_registry_syncable.h" 35#include "content/public/browser/user_metrics.h" 36#include "net/base/escape.h" 37#include "net/base/load_flags.h" 38#include "net/base/net_util.h" 39#include "net/http/http_request_headers.h" 40#include "net/url_request/url_fetcher.h" 41#include "net/url_request/url_request_status.h" 42#include "url/gurl.h" 43 44namespace { 45 46// TODO(hfung): The histogram code was copied and modified from 47// search_provider.cc. Refactor and consolidate the code. 48// We keep track in a histogram how many suggest requests we send, how 49// many suggest requests we invalidate (e.g., due to a user typing 50// another character), and how many replies we receive. 51// *** ADD NEW ENUMS AFTER ALL PREVIOUSLY DEFINED ONES! *** 52// (excluding the end-of-list enum value) 53// We do not want values of existing enums to change or else it screws 54// up the statistics. 55enum ZeroSuggestRequestsHistogramValue { 56 ZERO_SUGGEST_REQUEST_SENT = 1, 57 ZERO_SUGGEST_REQUEST_INVALIDATED, 58 ZERO_SUGGEST_REPLY_RECEIVED, 59 ZERO_SUGGEST_MAX_REQUEST_HISTOGRAM_VALUE 60}; 61 62void LogOmniboxZeroSuggestRequest( 63 ZeroSuggestRequestsHistogramValue request_value) { 64 UMA_HISTOGRAM_ENUMERATION("Omnibox.ZeroSuggestRequests", request_value, 65 ZERO_SUGGEST_MAX_REQUEST_HISTOGRAM_VALUE); 66} 67 68// The maximum relevance of the top match from this provider. 69const int kDefaultVerbatimZeroSuggestRelevance = 1300; 70 71// Relevance value to use if it was not set explicitly by the server. 72const int kDefaultZeroSuggestRelevance = 100; 73 74} // namespace 75 76// static 77ZeroSuggestProvider* ZeroSuggestProvider::Create( 78 AutocompleteProviderListener* listener, 79 Profile* profile) { 80 return new ZeroSuggestProvider(listener, profile); 81} 82 83// static 84void ZeroSuggestProvider::RegisterProfilePrefs( 85 user_prefs::PrefRegistrySyncable* registry) { 86 registry->RegisterStringPref( 87 prefs::kZeroSuggestCachedResults, 88 std::string(), 89 user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); 90} 91 92void ZeroSuggestProvider::Start(const AutocompleteInput& input, 93 bool minimal_changes) { 94 matches_.clear(); 95 if (input.type() == metrics::OmniboxInputType::INVALID) 96 return; 97 98 Stop(true); 99 field_trial_triggered_ = false; 100 field_trial_triggered_in_session_ = false; 101 results_from_cache_ = false; 102 permanent_text_ = input.text(); 103 current_query_ = input.current_url().spec(); 104 current_page_classification_ = input.current_page_classification(); 105 current_url_match_ = MatchForCurrentURL(); 106 107 const TemplateURL* default_provider = 108 template_url_service_->GetDefaultSearchProvider(); 109 if (default_provider == NULL) 110 return; 111 112 base::string16 prefix; 113 TemplateURLRef::SearchTermsArgs search_term_args(prefix); 114 GURL suggest_url(default_provider->suggestions_url_ref().ReplaceSearchTerms( 115 search_term_args, template_url_service_->search_terms_data())); 116 if (!suggest_url.is_valid()) 117 return; 118 119 // No need to send the current page URL in personalized suggest field trial. 120 if (CanSendURL(input.current_url(), suggest_url, default_provider, 121 current_page_classification_, profile_) && 122 !OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial()) { 123 // Update suggest_url to include the current_page_url. 124 search_term_args.current_page_url = current_query_; 125 suggest_url = GURL(default_provider->suggestions_url_ref(). 126 ReplaceSearchTerms( 127 search_term_args, 128 template_url_service_->search_terms_data())); 129 } else if (!CanShowZeroSuggestWithoutSendingURL(suggest_url, 130 input.current_url())) { 131 return; 132 } 133 134 done_ = false; 135 // TODO(jered): Consider adding locally-sourced zero-suggestions here too. 136 // These may be useful on the NTP or more relevant to the user than server 137 // suggestions, if based on local browsing history. 138 MaybeUseCachedSuggestions(); 139 Run(suggest_url); 140} 141 142void ZeroSuggestProvider::DeleteMatch(const AutocompleteMatch& match) { 143 if (OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial()) { 144 // Remove the deleted match from the cache, so it is not shown to the user 145 // again. Since we cannot remove just one result, blow away the cache. 146 profile_->GetPrefs()->SetString(prefs::kZeroSuggestCachedResults, 147 std::string()); 148 } 149 BaseSearchProvider::DeleteMatch(match); 150} 151 152void ZeroSuggestProvider::ResetSession() { 153 // The user has started editing in the omnibox, so leave 154 // |field_trial_triggered_in_session_| unchanged and set 155 // |field_trial_triggered_| to false since zero suggest is inactive now. 156 field_trial_triggered_ = false; 157} 158 159void ZeroSuggestProvider::ModifyProviderInfo( 160 metrics::OmniboxEventProto_ProviderInfo* provider_info) const { 161 if (!results_.suggest_results.empty() || !results_.navigation_results.empty()) 162 provider_info->set_times_returned_results_in_session(1); 163} 164 165ZeroSuggestProvider::ZeroSuggestProvider( 166 AutocompleteProviderListener* listener, 167 Profile* profile) 168 : BaseSearchProvider(listener, profile, 169 AutocompleteProvider::TYPE_ZERO_SUGGEST), 170 template_url_service_(TemplateURLServiceFactory::GetForProfile(profile)), 171 results_from_cache_(false), 172 weak_ptr_factory_(this) { 173} 174 175ZeroSuggestProvider::~ZeroSuggestProvider() { 176} 177 178bool ZeroSuggestProvider::StoreSuggestionResponse( 179 const std::string& json_data, 180 const base::Value& parsed_data) { 181 if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial() || 182 json_data.empty()) 183 return false; 184 profile_->GetPrefs()->SetString(prefs::kZeroSuggestCachedResults, json_data); 185 186 // If we received an empty result list, we should update the display, as it 187 // may be showing cached results that should not be shown. 188 const base::ListValue* root_list = NULL; 189 const base::ListValue* results_list = NULL; 190 if (parsed_data.GetAsList(&root_list) && 191 root_list->GetList(1, &results_list) && 192 results_list->empty()) 193 return false; 194 195 // We are finished with the request and want to bail early. 196 if (results_from_cache_) 197 done_ = true; 198 199 return results_from_cache_; 200} 201 202const TemplateURL* ZeroSuggestProvider::GetTemplateURL(bool is_keyword) const { 203 // Zero suggest provider should not receive keyword results. 204 DCHECK(!is_keyword); 205 return template_url_service_->GetDefaultSearchProvider(); 206} 207 208const AutocompleteInput ZeroSuggestProvider::GetInput(bool is_keyword) const { 209 return AutocompleteInput( 210 base::string16(), base::string16::npos, base::string16(), 211 GURL(current_query_), current_page_classification_, true, false, false, 212 true); 213} 214 215BaseSearchProvider::Results* ZeroSuggestProvider::GetResultsToFill( 216 bool is_keyword) { 217 DCHECK(!is_keyword); 218 return &results_; 219} 220 221bool ZeroSuggestProvider::ShouldAppendExtraParams( 222 const SuggestResult& result) const { 223 // We always use the default provider for search, so append the params. 224 return true; 225} 226 227void ZeroSuggestProvider::StopSuggest() { 228 if (suggest_results_pending_ > 0) 229 LogOmniboxZeroSuggestRequest(ZERO_SUGGEST_REQUEST_INVALIDATED); 230 suggest_results_pending_ = 0; 231 fetcher_.reset(); 232} 233 234void ZeroSuggestProvider::ClearAllResults() { 235 // We do not call Clear() on |results_| to retain |verbatim_relevance| 236 // value in the |results_| object. |verbatim_relevance| is used at the 237 // beginning of the next StartZeroSuggest() call to determine the current url 238 // match relevance. 239 results_.suggest_results.clear(); 240 results_.navigation_results.clear(); 241 current_query_.clear(); 242} 243 244int ZeroSuggestProvider::GetDefaultResultRelevance() const { 245 return kDefaultZeroSuggestRelevance; 246} 247 248void ZeroSuggestProvider::RecordDeletionResult(bool success) { 249 if (success) { 250 content::RecordAction( 251 base::UserMetricsAction("Omnibox.ZeroSuggestDelete.Success")); 252 } else { 253 content::RecordAction( 254 base::UserMetricsAction("Omnibox.ZeroSuggestDelete.Failure")); 255 } 256} 257 258void ZeroSuggestProvider::LogFetchComplete(bool success, bool is_keyword) { 259 LogOmniboxZeroSuggestRequest(ZERO_SUGGEST_REPLY_RECEIVED); 260} 261 262bool ZeroSuggestProvider::IsKeywordFetcher( 263 const net::URLFetcher* fetcher) const { 264 // ZeroSuggestProvider does not have a keyword provider. 265 DCHECK_EQ(fetcher, fetcher_.get()); 266 return false; 267} 268 269void ZeroSuggestProvider::UpdateMatches() { 270 done_ = true; 271 ConvertResultsToAutocompleteMatches(); 272} 273 274void ZeroSuggestProvider::AddSuggestResultsToMap( 275 const SuggestResults& results, 276 MatchMap* map) { 277 for (size_t i = 0; i < results.size(); ++i) 278 AddMatchToMap(results[i], std::string(), i, false, map); 279} 280 281AutocompleteMatch ZeroSuggestProvider::NavigationToMatch( 282 const NavigationResult& navigation) { 283 AutocompleteMatch match(this, navigation.relevance(), false, 284 navigation.type()); 285 match.destination_url = navigation.url(); 286 287 // Zero suggest results should always omit protocols and never appear bold. 288 const std::string languages( 289 profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)); 290 match.contents = net::FormatUrl(navigation.url(), languages, 291 net::kFormatUrlOmitAll, net::UnescapeRule::SPACES, NULL, NULL, NULL); 292 match.fill_into_edit += 293 AutocompleteInput::FormattedStringWithEquivalentMeaning(navigation.url(), 294 match.contents); 295 296 AutocompleteMatch::ClassifyLocationInString(base::string16::npos, 0, 297 match.contents.length(), ACMatchClassification::URL, 298 &match.contents_class); 299 300 match.description = 301 AutocompleteMatch::SanitizeString(navigation.description()); 302 AutocompleteMatch::ClassifyLocationInString(base::string16::npos, 0, 303 match.description.length(), ACMatchClassification::NONE, 304 &match.description_class); 305 return match; 306} 307 308void ZeroSuggestProvider::Run(const GURL& suggest_url) { 309 suggest_results_pending_ = 0; 310 const int kFetcherID = 1; 311 fetcher_.reset( 312 net::URLFetcher::Create(kFetcherID, 313 suggest_url, 314 net::URLFetcher::GET, this)); 315 fetcher_->SetRequestContext(profile_->GetRequestContext()); 316 fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES); 317 // Add Chrome experiment state to the request headers. 318 net::HttpRequestHeaders headers; 319 chrome_variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders( 320 fetcher_->GetOriginalURL(), profile_->IsOffTheRecord(), false, &headers); 321 fetcher_->SetExtraRequestHeaders(headers.ToString()); 322 fetcher_->Start(); 323 324 if (OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial()) { 325 most_visited_urls_.clear(); 326 history::TopSites* ts = profile_->GetTopSites(); 327 if (ts) { 328 ts->GetMostVisitedURLs( 329 base::Bind(&ZeroSuggestProvider::OnMostVisitedUrlsAvailable, 330 weak_ptr_factory_.GetWeakPtr()), false); 331 } 332 } 333 suggest_results_pending_ = 1; 334 LogOmniboxZeroSuggestRequest(ZERO_SUGGEST_REQUEST_SENT); 335} 336 337void ZeroSuggestProvider::OnMostVisitedUrlsAvailable( 338 const history::MostVisitedURLList& urls) { 339 most_visited_urls_ = urls; 340} 341 342void ZeroSuggestProvider::ConvertResultsToAutocompleteMatches() { 343 matches_.clear(); 344 345 const TemplateURL* default_provider = 346 template_url_service_->GetDefaultSearchProvider(); 347 // Fail if we can't set the clickthrough URL for query suggestions. 348 if (default_provider == NULL || !default_provider->SupportsReplacement( 349 template_url_service_->search_terms_data())) 350 return; 351 352 MatchMap map; 353 AddSuggestResultsToMap(results_.suggest_results, &map); 354 355 const int num_query_results = map.size(); 356 const int num_nav_results = results_.navigation_results.size(); 357 const int num_results = num_query_results + num_nav_results; 358 UMA_HISTOGRAM_COUNTS("ZeroSuggest.QueryResults", num_query_results); 359 UMA_HISTOGRAM_COUNTS("ZeroSuggest.URLResults", num_nav_results); 360 UMA_HISTOGRAM_COUNTS("ZeroSuggest.AllResults", num_results); 361 362 // Show Most Visited results after ZeroSuggest response is received. 363 if (OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial()) { 364 if (!current_url_match_.destination_url.is_valid()) 365 return; 366 matches_.push_back(current_url_match_); 367 int relevance = 600; 368 if (num_results > 0) { 369 UMA_HISTOGRAM_COUNTS( 370 "Omnibox.ZeroSuggest.MostVisitedResultsCounterfactual", 371 most_visited_urls_.size()); 372 } 373 const base::string16 current_query_string16( 374 base::ASCIIToUTF16(current_query_)); 375 const std::string languages( 376 profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)); 377 for (size_t i = 0; i < most_visited_urls_.size(); i++) { 378 const history::MostVisitedURL& url = most_visited_urls_[i]; 379 NavigationResult nav(*this, url.url, AutocompleteMatchType::NAVSUGGEST, 380 url.title, std::string(), false, relevance, true, 381 current_query_string16, languages); 382 matches_.push_back(NavigationToMatch(nav)); 383 --relevance; 384 } 385 return; 386 } 387 388 if (num_results == 0) 389 return; 390 391 // TODO(jered): Rip this out once the first match is decoupled from the 392 // current typing in the omnibox. 393 matches_.push_back(current_url_match_); 394 395 for (MatchMap::const_iterator it(map.begin()); it != map.end(); ++it) 396 matches_.push_back(it->second); 397 398 const NavigationResults& nav_results(results_.navigation_results); 399 for (NavigationResults::const_iterator it(nav_results.begin()); 400 it != nav_results.end(); ++it) 401 matches_.push_back(NavigationToMatch(*it)); 402} 403 404AutocompleteMatch ZeroSuggestProvider::MatchForCurrentURL() { 405 AutocompleteMatch match; 406 AutocompleteClassifierFactory::GetForProfile(profile_)->Classify( 407 permanent_text_, false, true, current_page_classification_, &match, NULL); 408 match.is_history_what_you_typed_match = false; 409 match.allowed_to_be_default_match = true; 410 411 // The placeholder suggestion for the current URL has high relevance so 412 // that it is in the first suggestion slot and inline autocompleted. It 413 // gets dropped as soon as the user types something. 414 match.relevance = GetVerbatimRelevance(); 415 416 return match; 417} 418 419int ZeroSuggestProvider::GetVerbatimRelevance() const { 420 return results_.verbatim_relevance >= 0 ? 421 results_.verbatim_relevance : kDefaultVerbatimZeroSuggestRelevance; 422} 423 424bool ZeroSuggestProvider::CanShowZeroSuggestWithoutSendingURL( 425 const GURL& suggest_url, 426 const GURL& current_page_url) const { 427 if (!ZeroSuggestEnabled(suggest_url, 428 template_url_service_->GetDefaultSearchProvider(), 429 current_page_classification_, profile_)) 430 return false; 431 432 // If we cannot send URLs, then only the MostVisited and Personalized 433 // variations can be shown. 434 if (!OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial() && 435 !OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial()) 436 return false; 437 438 // Only show zero suggest for HTTP[S] pages. 439 // TODO(mariakhomenko): We may be able to expand this set to include pages 440 // with other schemes (e.g. chrome://). That may require improvements to 441 // the formatting of the verbatim result returned by MatchForCurrentURL(). 442 if (!current_page_url.is_valid() || 443 ((current_page_url.scheme() != url::kHttpScheme) && 444 (current_page_url.scheme() != url::kHttpsScheme))) 445 return false; 446 447 return true; 448} 449 450void ZeroSuggestProvider::MaybeUseCachedSuggestions() { 451 if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial()) 452 return; 453 454 std::string json_data = profile_->GetPrefs()->GetString( 455 prefs::kZeroSuggestCachedResults); 456 if (!json_data.empty()) { 457 scoped_ptr<base::Value> data(DeserializeJsonData(json_data)); 458 if (data && ParseSuggestResults(*data.get(), false, &results_)) { 459 ConvertResultsToAutocompleteMatches(); 460 results_from_cache_ = !matches_.empty(); 461 } 462 } 463} 464