// 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 #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/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); } HistoryQuickProvider::~HistoryQuickProvider() {} 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((end_time - start_time).InMilliseconds())); } UpdateStarredStateOfMatches(); } } // TODO(mrossetti): Implement this function. (Will happen in next CL.) void HistoryQuickProvider::DeleteMatch(const AutocompleteMatch& match) {} 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.destination_url = info.url(); DCHECK(match.destination_url.is_valid()); // Format the URL autocomplete presentation. std::vector 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; }