| // 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/remote_suggestions_scheduler_impl.h" |
| |
| #include <cfloat> |
| #include <random> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/location.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/stringprintf.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/remote/remote_suggestions_provider.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 "net/base/network_change_notifier.h" |
| |
| namespace ntp_snippets { |
| |
| namespace { |
| |
| // The FetchingInterval enum specifies overlapping time intervals that are used |
| // for scheduling the next remote suggestion fetch. Therefore a timer is created |
| // for each interval. Initially all the timers are started at the same time. |
| // A fetch for a given interval is only performed at the moment (after the |
| // interval has elapsed) when the combination of two conditions associated with |
| // the interval is met. |
| // 1. Trigger contidion: |
| // - STARTUP_*: the user starts / switches to Chrome; |
| // - SHOWN_*: the suggestions surface is shown to the user; |
| // - PERSISTENT_*: the OS initiates the scheduled fetching task (which has |
| // been scheduled according to this interval). |
| // 2. Connectivity condition: |
| // - *_WIFI: the user is on a connection that the OS classifies as unmetered; |
| // - *_FALLBACK: holds for any functional internet connection. |
| // |
| // If a fetch failed, then only the corresponding timer is reset. The other |
| // timers are not touched. |
| enum class FetchingInterval { |
| PERSISTENT_FALLBACK, |
| PERSISTENT_WIFI, |
| STARTUP_FALLBACK, |
| STARTUP_WIFI, |
| SHOWN_FALLBACK, |
| SHOWN_WIFI, |
| COUNT |
| }; |
| |
| // The following arrays specify default values for remote suggestions fetch |
| // intervals corresponding to individual user classes. The user classes are |
| // defined by the user classifier. There must be an array for each user class. |
| // The values of each array specify a default time interval for the intervals |
| // defined by the enum FetchingInterval. The default time intervals defined in |
| // the arrays can be overridden using different variation parameters. |
| const double kDefaultFetchingIntervalHoursRareNtpUser[] = {96.0, 96.0, 24.0, |
| 24.0, 4.0, 4.0}; |
| const double kDefaultFetchingIntervalHoursActiveNtpUser[] = {48.0, 48.0, 24.0, |
| 24.0, 4.0, 4.0}; |
| const double kDefaultFetchingIntervalHoursActiveSuggestionsConsumer[] = { |
| 24.0, 24.0, 12.0, 12.0, 1.0, 1.0}; |
| |
| // For a simple comparison: fetching intervals that emulate the state really |
| // rolled out to 100% M58 Stable. Used for evaluation of later changes. DBL_MAX |
| // values simulate this interval being disabled. |
| // TODO(jkrcal): Remove when not needed any more, incl. the feature. Probably |
| // after M62 when CH is launched. |
| const double kM58FetchingIntervalHoursRareNtpUser[] = {48.0, 24.0, DBL_MAX, |
| DBL_MAX, 4.0, 4.0}; |
| const double kM58FetchingIntervalHoursActiveNtpUser[] = {24.0, 8.0, DBL_MAX, |
| DBL_MAX, 10.0, 10.0}; |
| const double kM58FetchingIntervalHoursActiveSuggestionsConsumer[] = { |
| 24.0, 6.0, DBL_MAX, DBL_MAX, 1.0, 1.0}; |
| |
| // Variation parameters than can be used to override the default fetching |
| // intervals. For backwards compatibility, we do not rename |
| // - the "fetching_" params (should be "persistent_fetching_"); |
| // - the "soft_" params (should be "shown_"). |
| const char* kFetchingIntervalParamNameRareNtpUser[] = { |
| "fetching_interval_hours-fallback-rare_ntp_user", |
| "fetching_interval_hours-wifi-rare_ntp_user", |
| "startup_fetching_interval_hours-fallback-rare_ntp_user", |
| "startup_fetching_interval_hours-wifi-rare_ntp_user", |
| "soft_fetching_interval_hours-fallback-rare_ntp_user", |
| "soft_fetching_interval_hours-wifi-rare_ntp_user"}; |
| const char* kFetchingIntervalParamNameActiveNtpUser[] = { |
| "fetching_interval_hours-fallback-active_ntp_user", |
| "fetching_interval_hours-wifi-active_ntp_user", |
| "startup_fetching_interval_hours-fallback-active_ntp_user", |
| "startup_fetching_interval_hours-wifi-active_ntp_user", |
| "soft_fetching_interval_hours-fallback-active_ntp_user", |
| "soft_fetching_interval_hours-wifi-active_ntp_user"}; |
| const char* kFetchingIntervalParamNameActiveSuggestionsConsumer[] = { |
| "fetching_interval_hours-fallback-active_suggestions_consumer", |
| "fetching_interval_hours-wifi-active_suggestions_consumer", |
| "startup_fetching_interval_hours-fallback-active_suggestions_consumer", |
| "startup_fetching_interval_hours-wifi-active_suggestions_consumer", |
| "soft_fetching_interval_hours-fallback-active_suggestions_consumer", |
| "soft_fetching_interval_hours-wifi-active_suggestions_consumer"}; |
| |
| static_assert( |
| static_cast<unsigned int>(FetchingInterval::COUNT) == |
| base::size(kDefaultFetchingIntervalHoursRareNtpUser) && |
| static_cast<unsigned int>(FetchingInterval::COUNT) == |
| base::size(kDefaultFetchingIntervalHoursActiveNtpUser) && |
| static_cast<unsigned int>(FetchingInterval::COUNT) == |
| base::size( |
| kDefaultFetchingIntervalHoursActiveSuggestionsConsumer) && |
| static_cast<unsigned int>(FetchingInterval::COUNT) == |
| base::size(kM58FetchingIntervalHoursRareNtpUser) && |
| static_cast<unsigned int>(FetchingInterval::COUNT) == |
| base::size(kM58FetchingIntervalHoursActiveNtpUser) && |
| static_cast<unsigned int>(FetchingInterval::COUNT) == |
| base::size(kM58FetchingIntervalHoursActiveSuggestionsConsumer) && |
| static_cast<unsigned int>(FetchingInterval::COUNT) == |
| base::size(kFetchingIntervalParamNameRareNtpUser) && |
| static_cast<unsigned int>(FetchingInterval::COUNT) == |
| base::size(kFetchingIntervalParamNameActiveNtpUser) && |
| static_cast<unsigned int>(FetchingInterval::COUNT) == |
| base::size(kFetchingIntervalParamNameActiveSuggestionsConsumer), |
| "Fill in all the info for fetching intervals."); |
| |
| // For backward compatibility "ntp_opened" value is kept and denotes the |
| // SURFACE_OPENED trigger type. |
| const char* const kTriggerTypeNames[] = {"persistent_scheduler_wake_up", |
| "ntp_opened", "browser_foregrounded", |
| "browser_cold_start"}; |
| |
| const char* const kTriggerTypesParamName = "scheduler_trigger_types"; |
| const char* const kTriggerTypesParamValueForEmptyList = "-"; |
| |
| const int kBlockBackgroundFetchesMinutesAfterClearingHistory = 30; |
| |
| // Variation parameter for minimal age of a fetch to be considered "stale". |
| const char kMinAgeForStaleFetchHoursParamName[] = |
| "min_age_for_stale_fetch_hours"; |
| |
| // Returns the time interval to use for scheduling remote suggestion fetches for |
| // the given interval and user_class. |
| base::TimeDelta GetDesiredFetchingInterval( |
| FetchingInterval interval, |
| UserClassifier::UserClass user_class) { |
| DCHECK(interval != FetchingInterval::COUNT); |
| const unsigned int index = static_cast<unsigned int>(interval); |
| DCHECK(index < base::size(kDefaultFetchingIntervalHoursRareNtpUser)); |
| |
| bool emulateM58 = base::FeatureList::IsEnabled( |
| kRemoteSuggestionsEmulateM58FetchingSchedule); |
| |
| double default_value_hours = 0.0; |
| const char* param_name = nullptr; |
| switch (user_class) { |
| case UserClassifier::UserClass::RARE_NTP_USER: |
| default_value_hours = |
| emulateM58 ? kM58FetchingIntervalHoursRareNtpUser[index] |
| : kDefaultFetchingIntervalHoursRareNtpUser[index]; |
| param_name = kFetchingIntervalParamNameRareNtpUser[index]; |
| break; |
| case UserClassifier::UserClass::ACTIVE_NTP_USER: |
| default_value_hours = |
| emulateM58 ? kM58FetchingIntervalHoursActiveNtpUser[index] |
| : kDefaultFetchingIntervalHoursActiveNtpUser[index]; |
| param_name = kFetchingIntervalParamNameActiveNtpUser[index]; |
| break; |
| case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER: |
| default_value_hours = |
| emulateM58 |
| ? kM58FetchingIntervalHoursActiveSuggestionsConsumer[index] |
| : kDefaultFetchingIntervalHoursActiveSuggestionsConsumer[index]; |
| param_name = kFetchingIntervalParamNameActiveSuggestionsConsumer[index]; |
| break; |
| } |
| |
| double value_hours = base::GetFieldTrialParamByFeatureAsDouble( |
| ntp_snippets::kArticleSuggestionsFeature, param_name, |
| default_value_hours); |
| |
| return base::TimeDelta::FromSecondsD(value_hours * 3600.0); |
| } |
| |
| void ReportTimeUntilFirstShownTrigger( |
| UserClassifier::UserClass user_class, |
| base::TimeDelta time_until_first_shown_trigger) { |
| switch (user_class) { |
| case UserClassifier::UserClass::RARE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilFirstShownTrigger." |
| "RareNTPUser", |
| time_until_first_shown_trigger, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilFirstShownTrigger." |
| "ActiveNTPUser", |
| time_until_first_shown_trigger, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilFirstShownTrigger." |
| "ActiveSuggestionsConsumer", |
| time_until_first_shown_trigger, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| } |
| } |
| |
| void ReportTimeUntilFirstStartupTrigger( |
| UserClassifier::UserClass user_class, |
| base::TimeDelta time_until_first_startup_trigger) { |
| switch (user_class) { |
| case UserClassifier::UserClass::RARE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilFirstStartupTrigger." |
| "RareNTPUser", |
| time_until_first_startup_trigger, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilFirstStartupTrigger." |
| "ActiveNTPUser", |
| time_until_first_startup_trigger, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilFirstStartupTrigger." |
| "ActiveSuggestionsConsumer", |
| time_until_first_startup_trigger, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| } |
| } |
| |
| void ReportTimeUntilShownFetch(UserClassifier::UserClass user_class, |
| base::TimeDelta time_until_shown_fetch) { |
| switch (user_class) { |
| case UserClassifier::UserClass::RARE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilSoftFetch." |
| "RareNTPUser", |
| time_until_shown_fetch, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilSoftFetch." |
| "ActiveNTPUser", |
| time_until_shown_fetch, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilSoftFetch." |
| "ActiveSuggestionsConsumer", |
| time_until_shown_fetch, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| } |
| } |
| |
| void ReportTimeUntilStartupFetch(UserClassifier::UserClass user_class, |
| base::TimeDelta time_until_startup_fetch) { |
| switch (user_class) { |
| case UserClassifier::UserClass::RARE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilStartupFetch." |
| "RareNTPUser", |
| time_until_startup_fetch, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilStartupFetch." |
| "ActiveNTPUser", |
| time_until_startup_fetch, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilStartupFetch." |
| "ActiveSuggestionsConsumer", |
| time_until_startup_fetch, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| } |
| } |
| |
| void ReportTimeUntilPersistentFetch( |
| UserClassifier::UserClass user_class, |
| base::TimeDelta time_until_persistent_fetch) { |
| switch (user_class) { |
| case UserClassifier::UserClass::RARE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilPersistentFetch." |
| "RareNTPUser", |
| time_until_persistent_fetch, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_NTP_USER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilPersistentFetch." |
| "ActiveNTPUser", |
| time_until_persistent_fetch, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER: |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "NewTabPage.ContentSuggestions.TimeUntilPersistentFetch." |
| "ActiveSuggestionsConsumer", |
| time_until_persistent_fetch, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| break; |
| } |
| } |
| |
| } // namespace |
| |
| class EulaState final : public web_resource::EulaAcceptedNotifier::Observer { |
| public: |
| EulaState(PrefService* local_state_prefs, base::Closure eula_accepted) |
| : eula_notifier_( |
| web_resource::EulaAcceptedNotifier::Create(local_state_prefs)), |
| eula_accepted_(eula_accepted) { |
| // EulaNotifier is not constructed on some platforms (such as desktop). |
| if (!eula_notifier_) { |
| return; |
| } |
| |
| // Register the observer. |
| eula_notifier_->Init(this); |
| } |
| |
| ~EulaState() override = default; |
| |
| bool IsEulaAccepted() { |
| if (!eula_notifier_) { |
| return true; |
| } |
| return eula_notifier_->IsEulaAccepted(); |
| } |
| |
| // EulaAcceptedNotifier::Observer implementation. |
| void OnEulaAccepted() override { |
| // Note that this code is only run if a previous call to IsEulaAccepted() |
| // returned false. In that case, the prefs are watched and this method gets |
| // executed once the setting flips to accepted. Hence, we can assume that |
| // at the time this code runs, a background-fetch trigger is queued in the |
| // scheduler. |
| eula_accepted_.Run(); |
| } |
| |
| private: |
| std::unique_ptr<web_resource::EulaAcceptedNotifier> eula_notifier_; |
| base::Callback<void()> eula_accepted_; |
| |
| DISALLOW_COPY_AND_ASSIGN(EulaState); |
| }; |
| |
| // static |
| RemoteSuggestionsSchedulerImpl::FetchingSchedule |
| RemoteSuggestionsSchedulerImpl::FetchingSchedule::Empty() { |
| return FetchingSchedule{base::TimeDelta(), base::TimeDelta(), |
| base::TimeDelta(), base::TimeDelta()}; |
| } |
| |
| bool RemoteSuggestionsSchedulerImpl::FetchingSchedule::operator==( |
| const FetchingSchedule& other) const { |
| return interval_persistent_wifi == other.interval_persistent_wifi && |
| interval_persistent_fallback == other.interval_persistent_fallback && |
| interval_startup_wifi == other.interval_startup_wifi && |
| interval_startup_fallback == other.interval_startup_fallback && |
| interval_shown_wifi == other.interval_shown_wifi && |
| interval_shown_fallback == other.interval_shown_fallback; |
| } |
| |
| bool RemoteSuggestionsSchedulerImpl::FetchingSchedule::operator!=( |
| const FetchingSchedule& other) const { |
| return !operator==(other); |
| } |
| |
| bool RemoteSuggestionsSchedulerImpl::FetchingSchedule::is_empty() const { |
| return interval_persistent_wifi.is_zero() && |
| interval_persistent_fallback.is_zero() && |
| interval_startup_wifi.is_zero() && |
| interval_startup_fallback.is_zero() && interval_shown_wifi.is_zero() && |
| interval_shown_fallback.is_zero(); |
| } |
| |
| base::TimeDelta |
| RemoteSuggestionsSchedulerImpl::FetchingSchedule::GetStalenessInterval() const { |
| // The default value for staleness is |interval_startup_wifi| which is not |
| // constant. It depends on user class and is configurable by field trial |
| // params as well. |
| return base::TimeDelta::FromHours(base::GetFieldTrialParamByFeatureAsInt( |
| ntp_snippets::kArticleSuggestionsFeature, |
| kMinAgeForStaleFetchHoursParamName, interval_startup_wifi.InHours())); |
| } |
| |
| // The TriggerType enum specifies values for the events that can trigger |
| // fetching remote suggestions. 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 RemoteSuggestionsSchedulerImpl::TriggerType { |
| PERSISTENT_SCHEDULER_WAKE_UP = 0, |
| SURFACE_OPENED = 1, |
| BROWSER_FOREGROUNDED = 2, |
| BROWSER_COLD_START = 3, |
| COUNT |
| }; |
| |
| RemoteSuggestionsSchedulerImpl::RemoteSuggestionsSchedulerImpl( |
| PersistentScheduler* persistent_scheduler, |
| const UserClassifier* user_classifier, |
| PrefService* profile_prefs, |
| PrefService* local_state_prefs, |
| base::Clock* clock, |
| Logger* debug_logger) |
| : persistent_scheduler_(persistent_scheduler), |
| provider_(nullptr), |
| background_fetch_in_progress_(false), |
| user_classifier_(user_classifier), |
| request_throttler_rare_ntp_user_( |
| profile_prefs, |
| RequestThrottler::RequestType:: |
| CONTENT_SUGGESTION_FETCHER_RARE_NTP_USER), |
| request_throttler_active_ntp_user_( |
| profile_prefs, |
| RequestThrottler::RequestType:: |
| CONTENT_SUGGESTION_FETCHER_ACTIVE_NTP_USER), |
| request_throttler_active_suggestions_consumer_( |
| profile_prefs, |
| RequestThrottler::RequestType:: |
| CONTENT_SUGGESTION_FETCHER_ACTIVE_SUGGESTIONS_CONSUMER), |
| time_until_first_shown_trigger_reported_(false), |
| time_until_first_startup_trigger_reported_(false), |
| eula_state_(std::make_unique<EulaState>( |
| local_state_prefs, |
| base::Bind(&RemoteSuggestionsSchedulerImpl::RunQueuedTriggersIfReady, |
| base::Unretained(this)))), |
| profile_prefs_(profile_prefs), |
| clock_(clock), |
| enabled_triggers_(GetEnabledTriggerTypes()), |
| debug_logger_(debug_logger) { |
| DCHECK(user_classifier); |
| DCHECK(profile_prefs); |
| DCHECK(debug_logger_); |
| } |
| |
| RemoteSuggestionsSchedulerImpl::~RemoteSuggestionsSchedulerImpl() = default; |
| |
| // static |
| void RemoteSuggestionsSchedulerImpl::RegisterProfilePrefs( |
| PrefRegistrySimple* registry) { |
| registry->RegisterTimeDeltaPref(prefs::kSnippetPersistentFetchingIntervalWifi, |
| base::TimeDelta()); |
| registry->RegisterTimeDeltaPref( |
| prefs::kSnippetPersistentFetchingIntervalFallback, base::TimeDelta()); |
| registry->RegisterTimeDeltaPref(prefs::kSnippetStartupFetchingIntervalWifi, |
| base::TimeDelta()); |
| registry->RegisterTimeDeltaPref( |
| prefs::kSnippetStartupFetchingIntervalFallback, base::TimeDelta()); |
| registry->RegisterTimeDeltaPref(prefs::kSnippetShownFetchingIntervalWifi, |
| base::TimeDelta()); |
| registry->RegisterTimeDeltaPref(prefs::kSnippetShownFetchingIntervalFallback, |
| base::TimeDelta()); |
| registry->RegisterTimePref(prefs::kSnippetLastFetchAttemptTime, base::Time()); |
| registry->RegisterTimePref(prefs::kSnippetLastSuccessfulFetchTime, |
| base::Time()); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::SetProvider( |
| RemoteSuggestionsProvider* provider) { |
| DCHECK(provider); |
| provider_ = provider; |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnProviderActivated() { |
| LoadLastFetchingSchedule(); |
| StartScheduling(); |
| RunQueuedTriggersIfReady(); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::RunQueuedTriggersIfReady() { |
| // Process any queued triggers if we're now ready for background fetches. |
| if (IsReadyForBackgroundFetches()) { |
| std::set<TriggerType> queued_triggers_copy; |
| queued_triggers_copy.swap(queued_triggers_); |
| for (const TriggerType trigger : queued_triggers_copy) { |
| RefetchIfAppropriate(trigger); |
| } |
| } |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnProviderDeactivated() { |
| StopScheduling(); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnSuggestionsCleared() { |
| debug_logger_->Log(FROM_HERE, /*message=*/std::string()); |
| // This should be called by |provider_| so it should exist. |
| DCHECK(provider_); |
| // Some user action causes suggestions to be cleared, we need to fetch as soon |
| // as possible. |
| ClearLastFetchAttemptTime(); |
| // We cannot guarantee that the surface is not visible when the event happens. |
| // To make sure the suggestions get replaced, we use the SURFACE_OPENED |
| // trigger. |
| RefetchIfAppropriate(TriggerType::SURFACE_OPENED); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnHistoryCleared() { |
| // Due to privacy, we should not fetch for a while (unless the user explicitly |
| // asks for new suggestions) to give sync the time to propagate the changes in |
| // history to the server. |
| background_fetches_allowed_after_ = |
| clock_->Now() + base::TimeDelta::FromMinutes( |
| kBlockBackgroundFetchesMinutesAfterClearingHistory); |
| // After that time elapses, we should fetch as soon as possible. |
| ClearLastFetchAttemptTime(); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnBrowserUpgraded() { |
| // After browser upgrade, persistent schedule needs to get reset. Force the |
| // reschedule by stopping and starting it again. |
| StopScheduling(); |
| StartScheduling(); |
| } |
| |
| bool RemoteSuggestionsSchedulerImpl::AcquireQuotaForInteractiveFetch() { |
| return AcquireQuota(/*interactive_request=*/true); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnInteractiveFetchFinished( |
| Status fetch_status) { |
| OnFetchCompleted(fetch_status); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnPersistentSchedulerWakeUp() { |
| debug_logger_->Log(FROM_HERE, /*message=*/std::string()); |
| RefetchIfAppropriate(TriggerType::PERSISTENT_SCHEDULER_WAKE_UP); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnBrowserForegrounded() { |
| debug_logger_->Log(FROM_HERE, /*message=*/std::string()); |
| // TODO(jkrcal): Consider that this is called whenever we open or return to an |
| // Activity. Therefore, keep work light for fast start up calls. |
| RefetchIfAppropriate(TriggerType::BROWSER_FOREGROUNDED); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnBrowserColdStart() { |
| debug_logger_->Log(FROM_HERE, /*message=*/std::string()); |
| // TODO(jkrcal): Consider that work here must be kept light for fast |
| // cold start ups. |
| RefetchIfAppropriate(TriggerType::BROWSER_COLD_START); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnSuggestionsSurfaceOpened() { |
| debug_logger_->Log(FROM_HERE, /*message=*/std::string()); |
| // TODO(jkrcal): Consider that this is called whenever we open an NTP. |
| // Therefore, keep work light for fast start up calls. |
| RefetchIfAppropriate(TriggerType::SURFACE_OPENED); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::StartScheduling() { |
| FetchingSchedule new_schedule = GetDesiredFetchingSchedule(); |
| |
| if (schedule_ == new_schedule) { |
| // Do not schedule if nothing has changed; |
| return; |
| } |
| |
| schedule_ = new_schedule; |
| StoreFetchingSchedule(); |
| ApplyPersistentFetchingSchedule(); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::StopScheduling() { |
| if (schedule_.is_empty()) { |
| // Do not unschedule if already switched off. |
| return; |
| } |
| |
| schedule_ = FetchingSchedule::Empty(); |
| StoreFetchingSchedule(); |
| ApplyPersistentFetchingSchedule(); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::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); |
| } |
| } |
| } |
| |
| RemoteSuggestionsSchedulerImpl::FetchingSchedule |
| RemoteSuggestionsSchedulerImpl::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_startup_wifi = |
| GetDesiredFetchingInterval(FetchingInterval::STARTUP_WIFI, user_class); |
| schedule.interval_startup_fallback = GetDesiredFetchingInterval( |
| FetchingInterval::STARTUP_FALLBACK, user_class); |
| schedule.interval_shown_wifi = |
| GetDesiredFetchingInterval(FetchingInterval::SHOWN_WIFI, user_class); |
| schedule.interval_shown_fallback = |
| GetDesiredFetchingInterval(FetchingInterval::SHOWN_FALLBACK, user_class); |
| |
| return schedule; |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::LoadLastFetchingSchedule() { |
| schedule_.interval_persistent_wifi = profile_prefs_->GetTimeDelta( |
| prefs::kSnippetPersistentFetchingIntervalWifi); |
| schedule_.interval_persistent_fallback = profile_prefs_->GetTimeDelta( |
| prefs::kSnippetPersistentFetchingIntervalFallback); |
| schedule_.interval_startup_wifi = |
| profile_prefs_->GetTimeDelta(prefs::kSnippetStartupFetchingIntervalWifi); |
| schedule_.interval_startup_fallback = profile_prefs_->GetTimeDelta( |
| prefs::kSnippetStartupFetchingIntervalFallback); |
| schedule_.interval_shown_wifi = |
| profile_prefs_->GetTimeDelta(prefs::kSnippetShownFetchingIntervalWifi); |
| schedule_.interval_shown_fallback = profile_prefs_->GetTimeDelta( |
| prefs::kSnippetShownFetchingIntervalFallback); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::StoreFetchingSchedule() { |
| profile_prefs_->SetTimeDelta(prefs::kSnippetPersistentFetchingIntervalWifi, |
| schedule_.interval_persistent_wifi); |
| profile_prefs_->SetTimeDelta( |
| prefs::kSnippetPersistentFetchingIntervalFallback, |
| schedule_.interval_persistent_fallback); |
| profile_prefs_->SetTimeDelta(prefs::kSnippetStartupFetchingIntervalWifi, |
| schedule_.interval_startup_wifi); |
| profile_prefs_->SetTimeDelta(prefs::kSnippetStartupFetchingIntervalFallback, |
| schedule_.interval_startup_fallback); |
| profile_prefs_->SetTimeDelta(prefs::kSnippetShownFetchingIntervalWifi, |
| schedule_.interval_shown_wifi); |
| profile_prefs_->SetTimeDelta(prefs::kSnippetShownFetchingIntervalFallback, |
| schedule_.interval_shown_fallback); |
| } |
| |
| bool RemoteSuggestionsSchedulerImpl::IsLastSuccessfulFetchStale() const { |
| // Avoid claiming staleness on the first fetch ever (after installing / |
| // upgrading Chrome to a version that writes this pref). We really do not |
| // know when was the last fetch. |
| if (!profile_prefs_->HasPrefPath(prefs::kSnippetLastSuccessfulFetchTime)) { |
| return false; |
| } |
| const base::Time last_successful_fetch_time = |
| profile_prefs_->GetTime(prefs::kSnippetLastSuccessfulFetchTime); |
| |
| return clock_->Now() - last_successful_fetch_time > |
| schedule_.GetStalenessInterval(); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::RefetchIfAppropriate(TriggerType trigger) { |
| debug_logger_->Log(FROM_HERE, /*message=*/std::string()); |
| |
| if (background_fetch_in_progress_) { |
| debug_logger_->Log(FROM_HERE, "stop due to ongoing fetch"); |
| return; |
| } |
| |
| if (enabled_triggers_.count(trigger) == 0) { |
| debug_logger_->Log(FROM_HERE, "stop due to disabled trigger"); |
| return; |
| } |
| |
| if (net::NetworkChangeNotifier::IsOffline()) { |
| // Do not let a request fail due to lack of internet connection. Then, such |
| // a failure would get logged and further requests would be blocked for a |
| // while (even after becoming online). |
| debug_logger_->Log(FROM_HERE, "stop due to being offline"); |
| return; |
| } |
| |
| if (!IsReadyForBackgroundFetches()) { |
| debug_logger_->Log(FROM_HERE, "delay until ready"); |
| queued_triggers_.insert(trigger); |
| return; |
| } |
| |
| const base::Time last_fetch_attempt_time = |
| profile_prefs_->GetTime(prefs::kSnippetLastFetchAttemptTime); |
| |
| if (trigger == TriggerType::SURFACE_OPENED && |
| !time_until_first_shown_trigger_reported_) { |
| time_until_first_shown_trigger_reported_ = true; |
| ReportTimeUntilFirstShownTrigger(user_classifier_->GetUserClass(), |
| clock_->Now() - last_fetch_attempt_time); |
| } |
| |
| if ((trigger == TriggerType::BROWSER_FOREGROUNDED || |
| trigger == TriggerType::BROWSER_COLD_START) && |
| !time_until_first_startup_trigger_reported_) { |
| time_until_first_startup_trigger_reported_ = true; |
| ReportTimeUntilFirstStartupTrigger(user_classifier_->GetUserClass(), |
| clock_->Now() - last_fetch_attempt_time); |
| } |
| |
| if (trigger != TriggerType::PERSISTENT_SCHEDULER_WAKE_UP && |
| !ShouldRefetchNow(last_fetch_attempt_time, trigger)) { |
| debug_logger_->Log(FROM_HERE, "stop, because too soon"); |
| return; |
| } |
| |
| if (!AcquireQuota(/*interactive_request=*/false)) { |
| debug_logger_->Log(FROM_HERE, "stop due to quota"); |
| return; |
| } |
| |
| base::TimeDelta diff = clock_->Now() - last_fetch_attempt_time; |
| switch (trigger) { |
| case TriggerType::PERSISTENT_SCHEDULER_WAKE_UP: |
| ReportTimeUntilPersistentFetch(user_classifier_->GetUserClass(), diff); |
| break; |
| case TriggerType::SURFACE_OPENED: |
| ReportTimeUntilShownFetch(user_classifier_->GetUserClass(), diff); |
| break; |
| case TriggerType::BROWSER_FOREGROUNDED: |
| case TriggerType::BROWSER_COLD_START: |
| ReportTimeUntilStartupFetch(user_classifier_->GetUserClass(), diff); |
| break; |
| case TriggerType::COUNT: |
| NOTREACHED(); |
| } |
| |
| UMA_HISTOGRAM_ENUMERATION( |
| "NewTabPage.ContentSuggestions.BackgroundFetchTrigger", |
| static_cast<int>(trigger), static_cast<int>(TriggerType::COUNT)); |
| |
| debug_logger_->Log(FROM_HERE, "issuing a fetch"); |
| background_fetch_in_progress_ = true; |
| |
| if ((trigger == TriggerType::BROWSER_COLD_START || |
| trigger == TriggerType::BROWSER_FOREGROUNDED || |
| trigger == TriggerType::SURFACE_OPENED) && |
| IsLastSuccessfulFetchStale()) { |
| provider_->RefetchWhileDisplaying( |
| base::BindOnce(&RemoteSuggestionsSchedulerImpl::RefetchFinished, |
| base::Unretained(this))); |
| return; |
| } |
| |
| provider_->RefetchInTheBackground( |
| base::BindOnce(&RemoteSuggestionsSchedulerImpl::RefetchFinished, |
| base::Unretained(this))); |
| } |
| |
| bool RemoteSuggestionsSchedulerImpl::ShouldRefetchNow( |
| base::Time last_fetch_attempt_time, |
| TriggerType trigger) { |
| // If we have no persistent scheduler to ask, err on the side of caution. |
| bool wifi = false; |
| if (persistent_scheduler_) { |
| wifi = persistent_scheduler_->IsOnUnmeteredConnection(); |
| } |
| |
| base::Time first_allowed_fetch_time = last_fetch_attempt_time; |
| if (trigger == TriggerType::SURFACE_OPENED) { |
| first_allowed_fetch_time += (wifi ? schedule_.interval_shown_wifi |
| : schedule_.interval_shown_fallback); |
| } else { |
| first_allowed_fetch_time += (wifi ? schedule_.interval_startup_wifi |
| : schedule_.interval_startup_fallback); |
| } |
| |
| base::Time now = clock_->Now(); |
| if (Logger::IsLoggingEnabled()) { |
| if (background_fetches_allowed_after_ > now) { |
| debug_logger_->Log( |
| FROM_HERE, |
| base::StringPrintf( |
| "due to privacy, next fetch is allowed after %s", |
| Logger::TimeToString(background_fetches_allowed_after_).c_str())); |
| } |
| if (first_allowed_fetch_time > now) { |
| debug_logger_->Log( |
| FROM_HERE, |
| base::StringPrintf( |
| "next fetch is scheduled after %s (as last fetch " |
| "attempt occured at %s)", |
| Logger::TimeToString(first_allowed_fetch_time).c_str(), |
| Logger::TimeToString(last_fetch_attempt_time).c_str())); |
| } |
| } |
| |
| return background_fetches_allowed_after_ <= now && |
| first_allowed_fetch_time <= now; |
| } |
| |
| bool RemoteSuggestionsSchedulerImpl::IsReadyForBackgroundFetches() const { |
| if (!provider_ || !provider_->ready()) { |
| return false; // Cannot fetch as remote suggestions provider does not |
| // exist or is not active yet. |
| } |
| |
| if (schedule_.is_empty()) { |
| return false; // Background fetches are disabled in general. |
| } |
| if (!eula_state_->IsEulaAccepted()) { |
| return false; // No background fetches are allowed before EULA is accepted. |
| } |
| |
| return true; |
| } |
| |
| bool RemoteSuggestionsSchedulerImpl::AcquireQuota(bool interactive_request) { |
| switch (user_classifier_->GetUserClass()) { |
| case UserClassifier::UserClass::RARE_NTP_USER: |
| return request_throttler_rare_ntp_user_.DemandQuotaForRequest( |
| interactive_request); |
| case UserClassifier::UserClass::ACTIVE_NTP_USER: |
| return request_throttler_active_ntp_user_.DemandQuotaForRequest( |
| interactive_request); |
| case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER: |
| return request_throttler_active_suggestions_consumer_ |
| .DemandQuotaForRequest(interactive_request); |
| } |
| NOTREACHED(); |
| return false; |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::RefetchFinished(Status fetch_status) { |
| background_fetch_in_progress_ = false; |
| OnFetchCompleted(fetch_status); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::OnFetchCompleted(Status fetch_status) { |
| profile_prefs_->SetTime(prefs::kSnippetLastFetchAttemptTime, clock_->Now()); |
| time_until_first_shown_trigger_reported_ = false; |
| time_until_first_startup_trigger_reported_ = false; |
| |
| // 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; |
| } |
| |
| profile_prefs_->SetTime(prefs::kSnippetLastSuccessfulFetchTime, |
| clock_->Now()); |
| |
| ApplyPersistentFetchingSchedule(); |
| } |
| |
| void RemoteSuggestionsSchedulerImpl::ClearLastFetchAttemptTime() { |
| profile_prefs_->ClearPref(prefs::kSnippetLastFetchAttemptTime); |
| // To mark the last fetch as stale, we need to keep the time in prefs, only |
| // making sure it is long ago. |
| profile_prefs_->SetTime(prefs::kSnippetLastSuccessfulFetchTime, base::Time()); |
| } |
| |
| std::set<RemoteSuggestionsSchedulerImpl::TriggerType> |
| RemoteSuggestionsSchedulerImpl::GetEnabledTriggerTypes() { |
| static_assert(static_cast<unsigned int>(TriggerType::COUNT) == |
| base::size(kTriggerTypeNames), |
| "Fill in names for trigger types."); |
| |
| std::string param_value = base::GetFieldTrialParamValueByFeature( |
| 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* const* 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<RemoteSuggestionsSchedulerImpl::TriggerType> |
| RemoteSuggestionsSchedulerImpl::GetDefaultEnabledTriggerTypes() { |
| return {TriggerType::PERSISTENT_SCHEDULER_WAKE_UP, |
| TriggerType::SURFACE_OPENED, TriggerType::BROWSER_COLD_START, |
| TriggerType::BROWSER_FOREGROUNDED}; |
| } |
| |
| } // namespace ntp_snippets |