| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/autocomplete/history_contents_provider.h" |
| |
| #include "base/callback.h" |
| #include "base/metrics/histogram.h" |
| #include "base/string_util.h" |
| #include "base/utf_string_conversions.h" |
| #include "chrome/browser/autocomplete/autocomplete_match.h" |
| #include "chrome/browser/bookmarks/bookmark_model.h" |
| #include "chrome/browser/bookmarks/bookmark_utils.h" |
| #include "chrome/browser/history/query_parser.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/url_constants.h" |
| #include "googleurl/src/url_util.h" |
| #include "net/base/net_util.h" |
| |
| using base::TimeTicks; |
| |
| namespace { |
| |
| // Number of days to search for full text results. The longer this is, the more |
| // time it will take. |
| const int kDaysToSearch = 30; |
| |
| // When processing the results from the history query, this structure points to |
| // a single result. It allows the results to be sorted and processed without |
| // modifying the larger and slower results structure. |
| struct MatchReference { |
| MatchReference(const history::URLResult* result, int relevance) |
| : result(result), |
| relevance(relevance) { |
| } |
| |
| const history::URLResult* result; |
| int relevance; // Score of relevance computed by CalculateRelevance. |
| }; |
| |
| // This is a > operator for MatchReference. |
| bool CompareMatchRelevance(const MatchReference& a, const MatchReference& b) { |
| if (a.relevance != b.relevance) |
| return a.relevance > b.relevance; |
| |
| // Want results in reverse-chronological order all else being equal. |
| return a.result->last_visit() > b.result->last_visit(); |
| } |
| |
| } // namespace |
| |
| using history::HistoryDatabase; |
| |
| HistoryContentsProvider::HistoryContentsProvider(ACProviderListener* listener, |
| Profile* profile) |
| : HistoryProvider(listener, profile, "HistoryContents"), |
| star_title_count_(0), |
| star_contents_count_(0), |
| title_count_(0), |
| contents_count_(0), |
| input_type_(AutocompleteInput::INVALID), |
| trim_http_(false), |
| have_results_(false) { |
| } |
| |
| void HistoryContentsProvider::Start(const AutocompleteInput& input, |
| bool minimal_changes) { |
| matches_.clear(); |
| |
| if (input.text().empty() || (input.type() == AutocompleteInput::INVALID) || |
| !profile_ || |
| // The history service or bookmark bar model must exist. |
| !(profile_->GetHistoryService(Profile::EXPLICIT_ACCESS) || |
| profile_->GetBookmarkModel())) { |
| Stop(); |
| return; |
| } |
| |
| // TODO(pkasting): http://b/888148 We disallow URL input and "URL-like" input |
| // (REQUESTED_URL or UNKNOWN with dots) because we get poor results for it, |
| // but we could get better results if we did better tokenizing instead. |
| if ((input.type() == AutocompleteInput::URL) || |
| (((input.type() == AutocompleteInput::REQUESTED_URL) || |
| (input.type() == AutocompleteInput::UNKNOWN)) && |
| (input.text().find('.') != string16::npos))) { |
| Stop(); |
| return; |
| } |
| |
| if (input.matches_requested() == AutocompleteInput::BEST_MATCH) { |
| // None of our results are applicable for best match. |
| Stop(); |
| return; |
| } |
| |
| // Change input type so matches will be marked up properly. |
| input_type_ = input.type(); |
| trim_http_ = !HasHTTPScheme(input.text()); |
| |
| // Decide what to do about any previous query/results. |
| if (!minimal_changes) { |
| // Any in-progress request is irrelevant, cancel it. |
| Stop(); |
| } else if (have_results_) { |
| // We finished the previous query and still have its results. Mark them up |
| // again for the new input. |
| ConvertResults(); |
| return; |
| } else if (!done_) { |
| // We're still running the previous query on the HistoryService. If we're |
| // allowed to keep running it, do so, and when it finishes, its results will |
| // get marked up for this new input. In synchronous_only mode, cancel the |
| // history query. |
| if (input.matches_requested() != AutocompleteInput::ALL_MATCHES) { |
| done_ = true; |
| request_consumer_.CancelAllRequests(); |
| } |
| ConvertResults(); |
| return; |
| } |
| |
| if (!results_.empty()) { |
| // Clear the results. We swap in an empty one as the easy way to clear it. |
| history::QueryResults empty_results; |
| results_.Swap(&empty_results); |
| } |
| |
| // Querying bookmarks is synchronous, so we always do it. |
| QueryBookmarks(input); |
| |
| // Convert the bookmark results. |
| ConvertResults(); |
| |
| if (input.matches_requested() == AutocompleteInput::ALL_MATCHES) { |
| HistoryService* history = |
| profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); |
| if (history) { |
| done_ = false; |
| |
| history::QueryOptions options; |
| options.SetRecentDayRange(kDaysToSearch); |
| options.max_count = kMaxMatches; |
| history->QueryHistory(input.text(), options, |
| &request_consumer_, |
| NewCallback(this, &HistoryContentsProvider::QueryComplete)); |
| } |
| } |
| } |
| |
| void HistoryContentsProvider::Stop() { |
| done_ = true; |
| request_consumer_.CancelAllRequests(); |
| |
| // Clear the results. We swap in an empty one as the easy way to clear it. |
| history::QueryResults empty_results; |
| results_.Swap(&empty_results); |
| have_results_ = false; |
| } |
| |
| HistoryContentsProvider::~HistoryContentsProvider() { |
| } |
| |
| void HistoryContentsProvider::QueryComplete(HistoryService::Handle handle, |
| history::QueryResults* results) { |
| results_.AppendResultsBySwapping(results, true); |
| have_results_ = true; |
| ConvertResults(); |
| |
| done_ = true; |
| if (listener_) |
| listener_->OnProviderUpdate(!matches_.empty()); |
| } |
| |
| void HistoryContentsProvider::ConvertResults() { |
| // Reset the relevance counters so that result relevance won't vary on |
| // subsequent passes of ConvertResults. |
| star_title_count_ = star_contents_count_ = title_count_ = contents_count_ = 0; |
| |
| // Make the result references and score the results. |
| std::vector<MatchReference> result_refs; |
| result_refs.reserve(results_.size()); |
| |
| // Results are sorted in decreasing order so we run the loop backwards so that |
| // the relevance increment favors the higher ranked results. |
| for (std::vector<history::URLResult*>::const_reverse_iterator i = |
| results_.rbegin(); i != results_.rend(); ++i) { |
| history::URLResult* result = *i; |
| MatchReference ref(result, CalculateRelevance(*result)); |
| result_refs.push_back(ref); |
| } |
| |
| // Get the top matches and add them. |
| size_t max_for_provider = std::min(kMaxMatches, result_refs.size()); |
| std::partial_sort(result_refs.begin(), result_refs.begin() + max_for_provider, |
| result_refs.end(), &CompareMatchRelevance); |
| matches_.clear(); |
| for (size_t i = 0; i < max_for_provider; i++) { |
| matches_.push_back(ResultToMatch(*result_refs[i].result, |
| result_refs[i].relevance)); |
| } |
| } |
| |
| static bool MatchInTitle(const history::URLResult& result) { |
| return !result.title_match_positions().empty(); |
| } |
| |
| AutocompleteMatch HistoryContentsProvider::ResultToMatch( |
| const history::URLResult& result, |
| int score) { |
| // TODO(sky): if matched title highlight matching words in title. |
| // Also show star in popup. |
| AutocompleteMatch match(this, score, true, MatchInTitle(result) ? |
| AutocompleteMatch::HISTORY_TITLE : AutocompleteMatch::HISTORY_BODY); |
| match.contents = StringForURLDisplay(result.url(), true, trim_http_); |
| match.fill_into_edit = |
| AutocompleteInput::FormattedStringWithEquivalentMeaning(result.url(), |
| match.contents); |
| match.destination_url = result.url(); |
| match.contents_class.push_back( |
| ACMatchClassification(0, ACMatchClassification::URL)); |
| match.description = result.title(); |
| match.starred = |
| (profile_->GetBookmarkModel() && |
| profile_->GetBookmarkModel()->IsBookmarked(result.url())); |
| |
| ClassifyDescription(result, &match); |
| return match; |
| } |
| |
| void HistoryContentsProvider::ClassifyDescription( |
| const history::URLResult& result, |
| AutocompleteMatch* match) const { |
| const Snippet::MatchPositions& title_matches = result.title_match_positions(); |
| |
| size_t offset = 0; |
| if (!title_matches.empty()) { |
| // Classify matches in the title. |
| for (Snippet::MatchPositions::const_iterator i = title_matches.begin(); |
| i != title_matches.end(); ++i) { |
| if (i->first != offset) { |
| match->description_class.push_back( |
| ACMatchClassification(offset, ACMatchClassification::NONE)); |
| } |
| match->description_class.push_back( |
| ACMatchClassification(i->first, ACMatchClassification::MATCH)); |
| offset = i->second; |
| } |
| } |
| if (offset != result.title().size()) { |
| match->description_class.push_back( |
| ACMatchClassification(offset, ACMatchClassification::NONE)); |
| } |
| } |
| |
| int HistoryContentsProvider::CalculateRelevance( |
| const history::URLResult& result) { |
| const bool in_title = MatchInTitle(result); |
| if (!profile_->GetBookmarkModel() || |
| !profile_->GetBookmarkModel()->IsBookmarked(result.url())) |
| return in_title ? (700 + title_count_++) : (500 + contents_count_++); |
| return in_title ? |
| (1000 + star_title_count_++) : (550 + star_contents_count_++); |
| } |
| |
| void HistoryContentsProvider::QueryBookmarks(const AutocompleteInput& input) { |
| BookmarkModel* bookmark_model = profile_->GetBookmarkModel(); |
| if (!bookmark_model) |
| return; |
| |
| DCHECK(results_.empty()); |
| |
| TimeTicks start_time = TimeTicks::Now(); |
| std::vector<bookmark_utils::TitleMatch> matches; |
| bookmark_model->GetBookmarksWithTitlesMatching(input.text(), |
| kMaxMatches, &matches); |
| for (size_t i = 0; i < matches.size(); ++i) |
| AddBookmarkTitleMatchToResults(matches[i]); |
| UMA_HISTOGRAM_TIMES("Omnibox.QueryBookmarksTime", |
| TimeTicks::Now() - start_time); |
| } |
| |
| void HistoryContentsProvider::AddBookmarkTitleMatchToResults( |
| const bookmark_utils::TitleMatch& match) { |
| history::URLResult url_result(match.node->GetURL(), match.match_positions); |
| url_result.set_title(match.node->GetTitle()); |
| results_.AppendURLBySwapping(&url_result); |
| } |