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 { 32 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. 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 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 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 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 160 HistoryContentsProvider::~HistoryContentsProvider() { 161 } 162 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 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 203 static bool MatchInTitle(const history::URLResult& result) { 204 return !result.title_match_positions().empty(); 205 } 206 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 230 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 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 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 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