Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(18)

Unified Diff: chrome/browser/signin/cross_device_promo.cc

Issue 1087933002: Cross Device Promo - Main Eligibility Flow (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: build.gn Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « chrome/browser/signin/cross_device_promo.h ('k') | chrome/browser/signin/cross_device_promo_factory.h » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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, &parameter_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));
+ }
+}
« no previous file with comments | « chrome/browser/signin/cross_device_promo.h ('k') | chrome/browser/signin/cross_device_promo_factory.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698