Commit c977d8c7 authored by Moe Ahmadi's avatar Moe Ahmadi Committed by Commit Bot

[NTP][RQ] Repeatable Queries Service

Provides repeatable query suggestions to be shown in the NTP MV tiles
when Google is the default search provider. The repeatable queries are
requested from the server for signed-in users and extracted from the
URL database for unauthenticated users.

Fixed: 1106396, 1110077
Change-Id: I749739cb4382d83f21256430428a60c5f1d563bd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2518773
Commit-Queue: Moe Ahmadi <mahmadi@chromium.org>
Reviewed-by: default avatarTommy Li <tommycli@chromium.org>
Reviewed-by: default avatarColin Blundell <blundell@chromium.org>
Reviewed-by: default avatarDavid Roger <droger@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarRamya Nagarajan <ramyan@chromium.org>
Reviewed-by: default avatarNicolas Ouellet-Payeur <nicolaso@chromium.org>
Cr-Commit-Position: refs/heads/master@{#825036}
parent ee92c905
...@@ -3762,6 +3762,8 @@ static_library("browser") { ...@@ -3762,6 +3762,8 @@ static_library("browser") {
"search/promos/promo_service_factory.cc", "search/promos/promo_service_factory.cc",
"search/promos/promo_service_factory.h", "search/promos/promo_service_factory.h",
"search/promos/promo_service_observer.h", "search/promos/promo_service_observer.h",
"search/repeatable_queries/repeatable_queries_service_factory.cc",
"search/repeatable_queries/repeatable_queries_service_factory.h",
"search/search_engine_base_url_tracker.cc", "search/search_engine_base_url_tracker.cc",
"search/search_engine_base_url_tracker.h", "search/search_engine_base_url_tracker.h",
"search/search_suggest/search_suggest_data.cc", "search/search_suggest/search_suggest_data.cc",
......
// Copyright 2020 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/search/repeatable_queries/repeatable_queries_service_factory.h"
#include "base/feature_list.h"
#include "chrome/browser/content_settings/cookie_settings_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/common/webui_url_constants.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/search/ntp_features.h"
#include "components/search/repeatable_queries/repeatable_queries_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
// static
RepeatableQueriesService* RepeatableQueriesServiceFactory::GetForProfile(
Profile* profile) {
return static_cast<RepeatableQueriesService*>(
GetInstance()->GetServiceForBrowserContext(profile, true));
}
// static
RepeatableQueriesServiceFactory*
RepeatableQueriesServiceFactory::GetInstance() {
return base::Singleton<RepeatableQueriesServiceFactory>::get();
}
RepeatableQueriesServiceFactory::RepeatableQueriesServiceFactory()
: BrowserContextKeyedServiceFactory(
"RepeatableQueriesService",
BrowserContextDependencyManager::GetInstance()) {
DependsOn(CookieSettingsFactory::GetInstance());
DependsOn(IdentityManagerFactory::GetInstance());
}
RepeatableQueriesServiceFactory::~RepeatableQueriesServiceFactory() = default;
KeyedService* RepeatableQueriesServiceFactory::BuildServiceInstanceFor(
content::BrowserContext* context) const {
if (!base::FeatureList::IsEnabled(ntp_features::kNtpRepeatableQueries)) {
return nullptr;
}
Profile* profile = Profile::FromBrowserContext(context);
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
history::HistoryService* history_service =
HistoryServiceFactory::GetForProfile(profile,
ServiceAccessType::EXPLICIT_ACCESS);
TemplateURLService* template_url_service =
TemplateURLServiceFactory::GetForProfile(profile);
auto url_loader_factory =
content::BrowserContext::GetDefaultStoragePartition(context)
->GetURLLoaderFactoryForBrowserProcess();
return new RepeatableQueriesService(identity_manager, history_service,
template_url_service, url_loader_factory,
GURL(chrome::kChromeUINewTabURL));
}
// Copyright 2020 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.
#ifndef CHROME_BROWSER_SEARCH_REPEATABLE_QUERIES_REPEATABLE_QUERIES_SERVICE_FACTORY_H_
#define CHROME_BROWSER_SEARCH_REPEATABLE_QUERIES_REPEATABLE_QUERIES_SERVICE_FACTORY_H_
#include "base/macros.h"
#include "base/memory/singleton.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
class RepeatableQueriesService;
class Profile;
class RepeatableQueriesServiceFactory
: public BrowserContextKeyedServiceFactory {
public:
// Returns the RepeatableQueriesService for |profile|.
static RepeatableQueriesService* GetForProfile(Profile* profile);
static RepeatableQueriesServiceFactory* GetInstance();
private:
friend struct base::DefaultSingletonTraits<RepeatableQueriesServiceFactory>;
RepeatableQueriesServiceFactory();
~RepeatableQueriesServiceFactory() override;
// Overridden from BrowserContextKeyedServiceFactory:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* profile) const override;
DISALLOW_COPY_AND_ASSIGN(RepeatableQueriesServiceFactory);
};
#endif // CHROME_BROWSER_SEARCH_REPEATABLE_QUERIES_REPEATABLE_QUERIES_SERVICE_FACTORY_H_
...@@ -151,6 +151,7 @@ class InMemoryURLIndex : public KeyedService, ...@@ -151,6 +151,7 @@ class InMemoryURLIndex : public KeyedService,
friend class history::HQPPerfTestOnePopularURL; friend class history::HQPPerfTestOnePopularURL;
friend class InMemoryURLIndexTest; friend class InMemoryURLIndexTest;
friend class InMemoryURLIndexCacheTest; friend class InMemoryURLIndexCacheTest;
friend class RepeatableQueriesServiceTest;
FRIEND_TEST_ALL_PREFIXES(InMemoryURLIndexTest, ExpireRow); FRIEND_TEST_ALL_PREFIXES(InMemoryURLIndexTest, ExpireRow);
FRIEND_TEST_ALL_PREFIXES(LimitedInMemoryURLIndexTest, Initialization); FRIEND_TEST_ALL_PREFIXES(LimitedInMemoryURLIndexTest, Initialization);
......
...@@ -6,6 +6,9 @@ static_library("search") { ...@@ -6,6 +6,9 @@ static_library("search") {
sources = [ sources = [
"ntp_features.cc", "ntp_features.cc",
"ntp_features.h", "ntp_features.h",
"repeatable_queries/repeatable_queries_service.cc",
"repeatable_queries/repeatable_queries_service.h",
"repeatable_queries/repeatable_queries_service_observer.h",
"search.cc", "search.cc",
"search.h", "search.h",
"search_provider_observer.cc", "search_provider_observer.cc",
...@@ -15,7 +18,15 @@ static_library("search") { ...@@ -15,7 +18,15 @@ static_library("search") {
deps = [ deps = [
"//base", "//base",
"//components/google/core/common", "//components/google/core/common",
"//components/history/core/browser",
"//components/keyed_service/core",
"//components/search_engines", "//components/search_engines",
"//components/signin/public/identity_manager",
"//components/variations/net",
"//net",
"//net/traffic_annotation",
"//services/data_decoder/public/cpp",
"//services/network/public/cpp",
"//url", "//url",
] ]
} }
...@@ -23,7 +34,10 @@ static_library("search") { ...@@ -23,7 +34,10 @@ static_library("search") {
source_set("unit_tests") { source_set("unit_tests") {
testonly = true testonly = true
sources = [ "search_unittest.cc" ] sources = [
"repeatable_queries/repeatable_queries_service_unittest.cc",
"search_unittest.cc",
]
if (is_android) { if (is_android) {
sources += [ "search_android_unittest.cc" ] sources += [ "search_android_unittest.cc" ]
} }
...@@ -31,7 +45,17 @@ source_set("unit_tests") { ...@@ -31,7 +45,17 @@ source_set("unit_tests") {
deps = [ deps = [
":search", ":search",
"//base", "//base",
"//base/test:test_support",
"//components/bookmarks/browser",
"//components/bookmarks/test",
"//components/history/core/test",
"//components/omnibox/browser:test_support",
"//components/search_engines:search_engines",
"//components/signin/public/base:test_support",
"//components/signin/public/identity_manager:test_support",
"//components/variations", "//components/variations",
"//services/data_decoder/public/cpp:test_support",
"//testing/gmock",
"//testing/gtest", "//testing/gtest",
] ]
} }
include_rules = [ include_rules = [
"+components/bookmarks/browser",
"+components/bookmarks/test",
"+components/google/core", "+components/google/core",
"+components/history/core",
"+components/keyed_service/core",
"+components/omnibox/browser",
"+components/search_engines", "+components/search_engines",
"+components/signin/public",
"+components/variations", "+components/variations",
"+net",
"+services/data_decoder/public",
"+services/network/public",
] ]
// Copyright 2020 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/search/repeatable_queries/repeatable_queries_service.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/json/json_writer.h"
#include "base/scoped_observer.h"
#include "base/values.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/keyword_search_term.h"
#include "components/history/core/browser/url_database.h"
#include "components/search/ntp_features.h"
#include "components/search/search_provider_observer.h"
#include "components/search_engines/template_url_service.h"
#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/variations/net/variations_http_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
namespace {
const char kXSSIResponsePreamble[] = ")]}'";
const size_t kMaxQueries = 2;
bool JsonToRepeatableQueriesData(const base::Value& root_value,
std::vector<RepeatableQuery>* data) {
// 1st element is the query. 2nd element is the list of results.
base::string16 query;
const base::ListValue* root_list = nullptr;
const base::ListValue* results_list = nullptr;
if (!root_value.GetAsList(&root_list) || !root_list->GetString(0, &query) ||
!query.empty() || !root_list->GetList(1, &results_list))
return false;
// Ignore the 3rd and 4th elements. 5th element is the key-value pairs from
// the Suggest server containing the deletion URLs.
const base::DictionaryValue* extras = nullptr;
const base::ListValue* suggestion_details = nullptr;
if (root_list->GetDictionary(4, &extras) &&
extras->GetList("google:suggestdetail", &suggestion_details) &&
suggestion_details->GetSize() != results_list->GetSize()) {
return false;
}
base::string16 suggestion;
for (size_t index = 0; results_list->GetString(index, &suggestion); ++index) {
RepeatableQuery result;
result.query = base::CollapseWhitespace(suggestion, false);
if (result.query.empty())
continue;
const base::DictionaryValue* suggestion_detail = nullptr;
if (suggestion_details->GetDictionary(index, &suggestion_detail)) {
suggestion_detail->GetString("du", &result.deletion_url);
}
data->push_back(result);
}
return !data->empty();
}
} // namespace
class RepeatableQueriesService::SigninObserver
: public signin::IdentityManager::Observer {
public:
SigninObserver(signin::IdentityManager* identity_manager,
base::RepeatingClosure callback)
: identity_manager_(identity_manager), callback_(std::move(callback)) {
if (identity_manager_) {
identity_manager_observer_.Add(identity_manager_);
}
}
~SigninObserver() override = default;
bool IsSignedIn() {
return identity_manager_ ? !identity_manager_->GetAccountsInCookieJar()
.signed_in_accounts.empty()
: false;
}
private:
// IdentityManager::Observer implementation.
void OnAccountsInCookieUpdated(
const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
const GoogleServiceAuthError& error) override {
callback_.Run();
}
ScopedObserver<signin::IdentityManager, signin::IdentityManager::Observer>
identity_manager_observer_{this};
// May be nullptr in tests.
signin::IdentityManager* const identity_manager_;
base::RepeatingClosure callback_;
};
RepeatableQueriesService::RepeatableQueriesService(
signin::IdentityManager* identity_manager,
history::HistoryService* history_service,
TemplateURLService* template_url_service,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const GURL& request_initiator_url)
: history_service_(history_service),
template_url_service_(template_url_service),
url_loader_factory_(url_loader_factory),
request_initiator_url_(request_initiator_url),
signin_observer_(std::make_unique<SigninObserver>(
identity_manager,
base::BindRepeating(&RepeatableQueriesService::SigninStatusChanged,
base::Unretained(this)))),
search_provider_observer_(std::make_unique<SearchProviderObserver>(
template_url_service,
base::BindRepeating(&RepeatableQueriesService::SearchProviderChanged,
base::Unretained(this)))) {
DCHECK(history_service_);
DCHECK(template_url_service_);
DCHECK(url_loader_factory_);
}
RepeatableQueriesService::~RepeatableQueriesService() = default;
void RepeatableQueriesService::Shutdown() {
for (auto& observer : observers_) {
observer.OnRepeatableQueriesServiceShuttingDown();
}
}
const std::vector<RepeatableQuery>&
RepeatableQueriesService::repeatable_queries() const {
return repeatable_queries_;
}
void RepeatableQueriesService::Refresh() {
if (!search_provider_observer()->is_google()) {
NotifyObservers();
return;
}
if (signin_observer()->IsSignedIn()) {
GetRepeatableQueriesFromServer();
} else {
GetRepeatableQueriesFromURLDatabase();
}
}
void RepeatableQueriesService::AddObserver(
RepeatableQueriesServiceObserver* observer) {
observers_.AddObserver(observer);
}
void RepeatableQueriesService::RemoveObserver(
RepeatableQueriesServiceObserver* observer) {
observers_.RemoveObserver(observer);
}
RepeatableQueriesService::SigninObserver*
RepeatableQueriesService::signin_observer() {
return signin_observer_.get();
}
SearchProviderObserver* RepeatableQueriesService::search_provider_observer() {
return search_provider_observer_.get();
}
void RepeatableQueriesService::SearchProviderChanged() {
// If we have cached data, clear it.
repeatable_queries_.clear();
Refresh();
}
void RepeatableQueriesService::SigninStatusChanged() {
// If we have cached data, clear it.
repeatable_queries_.clear();
Refresh();
}
GURL RepeatableQueriesService::GetRequestURL() {
TemplateURLRef::SearchTermsArgs search_terms_args;
search_terms_args.request_source = TemplateURLRef::NON_SEARCHBOX_NTP;
const TemplateURLRef& suggestion_url_ref =
template_url_service_->GetDefaultSearchProvider()->suggestions_url_ref();
const SearchTermsData& search_terms_data =
template_url_service_->search_terms_data();
return GURL(suggestion_url_ref.ReplaceSearchTerms(search_terms_args,
search_terms_data));
}
void RepeatableQueriesService::GetRepeatableQueriesFromServer() {
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("repeatable_queries_service", R"(
semantics {
sender: "Repeatable Queries Service"
description:
"Downloads search queries to be shown on the Most Visited "
"section of New Tab Page to signed-in users based on their "
"previous search history."
trigger:
"Displaying the new tab page, if Google is the "
"configured search provider, and the user is signed in."
data: "Google credentials if user is signed in."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting:
"Users can control this feature by selecting a non-Google default "
"search engine in Chrome settings under 'Search Engine', or by "
"signing out of the browser on the New Tab Page. Users can opt "
"out of this feature by switching to custom shortcuts."
chrome_policy {
DefaultSearchProviderEnabled {
policy_options {mode: MANDATORY}
DefaultSearchProviderEnabled: false
}
BrowserSignin {
policy_options {mode: MANDATORY}
BrowserSignin: 0
}
}
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
const GURL& request_url = GetRequestURL();
variations::AppendVariationsHeaderUnknownSignedIn(
request_url, variations::InIncognito::kNo, resource_request.get());
resource_request->url = request_url;
resource_request->request_initiator =
url::Origin::Create(request_initiator_url_);
simple_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
traffic_annotation);
simple_loader_->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&RepeatableQueriesService::RepeatableQueriesResponseLoaded,
weak_ptr_factory_.GetWeakPtr()),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void RepeatableQueriesService::RepeatableQueriesResponseLoaded(
std::unique_ptr<std::string> response) {
auto net_error = simple_loader_->NetError();
if (net_error != net::OK || !response) {
// In the case of network errors, keep the cached data, if any, but still
// notify observers of the finished load attempt.
NotifyObservers();
return;
}
if (base::StartsWith(*response, kXSSIResponsePreamble,
base::CompareCase::SENSITIVE)) {
*response = response->substr(strlen(kXSSIResponsePreamble));
}
data_decoder::DataDecoder::ParseJsonIsolated(
*response,
base::BindOnce(&RepeatableQueriesService::RepeatableQueriesParsed,
weak_ptr_factory_.GetWeakPtr()));
}
void RepeatableQueriesService::RepeatableQueriesParsed(
data_decoder::DataDecoder::ValueOrError result) {
std::vector<RepeatableQuery> data;
if (result.value && JsonToRepeatableQueriesData(*result.value, &data)) {
repeatable_queries_ = std::vector<RepeatableQuery>(
data.begin(), data.begin() + std::min(data.size(), kMaxQueries));
} else {
repeatable_queries_ = data;
}
NotifyObservers();
}
void RepeatableQueriesService::GetRepeatableQueriesFromURLDatabase() {
repeatable_queries_.clear();
// Fail if the in-memory URL database is not available.
history::URLDatabase* url_db = history_service_->InMemoryDatabase();
if (!url_db)
return;
auto results = url_db->GetMostRecentNormalizedKeywordSearchTerms(
template_url_service_->GetDefaultSearchProvider()->id(),
ntp_features::GetLocalHistoryRepeatableQueriesAgeThreshold());
const base::Time now = base::Time::Now();
const int kRecencyDecayUnitSec =
ntp_features::GetLocalHistoryRepeatableQueriesRecencyHalfLifeSeconds();
const double kFrequencyExponent =
ntp_features::GetLocalHistoryRepeatableQueriesFrequencyExponent();
auto CompareByFrecency = [&](const auto& a, const auto& b) {
return a.GetFrecency(now, kRecencyDecayUnitSec, kFrequencyExponent) >
b.GetFrecency(now, kRecencyDecayUnitSec, kFrequencyExponent);
};
std::sort(results.begin(), results.end(), CompareByFrecency);
for (const auto& result : results) {
RepeatableQuery repeatable_query;
repeatable_query.query = result.normalized_term;
repeatable_queries_.push_back(repeatable_query);
if (repeatable_queries_.size() >= kMaxQueries)
break;
}
NotifyObservers();
}
void RepeatableQueriesService::NotifyObservers() {
for (auto& observer : observers_) {
observer.OnRepeatableQueriesUpdated();
}
}
// Copyright 2020 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.
#ifndef COMPONENTS_SEARCH_REPEATABLE_QUERIES_REPEATABLE_QUERIES_SERVICE_H_
#define COMPONENTS_SEARCH_REPEATABLE_QUERIES_REPEATABLE_QUERIES_SERVICE_H_
#include <memory>
#include <string>
#include <vector>
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/search/repeatable_queries/repeatable_queries_service_observer.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "url/gurl.h"
class SearchProviderObserver;
class TemplateURLService;
namespace history {
class HistoryService;
} // namespace history
namespace network {
class SimpleURLLoader;
class SharedURLLoaderFactory;
} // namespace network
namespace signin {
class IdentityManager;
} // namespace signin
// Represents a repeatable query suggestion.
class RepeatableQuery {
public:
RepeatableQuery() = default;
~RepeatableQuery() = default;
bool operator==(const RepeatableQuery& other) const {
return query == other.query && deletion_url == other.deletion_url;
}
bool operator!=(const RepeatableQuery& other) const {
return !(this == &other);
}
// Repeatable query suggestion.
base::string16 query;
// The endpoint used for deleting the query suggestion on the server.
// Populated for server provided queries only.
std::string deletion_url;
};
// Provides repeatable query suggestions to be shown in the NTP Most Visited
// tiles when Google is the default search provider. The repeatable queries are
// requested from the server for signed-in users and extracted from the URL
// database for unauthenticated users.
class RepeatableQueriesService : public KeyedService {
public:
RepeatableQueriesService(
signin::IdentityManager* identity_manager,
history::HistoryService* history_service,
TemplateURLService* template_url_service,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const GURL& request_initiator_url);
~RepeatableQueriesService() override;
RepeatableQueriesService(const RepeatableQueriesService&) = delete;
RepeatableQueriesService& operator=(const RepeatableQueriesService&) = delete;
// KeyedService:
void Shutdown() override;
// Returns the currently cached repeatable query suggestions, if any.
const std::vector<RepeatableQuery>& repeatable_queries() const;
// If Google is the default search provider, asynchronously requests
// repeatable query suggestions from the server for signed-in users and
// synchronously extracts them from the URL database for
// unauthenticated users. Regardless of success, observers are notified via
// RepeatableQueriesServiceObserver::OnRepeatableQueriesUpdated.
void Refresh();
// Add/remove observers.
void AddObserver(RepeatableQueriesServiceObserver* observer);
void RemoveObserver(RepeatableQueriesServiceObserver* observer);
protected:
class SigninObserver;
virtual SigninObserver* signin_observer();
virtual SearchProviderObserver* search_provider_observer();
// Called when the default search provider changes.
void SearchProviderChanged();
// Called when the signin status changes.
void SigninStatusChanged();
// Returns the server request URL.
GURL GetRequestURL();
private:
// Requests repeatable queries from the server. Called for signed-in users.
void GetRepeatableQueriesFromServer();
void RepeatableQueriesResponseLoaded(std::unique_ptr<std::string> response);
void RepeatableQueriesParsed(data_decoder::DataDecoder::ValueOrError result);
// Queries the in-memory URLDatabase for the repeatable queries submitted
// to the default search provider. Called for unauthenticated users.
void GetRepeatableQueriesFromURLDatabase();
void NotifyObservers();
history::HistoryService* history_service_;
TemplateURLService* template_url_service_;
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
const GURL request_initiator_url_;
std::unique_ptr<SigninObserver> signin_observer_;
std::unique_ptr<SearchProviderObserver> search_provider_observer_;
base::ObserverList<RepeatableQueriesServiceObserver, true> observers_;
std::vector<RepeatableQuery> repeatable_queries_;
std::unique_ptr<network::SimpleURLLoader> simple_loader_;
base::WeakPtrFactory<RepeatableQueriesService> weak_ptr_factory_{this};
};
#endif // COMPONENTS_SEARCH_REPEATABLE_QUERIES_REPEATABLE_QUERIES_SERVICE_H_
// Copyright 2020 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.
#ifndef COMPONENTS_SEARCH_REPEATABLE_QUERIES_REPEATABLE_QUERIES_SERVICE_OBSERVER_H_
#define COMPONENTS_SEARCH_REPEATABLE_QUERIES_REPEATABLE_QUERIES_SERVICE_OBSERVER_H_
// Observer class for the RepeatableQueriesService.
class RepeatableQueriesServiceObserver : public base::CheckedObserver {
public:
// Called after a Refresh() call on the service, either directly or as a
// result of default search provider or signin status change. Note that this
// is called after each Refresh(), even if the network request failed, or if
// it didn't result in an actual change to the cached data. Observers can get
// the repeatable queries via RepeatableQueriesService::repeatable_queries().
virtual void OnRepeatableQueriesUpdated() = 0;
// Called when the service is shutting down allowing the observers to
// unregister themselves and clear references to the service.
virtual void OnRepeatableQueriesServiceShuttingDown() {}
};
#endif // COMPONENTS_SEARCH_REPEATABLE_QUERIES_REPEATABLE_QUERIES_SERVICE_OBSERVER_H_
// Copyright 2020 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/search/repeatable_queries/repeatable_queries_service.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/cancelable_callback.h"
#include "base/files/scoped_temp_dir.h"
#include "base/optional.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/url_database.h"
#include "components/history/core/test/history_service_test_util.h"
#include "components/omnibox/browser/in_memory_url_index.h"
#include "components/omnibox/browser/in_memory_url_index_test_util.h"
#include "components/search/search.h"
#include "components/search/search_provider_observer.h"
#include "components/search_engines/template_url_service.h"
#include "components/signin/public/base/test_signin_client.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using base::Time;
using base::TimeDelta;
namespace {
std::string GoodServerResponse() {
return R"()]}'
[
"",
[
"server query 1",
"server query 2",
"server query 3"
],
[],
[],
{
"google:suggestdetail":[
{
"du":"/delete?server+query+1"
},
{
"du":"/delete?server+query+2"
},
{
"du":"/delete?server+query+3"
}
]
}
])";
}
std::string BadServerResponse() {
return R"()]}'
[
"",
[
"server query 1",
"server query 2",
"server query 3"
],
[],
[],
{
"google:suggestdetail":[
{
"du":"/delete?server+query+1"
},
{
"du":"/delete?server+query+2"
},
]
}
])";
}
// Used to populate the URLDatabase.
struct TestURLData {
const TemplateURL* search_provider;
std::string search_terms;
int age_in_seconds;
int visit_count = 1;
std::string title = "";
int typed_count = 1;
bool hidden = false;
};
} // namespace
class MockSearchProviderObserver : public SearchProviderObserver {
public:
MockSearchProviderObserver()
: SearchProviderObserver(/*template_url_service=*/nullptr,
base::DoNothing::Repeatedly()) {}
~MockSearchProviderObserver() override = default;
MOCK_METHOD0(is_google, bool());
};
class TestRepeatableQueriesService : public RepeatableQueriesService {
public:
TestRepeatableQueriesService(
signin::IdentityManager* identity_manager,
history::HistoryService* history_service,
TemplateURLService* template_url_service,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const GURL& request_initiator_url)
: RepeatableQueriesService(identity_manager,
history_service,
template_url_service,
std::move(url_loader_factory),
request_initiator_url) {}
~TestRepeatableQueriesService() override = default;
MockSearchProviderObserver* search_provider_observer() override {
return &search_provider_observer_;
}
GURL GetRequestURL() { return RepeatableQueriesService::GetRequestURL(); }
void SearchProviderChanged() {
RepeatableQueriesService::SearchProviderChanged();
}
void SigninStatusChanged() {
RepeatableQueriesService::SigninStatusChanged();
}
testing::NiceMock<MockSearchProviderObserver> search_provider_observer_;
};
class RepeatableQueriesServiceTest : public ::testing::Test,
public RepeatableQueriesServiceObserver {
public:
RepeatableQueriesServiceTest() = default;
~RepeatableQueriesServiceTest() override = default;
void SetUp() override {
bookmark_model_ = bookmarks::TestBookmarkClient::CreateModel();
CHECK(history_dir_.CreateUniqueTempDir());
history_service_ = history::CreateHistoryService(
history_dir_.GetPath(), /*create_history_db=*/true);
in_memory_url_index_ = std::make_unique<InMemoryURLIndex>(
bookmark_model_.get(), history_service_.get(), nullptr,
history_dir_.GetPath(), SchemeSet());
in_memory_url_index_->Init();
template_url_service_ = std::make_unique<TemplateURLService>(nullptr, 0);
// Add the fallback default search provider to the TemplateURLService so
// that it gets a valid unique identifier. Make the newly added provider the
// user selected default search provider.
TemplateURL* default_provider = template_url_service_->Add(
std::make_unique<TemplateURL>(default_search_provider()->data()));
template_url_service_->SetUserSelectedDefaultSearchProvider(
default_provider);
// Verify that Google is the default search provider.
EXPECT_TRUE(
search::DefaultSearchProviderIsGoogle(template_url_service_.get()));
identity_env_ = std::make_unique<signin::IdentityTestEnvironment>(
&test_url_loader_factory_);
identity_env_->MakePrimaryAccountAvailable("example@gmail.com");
identity_env_->SetAutomaticIssueOfAccessTokens(true);
service_ = std::make_unique<TestRepeatableQueriesService>(
identity_env_->identity_manager(), history_service_.get(),
template_url_service_.get(),
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_),
GURL());
EXPECT_TRUE(service_->repeatable_queries().empty());
service_->AddObserver(this);
}
void TearDown() override {
// RepeatableQueriesService must be explicitly shut down so that its
// observers can unregister.
service_->Shutdown();
// InMemoryURLIndex must be explicitly shut down or it will DCHECK() in
// its destructor.
in_memory_url_index_->Shutdown();
// Needed to prevent leaks due to posted history index rebuild task.
task_environment_.RunUntilIdle();
}
const TemplateURL* default_search_provider() {
return template_url_service_->GetDefaultSearchProvider();
}
network::TestURLLoaderFactory* test_url_loader_factory() {
return &test_url_loader_factory_;
}
TestRepeatableQueriesService* service() { return service_.get(); }
void set_service_is_done(bool is_done) { service_is_done_ = is_done; }
void SignIn() {
AccountInfo account_info =
identity_env_->MakeAccountAvailable("test@email.com");
identity_env_->SetCookieAccounts({{account_info.email, account_info.gaia}});
}
void SignOut() { identity_env_->SetCookieAccounts({}); }
void RefreshAndMaybeWaitForService() {
service_is_done_ = false;
service_->Refresh();
MaybeWaitForService();
}
void MaybeWaitForService() {
if (!service_is_done_) {
service_run_loop_ = std::make_unique<base::RunLoop>();
// Quits in OnRepeatableQueriesUpdated when the service is done.
service_run_loop_->Run();
}
}
// Fills the URLDatabase with search URLs created using the provided data.
void FillURLDatabase(const std::vector<TestURLData>& url_data_list) {
const Time now = Time::Now();
for (const auto& entry : url_data_list) {
TemplateURLRef::SearchTermsArgs search_terms_args(
base::UTF8ToUTF16(entry.search_terms));
const auto& search_terms_data =
template_url_service_->search_terms_data();
std::string search_url =
entry.search_provider->url_ref().ReplaceSearchTerms(
search_terms_args, search_terms_data);
history_service_->AddPageWithDetails(
GURL(search_url), base::UTF8ToUTF16(entry.title), entry.visit_count,
entry.typed_count, now - TimeDelta::FromSeconds(entry.age_in_seconds),
entry.hidden, history::SOURCE_BROWSED);
history_service_->SetKeywordSearchTermsForURL(
GURL(search_url), entry.search_provider->id(),
base::UTF8ToUTF16(entry.search_terms));
WaitForHistoryService();
}
}
// Waits for history::HistoryService's async operations.
void WaitForHistoryService() {
history::BlockUntilHistoryProcessesPendingRequests(history_service_.get());
// MemoryURLIndex schedules tasks to rebuild its index on the history
// thread. Block here to make sure they are complete.
BlockUntilInMemoryURLIndexIsRefreshed(in_memory_url_index_.get());
}
private:
base::test::TaskEnvironment task_environment_;
std::unique_ptr<base::RunLoop> service_run_loop_;
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model_;
std::unique_ptr<history::HistoryService> history_service_;
std::unique_ptr<InMemoryURLIndex> in_memory_url_index_;
base::ScopedTempDir history_dir_;
std::unique_ptr<TemplateURLService> template_url_service_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
network::TestURLLoaderFactory test_url_loader_factory_;
std::unique_ptr<signin::IdentityTestEnvironment> identity_env_;
std::unique_ptr<TestRepeatableQueriesService> service_;
bool service_is_done_ = false;
// RepeatableQueriesServiceObserver
void OnRepeatableQueriesUpdated() override;
void OnRepeatableQueriesServiceShuttingDown() override;
};
void RepeatableQueriesServiceTest::OnRepeatableQueriesUpdated() {
service_is_done_ = true;
if (service_run_loop_) {
service_run_loop_->Quit();
}
}
void RepeatableQueriesServiceTest::OnRepeatableQueriesServiceShuttingDown() {
service_->RemoveObserver(this);
}
TEST_F(RepeatableQueriesServiceTest, SignedIn) {
SignIn();
test_url_loader_factory()->AddResponse(service()->GetRequestURL().spec(),
GoodServerResponse());
EXPECT_CALL(*service()->search_provider_observer(), is_google())
.WillOnce(testing::Return(true));
// Request a refresh.
RefreshAndMaybeWaitForService();
// The first two server suggestions are kept as repeatable queries.
std::vector<RepeatableQuery> expected_server_queries{
{base::ASCIIToUTF16("server query 1"), "/delete?server+query+1"},
{base::ASCIIToUTF16("server query 2"), "/delete?server+query+2"}};
EXPECT_EQ(expected_server_queries, service()->repeatable_queries());
}
TEST_F(RepeatableQueriesServiceTest, SignedIn_BadResponse) {
SignIn();
test_url_loader_factory()->AddResponse(service()->GetRequestURL().spec(),
GoodServerResponse());
EXPECT_CALL(*service()->search_provider_observer(), is_google())
.WillRepeatedly(testing::Return(true));
// Request a refresh.
RefreshAndMaybeWaitForService();
std::vector<RepeatableQuery> expected_server_queries{
{base::ASCIIToUTF16("server query 1"), "/delete?server+query+1"},
{base::ASCIIToUTF16("server query 2"), "/delete?server+query+2"}};
EXPECT_EQ(expected_server_queries, service()->repeatable_queries());
test_url_loader_factory()->AddResponse(service()->GetRequestURL().spec(),
BadServerResponse());
// Request a refresh.
RefreshAndMaybeWaitForService();
// Cached data is cleared.
EXPECT_TRUE(service()->repeatable_queries().empty());
}
TEST_F(RepeatableQueriesServiceTest, SignedIn_ErrorResponse) {
SignIn();
test_url_loader_factory()->AddResponse(service()->GetRequestURL().spec(),
GoodServerResponse());
EXPECT_CALL(*service()->search_provider_observer(), is_google())
.WillRepeatedly(testing::Return(true));
// Request a refresh.
RefreshAndMaybeWaitForService();
std::vector<RepeatableQuery> expected_server_queries{
{base::ASCIIToUTF16("server query 1"), "/delete?server+query+1"},
{base::ASCIIToUTF16("server query 2"), "/delete?server+query+2"}};
EXPECT_EQ(expected_server_queries, service()->repeatable_queries());
test_url_loader_factory()->AddResponse(
service()->GetRequestURL(), network::mojom::URLResponseHead::New(),
std::string(), network::URLLoaderCompletionStatus(net::HTTP_NOT_FOUND));
// Request a refresh.
RefreshAndMaybeWaitForService();
// Cached data is kept.
EXPECT_EQ(expected_server_queries, service()->repeatable_queries());
}
TEST_F(RepeatableQueriesServiceTest, SignedIn_DefaultSearchProviderChanged) {
SignIn();
test_url_loader_factory()->AddResponse(service()->GetRequestURL().spec(),
GoodServerResponse());
EXPECT_CALL(*service()->search_provider_observer(), is_google())
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(false));
// Request a refresh.
RefreshAndMaybeWaitForService();
std::vector<RepeatableQuery> expected_server_queries{
{base::ASCIIToUTF16("server query 1"), "/delete?server+query+1"},
{base::ASCIIToUTF16("server query 2"), "/delete?server+query+2"}};
EXPECT_EQ(expected_server_queries, service()->repeatable_queries());
set_service_is_done(false);
// Simulate DSP change. Requests a refresh.
service()->SearchProviderChanged();
MaybeWaitForService();
// Cached data is cleared.
EXPECT_TRUE(service()->repeatable_queries().empty());
}
TEST_F(RepeatableQueriesServiceTest, SignedIn_SigninStatusChanged) {
SignIn();
test_url_loader_factory()->AddResponse(service()->GetRequestURL().spec(),
GoodServerResponse());
EXPECT_CALL(*service()->search_provider_observer(), is_google())
.WillRepeatedly(testing::Return(true));
// Request a refresh.
RefreshAndMaybeWaitForService();
std::vector<RepeatableQuery> expected_server_queries{
{base::ASCIIToUTF16("server query 1"), "/delete?server+query+1"},
{base::ASCIIToUTF16("server query 2"), "/delete?server+query+2"}};
EXPECT_EQ(expected_server_queries, service()->repeatable_queries());
int original_query_age =
history::kAutocompleteDuplicateVisitIntervalThreshold.InSeconds() + 3;
FillURLDatabase({
// Issued far enough from the original query; won't be ignored:
{default_search_provider(), "more recent local query",
/*age_in_seconds=*/0},
// Issued far enough from the original query; won't be ignored:
{default_search_provider(), "less recent local query",
/*age_in_seconds=*/1},
{default_search_provider(), "less recent local query",
/*age_in_seconds=*/original_query_age},
{default_search_provider(), "more recent local query",
/*age_in_seconds=*/original_query_age},
});
set_service_is_done(false);
SignOut(); // Requests a refresh.
MaybeWaitForService();
// Cached data is updated to local results.
std::vector<RepeatableQuery> expected_local_queries{
{base::ASCIIToUTF16("more recent local query"), ""},
{base::ASCIIToUTF16("less recent local query"), ""}};
EXPECT_EQ(expected_local_queries, service()->repeatable_queries());
}
TEST_F(RepeatableQueriesServiceTest, SignedOut_DefaultSearchProviderChanged) {
int original_query_age =
history::kAutocompleteDuplicateVisitIntervalThreshold.InSeconds() + 3;
FillURLDatabase({
// Issued far enough from the original query; won't be ignored:
{default_search_provider(), "more recent local query",
/*age_in_seconds=*/0},
// Issued far enough from the original query; won't be ignored:
{default_search_provider(), "less recent local query",
/*age_in_seconds=*/1},
{default_search_provider(), "less recent local query",
/*age_in_seconds=*/original_query_age},
{default_search_provider(), "more recent local query",
/*age_in_seconds=*/original_query_age},
});
EXPECT_CALL(*service()->search_provider_observer(), is_google())
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(false));
// Request a refresh.
RefreshAndMaybeWaitForService();
std::vector<RepeatableQuery> expected_local_queries{
{base::ASCIIToUTF16("more recent local query"), ""},
{base::ASCIIToUTF16("less recent local query"), ""}};
EXPECT_EQ(expected_local_queries, service()->repeatable_queries());
set_service_is_done(false);
// Simulate DSP change. Requests a refresh.
service()->SearchProviderChanged();
MaybeWaitForService();
// Cached data is cleared.
EXPECT_TRUE(service()->repeatable_queries().empty());
}
TEST_F(RepeatableQueriesServiceTest, SignedOut_SigninStatusChanged) {
int original_query_age =
history::kAutocompleteDuplicateVisitIntervalThreshold.InSeconds() + 3;
FillURLDatabase({
// Issued far enough from the original query; won't be ignored:
{default_search_provider(), "more recent local query",
/*age_in_seconds=*/0},
// Issued far enough from the original query; won't be ignored:
{default_search_provider(), "less recent local query",
/*age_in_seconds=*/1},
{default_search_provider(), "less recent local query",
/*age_in_seconds=*/original_query_age},
{default_search_provider(), "more recent local query",
/*age_in_seconds=*/original_query_age},
});
EXPECT_CALL(*service()->search_provider_observer(), is_google())
.WillRepeatedly(testing::Return(true));
// Request a refresh.
RefreshAndMaybeWaitForService();
std::vector<RepeatableQuery> expected_local_queries{
{base::ASCIIToUTF16("more recent local query"), ""},
{base::ASCIIToUTF16("less recent local query"), ""}};
EXPECT_EQ(expected_local_queries, service()->repeatable_queries());
test_url_loader_factory()->AddResponse(service()->GetRequestURL().spec(),
GoodServerResponse());
set_service_is_done(false);
SignIn(); // Requests a refresh.
MaybeWaitForService();
// Cached data is updated to server results.
std::vector<RepeatableQuery> expected_server_queries{
{base::ASCIIToUTF16("server query 1"), "/delete?server+query+1"},
{base::ASCIIToUTF16("server query 2"), "/delete?server+query+2"}};
EXPECT_EQ(expected_server_queries, service()->repeatable_queries());
}
...@@ -10,21 +10,25 @@ SearchProviderObserver::SearchProviderObserver(TemplateURLService* service, ...@@ -10,21 +10,25 @@ SearchProviderObserver::SearchProviderObserver(TemplateURLService* service,
: service_(service), : service_(service),
is_google_(search::DefaultSearchProviderIsGoogle(service_)), is_google_(search::DefaultSearchProviderIsGoogle(service_)),
callback_(std::move(callback)) { callback_(std::move(callback)) {
DCHECK(service_); if (service_) {
service_->AddObserver(this); service_observer_.Add(service_);
}
} }
SearchProviderObserver::~SearchProviderObserver() { SearchProviderObserver::~SearchProviderObserver() = default;
if (service_)
service_->RemoveObserver(this); bool SearchProviderObserver::is_google() {
return is_google_;
} }
void SearchProviderObserver::OnTemplateURLServiceChanged() { void SearchProviderObserver::OnTemplateURLServiceChanged() {
DCHECK(service_);
is_google_ = search::DefaultSearchProviderIsGoogle(service_); is_google_ = search::DefaultSearchProviderIsGoogle(service_);
callback_.Run(); callback_.Run();
} }
void SearchProviderObserver::OnTemplateURLServiceShuttingDown() { void SearchProviderObserver::OnTemplateURLServiceShuttingDown() {
service_->RemoveObserver(this); DCHECK(service_);
service_observer_.Remove(service_);
service_ = nullptr; service_ = nullptr;
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#ifndef COMPONENTS_SEARCH_SEARCH_PROVIDER_OBSERVER_H_ #ifndef COMPONENTS_SEARCH_SEARCH_PROVIDER_OBSERVER_H_
#define COMPONENTS_SEARCH_SEARCH_PROVIDER_OBSERVER_H_ #define COMPONENTS_SEARCH_SEARCH_PROVIDER_OBSERVER_H_
#include "base/scoped_observer.h"
#include "components/search_engines/template_url_service.h" #include "components/search_engines/template_url_service.h"
#include "components/search_engines/template_url_service_observer.h" #include "components/search_engines/template_url_service_observer.h"
...@@ -15,17 +16,18 @@ class SearchProviderObserver : public TemplateURLServiceObserver { ...@@ -15,17 +16,18 @@ class SearchProviderObserver : public TemplateURLServiceObserver {
public: public:
explicit SearchProviderObserver(TemplateURLService* service, explicit SearchProviderObserver(TemplateURLService* service,
base::RepeatingClosure callback); base::RepeatingClosure callback);
~SearchProviderObserver() override; ~SearchProviderObserver() override;
bool is_google() { return is_google_; } virtual bool is_google();
TemplateURLService* template_url_service() { return service_; }
private: private:
// TemplateURLServiceObserver: // TemplateURLServiceObserver:
void OnTemplateURLServiceChanged() override; void OnTemplateURLServiceChanged() override;
void OnTemplateURLServiceShuttingDown() override; void OnTemplateURLServiceShuttingDown() override;
ScopedObserver<TemplateURLService, TemplateURLServiceObserver>
service_observer_{this};
// May be nullptr in tests.
TemplateURLService* service_; TemplateURLService* service_;
bool is_google_; bool is_google_;
base::RepeatingClosure callback_; base::RepeatingClosure callback_;
......
...@@ -276,6 +276,7 @@ Refer to README.md for content description and update process. ...@@ -276,6 +276,7 @@ Refer to README.md for content description and update process.
<item id="remoting_telemetry_log_writer" added_in_milestone="86" hash_code="107268760" type="0" content_hash_code="81741595" os_list="linux,windows" file_path="remoting/base/telemetry_log_writer.cc"/> <item id="remoting_telemetry_log_writer" added_in_milestone="86" hash_code="107268760" type="0" content_hash_code="81741595" os_list="linux,windows" file_path="remoting/base/telemetry_log_writer.cc"/>
<item id="render_view_context_menu" added_in_milestone="62" hash_code="25844439" type="0" content_hash_code="69471170" os_list="linux,windows" file_path="chrome/browser/renderer_context_menu/render_view_context_menu.cc"/> <item id="render_view_context_menu" added_in_milestone="62" hash_code="25844439" type="0" content_hash_code="69471170" os_list="linux,windows" file_path="chrome/browser/renderer_context_menu/render_view_context_menu.cc"/>
<item id="renderer_initiated_download" added_in_milestone="62" hash_code="116443055" type="0" content_hash_code="37846436" os_list="linux,windows" file_path="content/browser/renderer_host/render_frame_host_impl.cc"/> <item id="renderer_initiated_download" added_in_milestone="62" hash_code="116443055" type="0" content_hash_code="37846436" os_list="linux,windows" file_path="content/browser/renderer_host/render_frame_host_impl.cc"/>
<item id="repeatable_queries_service" added_in_milestone="88" hash_code="5394442" type="0" content_hash_code="115294794" os_list="linux,windows" file_path="components/search/repeatable_queries/repeatable_queries_service.cc"/>
<item id="reporting" added_in_milestone="62" hash_code="109891200" type="0" content_hash_code="125758928" os_list="linux,windows" file_path="net/reporting/reporting_uploader.cc"/> <item id="reporting" added_in_milestone="62" hash_code="109891200" type="0" content_hash_code="125758928" os_list="linux,windows" file_path="net/reporting/reporting_uploader.cc"/>
<item id="resource_dispatcher_host" added_in_milestone="62" hash_code="81157007" type="0" deprecated="2019-07-30" content_hash_code="35725167" file_path=""/> <item id="resource_dispatcher_host" added_in_milestone="62" hash_code="81157007" type="0" deprecated="2019-07-30" content_hash_code="35725167" file_path=""/>
<item id="resource_prefetch" added_in_milestone="62" hash_code="110815970" type="0" deprecated="2018-02-28" content_hash_code="39251261" file_path=""/> <item id="resource_prefetch" added_in_milestone="62" hash_code="110815970" type="0" deprecated="2018-02-28" content_hash_code="39251261" file_path=""/>
......
...@@ -392,6 +392,7 @@ hidden="true" so that these annotations don't show up in the document. ...@@ -392,6 +392,7 @@ hidden="true" so that these annotations don't show up in the document.
<traffic_annotation unique_id="popular_sites_fetch"/> <traffic_annotation unique_id="popular_sites_fetch"/>
<traffic_annotation unique_id="search_suggest_service"/> <traffic_annotation unique_id="search_suggest_service"/>
<traffic_annotation unique_id="remote_suggestions_provider"/> <traffic_annotation unique_id="remote_suggestions_provider"/>
<traffic_annotation unique_id="repeatable_queries_service"/>
<traffic_annotation unique_id="promo_service"/> <traffic_annotation unique_id="promo_service"/>
<traffic_annotation unique_id="task_module_service"/> <traffic_annotation unique_id="task_module_service"/>
</sender> </sender>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment