zero_suggest_provider.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/zero_suggest_provider.h" 6 7#include "base/callback.h" 8#include "base/json/json_string_value_serializer.h" 9#include "base/prefs/pref_service.h" 10#include "base/string16.h" 11#include "base/string_util.h" 12#include "base/time.h" 13#include "base/utf_string_conversions.h" 14#include "chrome/browser/autocomplete/autocomplete_input.h" 15#include "chrome/browser/autocomplete/autocomplete_match.h" 16#include "chrome/browser/autocomplete/autocomplete_provider_listener.h" 17#include "chrome/browser/profiles/profile.h" 18#include "chrome/browser/search_engines/template_url_service.h" 19#include "chrome/browser/search_engines/template_url_service_factory.h" 20#include "chrome/common/pref_names.h" 21#include "chrome/common/url_constants.h" 22#include "googleurl/src/gurl.h" 23#include "net/base/load_flags.h" 24#include "net/http/http_response_headers.h" 25#include "net/url_request/url_fetcher.h" 26#include "net/url_request/url_request_status.h" 27 28namespace { 29// The relevance of the top match from this provider. 30const int kMaxZeroSuggestRelevance = 100; 31} // namespace 32 33// static 34ZeroSuggestProvider* ZeroSuggestProvider::Create( 35 AutocompleteProviderListener* listener, 36 Profile* profile) { 37 if (profile && !profile->IsOffTheRecord() && profile->GetPrefs()) { 38 std::string url_prefix = profile->GetPrefs()->GetString( 39 prefs::kInstantUIZeroSuggestUrlPrefix); 40 if (!url_prefix.empty()) 41 return new ZeroSuggestProvider(listener, profile, url_prefix); 42 } 43 return NULL; 44} 45 46ZeroSuggestProvider::ZeroSuggestProvider( 47 AutocompleteProviderListener* listener, 48 Profile* profile, 49 const std::string& url_prefix) 50 : AutocompleteProvider(listener, profile, 51 AutocompleteProvider::TYPE_ZERO_SUGGEST), 52 url_prefix_(url_prefix), 53 template_url_service_(TemplateURLServiceFactory::GetForProfile(profile)) { 54} 55 56void ZeroSuggestProvider::Start(const AutocompleteInput& input, 57 bool /*minimal_changes*/) { 58 UpdateMatches(input.text()); 59} 60 61void ZeroSuggestProvider::StartZeroSuggest(const GURL& url, 62 const string16& user_text) { 63 DCHECK(url.is_valid()); 64 // Do not query non-http URLs. There will be no useful suggestions for https 65 // or chrome URLs. 66 if (url.scheme() != chrome::kHttpScheme) 67 return; 68 matches_.clear(); 69 done_ = false; 70 user_text_ = user_text; 71 current_query_ = url.spec(); 72 // TODO(jered): Consider adding locally-sourced zero-suggestions here too. 73 // These may be useful on the NTP or more relevant to the user than server 74 // suggestions, if based on local browsing history. 75 Run(); 76} 77 78void ZeroSuggestProvider::Stop(bool clear_cached_results) { 79 fetcher_.reset(); 80 done_ = true; 81 if (clear_cached_results) { 82 results_.clear(); 83 current_query_.clear(); 84 } 85} 86 87void ZeroSuggestProvider::OnURLFetchComplete(const net::URLFetcher* source) { 88 std::string json_data; 89 source->GetResponseAsString(&json_data); 90 const bool request_succeeded = 91 source->GetStatus().is_success() && source->GetResponseCode() == 200; 92 93 bool results_updated = false; 94 if (request_succeeded) { 95 JSONStringValueSerializer deserializer(json_data); 96 deserializer.set_allow_trailing_comma(true); 97 scoped_ptr<Value> data(deserializer.Deserialize(NULL, NULL)); 98 results_updated = 99 data.get() && ParseSuggestResults(data.get()) && !results_.empty(); 100 } 101 done_ = true; 102 103 if (results_updated) { 104 ConvertResultsToAutocompleteMatches(); 105 listener_->OnProviderUpdate(true); 106 } 107} 108 109ZeroSuggestProvider::~ZeroSuggestProvider() { 110} 111 112void ZeroSuggestProvider::Run() { 113 const int kFetcherID = 1; 114 fetcher_.reset( 115 net::URLFetcher::Create(kFetcherID, 116 GURL(url_prefix_ + current_query_), 117 net::URLFetcher::GET, this)); 118 fetcher_->SetRequestContext(profile_->GetRequestContext()); 119 fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES); 120 fetcher_->Start(); 121} 122 123void ZeroSuggestProvider::UpdateMatches(const string16& user_text) { 124 user_text_ = user_text; 125 const size_t prev_num_matches = matches_.size(); 126 ConvertResultsToAutocompleteMatches(); 127 if (matches_.size() != prev_num_matches) 128 listener_->OnProviderUpdate(true); 129} 130 131bool ZeroSuggestProvider::ParseSuggestResults(Value* root_val) { 132 std::string query; 133 ListValue* root_list = NULL; 134 ListValue* results = NULL; 135 if (!root_val->GetAsList(&root_list) || !root_list->GetString(0, &query) || 136 (query != current_query_) || !root_list->GetList(1, &results)) 137 return false; 138 139 results_.clear(); 140 ListValue* one_result = NULL; 141 for (size_t index = 0; results->GetList(index, &one_result); ++index) { 142 string16 result; 143 one_result->GetString(0, &result); 144 if (result.empty()) 145 continue; 146 results_.push_back(result); 147 } 148 149 return true; 150} 151 152void ZeroSuggestProvider::ConvertResultsToAutocompleteMatches() { 153 const TemplateURL* search_provider = 154 template_url_service_->GetDefaultSearchProvider(); 155 // Fail if we can't set the clickthrough URL for query suggestions. 156 if (search_provider == NULL || !search_provider->SupportsReplacement()) 157 return; 158 matches_.clear(); 159 // Do not add anything if there are no results for this URL. 160 if (results_.empty()) 161 return; 162 AddMatchForCurrentURL(); 163 for (size_t i = 0; i < results_.size(); ++i) 164 AddMatchForResult(search_provider, i, results_[i]); 165} 166 167// TODO(jered): Rip this out once the first match is decoupled from the current 168// typing in the omnibox. 169void ZeroSuggestProvider::AddMatchForCurrentURL() { 170 // If the user has typed something besides the current url, they probably 171 // don't intend to refresh it. 172 const string16 current_query_text = ASCIIToUTF16(current_query_); 173 const bool user_text_is_url = user_text_ == current_query_text; 174 if (user_text_.empty() || user_text_is_url) { 175 // The placeholder suggestion for the current URL has high relevance so 176 // that it is in the first suggestion slot and inline autocompleted. It 177 // gets dropped as soon as the user types something. 178 AutocompleteMatch match(this, kMaxZeroSuggestRelevance, false, 179 AutocompleteMatch::NAVSUGGEST); 180 match.destination_url = GURL(current_query_); 181 match.contents = current_query_text; 182 if (!user_text_is_url) { 183 match.fill_into_edit = current_query_text; 184 match.inline_autocomplete_offset = 0; 185 } 186 AutocompleteMatch::ClassifyLocationInString(0, current_query_.size(), 187 match.contents.length(), ACMatchClassification::URL, 188 &match.contents_class); 189 matches_.push_back(match); 190 } 191} 192 193void ZeroSuggestProvider::AddMatchForResult( 194 const TemplateURL* search_provider, 195 size_t result_index, 196 const string16& result) { 197 // TODO(jered): Rip out user_text_is_url logic when AddMatchForCurrentURL 198 // goes away. 199 const string16 current_query_text = ASCIIToUTF16(current_query_); 200 const bool user_text_is_url = user_text_ == current_query_text; 201 const bool kCaseInsensitve = false; 202 if (!user_text_.empty() && !user_text_is_url && 203 !StartsWith(result, user_text_, kCaseInsensitve)) 204 // This suggestion isn't relevant for the current prefix. 205 return; 206 // This bogus relevance puts suggestions below the placeholder from 207 // AddMatchForCurrentURL(), but very low so that after the user starts typing 208 // zero-suggestions go away when there are other suggestions. 209 // TODO(jered): Use real scores from the suggestion server. 210 const int suggestion_relevance = kMaxZeroSuggestRelevance - matches_.size(); 211 AutocompleteMatch match(this, suggestion_relevance, false, 212 AutocompleteMatch::SEARCH_SUGGEST); 213 match.contents = result; 214 match.fill_into_edit = result; 215 if (!user_text_is_url && user_text_ != result) 216 match.inline_autocomplete_offset = user_text_.length(); 217 218 // Build a URL for this query using the default search provider. 219 const TemplateURLRef& search_url = search_provider->url_ref(); 220 DCHECK(search_url.SupportsReplacement()); 221 match.search_terms_args.reset( 222 new TemplateURLRef::SearchTermsArgs(result)); 223 match.search_terms_args->original_query = string16(); 224 match.search_terms_args->accepted_suggestion = result_index; 225 match.destination_url = 226 GURL(search_url.ReplaceSearchTerms(*match.search_terms_args.get())); 227 228 if (user_text_.empty() || user_text_is_url || user_text_ == result) { 229 match.contents_class.push_back( 230 ACMatchClassification(0, ACMatchClassification::NONE)); 231 } else { 232 // Style to look like normal search suggestions. 233 match.contents_class.push_back( 234 ACMatchClassification(0, ACMatchClassification::DIM)); 235 match.contents_class.push_back( 236 ACMatchClassification(user_text_.length(), ACMatchClassification::NONE)); 237 } 238 match.transition = content::PAGE_TRANSITION_GENERATED; 239 240 matches_.push_back(match); 241} 242