blob: ce3ab4d73a2ed69ddb6b9ebd49aff9f9624df1d3 [file] [log] [blame]
// Copyright 2017 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/doodle/doodle_service.h"
#include <utility>
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/values.h"
#include "components/data_use_measurement/core/data_use_user_data.h"
#include "components/doodle/pref_names.h"
#include "components/image_fetcher/core/image_fetcher.h"
#include "components/image_fetcher/core/request_metadata.h"
#include "components/prefs/pref_registry.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "ui/gfx/image/image.h"
namespace doodle {
namespace {
// The maximum time-to-live we'll accept; any larger values will be clamped to
// this one. This is a last resort in case the server sends bad data.
const int64_t kMaxTimeToLiveSecs = 30 * 24 * 60 * 60; // 30 days
// The default value for DoodleService::min_refresh_interval_.
const int64_t kDefaultMinRefreshIntervalSecs = 15 * 60; // 15 minutes
// The maximum number of bytes to allow in a doodle image. This limits the
// downloaded data to an amount that real images are unlikely to exceed. It is a
// safeguard against server-side errors.
const int64_t kMaxImageDownloadBytes = 1024 * 1024;
} // namespace
// static
void DoodleService::RegisterProfilePrefs(PrefRegistrySimple* pref_registry) {
pref_registry->RegisterDictionaryPref(
prefs::kCachedConfig, base::MakeUnique<base::DictionaryValue>(),
PrefRegistry::LOSSY_PREF);
pref_registry->RegisterInt64Pref(prefs::kCachedConfigExpiry, 0,
PrefRegistry::LOSSY_PREF);
}
DoodleService::DoodleService(
PrefService* pref_service,
std::unique_ptr<DoodleFetcher> fetcher,
std::unique_ptr<base::OneShotTimer> expiry_timer,
std::unique_ptr<base::Clock> clock,
std::unique_ptr<base::TickClock> tick_clock,
base::Optional<base::TimeDelta> override_min_refresh_interval,
std::unique_ptr<image_fetcher::ImageFetcher> image_fetcher)
: pref_service_(pref_service),
fetcher_(std::move(fetcher)),
expiry_timer_(std::move(expiry_timer)),
clock_(std::move(clock)),
tick_clock_(std::move(tick_clock)),
min_refresh_interval_(
override_min_refresh_interval.has_value()
? override_min_refresh_interval.value()
: base::TimeDelta::FromSeconds(kDefaultMinRefreshIntervalSecs)),
image_fetcher_(std::move(image_fetcher)) {
DCHECK(pref_service_);
DCHECK(fetcher_);
DCHECK(expiry_timer_);
DCHECK(clock_);
DCHECK(tick_clock_);
DCHECK(image_fetcher_);
image_fetcher_->SetImageDownloadLimit(kMaxImageDownloadBytes);
image_fetcher_->SetDataUseServiceName(
data_use_measurement::DataUseUserData::DOODLE);
base::Time expiry_date = base::Time::FromInternalValue(
pref_service_->GetInt64(prefs::kCachedConfigExpiry));
base::Optional<DoodleConfig> config = DoodleConfig::FromDictionary(
*pref_service_->GetDictionary(prefs::kCachedConfig), base::nullopt);
DoodleState state =
config.has_value() ? DoodleState::AVAILABLE : DoodleState::NO_DOODLE;
HandleNewConfig(state, expiry_date - clock_->Now(), config);
// If we don't have a cached doodle, immediately start a fetch, so that the
// first NTP will get it quicker.
if (state == DoodleState::NO_DOODLE) {
Refresh();
}
}
DoodleService::~DoodleService() = default;
void DoodleService::Shutdown() {}
void DoodleService::GetImage(const ImageCallback& callback) {
if (!cached_config_.has_value()) {
callback.Run(gfx::Image());
return;
}
// If there is a CTA image, that means the main image is animated. Show the
// non-animated CTA image first, and load the animated one only when the
// user requests it.
bool has_cta = cached_config_->large_cta_image.has_value();
const GURL& image_url = has_cta ? cached_config_->large_cta_image->url
: cached_config_->large_image.url;
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("doodle_service", R"(
semantics {
sender: "Doodle Service"
description:
"Downloads the Doodle image if Google is the configured search "
"provider."
trigger: "Displaying the new tab page on Android."
data: "None."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: false
setting:
"Choosing a non-Google search engine in Chromium settings under "
"'Search Engine' disables this feature."
chrome_policy {
DefaultSearchProviderEnabled {
policy_options {mode: MANDATORY}
DefaultSearchProviderEnabled: false
}
}
})");
image_fetcher_->StartOrQueueNetworkRequest(
image_url.spec(), image_url,
base::Bind(&DoodleService::ImageFetched, base::Unretained(this),
callback),
traffic_annotation);
}
void DoodleService::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void DoodleService::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void DoodleService::Refresh() {
// If we're already waiting for a fetch, don't start another one. The
// observers will get notified when the ongoing fetch returns.
if (fetcher_->IsFetchInProgress()) {
return;
}
base::TimeTicks now_ticks = tick_clock_->NowTicks();
// Check if we have passed the minimum refresh interval.
base::TimeDelta time_since_fetch = now_ticks - last_successful_fetch_;
if (time_since_fetch < min_refresh_interval_) {
RecordDownloadMetrics(OUTCOME_REFRESH_INTERVAL_NOT_PASSED,
base::TimeDelta());
for (auto& observer : observers_) {
observer.OnDoodleConfigRevalidated(/*from_cache=*/true);
}
return;
}
fetcher_->FetchDoodle(base::BindOnce(&DoodleService::DoodleFetched,
base::Unretained(this), now_ticks));
}
// static
bool DoodleService::DownloadOutcomeIsSuccess(DownloadOutcome outcome) {
switch (outcome) {
case OUTCOME_NEW_DOODLE:
case OUTCOME_REVALIDATED_DOODLE:
case OUTCOME_CHANGED_DOODLE:
case OUTCOME_NO_DOODLE:
return true;
case OUTCOME_EXPIRED:
case OUTCOME_DOWNLOAD_ERROR:
case OUTCOME_PARSING_ERROR:
case OUTCOME_REFRESH_INTERVAL_NOT_PASSED:
return false;
case OUTCOME_COUNT:
NOTREACHED();
}
return false;
}
// static
void DoodleService::RecordDownloadMetrics(DownloadOutcome outcome,
base::TimeDelta download_time) {
UMA_HISTOGRAM_ENUMERATION("Doodle.ConfigDownloadOutcome", outcome,
OUTCOME_COUNT);
if (DownloadOutcomeIsSuccess(outcome)) {
UMA_HISTOGRAM_MEDIUM_TIMES("Doodle.ConfigDownloadTime", download_time);
}
}
// static
DoodleService::DownloadOutcome DoodleService::DetermineDownloadOutcome(
const base::Optional<DoodleConfig>& old_config,
const base::Optional<DoodleConfig>& new_config,
DoodleState state,
bool expired) {
// First, handle error cases: *_ERROR or EXPIRED override other outcomes.
switch (state) {
case DoodleState::AVAILABLE:
if (expired) {
return OUTCOME_EXPIRED;
}
break;
case DoodleState::NO_DOODLE:
break;
case DoodleState::DOWNLOAD_ERROR:
return OUTCOME_DOWNLOAD_ERROR;
case DoodleState::PARSING_ERROR:
return OUTCOME_PARSING_ERROR;
}
if (!new_config.has_value()) {
return OUTCOME_NO_DOODLE;
}
if (!old_config.has_value()) {
return OUTCOME_NEW_DOODLE;
}
if (old_config.value() != new_config.value()) {
return OUTCOME_CHANGED_DOODLE;
}
return OUTCOME_REVALIDATED_DOODLE;
}
void DoodleService::DoodleFetched(
base::TimeTicks start_time,
DoodleState state,
base::TimeDelta time_to_live,
const base::Optional<DoodleConfig>& doodle_config) {
base::TimeTicks now_ticks = tick_clock_->NowTicks();
DownloadOutcome outcome = HandleNewConfig(state, time_to_live, doodle_config);
if (DownloadOutcomeIsSuccess(outcome)) {
last_successful_fetch_ = now_ticks;
}
base::TimeDelta download_time = now_ticks - start_time;
RecordDownloadMetrics(outcome, download_time);
}
DoodleService::DownloadOutcome DoodleService::HandleNewConfig(
DoodleState state,
base::TimeDelta time_to_live,
const base::Optional<DoodleConfig>& doodle_config) {
// Clamp the time-to-live to some reasonable maximum.
if (time_to_live.InSeconds() > kMaxTimeToLiveSecs) {
time_to_live = base::TimeDelta::FromSeconds(kMaxTimeToLiveSecs);
DLOG(WARNING) << "Clamping TTL to " << kMaxTimeToLiveSecs << " seconds!";
}
// Handle the case where the new config is already expired.
bool expired = time_to_live <= base::TimeDelta();
const base::Optional<DoodleConfig>& new_config =
expired ? base::nullopt : doodle_config;
// Determine the download outcome *before* updating the cached config.
DownloadOutcome outcome =
DetermineDownloadOutcome(cached_config_, new_config, state, expired);
// Note that this checks both for existence changes as well as changes of the
// configs themselves.
if (cached_config_ != new_config) {
UpdateCachedConfig(time_to_live, new_config);
for (auto& observer : observers_) {
observer.OnDoodleConfigUpdated(cached_config_);
}
} else {
for (auto& observer : observers_) {
observer.OnDoodleConfigRevalidated(/*from_cache=*/false);
}
}
// Even if the configs are identical, the time-to-live might have changed.
// (Re-)schedule the cache expiry.
if (cached_config_.has_value()) {
expiry_timer_->Start(
FROM_HERE, time_to_live,
base::Bind(&DoodleService::DoodleExpired, base::Unretained(this)));
} else {
expiry_timer_->Stop();
}
return outcome;
}
void DoodleService::UpdateCachedConfig(
base::TimeDelta time_to_live,
const base::Optional<DoodleConfig>& new_config) {
DCHECK(cached_config_ != new_config);
cached_config_ = new_config;
if (cached_config_.has_value()) {
pref_service_->Set(prefs::kCachedConfig, *cached_config_->ToDictionary());
base::Time expiry_date = clock_->Now() + time_to_live;
pref_service_->SetInt64(prefs::kCachedConfigExpiry,
expiry_date.ToInternalValue());
} else {
pref_service_->ClearPref(prefs::kCachedConfig);
pref_service_->ClearPref(prefs::kCachedConfigExpiry);
}
}
void DoodleService::DoodleExpired() {
DCHECK(cached_config_.has_value());
HandleNewConfig(DoodleState::NO_DOODLE, base::TimeDelta(), base::nullopt);
}
void DoodleService::ImageFetched(
const ImageCallback& callback,
const std::string& id,
const gfx::Image& image,
const image_fetcher::RequestMetadata& metadata) {
if (image.IsEmpty()) {
DLOG(WARNING) << "Failed to download doodle image";
} else {
// TODO(treib): Rename this to "Doodle.*" after we've decided what to do
// about crbug.com/719513.
UMA_HISTOGRAM_BOOLEAN("NewTabPage.LogoImageDownloaded",
metadata.from_http_cache);
}
callback.Run(image);
}
} // namespace doodle