Index: chrome/browser/signin/cross_device_promo.cc |
diff --git a/chrome/browser/signin/cross_device_promo.cc b/chrome/browser/signin/cross_device_promo.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..095591dfe9e60de8e0a45fab203b4d3a0f449359 |
--- /dev/null |
+++ b/chrome/browser/signin/cross_device_promo.cc |
@@ -0,0 +1,428 @@ |
+// Copyright 2015 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/signin/cross_device_promo.h" |
+ |
+#include "base/metrics/histogram_macros.h" |
+#include "base/prefs/pref_service.h" |
+#include "base/rand_util.h" |
+#include "base/strings/string_number_conversions.h" |
+#include "base/time/time.h" |
+#include "chrome/common/pref_names.h" |
+#include "components/signin/core/browser/signin_client.h" |
+#include "components/signin/core/browser/signin_manager.h" |
+#include "components/signin/core/browser/signin_metrics.h" |
+#include "components/variations/variations_associated_data.h" |
+#include "net/cookies/canonical_cookie.h" |
+ |
+namespace { |
+ |
+const int kDelayUntilGettingDeviceActivityInMS = 3000; |
+const int kDefaultBrowsingSessionDurationInMinutes = 15; |
+ |
+// Helper method to set a parameter based on a particular variable from the |
+// configuration. Returns false if the parameter was not found or the conversion |
+// could not succeed. |
+bool SetParameterFromVariation( |
+ const std::string& variation_parameter, |
+ base::TimeDelta* local_parameter, |
+ base::Callback<base::TimeDelta(int)> conversion) { |
+ std::string parameter_as_string = variations::GetVariationParamValue( |
+ "CrossDevicePromo", variation_parameter); |
+ if (parameter_as_string.empty()) |
+ return false; |
+ |
+ int parameter_as_int; |
+ if (!base::StringToInt(parameter_as_string, ¶meter_as_int)) |
+ return false; |
+ |
+ *local_parameter = conversion.Run(parameter_as_int); |
+ return true; |
+} |
+ |
+} // namespace |
+ |
+CrossDevicePromo::CrossDevicePromo( |
+ SigninManager* signin_manager, |
+ GaiaCookieManagerService* cookie_manager_service, |
+ SigninClient* signin_client, |
+ PrefService* pref_service) |
+ : initialized_(false), |
+ signin_manager_(signin_manager), |
+ cookie_manager_service_(cookie_manager_service), |
+ prefs_(pref_service), |
+ signin_client_(signin_client), |
+ is_throttled_(true), |
+ start_last_browsing_session_(base::Time()) { |
+ VLOG(1) << "CrossDevicePromo::CrossDevicePromo."; |
+ DCHECK(signin_manager_); |
+ DCHECK(cookie_manager_service_); |
+ DCHECK(prefs_); |
+ DCHECK(signin_client_); |
+ Init(); |
+} |
+ |
+CrossDevicePromo::~CrossDevicePromo() { |
+} |
+ |
+void CrossDevicePromo::Shutdown() { |
+ VLOG(1) << "CrossDevicePromo::Shutdown."; |
+ UnregisterForCookieChanges(); |
+ if (start_last_browsing_session_ != base::Time()) |
+ signin_metrics::LogBrowsingSessionDuration(start_last_browsing_session_); |
+} |
+ |
+void CrossDevicePromo::AddObserver(CrossDevicePromo::Observer* observer) { |
+ observer_list_.AddObserver(observer); |
+} |
+ |
+void CrossDevicePromo::RemoveObserver(CrossDevicePromo::Observer* observer) { |
+ observer_list_.RemoveObserver(observer); |
+} |
+ |
+bool CrossDevicePromo::IsPromoActive() { |
+ return prefs_->GetBoolean(prefs::kCrossDevicePromoActive); |
+} |
+ |
+void CrossDevicePromo::Init() { |
+ DCHECK(!initialized_); |
+ // We need a default value for this as it is referenced early in |
+ // UpdateLastActiveTime and we want to gather as many stats about Browsing |
+ // Sessions as possible. |
+ inactivity_between_browsing_sessions_ = |
+ base::TimeDelta::FromMinutes(kDefaultBrowsingSessionDurationInMinutes); |
+ |
+ if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) { |
+ signin_metrics::LogXDevicePromoInitialized( |
+ signin_metrics::UNINITIALIZED_OPTED_OUT); |
+ return; |
+ } |
+ |
+ if (!SetParameterFromVariation("HoursBetweenSyncDeviceChecks", |
+ &delay_until_next_list_devices_, |
+ base::Bind(&base::TimeDelta::FromHours)) || |
+ !SetParameterFromVariation("DaysToVerifySingleUserProfile", |
+ &single_account_duration_threshold_, |
+ base::Bind(&base::TimeDelta::FromDays)) || |
+ !SetParameterFromVariation("MinutesBetweenBrowsingSessions", |
+ &inactivity_between_browsing_sessions_, |
+ base::Bind(&base::TimeDelta::FromMinutes)) || |
+ !SetParameterFromVariation("MinutesMaxContextSwitchDuration", |
+ &context_switch_duration_, |
+ base::Bind(&base::TimeDelta::FromMinutes))) { |
+ signin_metrics::LogXDevicePromoInitialized( |
+ signin_metrics::NO_VARIATIONS_CONFIG); |
+ return; |
+ } |
+ |
+ std::string throttle = |
+ variations::GetVariationParamValue("CrossDevicePromo", "RPCThrottle"); |
+ uint64 throttle_percent; |
+ if (throttle.empty() || !base::StringToUint64(throttle, &throttle_percent)) { |
+ signin_metrics::LogXDevicePromoInitialized( |
+ signin_metrics::NO_VARIATIONS_CONFIG); |
+ return; |
+ } |
+ |
+ is_throttled_ = |
+ throttle_percent && base::RandGenerator(100) < throttle_percent; |
+ |
+ VLOG(1) << "CrossDevicePromo::Init. Service initialized. Parameters: " |
+ << "Hour between RPC checks: " |
+ << delay_until_next_list_devices_.InHours() |
+ << " Days to verify an account in the cookie: " |
+ << single_account_duration_threshold_.InDays() |
+ << " Minutes between browsing sessions: " |
+ << inactivity_between_browsing_sessions_.InMinutes() |
+ << " Window (in minutes) for a context switch: " |
+ << context_switch_duration_.InMinutes() |
+ << " Throttle rate for RPC calls: " << throttle_percent |
+ << " This promo is throttled: " << is_throttled_; |
+ RegisterForCookieChanges(); |
+ initialized_ = true; |
+ signin_metrics::LogXDevicePromoInitialized(signin_metrics::INITIALIZED); |
+ return; |
+} |
+ |
+void CrossDevicePromo::OptOut() { |
+ VLOG(1) << "CrossDevicePromo::OptOut."; |
+ UnregisterForCookieChanges(); |
+ prefs_->SetBoolean(prefs::kCrossDevicePromoOptedOut, true); |
+ MarkPromoInactive(); |
+} |
+ |
+bool CrossDevicePromo::VerifyPromoEligibleReadOnly() { |
+ if (!initialized_) |
+ return false; |
+ |
+ if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) |
+ return false; |
+ |
+ if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie)) |
+ return false; |
+ |
+ if (GetTimePref(prefs::kCrossDevicePromoObservedSingleAccountCookie) + |
+ single_account_duration_threshold_ > base::Time::Now()) { |
+ return false; |
+ } |
+ |
+ return true; |
+} |
+ |
+bool CrossDevicePromo::CheckPromoEligibility() { |
+ if (!initialized_) { |
+ // In tests the variations may not be present when Init() was first called. |
+ Init(); |
+ if (!initialized_) |
+ return false; |
+ } |
+ |
+ if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) { |
+ signin_metrics::LogXDevicePromoEligible(signin_metrics::OPTED_OUT); |
+ return false; |
+ } |
+ |
+ if (signin_manager_->IsAuthenticated()) { |
+ signin_metrics::LogXDevicePromoEligible(signin_metrics::SIGNED_IN); |
+ return false; |
+ } |
+ |
+ base::Time cookie_has_one_account_since = GetTimePref( |
+ prefs::kCrossDevicePromoObservedSingleAccountCookie); |
+ if (!prefs_->HasPrefPath( |
+ prefs::kCrossDevicePromoObservedSingleAccountCookie) || |
+ cookie_has_one_account_since + single_account_duration_threshold_ > |
+ base::Time::Now()) { |
+ signin_metrics::LogXDevicePromoEligible( |
+ signin_metrics::NOT_SINGLE_GAIA_ACCOUNT); |
+ return false; |
+ } |
+ |
+ // This is the first time the promo's being run; determine when to call the |
+ // DeviceActivityFetcher. |
+ if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNextFetchListDevicesTime)) { |
+ const int minutes_until_next_activity_fetch = |
+ base::RandGenerator(delay_until_next_list_devices_.InMinutes()); |
+ const base::Time time_of_next_device_activity_fetch = base::Time::Now() + |
+ base::TimeDelta::FromMinutes(minutes_until_next_activity_fetch); |
+ prefs_->SetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime, |
+ time_of_next_device_activity_fetch.ToInternalValue()); |
+ signin_metrics::LogXDevicePromoEligible( |
+ signin_metrics::UNKNOWN_COUNT_DEVICES); |
+ return false; |
+ } |
+ |
+ // We have no knowledge of other device activity yet. |
+ if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNumDevices)) { |
+ base::Time time_next_list_devices = GetTimePref( |
+ prefs::kCrossDevicePromoNextFetchListDevicesTime); |
+ // Not time yet to poll the list of devices. |
+ if (time_next_list_devices > base::Time::Now()) { |
+ signin_metrics::LogXDevicePromoEligible( |
+ signin_metrics::UNKNOWN_COUNT_DEVICES); |
+ return false; |
+ } |
+ // We're not eligible... but might be! Track metrics in the results. |
+ GetDevicesActivityForAccountInCookie(); |
+ return false; |
+ } |
+ |
+ int num_devices = prefs_->GetInteger(prefs::kCrossDevicePromoNumDevices); |
+ base::Time time_next_list_devices = GetTimePref( |
+ prefs::kCrossDevicePromoNextFetchListDevicesTime); |
+ if (time_next_list_devices < base::Time::Now()) { |
+ GetDevicesActivityForAccountInCookie(); |
+ } else if (num_devices == 0) { |
+ signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES); |
+ return false; |
+ } |
+ |
+ DCHECK(VerifyPromoEligibleReadOnly()); |
+ return true; |
+} |
+ |
+void CrossDevicePromo::MaybeBrowsingSessionStarted( |
+ const base::Time& previous_last_active) { |
+ // In tests, or the first call for a profile, don't pass go. |
+ if (previous_last_active == base::Time()) |
+ return; |
+ |
+ base::Time time_now = base::Time::Now(); |
+ // Determine how often this method is called. Need an estimate for QPS. |
+ UMA_HISTOGRAM_CUSTOM_COUNTS( |
+ "Signin.XDevicePromo.BrowsingSessionDurationComputed", |
+ (base::Time::Now() - previous_last_active).InMinutes(), 1, |
+ base::TimeDelta::FromDays(30).InMinutes(), 50); |
+ |
+ // Check if this is a different browsing session since the last call. |
+ if (time_now - previous_last_active <= |
+ inactivity_between_browsing_sessions_) { |
+ VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime. Same browsing session " |
+ "as the last call."; |
+ return; |
+ } |
+ |
+ if (start_last_browsing_session_ != base::Time()) |
+ signin_metrics::LogBrowsingSessionDuration(previous_last_active); |
+ |
+ start_last_browsing_session_ = time_now; |
+ |
+ if (!CheckPromoEligibility()) { |
+ VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Ineligible for promo."; |
+ if (IsPromoActive()) |
+ MarkPromoInactive(); |
+ return; |
+ } |
+ |
+ // Check if there is a record of recent browsing activity on another device. |
+ // If there is none, we set a timer to update the records after a small delay |
+ // to ensure server-side data is synchronized. |
+ base::Time device_last_active = GetTimePref( |
+ prefs::kCrossDevicePromoLastDeviceActiveTime); |
+ if (time_now - device_last_active < context_switch_duration_) { |
+ VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; promo active."; |
+ signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE); |
+ MarkPromoActive(); |
+ return; |
+ } |
+ |
+ // Check for recency of device activity unless a check is already being |
+ // executed because the number of devices is being updated. |
+ if (!device_activity_fetcher_.get()) { |
+ VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Check device activity."; |
+ device_activity_timer_.Start( |
+ FROM_HERE, |
+ base::TimeDelta::FromMilliseconds(kDelayUntilGettingDeviceActivityInMS), |
+ this, &CrossDevicePromo::GetDevicesActivityForAccountInCookie); |
+ } |
+} |
+ |
+void CrossDevicePromo::GetDevicesActivityForAccountInCookie() { |
+ // Don't start a fetch while one is processing. |
+ if (device_activity_fetcher_.get()) |
+ return; |
+ |
+ if (is_throttled_) { |
+ signin_metrics::LogXDevicePromoEligible( |
+ signin_metrics::THROTTLED_FETCHING_DEVICE_ACTIVITY); |
+ return; |
+ } |
+ |
+ VLOG(1) << "CrossDevicePromo::GetDevicesActivityForAccountInCookie. Start."; |
+ DCHECK(VerifyPromoEligibleReadOnly()); |
+ device_activity_fetcher_.reset( |
+ new DeviceActivityFetcher(signin_client_, this)); |
+ device_activity_fetcher_->Start(); |
+} |
+ |
+void CrossDevicePromo::OnFetchDeviceActivitySuccess( |
+ const std::vector<DeviceActivityFetcher::DeviceActivity>& devices) { |
+ const base::Time time_now = base::Time::Now(); |
+ VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivitySuccess. " |
+ << devices.size() << " devices."; |
+ prefs_->SetInt64( |
+ prefs::kCrossDevicePromoNextFetchListDevicesTime, |
+ (time_now + delay_until_next_list_devices_).ToInternalValue()); |
+ prefs_->SetInteger(prefs::kCrossDevicePromoNumDevices, devices.size()); |
+ |
+ if (devices.empty()) { |
+ signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES); |
+ return; |
+ } |
+ |
+ base::Time most_recent_last_active = |
+ std::max_element(devices.begin(), devices.end(), |
+ [](const DeviceActivityFetcher::DeviceActivity& first, |
+ const DeviceActivityFetcher::DeviceActivity& second) { |
+ return first.last_active < second.last_active; |
+ })->last_active; |
+ |
+ prefs_->SetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime, |
+ most_recent_last_active.ToInternalValue()); |
+ |
+ if (time_now - most_recent_last_active < context_switch_duration_) { |
+ // Make sure eligibility wasn't lost while executing the remote call. |
+ if (!VerifyPromoEligibleReadOnly()) |
+ return; |
+ |
+ // The context switch will only be valid for so long. Schedule another |
+ // DeviceActivity check for when our switch would expire to check for more |
+ // recent activity. |
+ if (!device_activity_timer_.IsRunning()) { |
+ base::TimeDelta time_to_next_check = most_recent_last_active + |
+ context_switch_duration_ - |
+ time_now; |
+ device_activity_timer_.Start( |
+ FROM_HERE, time_to_next_check, this, |
+ &CrossDevicePromo::GetDevicesActivityForAccountInCookie); |
+ } |
+ |
+ signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE); |
+ MarkPromoActive(); |
+ } else { |
+ signin_metrics::LogXDevicePromoEligible(signin_metrics::NO_ACTIVE_DEVICES); |
+ MarkPromoInactive(); |
+ } |
+ device_activity_fetcher_.reset(); |
+} |
+ |
+void CrossDevicePromo::OnFetchDeviceActivityFailure() { |
+ VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivityFailure."; |
+ signin_metrics::LogXDevicePromoEligible( |
+ signin_metrics::ERROR_FETCHING_DEVICE_ACTIVITY); |
+ device_activity_fetcher_.reset(); |
+} |
+ |
+void CrossDevicePromo::RegisterForCookieChanges() { |
+ cookie_manager_service_->AddObserver(this); |
+} |
+ |
+void CrossDevicePromo::UnregisterForCookieChanges() { |
+ cookie_manager_service_->RemoveObserver(this); |
+} |
+ |
+base::Time CrossDevicePromo::GetTimePref(const std::string& pref) { |
+ return base::Time::FromInternalValue(prefs_->GetInt64(pref)); |
+} |
+ |
+void CrossDevicePromo::OnGaiaAccountsInCookieUpdated( |
+ const std::vector<std::pair<std::string, bool>>& accounts, |
+ const GoogleServiceAuthError& error) { |
+ VLOG(1) << "CrossDevicePromo::OnGaiaAccountsInCookieUpdated. " |
+ << accounts.size() << " accounts with auth error " << error.state(); |
+ if (error.state() != GoogleServiceAuthError::State::NONE) |
+ return; |
+ |
+ // Multiple accounts seen - clear the pref. |
+ bool single_account = accounts.size() == 1; |
+ bool has_pref = |
+ prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie); |
+ if (!single_account && has_pref) { |
+ prefs_->ClearPref(prefs::kCrossDevicePromoObservedSingleAccountCookie); |
+ MarkPromoInactive(); |
+ } else if (single_account && !has_pref) { |
+ prefs_->SetInt64(prefs::kCrossDevicePromoObservedSingleAccountCookie, |
+ base::Time::Now().ToInternalValue()); |
+ } |
+} |
+ |
+void CrossDevicePromo::MarkPromoActive() { |
+ VLOG(1) << "CrossDevicePromo::MarkPromoActive."; |
+ DCHECK(!prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)); |
+ |
+ if (!prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) { |
+ prefs_->SetBoolean(prefs::kCrossDevicePromoActive, true); |
+ FOR_EACH_OBSERVER(CrossDevicePromo::Observer, observer_list_, |
+ OnPromoActivationChanged(true)); |
+ } |
+} |
+ |
+void CrossDevicePromo::MarkPromoInactive() { |
+ VLOG(1) << "CrossDevicePromo::MarkPromoInactive."; |
+ if (prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) { |
+ prefs_->SetBoolean(prefs::kCrossDevicePromoActive, false); |
+ FOR_EACH_OBSERVER(CrossDevicePromo::Observer, observer_list_, |
+ OnPromoActivationChanged(false)); |
+ } |
+} |