| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/omnibox/browser/local_history_zero_suggest_provider.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/i18n/case_conversion.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "components/google/core/common/google_util.h" |
| #include "components/history/core/browser/history_database.h" |
| #include "components/history/core/browser/history_service.h" |
| #include "components/history/core/browser/history_types.h" |
| #include "components/history/core/browser/keyword_search_term.h" |
| #include "components/history/core/browser/keyword_search_term_util.h" |
| #include "components/history/core/browser/url_database.h" |
| #include "components/omnibox/browser/autocomplete_enums.h" |
| #include "components/omnibox/browser/autocomplete_input.h" |
| #include "components/omnibox/browser/autocomplete_match.h" |
| #include "components/omnibox/browser/autocomplete_match_classification.h" |
| #include "components/omnibox/browser/autocomplete_provider_client.h" |
| #include "components/omnibox/browser/autocomplete_provider_listener.h" |
| #include "components/omnibox/browser/autocomplete_result.h" |
| #include "components/omnibox/browser/omnibox_field_trial.h" |
| #include "components/omnibox/browser/omnibox_prefs.h" |
| #include "components/omnibox/browser/page_classification_functions.h" |
| #include "components/omnibox/browser/suggestion_group_util.h" |
| #include "components/omnibox/browser/zero_suggest_provider.h" |
| #include "components/omnibox/common/omnibox_feature_configs.h" |
| #include "components/omnibox/common/omnibox_features.h" |
| #include "components/search/search.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "third_party/metrics_proto/omnibox_focus_type.pb.h" |
| #include "third_party/omnibox_proto/navigational_intent.pb.h" |
| #include "url/gurl.h" |
| |
| using metrics::OmniboxInputType; |
| |
| namespace { |
| |
| // Extracts the search terms from |url|. Collapses whitespaces, converts them to |
| // lowercase and returns them. |template_url_service| must not be null. |
| std::u16string GetSearchTermsFromURL(const GURL& url, |
| TemplateURLService* template_url_service) { |
| DCHECK(template_url_service); |
| const TemplateURL* default_provider = |
| template_url_service->GetDefaultSearchProvider(); |
| DCHECK(default_provider); |
| |
| std::u16string search_terms; |
| default_provider->ExtractSearchTermsFromURL( |
| url, template_url_service->search_terms_data(), &search_terms); |
| return history::NormalizeTerm(search_terms); |
| } |
| |
| // Whether zero suggest suggestions are allowed in the given context. |
| // Invoked early, confirms all the conditions for zero suggestions are met. |
| bool AllowLocalHistoryZeroSuggestSuggestions(AutocompleteProviderClient* client, |
| const AutocompleteInput& input) { |
| // Allow local history zero-suggest only when the user is not in incognito |
| // mode. |
| if (client->IsOffTheRecord()) |
| return false; |
| |
| // Allow local history zero-suggest only when the user has set up Google as |
| // their default search engine. |
| if (!search::DefaultSearchProviderIsGoogle(client->GetTemplateURLService())) { |
| return false; |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| omnibox::kLocalHistoryZeroSuggestBeyondNTP)) { |
| // Allow local history zero-suggest where remote zero-suggest is eligible. |
| const auto [result_type, _] = |
| ZeroSuggestProvider::GetResultTypeAndEligibility(client, input); |
| return result_type != ZeroSuggestProvider::ResultType::kNone; |
| } |
| |
| // Allow local history query suggestions only when the omnibox is empty and is |
| // focused from the NTP. |
| return input.focus_type() == metrics::OmniboxFocusType::INTERACTION_FOCUS && |
| input.type() == OmniboxInputType::EMPTY && |
| omnibox::IsNTPPage(input.current_page_classification()); |
| } |
| |
| } // namespace |
| |
| // static |
| LocalHistoryZeroSuggestProvider* LocalHistoryZeroSuggestProvider::Create( |
| AutocompleteProviderClient* client, |
| AutocompleteProviderListener* listener) { |
| return new LocalHistoryZeroSuggestProvider(client, listener); |
| } |
| |
| void LocalHistoryZeroSuggestProvider::Start(const AutocompleteInput& input, |
| bool minimal_changes) { |
| TRACE_EVENT0("omnibox", "LocalHistoryZeroSuggestProvider::Start"); |
| Stop(AutocompleteStopReason::kClobbered); |
| if (!AllowLocalHistoryZeroSuggestSuggestions(client_, input)) { |
| return; |
| } |
| |
| QueryURLDatabase(input); |
| } |
| |
| void LocalHistoryZeroSuggestProvider::DeleteMatch( |
| const AutocompleteMatch& match) { |
| SCOPED_UMA_HISTOGRAM_TIMER("Omnibox.LocalHistoryZeroSuggest.SyncDeleteTime"); |
| |
| history::HistoryService* history_service = client_->GetHistoryService(); |
| if (!history_service) |
| return; |
| |
| TemplateURLService* template_url_service = client_->GetTemplateURLService(); |
| if (!template_url_service || |
| !template_url_service->GetDefaultSearchProvider()) { |
| return; |
| } |
| |
| history::URLDatabase* url_db = history_service->InMemoryDatabase(); |
| if (!url_db) |
| return; |
| |
| // Even if local history uses the non-normalized term for its match |
| // contents, the db should still delete the normalized version of the |
| // contents in order to delete all duplicates. Technically, always calling |
| // `ToLower()` shouldn't hurt, but this is safer. |
| auto omnibox_mia_zps_config = omnibox_feature_configs::MiaZPS::Get(); |
| const std::u16string& match_contents = |
| (omnibox_mia_zps_config.enabled && |
| omnibox_mia_zps_config.local_history_non_normalized_contents) |
| ? history::NormalizeTerm(match.contents) |
| : match.contents; |
| |
| // TODO(crbug.com/421889863): Consider using |
| // `DeleteMatchingURLsForKeywordFromHistory()` for deletion of term. |
| // Deletes all the search terms matching the query suggestion. |
| url_db->DeleteKeywordSearchTermForNormalizedTerm( |
| template_url_service->GetDefaultSearchProvider()->id(), match_contents); |
| |
| // Generate a Google search URL. Note that the search URL returned by |
| // TemplateURL::GenerateSearchURL() cannot be used here as it contains |
| // Chrome specific query params and therefore only matches search queries |
| // issued from Chrome and not those from the Web. |
| GURL google_base_url( |
| template_url_service->search_terms_data().GoogleBaseURLValue()); |
| std::string google_search_url = |
| google_util::GetGoogleSearchURL(google_base_url).spec(); |
| |
| // Query the HistoryService for fresh Google search URLs. Note that the |
| // performance overhead of querying the HistoryService can be tolerated here |
| // due to the small percentage of suggestions getting deleted relative to the |
| // number of suggestions shown and the async nature of this lookup. |
| history::QueryOptions opts; |
| opts.duplicate_policy = history::QueryOptions::KEEP_ALL_DUPLICATES; |
| opts.begin_time = base::Time::Now() - base::Days(90); // Full history length. |
| history_service->QueryHistory( |
| base::ASCIIToUTF16(google_search_url), opts, |
| base::BindOnce(&LocalHistoryZeroSuggestProvider::OnHistoryQueryResults, |
| weak_ptr_factory_.GetWeakPtr(), match_contents, |
| base::TimeTicks::Now()), |
| &history_task_tracker_); |
| |
| // Immediately update the list of matches to reflect the match was deleted. |
| std::erase_if(matches_, [&](const auto& item) { |
| return match.contents == item.contents; |
| }); |
| } |
| |
| LocalHistoryZeroSuggestProvider::LocalHistoryZeroSuggestProvider( |
| AutocompleteProviderClient* client, |
| AutocompleteProviderListener* listener) |
| : AutocompleteProvider( |
| AutocompleteProvider::TYPE_ZERO_SUGGEST_LOCAL_HISTORY), |
| max_matches_(AutocompleteResult::GetMaxMatches(true)), |
| client_(client) { |
| AddListener(listener); |
| } |
| |
| LocalHistoryZeroSuggestProvider::~LocalHistoryZeroSuggestProvider() = default; |
| |
| void LocalHistoryZeroSuggestProvider::QueryURLDatabase( |
| const AutocompleteInput& input) { |
| done_ = true; |
| matches_.clear(); |
| |
| history::HistoryService* const history_service = client_->GetHistoryService(); |
| if (!history_service) |
| return; |
| |
| // Fail if the in-memory URL database is not available. |
| history::URLDatabase* url_db = history_service->InMemoryDatabase(); |
| if (!url_db) |
| return; |
| |
| // Fail if we can't set the clickthrough URL for query suggestions. |
| TemplateURLService* template_url_service = client_->GetTemplateURLService(); |
| if (!template_url_service || |
| !template_url_service->GetDefaultSearchProvider()) { |
| return; |
| } |
| |
| history::KeywordSearchTermVisitList results; |
| const base::ElapsedTimer db_query_timer; |
| auto enumerator = url_db->CreateKeywordSearchTermVisitEnumerator( |
| template_url_service->GetDefaultSearchProvider()->id()); |
| if (enumerator) { |
| history::GetAutocompleteSearchTermsFromEnumerator( |
| *enumerator, max_matches_, history::SearchTermRankingPolicy::kFrecency, |
| &results); |
| } |
| DCHECK_LE(results.size(), max_matches_); |
| base::UmaHistogramTimes( |
| "Omnibox.LocalHistoryZeroSuggest.SearchTermsExtractionTimeV2", |
| db_query_timer.Elapsed()); |
| |
| auto omnibox_mia_zps_config = omnibox_feature_configs::MiaZPS::Get(); |
| |
| int relevance = omnibox::kLocalHistoryZeroSuggestRelevance; |
| for (const auto& result : results) { |
| const std::u16string& suggestion_term = |
| (omnibox_mia_zps_config.enabled && |
| omnibox_mia_zps_config.local_history_non_normalized_contents) |
| ? result->term |
| : result->normalized_term; |
| SearchSuggestionParser::SuggestResult suggestion( |
| /*suggestion=*/suggestion_term, AutocompleteMatchType::SEARCH_HISTORY, |
| /*suggest_type=*/omnibox::TYPE_NATIVE_CHROME, |
| /*subtypes=*/{}, /*from_keyword=*/false, |
| /*navigational_intent=*/omnibox::NAV_INTENT_NONE, relevance--, |
| /*relevance_from_server=*/false, |
| /*input_text=*/base::ASCIIToUTF16(std::string())); |
| |
| // If the appropriate header text and section are not provided by the server |
| // the default omnibox::SECTION_PERSONALIZED_ZERO_SUGGEST will be used and |
| // the local history zero-prefix suggestions will be shown without a header. |
| suggestion.set_suggestion_group_id( |
| omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST); |
| |
| AutocompleteMatch match = BaseSearchProvider::CreateSearchSuggestion( |
| this, input, /*in_keyword_mode=*/false, suggestion, |
| template_url_service->GetDefaultSearchProvider(), |
| template_url_service->search_terms_data(), |
| TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, |
| /*append_extra_query_params_from_command_line*/ true); |
| match.deletable = client_->AllowDeletingBrowserHistory(); |
| matches_.push_back(match); |
| } |
| } |
| |
| void LocalHistoryZeroSuggestProvider::OnHistoryQueryResults( |
| const std::u16string& suggestion, |
| const base::TimeTicks& query_time, |
| history::QueryResults results) { |
| history::HistoryService* history_service = client_->GetHistoryService(); |
| if (!history_service) |
| return; |
| |
| TemplateURLService* template_url_service = client_->GetTemplateURLService(); |
| if (!template_url_service || |
| !template_url_service->GetDefaultSearchProvider()) { |
| return; |
| } |
| |
| // Delete the matching URLs that would generate |suggestion|. |
| std::vector<GURL> urls_to_delete; |
| for (const auto& result : results) { |
| std::u16string search_terms = |
| GetSearchTermsFromURL(result.url(), template_url_service); |
| if (search_terms == suggestion) |
| urls_to_delete.push_back(result.url()); |
| } |
| history_service->DeleteURLs(urls_to_delete); |
| |
| UMA_HISTOGRAM_TIMES("Omnibox.LocalHistoryZeroSuggest.AsyncDeleteTime", |
| base::TimeTicks::Now() - query_time); |
| } |