blob: cd4b053edf1c7083b14f13e6c7f8db6e290bd971 [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/doodle/pref_names.h"
#include "components/prefs/pref_registry.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.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
} // 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)
: 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)) {
DCHECK(pref_service_);
DCHECK(fetcher_);
DCHECK(expiry_timer_);
DCHECK(clock_);
DCHECK(tick_clock_);
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);
}
DoodleService::~DoodleService() = default;
void DoodleService::Shutdown() {}
void DoodleService::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void DoodleService::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void DoodleService::Refresh() {
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());
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);
// If the config changed, update our cache and notify observers.
// 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_);
}
}
// 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);
}
} // namespace doodle