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