| // Copyright (c) 2012 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_quick_provider.h" |
| |
| #include <vector> |
| |
| #include "base/basictypes.h" |
| #include "base/command_line.h" |
| #include "base/i18n/break_iterator.h" |
| #include "base/logging.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram.h" |
| #include "base/string_number_conversions.h" |
| #include "base/string_util.h" |
| #include "base/time.h" |
| #include "base/utf_string_conversions.h" |
| #include "chrome/browser/autocomplete/autocomplete_field_trial.h" |
| #include "chrome/browser/history/history.h" |
| #include "chrome/browser/history/in_memory_url_index.h" |
| #include "chrome/browser/history/in_memory_url_index_types.h" |
| #include "chrome/browser/history/scored_history_match.h" |
| #include "chrome/browser/net/url_fixer_upper.h" |
| #include "chrome/browser/prefs/pref_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/url_constants.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/notification_source.h" |
| #include "content/public/browser/notification_types.h" |
| #include "googleurl/src/url_parse.h" |
| #include "googleurl/src/url_util.h" |
| #include "net/base/escape.h" |
| #include "net/base/net_util.h" |
| |
| using history::InMemoryURLIndex; |
| using history::ScoredHistoryMatch; |
| using history::ScoredHistoryMatches; |
| |
| bool HistoryQuickProvider::disabled_ = false; |
| |
| HistoryQuickProvider::HistoryQuickProvider(ACProviderListener* listener, |
| Profile* profile) |
| : HistoryProvider(listener, profile, "HistoryQuickProvider"), |
| languages_(profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)) { |
| enum InliningOption { |
| INLINING_PROHIBITED = 0, |
| INLINING_ALLOWED = 1, |
| INLINING_AUTO_BUT_NOT_IN_FIELD_TRIAL = 2, |
| INLINING_FIELD_TRIAL_DEFAULT_GROUP = 3, |
| INLINING_FIELD_TRIAL_EXPERIMENT_GROUP = 4, |
| NUM_OPTIONS = 5 |
| }; |
| // should always be overwritten |
| InliningOption inlining_option = NUM_OPTIONS; |
| |
| const std::string switch_value = CommandLine::ForCurrentProcess()-> |
| GetSwitchValueASCII(switches::kOmniboxInlineHistoryQuickProvider); |
| if (switch_value == switches::kOmniboxInlineHistoryQuickProviderAllowed) { |
| inlining_option = INLINING_ALLOWED; |
| always_prevent_inline_autocomplete_ = false; |
| } else if (switch_value == |
| switches::kOmniboxInlineHistoryQuickProviderProhibited) { |
| inlining_option = INLINING_PROHIBITED; |
| always_prevent_inline_autocomplete_ = true; |
| } else { |
| // We'll assume any other flag means automatic. |
| // Automatic means eligible for the field trial. |
| |
| // For the field trial stuff to work correctly, we must be running |
| // on the same thread as the thread that created the field trial, |
| // which happens via a call to AutocompleteFieldTrial::Active in |
| // chrome_browser_main.cc on the main thread. Let's check this to |
| // be sure. We check "if we've heard of the UI thread then we'd better |
| // be on it." The first part is necessary so unit tests pass. (Many |
| // unit tests don't set up the threading naming system; hence |
| // CurrentlyOn(UI thread) will fail.) |
| DCHECK(!content::BrowserThread::IsWellKnownThread( |
| content::BrowserThread::UI) || |
| content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| if (AutocompleteFieldTrial::InDisallowInlineHQPFieldTrial()) { |
| if (AutocompleteFieldTrial:: |
| InDisallowInlineHQPFieldTrialExperimentGroup()) { |
| always_prevent_inline_autocomplete_ = true; |
| inlining_option = INLINING_FIELD_TRIAL_EXPERIMENT_GROUP; |
| } else { |
| always_prevent_inline_autocomplete_ = false; |
| inlining_option = INLINING_FIELD_TRIAL_DEFAULT_GROUP; |
| } |
| } else { |
| always_prevent_inline_autocomplete_ = false; |
| inlining_option = INLINING_AUTO_BUT_NOT_IN_FIELD_TRIAL; |
| } |
| } |
| |
| // Add a beacon to the logs that'll allow us to identify later what |
| // inlining state a user is in. Do this by incrementing a bucket in |
| // a histogram, where the bucket represents the user's inlining state. |
| UMA_HISTOGRAM_ENUMERATION( |
| "Omnibox.InlineHistoryQuickProviderFieldTrialBeacon", |
| inlining_option, NUM_OPTIONS); |
| } |
| |
| void HistoryQuickProvider::Start(const AutocompleteInput& input, |
| bool minimal_changes) { |
| matches_.clear(); |
| if (disabled_) |
| return; |
| |
| // Don't bother with INVALID and FORCED_QUERY. Also pass when looking for |
| // BEST_MATCH and there is no inline autocompletion because none of the HQP |
| // matches can score highly enough to qualify. |
| if ((input.type() == AutocompleteInput::INVALID) || |
| (input.type() == AutocompleteInput::FORCED_QUERY) || |
| (input.matches_requested() == AutocompleteInput::BEST_MATCH && |
| input.prevent_inline_autocomplete())) |
| return; |
| |
| autocomplete_input_ = input; |
| |
| // TODO(pkasting): We should just block here until this loads. Any time |
| // someone unloads the history backend, we'll get inconsistent inline |
| // autocomplete behavior here. |
| if (GetIndex()) { |
| base::TimeTicks start_time = base::TimeTicks::Now(); |
| DoAutocomplete(); |
| if (input.text().length() < 6) { |
| base::TimeTicks end_time = base::TimeTicks::Now(); |
| std::string name = "HistoryQuickProvider.QueryIndexTime." + |
| base::IntToString(input.text().length()); |
| base::Histogram* counter = base::Histogram::FactoryGet( |
| name, 1, 1000, 50, base::Histogram::kUmaTargetedHistogramFlag); |
| counter->Add(static_cast<int>((end_time - start_time).InMilliseconds())); |
| } |
| UpdateStarredStateOfMatches(); |
| } |
| } |
| |
| // TODO(mrossetti): Implement this function. (Will happen in next CL.) |
| void HistoryQuickProvider::DeleteMatch(const AutocompleteMatch& match) {} |
| |
| HistoryQuickProvider::~HistoryQuickProvider() {} |
| |
| void HistoryQuickProvider::DoAutocomplete() { |
| // Get the matching URLs from the DB. |
| string16 term_string = autocomplete_input_.text(); |
| ScoredHistoryMatches matches = GetIndex()->HistoryItemsForTerms(term_string); |
| if (matches.empty()) |
| return; |
| |
| // Loop over every result and add it to matches_. In the process, |
| // guarantee that scores are decreasing. |max_match_score| keeps |
| // track of the highest score we can assign to any later results we |
| // see. Also, if we're not allowing inline autocompletions in |
| // general, artificially reduce the starting |max_match_score| |
| // (which therefore applies to all results) to something low enough |
| // that guarantees no result will be offered as an autocomplete |
| // suggestion. In addition, even if we allow inlining of |
| // suggestions in general, we also reduce the starting |
| // |max_match_score| if our top suggestion is not inlineable to make |
| // sure it never gets attempted to be offered as an inline |
| // suggestion. Note that this strategy will allow a funky case: |
| // suppose we're allowing inlining in general. If the second result |
| // is marked as cannot inline yet has a score that would make it |
| // inlineable, it will keep its score. This is a bit odd--a |
| // non-inlineable result with a score high enough to make it |
| // eligible for inlining will keep its high score--but it's okay |
| // because there is a higher scoring result that is required to be |
| // shown before this result. Hence, this result, the second in the |
| // set, will never be inlined. (The autocomplete UI keeps results |
| // in relevance score order.) |
| int max_match_score = (PreventInlineAutocomplete(autocomplete_input_) || |
| !matches.begin()->can_inline) ? |
| (AutocompleteResult::kLowestDefaultScore - 1) : |
| matches.begin()->raw_score; |
| for (ScoredHistoryMatches::const_iterator match_iter = matches.begin(); |
| match_iter != matches.end(); ++match_iter) { |
| const ScoredHistoryMatch& history_match(*match_iter); |
| // Set max_match_score to the score we'll assign this result: |
| max_match_score = std::min(max_match_score, history_match.raw_score); |
| matches_.push_back(QuickMatchToACMatch(history_match, max_match_score)); |
| // Mark this max_match_score as being used: |
| max_match_score--; |
| } |
| } |
| |
| AutocompleteMatch HistoryQuickProvider::QuickMatchToACMatch( |
| const ScoredHistoryMatch& history_match, |
| int score) { |
| const history::URLRow& info = history_match.url_info; |
| AutocompleteMatch match(this, score, !!info.visit_count(), |
| history_match.url_matches.empty() ? |
| AutocompleteMatch::HISTORY_TITLE : AutocompleteMatch::HISTORY_URL); |
| match.typed_count = info.typed_count(); |
| match.destination_url = info.url(); |
| DCHECK(match.destination_url.is_valid()); |
| |
| // Format the URL autocomplete presentation. |
| std::vector<size_t> offsets = |
| OffsetsFromTermMatches(history_match.url_matches); |
| const net::FormatUrlTypes format_types = net::kFormatUrlOmitAll & |
| ~(!history_match.match_in_scheme ? 0 : net::kFormatUrlOmitHTTP); |
| match.fill_into_edit = |
| AutocompleteInput::FormattedStringWithEquivalentMeaning(info.url(), |
| net::FormatUrlWithOffsets(info.url(), languages_, format_types, |
| net::UnescapeRule::SPACES, NULL, NULL, &offsets)); |
| history::TermMatches new_matches = |
| ReplaceOffsetsInTermMatches(history_match.url_matches, offsets); |
| match.contents = net::FormatUrl(info.url(), languages_, format_types, |
| net::UnescapeRule::SPACES, NULL, NULL, NULL); |
| match.contents_class = |
| SpansFromTermMatch(new_matches, match.contents.length(), true); |
| |
| if (!history_match.can_inline) { |
| match.inline_autocomplete_offset = string16::npos; |
| } else { |
| DCHECK(!new_matches.empty()); |
| match.inline_autocomplete_offset = new_matches[0].offset + |
| new_matches[0].length; |
| // The following will happen if the user has typed an URL with a scheme |
| // and the last character typed is a slash because that slash is removed |
| // by the FormatURLWithOffsets call above. |
| if (match.inline_autocomplete_offset > match.fill_into_edit.length()) |
| match.inline_autocomplete_offset = match.fill_into_edit.length(); |
| } |
| |
| // Format the description autocomplete presentation. |
| match.description = info.title(); |
| match.description_class = SpansFromTermMatch( |
| history_match.title_matches, match.description.length(), false); |
| |
| return match; |
| } |
| |
| history::InMemoryURLIndex* HistoryQuickProvider::GetIndex() { |
| if (index_for_testing_.get()) |
| return index_for_testing_.get(); |
| |
| HistoryService* const history_service = |
| profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); |
| if (!history_service) |
| return NULL; |
| |
| return history_service->InMemoryIndex(); |
| } |
| |
| // static |
| ACMatchClassifications HistoryQuickProvider::SpansFromTermMatch( |
| const history::TermMatches& matches, |
| size_t text_length, |
| bool is_url) { |
| ACMatchClassification::Style url_style = |
| is_url ? ACMatchClassification::URL : ACMatchClassification::NONE; |
| ACMatchClassifications spans; |
| if (matches.empty()) { |
| if (text_length) |
| spans.push_back(ACMatchClassification(0, url_style)); |
| return spans; |
| } |
| if (matches[0].offset) |
| spans.push_back(ACMatchClassification(0, url_style)); |
| size_t match_count = matches.size(); |
| for (size_t i = 0; i < match_count;) { |
| size_t offset = matches[i].offset; |
| spans.push_back(ACMatchClassification(offset, |
| ACMatchClassification::MATCH | url_style)); |
| // Skip all adjacent matches. |
| do { |
| offset += matches[i].length; |
| ++i; |
| } while ((i < match_count) && (offset == matches[i].offset)); |
| if (offset < text_length) |
| spans.push_back(ACMatchClassification(offset, url_style)); |
| } |
| |
| return spans; |
| } |