blob: 6b071b290944f34dccfa26922daf6fa18b7ae545 [file] [log] [blame]
// 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/new_tab_page/modules/task_module/task_module_service.h"
#include "base/containers/contains.h"
#include "base/containers/cxx20_erase.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/google/core/common/google_util.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/search/ntp_features.h"
#include "components/variations/net/variations_http_headers.h"
#include "net/base/url_util.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"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
namespace {
const char kXSSIResponsePreamble[] = ")]}'";
const char* GetPath(task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return "/async/newtab_recipe_tasks";
case task_module::mojom::TaskModuleType::kShopping:
return "/async/newtab_shopping_tasks";
}
}
// We return a reference so that base::FeatureList::CheckFeatureIdentity
// succeeds.
const base::Feature& GetFeature(
task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return ntp_features::kNtpRecipeTasksModule;
case task_module::mojom::TaskModuleType::kShopping:
return ntp_features::kNtpShoppingTasksModule;
}
}
const char* GetDataParam(task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return ntp_features::kNtpRecipeTasksModuleDataParam;
case task_module::mojom::TaskModuleType::kShopping:
return ntp_features::kNtpShoppingTasksModuleDataParam;
}
}
const char* GetCacheMaxAgeSParam(
task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return ntp_features::kNtpRecipeTasksModuleCacheMaxAgeSParam;
case task_module::mojom::TaskModuleType::kShopping:
return ntp_features::kNtpShoppingTasksModuleCacheMaxAgeSParam;
}
}
GURL GetApiUrl(task_module::mojom::TaskModuleType task_module_type,
const std::string& application_locale) {
GURL google_base_url = google_util::CommandLineGoogleBaseURL();
if (!google_base_url.is_valid()) {
google_base_url = GURL(google_util::kGoogleHomepageURL);
}
auto url = net::AppendQueryParameter(
google_base_url.Resolve(GetPath(task_module_type)), "hl",
application_locale);
if (base::GetFieldTrialParamValueByFeature(GetFeature(task_module_type),
GetDataParam(task_module_type)) ==
"fake") {
url = google_util::AppendToAsyncQueryParam(url, "fake_data", "1");
}
int cache_max_age_s = base::GetFieldTrialParamByFeatureAsInt(
GetFeature(task_module_type), GetCacheMaxAgeSParam(task_module_type), 0);
if (cache_max_age_s > 0) {
url = google_util::AppendToAsyncQueryParam(
url, "cache_max_age_s", base::NumberToString(cache_max_age_s));
}
return url;
}
const char* GetTasksKey(task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return "recipe_tasks";
case task_module::mojom::TaskModuleType::kShopping:
return "shopping_tasks";
}
}
const char* GetTaskItemsKey(
task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return "recipes";
case task_module::mojom::TaskModuleType::kShopping:
return "products";
}
}
const char* GetTaskItemsName(
task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return "Recipes";
case task_module::mojom::TaskModuleType::kShopping:
return "Products";
}
}
const char* GetDismissedTasksPrefName(
task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return "NewTabPage.DismissedRecipeTasks";
case task_module::mojom::TaskModuleType::kShopping:
return "NewTabPage.DismissedShoppingTasks";
}
}
const char* GetModuleName(task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return "RecipeTasks";
case task_module::mojom::TaskModuleType::kShopping:
return "ShoppingTasks";
}
}
std::string GetViewedItemText(int viewed_timestamp) {
// GWS timestamps are relative to the Unix Epoch.
auto viewed_time = base::Time::UnixEpoch() + base::Seconds(viewed_timestamp);
auto viewed_delta = base::Time::Now() - viewed_time;
// Viewing items in the future is not supported. Assume the item was viewed
// today to account for small shifts between the local and server clock.
if (viewed_delta.InSeconds() < 0) {
viewed_delta = base::TimeDelta();
}
if (viewed_delta.InDays() < 1) {
return l10n_util::GetStringUTF8(
IDS_NTP_MODULES_STATEFUL_TASKS_VIEWED_TODAY);
}
return base::UTF16ToUTF8(l10n_util::GetStringFUTF16(
IDS_NTP_MODULES_STATEFUL_TASKS_VIEWED_AGO,
ui::TimeFormat::SimpleWithMonthAndYear(
ui::TimeFormat::Format::FORMAT_ELAPSED,
ui::TimeFormat::Length::LENGTH_LONG, viewed_delta,
/*use_month_and_year=*/true)));
}
std::string GetRecommendedItemText(
task_module::mojom::TaskModuleType task_module_type) {
switch (task_module_type) {
case task_module::mojom::TaskModuleType::kRecipe:
return l10n_util::GetStringUTF8(IDS_NTP_MODULES_RECIPE_TASKS_RECOMMENDED);
case task_module::mojom::TaskModuleType::kShopping:
return l10n_util::GetStringUTF8(IDS_NTP_MODULES_SHOPPING_TASKS_RELATED);
}
}
} // namespace
TaskModuleService::TaskModuleService(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
Profile* profile,
const std::string& application_locale)
: profile_(profile),
url_loader_factory_(url_loader_factory),
application_locale_(application_locale) {}
TaskModuleService::~TaskModuleService() = default;
// static
void TaskModuleService::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterListPref(
GetDismissedTasksPrefName(task_module::mojom::TaskModuleType::kRecipe));
registry->RegisterListPref(
GetDismissedTasksPrefName(task_module::mojom::TaskModuleType::kShopping));
}
void TaskModuleService::Shutdown() {}
void TaskModuleService::GetPrimaryTask(
task_module::mojom::TaskModuleType task_module_type,
TaskModuleCallback callback) {
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("task_module_service", R"(
semantics {
sender: "Task Module Service"
description: "This service downloads tasks, which is information "
"related to the user's currently active search journeys such as "
"visited and recommended task items, such as products to purchase "
"or recipes. "
"Tasks will be displayed on the new tab page to help the user to "
"continue their search journey. Tasks are queried on every new tab "
"page load."
trigger:
"Displaying the new tab page on Desktop, if Google is the "
"configured search provider and the user is signed in."
data: "Credentials if user is signed in."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting:
"Users can control this feature via selecting a non-Google default "
"search engine in Chrome settings under 'Search Engine' or by "
"signing out."
chrome_policy {
DefaultSearchProviderEnabled {
policy_options {mode: MANDATORY}
DefaultSearchProviderEnabled: false
}
BrowserSignin {
policy_options {mode: MANDATORY}
BrowserSignin: 0
}
}
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = GetApiUrl(task_module_type, application_locale_);
resource_request->credentials_mode =
network::mojom::CredentialsMode::kInclude;
resource_request->request_initiator =
url::Origin::Create(GURL(chrome::kChromeUINewTabURL));
variations::AppendVariationsHeaderUnknownSignedIn(
resource_request->url,
/* Modules are only shown in non-incognito. */
variations::InIncognito::kNo, resource_request.get());
loaders_.push_back(network::SimpleURLLoader::Create(
std::move(resource_request), traffic_annotation));
loaders_.back()->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&TaskModuleService::OnDataLoaded,
weak_ptr_factory_.GetWeakPtr(), task_module_type,
loaders_.back().get(), std::move(callback)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void TaskModuleService::DismissTask(
task_module::mojom::TaskModuleType task_module_type,
const std::string& task_name) {
ListPrefUpdate update(profile_->GetPrefs(),
GetDismissedTasksPrefName(task_module_type));
base::Value task_name_value(task_name);
if (!base::Contains(update->GetList(), task_name_value))
update->Append(std::move(task_name_value));
}
void TaskModuleService::RestoreTask(
task_module::mojom::TaskModuleType task_module_type,
const std::string& task_name) {
ListPrefUpdate update(profile_->GetPrefs(),
GetDismissedTasksPrefName(task_module_type));
update->EraseListValue(base::Value(task_name));
}
void TaskModuleService::OnDataLoaded(
task_module::mojom::TaskModuleType task_module_type,
network::SimpleURLLoader* loader,
TaskModuleCallback callback,
std::unique_ptr<std::string> response) {
auto net_error = loader->NetError();
bool loaded_from_cache = loader->LoadedFromCache();
base::EraseIf(loaders_, [loader](const auto& target) {
return loader == target.get();
});
if (!loaded_from_cache) {
base::UmaHistogramSparse(
"NewTabPage.Modules.DataRequest",
base::PersistentHash(GetTasksKey(task_module_type)));
}
if (net_error != net::OK || !response) {
std::move(callback).Run(nullptr);
return;
}
if (base::StartsWith(*response, kXSSIResponsePreamble,
base::CompareCase::SENSITIVE)) {
*response = response->substr(strlen(kXSSIResponsePreamble));
}
data_decoder::DataDecoder::ParseJsonIsolated(
*response, base::BindOnce(&TaskModuleService::OnJsonParsed,
weak_ptr_factory_.GetWeakPtr(),
task_module_type, std::move(callback)));
}
void TaskModuleService::OnJsonParsed(
task_module::mojom::TaskModuleType task_module_type,
TaskModuleCallback callback,
data_decoder::DataDecoder::ValueOrError result) {
if (!result.value) {
std::move(callback).Run(nullptr);
return;
}
// We receive a list of tasks ordered from highest to lowest priority. We only
// support showing a single task though. Therefore, pick the first task.
auto* tasks = result.value->FindListPath(
base::StringPrintf("update.%s", GetTasksKey(task_module_type)));
if (!tasks || tasks->GetList().size() == 0) {
std::move(callback).Run(nullptr);
return;
}
for (const auto& task : tasks->GetList()) {
auto* title = task.FindStringPath("title");
auto* task_name = task.FindStringPath("task_name");
auto* task_items = task.FindListPath(GetTaskItemsKey(task_module_type));
auto* related_searches = task.FindListPath("related_searches");
if (!title || !task_name || !task_items || !related_searches ||
task_items->GetList().size() == 0) {
continue;
}
if (IsTaskDismissed(task_module_type, *task_name)) {
continue;
}
std::vector<task_module::mojom::TaskItemPtr> mojo_task_items;
for (const auto& task_item : task_items->GetList()) {
auto* name = task_item.FindStringPath("name");
auto* image_url = task_item.FindStringPath("image_url");
auto* price = task_item.FindStringPath("price");
auto viewed_timestamp = task_item.FindIntPath("viewed_timestamp.seconds");
auto* site_name = task_item.FindStringPath("site_name");
auto* target_url = task_item.FindStringPath("target_url");
if (!name || !image_url || !target_url) {
continue;
}
if (task_module::mojom::TaskModuleType::kShopping == task_module_type &&
!price) {
continue;
}
auto mojom_task_item = task_module::mojom::TaskItem::New();
mojom_task_item->name = *name;
mojom_task_item->image_url = GURL(*image_url);
mojom_task_item->info = viewed_timestamp
? GetViewedItemText(*viewed_timestamp)
: GetRecommendedItemText(task_module_type);
if (task_module_type == task_module::mojom::TaskModuleType::kRecipe &&
site_name) {
mojom_task_item->site_name = *site_name;
}
mojom_task_item->target_url = GURL(*target_url);
if (task_module_type == task_module::mojom::TaskModuleType::kShopping) {
mojom_task_item->price = *price;
}
mojo_task_items.push_back(std::move(mojom_task_item));
}
std::vector<task_module::mojom::RelatedSearchPtr> mojo_related_searches;
for (const auto& related_search : related_searches->GetList()) {
auto* text = related_search.FindStringPath("text");
auto* target_url = related_search.FindStringPath("target_url");
if (!text || !target_url) {
continue;
}
auto mojo_related_search = task_module::mojom::RelatedSearch::New();
mojo_related_search->text = *text;
mojo_related_search->target_url = GURL(*target_url);
mojo_related_searches.push_back(std::move(mojo_related_search));
}
auto mojo_task = task_module::mojom::Task::New();
mojo_task->title = *title;
mojo_task->name = *task_name;
base::UmaHistogramCounts100(
base::StringPrintf("NewTabPage.%s.%sDownloadCount",
GetModuleName(task_module_type),
GetTaskItemsName(task_module_type)),
mojo_task_items.size());
mojo_task->task_items = std::move(mojo_task_items);
base::UmaHistogramCounts100(
base::StringPrintf("NewTabPage.%s.RelatedSearchDownloadCount",
GetModuleName(task_module_type)),
mojo_related_searches.size());
mojo_task->related_searches = std::move(mojo_related_searches);
std::move(callback).Run(std::move(mojo_task));
return;
}
std::move(callback).Run(nullptr);
}
bool TaskModuleService::IsTaskDismissed(
task_module::mojom::TaskModuleType task_module_type,
const std::string& task_name) {
if (base::FeatureList::IsEnabled(ntp_features::kNtpModulesRedesigned)) {
return false;
}
const base::Value* dismissed_tasks = profile_->GetPrefs()->GetList(
GetDismissedTasksPrefName(task_module_type));
DCHECK(dismissed_tasks);
return base::Contains(dismissed_tasks->GetList(), base::Value(task_name));
}