blob: 0b79eac771a1af38bc594bdeeb93ee274b3fdae0 [file] [log] [blame]
// Copyright 2016 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/ntp_snippets/remote/scheduling_remote_suggestions_provider.h"
#include <random>
#include <string>
#include <utility>
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_split.h"
#include "base/time/clock.h"
#include "components/ntp_snippets/features.h"
#include "components/ntp_snippets/pref_names.h"
#include "components/ntp_snippets/remote/persistent_scheduler.h"
#include "components/ntp_snippets/status.h"
#include "components/ntp_snippets/user_classifier.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/variations/variations_associated_data.h"
namespace ntp_snippets {
namespace {
enum class FetchingInterval {
PERSISTENT_FALLBACK,
PERSISTENT_WIFI,
SOFT_ON_USAGE_EVENT,
COUNT
};
// Default values for fetching intervals, fallback and wifi.
const double kDefaultFetchingIntervalRareNtpUser[] = {48.0, 24.0, 12.0};
const double kDefaultFetchingIntervalActiveNtpUser[] = {24.0, 6.0, 2.0};
const double kDefaultFetchingIntervalActiveSuggestionsConsumer[] = {24.0, 6.0,
2.0};
// Variation parameters than can the default fetching intervals.
const char* kFetchingIntervalParamNameRareNtpUser[] = {
"fetching_interval_hours-fallback-rare_ntp_user",
"fetching_interval_hours-wifi-rare_ntp_user",
"soft_fetching_interval_hours-active-rare_ntp_user"};
const char* kFetchingIntervalParamNameActiveNtpUser[] = {
"fetching_interval_hours-fallback-active_ntp_user",
"fetching_interval_hours-wifi-active_ntp_user",
"soft_fetching_interval_hours-active-active_ntp_user"};
const char* kFetchingIntervalParamNameActiveSuggestionsConsumer[] = {
"fetching_interval_hours-fallback-active_suggestions_consumer",
"fetching_interval_hours-wifi-active_suggestions_consumer",
"soft_fetching_interval_hours-active-active_suggestions_consumer"};
static_assert(
static_cast<unsigned int>(FetchingInterval::COUNT) ==
arraysize(kDefaultFetchingIntervalRareNtpUser) &&
static_cast<unsigned int>(FetchingInterval::COUNT) ==
arraysize(kDefaultFetchingIntervalActiveNtpUser) &&
static_cast<unsigned int>(FetchingInterval::COUNT) ==
arraysize(kDefaultFetchingIntervalActiveSuggestionsConsumer) &&
static_cast<unsigned int>(FetchingInterval::COUNT) ==
arraysize(kFetchingIntervalParamNameRareNtpUser) &&
static_cast<unsigned int>(FetchingInterval::COUNT) ==
arraysize(kFetchingIntervalParamNameActiveNtpUser) &&
static_cast<unsigned int>(FetchingInterval::COUNT) ==
arraysize(kFetchingIntervalParamNameActiveSuggestionsConsumer),
"Fill in all the info for fetching intervals.");
const char* kTriggerTypeNames[] = {"persistent_scheduler_wake_up", "ntp_opened",
"browser_foregrounded",
"browser_cold_start"};
const char* kTriggerTypesParamName = "scheduler_trigger_types";
const char* kTriggerTypesParamValueForEmptyList = "-";
base::TimeDelta GetDesiredFetchingInterval(
FetchingInterval interval,
UserClassifier::UserClass user_class) {
double default_value_hours = 0.0;
DCHECK(interval != FetchingInterval::COUNT);
const unsigned int index = static_cast<unsigned int>(interval);
DCHECK(index < arraysize(kDefaultFetchingIntervalRareNtpUser));
const char* param_name = nullptr;
switch (user_class) {
case UserClassifier::UserClass::RARE_NTP_USER:
default_value_hours = kDefaultFetchingIntervalRareNtpUser[index];
param_name = kFetchingIntervalParamNameRareNtpUser[index];
break;
case UserClassifier::UserClass::ACTIVE_NTP_USER:
default_value_hours = kDefaultFetchingIntervalActiveNtpUser[index];
param_name = kFetchingIntervalParamNameActiveNtpUser[index];
break;
case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER:
default_value_hours =
kDefaultFetchingIntervalActiveSuggestionsConsumer[index];
param_name = kFetchingIntervalParamNameActiveSuggestionsConsumer[index];
break;
}
double value_hours = variations::GetVariationParamByFeatureAsDouble(
ntp_snippets::kArticleSuggestionsFeature, param_name,
default_value_hours);
return base::TimeDelta::FromSecondsD(value_hours * 3600.0);
}
} // namespace
// static
SchedulingRemoteSuggestionsProvider::FetchingSchedule
SchedulingRemoteSuggestionsProvider::FetchingSchedule::Empty() {
return FetchingSchedule{base::TimeDelta(), base::TimeDelta(),
base::TimeDelta()};
}
bool SchedulingRemoteSuggestionsProvider::FetchingSchedule::operator==(
const FetchingSchedule& other) const {
return interval_persistent_wifi == other.interval_persistent_wifi &&
interval_persistent_fallback == other.interval_persistent_fallback &&
interval_soft_on_usage_event == other.interval_soft_on_usage_event;
}
bool SchedulingRemoteSuggestionsProvider::FetchingSchedule::operator!=(
const FetchingSchedule& other) const {
return !operator==(other);
}
bool SchedulingRemoteSuggestionsProvider::FetchingSchedule::is_empty() const {
return interval_persistent_wifi.is_zero() &&
interval_persistent_fallback.is_zero() &&
interval_soft_on_usage_event.is_zero();
}
// These values are written to logs. New enum values can be added, but existing
// enums must never be renumbered or deleted and reused. When adding new
// entries, also update the array |kTriggerTypeNames| above.
enum class SchedulingRemoteSuggestionsProvider::TriggerType {
PERSISTENT_SCHEDULER_WAKE_UP = 0,
NTP_OPENED = 1,
BROWSER_FOREGROUNDED = 2,
BROWSER_COLD_START = 3,
COUNT
};
SchedulingRemoteSuggestionsProvider::SchedulingRemoteSuggestionsProvider(
Observer* observer,
std::unique_ptr<RemoteSuggestionsProvider> provider,
PersistentScheduler* persistent_scheduler,
const UserClassifier* user_classifier,
PrefService* pref_service,
std::unique_ptr<base::Clock> clock)
: RemoteSuggestionsProvider(observer),
RemoteSuggestionsScheduler(),
provider_(std::move(provider)),
persistent_scheduler_(persistent_scheduler),
background_fetch_in_progress_(false),
user_classifier_(user_classifier),
pref_service_(pref_service),
clock_(std::move(clock)),
enabled_triggers_(GetEnabledTriggerTypes()) {
DCHECK(user_classifier);
DCHECK(pref_service);
LoadLastFetchingSchedule();
provider_->SetProviderStatusCallback(
base::MakeUnique<RemoteSuggestionsProvider::ProviderStatusCallback>(
base::BindRepeating(
&SchedulingRemoteSuggestionsProvider::OnProviderStatusChanged,
base::Unretained(this))));
}
SchedulingRemoteSuggestionsProvider::~SchedulingRemoteSuggestionsProvider() =
default;
// static
void SchedulingRemoteSuggestionsProvider::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterInt64Pref(prefs::kSnippetPersistentFetchingIntervalWifi, 0);
registry->RegisterInt64Pref(prefs::kSnippetPersistentFetchingIntervalFallback,
0);
registry->RegisterInt64Pref(prefs::kSnippetSoftFetchingIntervalOnUsageEvent,
0);
registry->RegisterInt64Pref(prefs::kSnippetLastFetchAttempt, 0);
}
void SchedulingRemoteSuggestionsProvider::RescheduleFetching() {
// Force the reschedule by stopping and starting it again.
StopScheduling();
StartScheduling();
}
void SchedulingRemoteSuggestionsProvider::OnPersistentSchedulerWakeUp() {
RefetchInTheBackgroundIfEnabled(TriggerType::PERSISTENT_SCHEDULER_WAKE_UP);
}
void SchedulingRemoteSuggestionsProvider::OnBrowserForegrounded() {
// TODO(jkrcal): Consider that this is called whenever we open or return to an
// Activity. Therefore, keep work light for fast start up calls.
if (!ShouldRefetchInTheBackgroundNow()) {
return;
}
RefetchInTheBackgroundIfEnabled(TriggerType::BROWSER_FOREGROUNDED);
}
void SchedulingRemoteSuggestionsProvider::OnBrowserColdStart() {
// TODO(fhorschig|jkrcal): Consider that work here must be kept light for fast
// cold start ups.
if (!ShouldRefetchInTheBackgroundNow()) {
return;
}
RefetchInTheBackgroundIfEnabled(TriggerType::BROWSER_COLD_START);
}
void SchedulingRemoteSuggestionsProvider::OnNTPOpened() {
if (!ShouldRefetchInTheBackgroundNow()) {
return;
}
RefetchInTheBackgroundIfEnabled(TriggerType::NTP_OPENED);
}
void SchedulingRemoteSuggestionsProvider::SetProviderStatusCallback(
std::unique_ptr<ProviderStatusCallback> callback) {
provider_->SetProviderStatusCallback(std::move(callback));
}
void SchedulingRemoteSuggestionsProvider::RefetchInTheBackground(
std::unique_ptr<FetchStatusCallback> callback) {
if (background_fetch_in_progress_) {
if (callback) {
callback->Run(
Status(StatusCode::TEMPORARY_ERROR, "Background fetch in progress"));
}
return;
}
background_fetch_in_progress_ = true;
RemoteSuggestionsProvider::FetchStatusCallback wrapper_callback = base::Bind(
&SchedulingRemoteSuggestionsProvider::RefetchInTheBackgroundFinished,
base::Unretained(this), base::Passed(&callback));
provider_->RefetchInTheBackground(
base::MakeUnique<RemoteSuggestionsProvider::FetchStatusCallback>(
std::move(wrapper_callback)));
}
const RemoteSuggestionsFetcher*
SchedulingRemoteSuggestionsProvider::suggestions_fetcher_for_debugging() const {
return provider_->suggestions_fetcher_for_debugging();
}
CategoryStatus SchedulingRemoteSuggestionsProvider::GetCategoryStatus(
Category category) {
return provider_->GetCategoryStatus(category);
}
CategoryInfo SchedulingRemoteSuggestionsProvider::GetCategoryInfo(
Category category) {
return provider_->GetCategoryInfo(category);
}
void SchedulingRemoteSuggestionsProvider::DismissSuggestion(
const ContentSuggestion::ID& suggestion_id) {
provider_->DismissSuggestion(suggestion_id);
}
void SchedulingRemoteSuggestionsProvider::FetchSuggestionImage(
const ContentSuggestion::ID& suggestion_id,
const ImageFetchedCallback& callback) {
provider_->FetchSuggestionImage(suggestion_id, callback);
}
void SchedulingRemoteSuggestionsProvider::Fetch(
const Category& category,
const std::set<std::string>& known_suggestion_ids,
const FetchDoneCallback& callback) {
provider_->Fetch(
category, known_suggestion_ids,
base::Bind(&SchedulingRemoteSuggestionsProvider::FetchFinished,
base::Unretained(this), callback));
}
void SchedulingRemoteSuggestionsProvider::ReloadSuggestions() {
provider_->ReloadSuggestions();
}
void SchedulingRemoteSuggestionsProvider::ClearHistory(
base::Time begin,
base::Time end,
const base::Callback<bool(const GURL& url)>& filter) {
provider_->ClearHistory(begin, end, filter);
}
void SchedulingRemoteSuggestionsProvider::ClearCachedSuggestions(
Category category) {
provider_->ClearCachedSuggestions(category);
}
void SchedulingRemoteSuggestionsProvider::OnSignInStateChanged() {
provider_->OnSignInStateChanged();
}
void SchedulingRemoteSuggestionsProvider::GetDismissedSuggestionsForDebugging(
Category category,
const DismissedSuggestionsCallback& callback) {
provider_->GetDismissedSuggestionsForDebugging(category, callback);
}
void SchedulingRemoteSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
Category category) {
provider_->ClearDismissedSuggestionsForDebugging(category);
}
void SchedulingRemoteSuggestionsProvider::OnProviderStatusChanged(
RemoteSuggestionsProvider::ProviderStatus status) {
switch (status) {
case RemoteSuggestionsProvider::ProviderStatus::ACTIVE:
StartScheduling();
return;
case RemoteSuggestionsProvider::ProviderStatus::INACTIVE:
StopScheduling();
return;
}
NOTREACHED();
}
void SchedulingRemoteSuggestionsProvider::StartScheduling() {
FetchingSchedule new_schedule = GetDesiredFetchingSchedule();
if (schedule_ == new_schedule) {
// Do not schedule if nothing has changed;
return;
}
schedule_ = new_schedule;
StoreFetchingSchedule();
ApplyPersistentFetchingSchedule();
}
void SchedulingRemoteSuggestionsProvider::StopScheduling() {
if (schedule_.is_empty()) {
// Do not unschedule if already switched off.
return;
}
schedule_ = FetchingSchedule::Empty();
StoreFetchingSchedule();
ApplyPersistentFetchingSchedule();
}
void SchedulingRemoteSuggestionsProvider::ApplyPersistentFetchingSchedule() {
// The scheduler only exists on Android so far, it's null on other platforms.
if (persistent_scheduler_) {
if (schedule_.is_empty()) {
persistent_scheduler_->Unschedule();
} else {
persistent_scheduler_->Schedule(schedule_.interval_persistent_wifi,
schedule_.interval_persistent_fallback);
}
}
}
SchedulingRemoteSuggestionsProvider::FetchingSchedule
SchedulingRemoteSuggestionsProvider::GetDesiredFetchingSchedule() const {
UserClassifier::UserClass user_class = user_classifier_->GetUserClass();
FetchingSchedule schedule;
schedule.interval_persistent_wifi =
GetDesiredFetchingInterval(FetchingInterval::PERSISTENT_WIFI, user_class);
schedule.interval_persistent_fallback = GetDesiredFetchingInterval(
FetchingInterval::PERSISTENT_FALLBACK, user_class);
schedule.interval_soft_on_usage_event = GetDesiredFetchingInterval(
FetchingInterval::SOFT_ON_USAGE_EVENT, user_class);
return schedule;
}
void SchedulingRemoteSuggestionsProvider::LoadLastFetchingSchedule() {
schedule_.interval_persistent_wifi = base::TimeDelta::FromInternalValue(
pref_service_->GetInt64(prefs::kSnippetPersistentFetchingIntervalWifi));
schedule_.interval_persistent_fallback =
base::TimeDelta::FromInternalValue(pref_service_->GetInt64(
prefs::kSnippetPersistentFetchingIntervalFallback));
schedule_.interval_soft_on_usage_event = base::TimeDelta::FromInternalValue(
pref_service_->GetInt64(prefs::kSnippetSoftFetchingIntervalOnUsageEvent));
}
void SchedulingRemoteSuggestionsProvider::StoreFetchingSchedule() {
pref_service_->SetInt64(prefs::kSnippetPersistentFetchingIntervalWifi,
schedule_.interval_persistent_wifi.ToInternalValue());
pref_service_->SetInt64(
prefs::kSnippetPersistentFetchingIntervalFallback,
schedule_.interval_persistent_fallback.ToInternalValue());
pref_service_->SetInt64(
prefs::kSnippetSoftFetchingIntervalOnUsageEvent,
schedule_.interval_soft_on_usage_event.ToInternalValue());
}
void SchedulingRemoteSuggestionsProvider::RefetchInTheBackgroundIfEnabled(
SchedulingRemoteSuggestionsProvider::TriggerType trigger) {
if (BackgroundFetchesDisabled(trigger)) {
return;
}
UMA_HISTOGRAM_ENUMERATION(
"NewTabPage.ContentSuggestions.BackgroundFetchTrigger",
static_cast<int>(trigger), static_cast<int>(TriggerType::COUNT));
RefetchInTheBackground(/*callback=*/nullptr);
}
bool SchedulingRemoteSuggestionsProvider::ShouldRefetchInTheBackgroundNow() {
base::Time first_allowed_fetch_time =
base::Time::FromInternalValue(
pref_service_->GetInt64(prefs::kSnippetLastFetchAttempt)) +
schedule_.interval_soft_on_usage_event;
return first_allowed_fetch_time <= clock_->Now();
}
bool SchedulingRemoteSuggestionsProvider::BackgroundFetchesDisabled(
SchedulingRemoteSuggestionsProvider::TriggerType trigger) const {
if (schedule_.is_empty()) {
return true; // Background fetches are disabled in general.
}
if (enabled_triggers_.count(trigger) == 0) {
return true; // Background fetches for |trigger| are not enabled.
}
return false;
}
void SchedulingRemoteSuggestionsProvider::FetchFinished(
const FetchDoneCallback& callback,
Status fetch_status,
std::vector<ContentSuggestion> suggestions) {
OnFetchCompleted(fetch_status);
if (callback) {
callback.Run(fetch_status, std::move(suggestions));
}
}
void SchedulingRemoteSuggestionsProvider::RefetchInTheBackgroundFinished(
std::unique_ptr<FetchStatusCallback> callback,
Status fetch_status) {
background_fetch_in_progress_ = false;
OnFetchCompleted(fetch_status);
if (callback) {
callback->Run(fetch_status);
}
}
void SchedulingRemoteSuggestionsProvider::OnFetchCompleted(
Status fetch_status) {
pref_service_->SetInt64(prefs::kSnippetLastFetchAttempt,
clock_->Now().ToInternalValue());
// Reschedule after a fetch. The persistent schedule is applied only after a
// successful fetch. After a failed fetch, we want to keep the previous
// persistent schedule intact so that we eventually get a persistent
// fallback fetch (if the wifi persistent fetches keep failing).
if (fetch_status.code != StatusCode::SUCCESS) {
return;
}
ApplyPersistentFetchingSchedule();
}
std::set<SchedulingRemoteSuggestionsProvider::TriggerType>
SchedulingRemoteSuggestionsProvider::GetEnabledTriggerTypes() {
static_assert(static_cast<unsigned int>(TriggerType::COUNT) ==
arraysize(kTriggerTypeNames),
"Fill in names for trigger types.");
std::string param_value = variations::GetVariationParamValueByFeature(
ntp_snippets::kArticleSuggestionsFeature, kTriggerTypesParamName);
if (param_value == kTriggerTypesParamValueForEmptyList) {
return std::set<TriggerType>();
}
std::vector<std::string> tokens = base::SplitString(
param_value, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (tokens.empty()) {
return GetDefaultEnabledTriggerTypes();
}
std::set<TriggerType> enabled_types;
for (const auto& token : tokens) {
auto it = std::find(std::begin(kTriggerTypeNames),
std::end(kTriggerTypeNames), token);
if (it == std::end(kTriggerTypeNames)) {
DLOG(WARNING) << "Failed to parse variation param "
<< kTriggerTypesParamName << " with string value "
<< param_value
<< " into a comma-separated list of keywords. "
<< "Unknown token " << token
<< " found. Falling back to default value.";
return GetDefaultEnabledTriggerTypes();
}
// Add the enabled type represented by |token| into the result set.
enabled_types.insert(
static_cast<TriggerType>(it - std::begin(kTriggerTypeNames)));
}
return enabled_types;
}
std::set<SchedulingRemoteSuggestionsProvider::TriggerType>
SchedulingRemoteSuggestionsProvider::GetDefaultEnabledTriggerTypes() {
return {TriggerType::PERSISTENT_SCHEDULER_WAKE_UP, TriggerType::NTP_OPENED,
TriggerType::BROWSER_FOREGROUNDED};
}
} // namespace ntp_snippets