1 // Copyright (c) 2011 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/history_contents_provider.h"
6
7 #include "base/callback.h"
8 #include "base/metrics/histogram.h"
9 #include "base/string_util.h"
10 #include "base/utf_string_conversions.h"
11 #include "chrome/browser/autocomplete/autocomplete_match.h"
12 #include "chrome/browser/bookmarks/bookmark_model.h"
13 #include "chrome/browser/bookmarks/bookmark_utils.h"
14 #include "chrome/browser/history/query_parser.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/common/url_constants.h"
17 #include "googleurl/src/url_util.h"
18 #include "net/base/net_util.h"
19
20 using base::TimeTicks;
21
22 namespace {
23
24 // Number of days to search for full text results. The longer this is, the more
25 // time it will take.
26 const int kDaysToSearch = 30;
27
28 // When processing the results from the history query, this structure points to
29 // a single result. It allows the results to be sorted and processed without
30 // modifying the larger and slower results structure.
31 struct MatchReference {
MatchReference__anon0e05c9280111::MatchReference32 MatchReference(const history::URLResult* result, int relevance)
33 : result(result),
34 relevance(relevance) {
35 }
36
37 const history::URLResult* result;
38 int relevance; // Score of relevance computed by CalculateRelevance.
39 };
40
41 // This is a > operator for MatchReference.
CompareMatchRelevance(const MatchReference & a,const MatchReference & b)42 bool CompareMatchRelevance(const MatchReference& a, const MatchReference& b) {
43 if (a.relevance != b.relevance)
44 return a.relevance > b.relevance;
45
46 // Want results in reverse-chronological order all else being equal.
47 return a.result->last_visit() > b.result->last_visit();
48 }
49
50 } // namespace
51
52 using history::HistoryDatabase;
53
HistoryContentsProvider(ACProviderListener * listener,Profile * profile)54 HistoryContentsProvider::HistoryContentsProvider(ACProviderListener* listener,
55 Profile* profile)
56 : HistoryProvider(listener, profile, "HistoryContents"),
57 star_title_count_(0),
58 star_contents_count_(0),
59 title_count_(0),
60 contents_count_(0),
61 input_type_(AutocompleteInput::INVALID),
62 trim_http_(false),
63 have_results_(false) {
64 }
65
Start(const AutocompleteInput & input,bool minimal_changes)66 void HistoryContentsProvider::Start(const AutocompleteInput& input,
67 bool minimal_changes) {
68 matches_.clear();
69
70 if (input.text().empty() || (input.type() == AutocompleteInput::INVALID) ||
71 !profile_ ||
72 // The history service or bookmark bar model must exist.
73 !(profile_->GetHistoryService(Profile::EXPLICIT_ACCESS) ||
74 profile_->GetBookmarkModel())) {
75 Stop();
76 return;
77 }
78
79 // TODO(pkasting): http://b/888148 We disallow URL input and "URL-like" input
80 // (REQUESTED_URL or UNKNOWN with dots) because we get poor results for it,
81 // but we could get better results if we did better tokenizing instead.
82 if ((input.type() == AutocompleteInput::URL) ||
83 (((input.type() == AutocompleteInput::REQUESTED_URL) ||
84 (input.type() == AutocompleteInput::UNKNOWN)) &&
85 (input.text().find('.') != string16::npos))) {
86 Stop();
87 return;
88 }
89
90 if (input.matches_requested() == AutocompleteInput::BEST_MATCH) {
91 // None of our results are applicable for best match.
92 Stop();
93 return;
94 }
95
96 // Change input type so matches will be marked up properly.
97 input_type_ = input.type();
98 trim_http_ = !HasHTTPScheme(input.text());
99
100 // Decide what to do about any previous query/results.
101 if (!minimal_changes) {
102 // Any in-progress request is irrelevant, cancel it.
103 Stop();
104 } else if (have_results_) {
105 // We finished the previous query and still have its results. Mark them up
106 // again for the new input.
107 ConvertResults();
108 return;
109 } else if (!done_) {
110 // We're still running the previous query on the HistoryService. If we're
111 // allowed to keep running it, do so, and when it finishes, its results will
112 // get marked up for this new input. In synchronous_only mode, cancel the
113 // history query.
114 if (input.matches_requested() != AutocompleteInput::ALL_MATCHES) {
115 done_ = true;
116 request_consumer_.CancelAllRequests();
117 }
118 ConvertResults();
119 return;
120 }
121
122 if (!results_.empty()) {
123 // Clear the results. We swap in an empty one as the easy way to clear it.
124 history::QueryResults empty_results;
125 results_.Swap(&empty_results);
126 }
127
128 // Querying bookmarks is synchronous, so we always do it.
129 QueryBookmarks(input);
130
131 // Convert the bookmark results.
132 ConvertResults();
133
134 if (input.matches_requested() == AutocompleteInput::ALL_MATCHES) {
135 HistoryService* history =
136 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
137 if (history) {
138 done_ = false;
139
140 history::QueryOptions options;
141 options.SetRecentDayRange(kDaysToSearch);
142 options.max_count = kMaxMatches;
143 history->QueryHistory(input.text(), options,
144 &request_consumer_,
145 NewCallback(this, &HistoryContentsProvider::QueryComplete));
146 }
147 }
148 }
149
Stop()150 void HistoryContentsProvider::Stop() {
151 done_ = true;
152 request_consumer_.CancelAllRequests();
153
154 // Clear the results. We swap in an empty one as the easy way to clear it.
155 history::QueryResults empty_results;
156 results_.Swap(&empty_results);
157 have_results_ = false;
158 }
159
~HistoryContentsProvider()160 HistoryContentsProvider::~HistoryContentsProvider() {
161 }
162
QueryComplete(HistoryService::Handle handle,history::QueryResults * results)163 void HistoryContentsProvider::QueryComplete(HistoryService::Handle handle,
164 history::QueryResults* results) {
165 results_.AppendResultsBySwapping(results, true);
166 have_results_ = true;
167 ConvertResults();
168
169 done_ = true;
170 if (listener_)
171 listener_->OnProviderUpdate(!matches_.empty());
172 }
173
ConvertResults()174 void HistoryContentsProvider::ConvertResults() {
175 // Reset the relevance counters so that result relevance won't vary on
176 // subsequent passes of ConvertResults.
177 star_title_count_ = star_contents_count_ = title_count_ = contents_count_ = 0;
178
179 // Make the result references and score the results.
180 std::vector<MatchReference> result_refs;
181 result_refs.reserve(results_.size());
182
183 // Results are sorted in decreasing order so we run the loop backwards so that
184 // the relevance increment favors the higher ranked results.
185 for (std::vector<history::URLResult*>::const_reverse_iterator i =
186 results_.rbegin(); i != results_.rend(); ++i) {
187 history::URLResult* result = *i;
188 MatchReference ref(result, CalculateRelevance(*result));
189 result_refs.push_back(ref);
190 }
191
192 // Get the top matches and add them.
193 size_t max_for_provider = std::min(kMaxMatches, result_refs.size());
194 std::partial_sort(result_refs.begin(), result_refs.begin() + max_for_provider,
195 result_refs.end(), &CompareMatchRelevance);
196 matches_.clear();
197 for (size_t i = 0; i < max_for_provider; i++) {
198 matches_.push_back(ResultToMatch(*result_refs[i].result,
199 result_refs[i].relevance));
200 }
201 }
202
MatchInTitle(const history::URLResult & result)203 static bool MatchInTitle(const history::URLResult& result) {
204 return !result.title_match_positions().empty();
205 }
206
ResultToMatch(const history::URLResult & result,int score)207 AutocompleteMatch HistoryContentsProvider::ResultToMatch(
208 const history::URLResult& result,
209 int score) {
210 // TODO(sky): if matched title highlight matching words in title.
211 // Also show star in popup.
212 AutocompleteMatch match(this, score, true, MatchInTitle(result) ?
213 AutocompleteMatch::HISTORY_TITLE : AutocompleteMatch::HISTORY_BODY);
214 match.contents = StringForURLDisplay(result.url(), true, trim_http_);
215 match.fill_into_edit =
216 AutocompleteInput::FormattedStringWithEquivalentMeaning(result.url(),
217 match.contents);
218 match.destination_url = result.url();
219 match.contents_class.push_back(
220 ACMatchClassification(0, ACMatchClassification::URL));
221 match.description = result.title();
222 match.starred =
223 (profile_->GetBookmarkModel() &&
224 profile_->GetBookmarkModel()->IsBookmarked(result.url()));
225
226 ClassifyDescription(result, &match);
227 return match;
228 }
229
ClassifyDescription(const history::URLResult & result,AutocompleteMatch * match) const230 void HistoryContentsProvider::ClassifyDescription(
231 const history::URLResult& result,
232 AutocompleteMatch* match) const {
233 const Snippet::MatchPositions& title_matches = result.title_match_positions();
234
235 size_t offset = 0;
236 if (!title_matches.empty()) {
237 // Classify matches in the title.
238 for (Snippet::MatchPositions::const_iterator i = title_matches.begin();
239 i != title_matches.end(); ++i) {
240 if (i->first != offset) {
241 match->description_class.push_back(
242 ACMatchClassification(offset, ACMatchClassification::NONE));
243 }
244 match->description_class.push_back(
245 ACMatchClassification(i->first, ACMatchClassification::MATCH));
246 offset = i->second;
247 }
248 }
249 if (offset != result.title().size()) {
250 match->description_class.push_back(
251 ACMatchClassification(offset, ACMatchClassification::NONE));
252 }
253 }
254
CalculateRelevance(const history::URLResult & result)255 int HistoryContentsProvider::CalculateRelevance(
256 const history::URLResult& result) {
257 const bool in_title = MatchInTitle(result);
258 if (!profile_->GetBookmarkModel() ||
259 !profile_->GetBookmarkModel()->IsBookmarked(result.url()))
260 return in_title ? (700 + title_count_++) : (500 + contents_count_++);
261 return in_title ?
262 (1000 + star_title_count_++) : (550 + star_contents_count_++);
263 }
264
QueryBookmarks(const AutocompleteInput & input)265 void HistoryContentsProvider::QueryBookmarks(const AutocompleteInput& input) {
266 BookmarkModel* bookmark_model = profile_->GetBookmarkModel();
267 if (!bookmark_model)
268 return;
269
270 DCHECK(results_.empty());
271
272 TimeTicks start_time = TimeTicks::Now();
273 std::vector<bookmark_utils::TitleMatch> matches;
274 bookmark_model->GetBookmarksWithTitlesMatching(input.text(),
275 kMaxMatches, &matches);
276 for (size_t i = 0; i < matches.size(); ++i)
277 AddBookmarkTitleMatchToResults(matches[i]);
278 UMA_HISTOGRAM_TIMES("Omnibox.QueryBookmarksTime",
279 TimeTicks::Now() - start_time);
280 }
281
AddBookmarkTitleMatchToResults(const bookmark_utils::TitleMatch & match)282 void HistoryContentsProvider::AddBookmarkTitleMatchToResults(
283 const bookmark_utils::TitleMatch& match) {
284 history::URLResult url_result(match.node->GetURL(), match.match_positions);
285 url_result.set_title(match.node->GetTitle());
286 results_.AppendURLBySwapping(&url_result);
287 }
288