blob: 36f611ee2d776a8ff92cacafe434d8163a09d0c1 [file] [log] [blame]
// Copyright 2019 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 "components/omnibox/browser/local_history_zero_suggest_provider.h"
#include <set>
#include <string>
#include "base/bind.h"
#include "base/i18n/case_conversion.h"
#include "base/logging.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "components/google/core/common/google_util.h"
#include "components/history/core/browser/history_database.h"
#include "components/history/core/browser/history_db_task.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/url_database.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/base_search_provider.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_pref_names.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/search_engines/template_url_service.h"
#include "url/gurl.h"
namespace {
// Default relevance for the LocalHistoryZeroSuggestProvider query suggestions.
const int kLocalHistoryZeroSuggestRelevance = 500;
// ProviderHistoryDBTask wraps two non-null callbacks into a HistoryDBTask to
// passed to HistoryService::ScheduleDBTask. |request_callback| gets invoked on
// the history backend thread when the on-disk history database becomes
// available in order to query the database and populate |url_matches_|.
// |result_callback_| is then invoked with |url_matches_| back on the main
// thread.
class ProviderHistoryDBTask : public history::HistoryDBTask {
public:
using RequestCallback =
base::OnceCallback<history::URLRows(history::URLDatabase* url_db)>;
using ResultCallback = base::OnceCallback<void(const history::URLRows&)>;
ProviderHistoryDBTask(RequestCallback request_callback,
ResultCallback result_callback)
: request_callback_(std::move(request_callback)),
result_callback_(std::move(result_callback)) {
DCHECK(!request_callback_.is_null());
DCHECK(!result_callback_.is_null());
}
~ProviderHistoryDBTask() override {}
private:
// history::HistoryDBTask
bool RunOnDBThread(history::HistoryBackend* history_backend,
history::HistoryDatabase* db) override {
// Query the database.
if (db)
url_matches_ = std::move(request_callback_).Run(db);
return true;
}
void DoneRunOnMainThread() override {
// Return the query results to the originating thread.
std::move(result_callback_).Run(url_matches_);
}
RequestCallback request_callback_;
ResultCallback result_callback_;
history::URLRows url_matches_;
DISALLOW_COPY_AND_ASSIGN(ProviderHistoryDBTask);
};
// Queries |url_db| for URLs matching |google_search_url| prefix and returns
// the results. |url_db| must not be null. May be called on the history backend
// thread.
history::URLRows QueryURLDatabaseOnHistoryThread(
const std::string& google_search_url,
const size_t provider_max_matches,
history::URLDatabase* url_db) {
DCHECK(url_db);
// Request 5x more URLs than the number of matches the provider intends to
// return hoping to have enough URLs to work with once duplicates are filtered
// out.
history::URLRows url_matches;
url_db->AutocompleteForPrefix(google_search_url, 5 * provider_max_matches,
false, &url_matches);
return url_matches;
}
// Extracts the search terms from |url|. Collapses whitespaces, converts them to
// lowercase and returns them. |template_url_service| must not be null.
base::string16 GetSearchTermsFromURL(const GURL& url,
TemplateURLService* template_url_service) {
DCHECK(template_url_service);
base::string16 search_terms;
template_url_service->GetDefaultSearchProvider()->ExtractSearchTermsFromURL(
url, template_url_service->search_terms_data(), &search_terms);
return base::i18n::ToLower(base::CollapseWhitespace(search_terms, false));
}
} // namespace
// static
const char LocalHistoryZeroSuggestProvider::kZeroSuggestLocalVariant[] =
"Local";
// 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");
history_task_tracker_.TryCancel(history_db_task_id_);
done_ = true;
matches_.clear();
// Allow local history query suggestions only when the user is unauthenticated
// and is not in an off-the-record context.
if (client_->IsAuthenticated() || client_->IsOffTheRecord())
return;
// Allow local history query suggestions only when the omnibox is empty and is
// focused from the NTP.
if (!input.from_omnibox_focus() ||
input.type() != metrics::OmniboxInputType::EMPTY ||
!BaseSearchProvider::IsNTPPage(input.current_page_classification())) {
return;
}
// Allow local history query suggestions only when the user has set up Google
// as their default search engine.
TemplateURLService* template_url_service = client_->GetTemplateURLService();
if (!template_url_service ||
!template_url_service->GetDefaultSearchProvider() ||
template_url_service->GetDefaultSearchProvider()->GetEngineType(
template_url_service->search_terms_data()) != SEARCH_ENGINE_GOOGLE) {
return;
}
if (!base::Contains(OmniboxFieldTrial::GetZeroSuggestVariants(
input.current_page_classification()),
kZeroSuggestLocalVariant)) {
return;
}
history::HistoryService* const history_service = client_->GetHistoryService();
if (!history_service)
return;
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 in-memory URL database, if available. Otherwise, schedule a
// task to query the on-disk history database on the history backend thread.
history::URLDatabase* url_db = history_service->InMemoryDatabase();
if (url_db) {
const auto url_matches = QueryURLDatabaseOnHistoryThread(
google_search_url, max_matches_, url_db);
OnQueryURLDatabaseComplete(input, std::move(url_matches));
} else {
done_ = false;
history_db_task_id_ = history_service->ScheduleDBTask(
FROM_HERE,
std::make_unique<ProviderHistoryDBTask>(
base::BindOnce(QueryURLDatabaseOnHistoryThread, google_search_url,
max_matches_),
base::BindOnce(
&LocalHistoryZeroSuggestProvider::OnQueryURLDatabaseComplete,
weak_ptr_factory_.GetWeakPtr(), input)),
&history_task_tracker_);
}
}
void LocalHistoryZeroSuggestProvider::DeleteMatch(
const AutocompleteMatch& match) {
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;
}
// Generate a Google search URL for the suggestion by replacing the search
// terms in the Google search URL template. Note that the suggestion's
// original search URL cannot be used here as it is unique due to the assisted
// query stats (aka AQS) query param which contains impressions of
// autocomplete matches shown at query submission time.
GURL google_search_url;
TemplateURLRef::SearchTermsArgs search_terms_args(match.contents);
const auto* default_search_provider =
template_url_service->GetDefaultSearchProvider();
const auto& search_terms_data = template_url_service->search_terms_data();
default_search_provider->ReplaceSearchTermsInURL(
default_search_provider->GenerateSearchURL(search_terms_data),
search_terms_args, search_terms_data, &google_search_url);
// Query the HistoryService for URLs matching the Google search URL. Note that
// this step is necessary here because 1) there may be matching fresh URLs
// that were not encountered in the suggestion creation phase due to looking
// up a maximum number of URLs in that phase. and 2) the performance overhead
// of requerying the HistoryService cannot be tolerated in the suggestion
// creation phase. Here on the other hand it can due to the small percentage
// of suggestions getting deleted relative to the number of suggestions shown.
history::QueryOptions opts;
opts.duplicate_policy = history::QueryOptions::KEEP_ALL_DUPLICATES;
opts.begin_time = history::AutocompleteAgeThreshold();
history_service->QueryHistory(
base::ASCIIToUTF16(google_search_url.spec()), opts,
base::BindOnce(&LocalHistoryZeroSuggestProvider::OnHistoryQueryResults,
weak_ptr_factory_.GetWeakPtr(), match.contents),
&history_task_tracker_);
// Prevent the deleted suggestion from being resuggested until the
// corresponding URLs are asynchronously deleted.
deleted_suggestions_set_.insert(match.contents);
// Immediately update the list of matches to reflect the match was deleted.
base::EraseIf(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(/*is_zero_suggest=*/true)),
client_(client),
listener_(listener) {}
LocalHistoryZeroSuggestProvider::~LocalHistoryZeroSuggestProvider() {}
void LocalHistoryZeroSuggestProvider::OnQueryURLDatabaseComplete(
const AutocompleteInput& input,
const history::URLRows& url_matches) {
done_ = true;
matches_.clear();
// Fail if we can't extract the search terms from the URL matches.
TemplateURLService* template_url_service = client_->GetTemplateURLService();
if (!template_url_service ||
!template_url_service->GetDefaultSearchProvider()) {
return;
}
// Used to filter out duplicate query suggestions.
std::set<base::string16> seen_suggestions_set;
int relevance = kLocalHistoryZeroSuggestRelevance;
for (size_t i = 0; i < url_matches.size(); i++) {
const history::URLRow& url_row = url_matches[i];
const GURL& url = url_row.url();
// Discard the URL if it is not valid or fresh enough.
if (!url.is_valid() ||
url_row.last_visit() < history::AutocompleteAgeThreshold()) {
continue;
}
base::string16 search_terms =
GetSearchTermsFromURL(url, template_url_service);
if (search_terms.empty())
continue;
// Filter out duplicate query suggestions.
if (seen_suggestions_set.count(search_terms))
continue;
seen_suggestions_set.insert(search_terms);
// Filter out deleted query suggestions.
if (deleted_suggestions_set_.count(search_terms))
continue;
SearchSuggestionParser::SuggestResult suggestion(
/*suggestion=*/search_terms, AutocompleteMatchType::SEARCH_HISTORY,
/*subtype_identifier=*/0, /*from_keyword=*/false, relevance--,
/*relevance_from_server=*/0,
/*input_text=*/base::ASCIIToUTF16(std::string()));
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 = true;
matches_.push_back(match);
if (matches_.size() >= max_matches_)
break;
}
listener_->OnProviderUpdate(true);
}
void LocalHistoryZeroSuggestProvider::OnHistoryQueryResults(
const base::string16& suggestion,
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) {
base::string16 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);
}