MD Downloads: UI review feedback
[chromium-blink-merge.git] / chrome / browser / signin / cross_device_promo.cc
blob913d65ea7bc2ea50404db7ede9f6b9626e7dfd9e
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/signin/cross_device_promo.h"
7 #include "base/metrics/histogram_macros.h"
8 #include "base/prefs/pref_service.h"
9 #include "base/rand_util.h"
10 #include "base/strings/string_number_conversions.h"
11 #include "base/time/time.h"
12 #include "chrome/common/pref_names.h"
13 #include "components/signin/core/browser/signin_client.h"
14 #include "components/signin/core/browser/signin_manager.h"
15 #include "components/signin/core/browser/signin_metrics.h"
16 #include "components/variations/variations_associated_data.h"
18 namespace {
20 // Helper method to set a |local_parameter| to the result of fetching a
21 // |variation_parameter| from the variation seed, converting to an int, and then
22 // applying |conversion| to create a TimeDelta. Returns false if the
23 // |variation_parameter| was not found or the string could not be parsed to an
24 // int.
25 bool SetParameterFromVariation(
26 const std::string& variation_parameter,
27 base::Callback<base::TimeDelta(int)> conversion,
28 base::TimeDelta* local_parameter) {
29 std::string parameter_as_string = variations::GetVariationParamValue(
30 CrossDevicePromo::kCrossDevicePromoFieldTrial, variation_parameter);
31 if (parameter_as_string.empty())
32 return false;
34 int parameter_as_int;
35 if (!base::StringToInt(parameter_as_string, &parameter_as_int))
36 return false;
38 *local_parameter = conversion.Run(parameter_as_int);
39 return true;
42 } // namespace
44 // static
45 const char CrossDevicePromo::kCrossDevicePromoFieldTrial[] = "CrossDevicePromo";
46 const char CrossDevicePromo::kParamHoursBetweenDeviceActivityChecks[] =
47 "HoursBetweenDeviceActivityChecks";
48 const char CrossDevicePromo::kParamDaysToVerifySingleUserProfile[] =
49 "DaysToVerifySingleUserProfile";
50 const char CrossDevicePromo::kParamMinutesBetweenBrowsingSessions[] =
51 "MinutesBetweenBrowsingSessions";
52 const char CrossDevicePromo::kParamMinutesMaxContextSwitchDuration[] =
53 "MinutesMaxContextSwitchDuration";
54 const char CrossDevicePromo::kParamRPCThrottle[] =
55 "RPCThrottle";
57 CrossDevicePromo::CrossDevicePromo(
58 SigninManager* signin_manager,
59 GaiaCookieManagerService* cookie_manager_service,
60 SigninClient* signin_client,
61 PrefService* pref_service)
62 : initialized_(false),
63 signin_manager_(signin_manager),
64 cookie_manager_service_(cookie_manager_service),
65 prefs_(pref_service),
66 signin_client_(signin_client),
67 is_throttled_(true),
68 start_last_browsing_session_(base::Time()) {
69 VLOG(1) << "CrossDevicePromo::CrossDevicePromo.";
70 DCHECK(signin_manager_);
71 DCHECK(cookie_manager_service_);
72 DCHECK(prefs_);
73 DCHECK(signin_client_);
74 Init();
77 CrossDevicePromo::~CrossDevicePromo() {
80 void CrossDevicePromo::Shutdown() {
81 VLOG(1) << "CrossDevicePromo::Shutdown.";
82 UnregisterForCookieChanges();
83 if (start_last_browsing_session_ != base::Time())
84 signin_metrics::LogBrowsingSessionDuration(start_last_browsing_session_);
87 void CrossDevicePromo::AddObserver(CrossDevicePromo::Observer* observer) {
88 observer_list_.AddObserver(observer);
91 void CrossDevicePromo::RemoveObserver(CrossDevicePromo::Observer* observer) {
92 observer_list_.RemoveObserver(observer);
95 void CrossDevicePromo::OnGaiaAccountsInCookieUpdated(
96 const std::vector<gaia::ListedAccount>& accounts,
97 const GoogleServiceAuthError& error) {
98 VLOG(1) << "CrossDevicePromo::OnGaiaAccountsInCookieUpdated. "
99 << accounts.size() << " accounts with auth error " << error.state();
100 if (error.state() != GoogleServiceAuthError::State::NONE)
101 return;
103 const bool single_account = accounts.size() == 1;
104 const bool has_pref =
105 prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie);
106 if (!single_account && has_pref) {
107 prefs_->ClearPref(prefs::kCrossDevicePromoObservedSingleAccountCookie);
108 MarkPromoShouldNotBeShown();
109 } else if (single_account && !has_pref) {
110 SetTimePref(prefs::kCrossDevicePromoObservedSingleAccountCookie,
111 base::Time::Now());
115 void CrossDevicePromo::OnFetchDeviceActivitySuccess(
116 const std::vector<DeviceActivityFetcher::DeviceActivity>& devices) {
117 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivitySuccess. "
118 << devices.size() << " devices.";
119 DetermineEligibilityFromDeviceActivity(devices);
121 // |device_activity_fetcher_| must be destroyed last as that object is what
122 // called OnFetchDeviceActivitySuccess().
123 // TODO(mlerman): Mark the DeviceActivityFetcher as stopped() or completed()
124 // rather than deleting the object.
125 device_activity_fetcher_.reset();
128 void CrossDevicePromo::OnFetchDeviceActivityFailure() {
129 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivityFailure.";
130 signin_metrics::LogXDevicePromoEligible(
131 signin_metrics::ERROR_FETCHING_DEVICE_ACTIVITY);
133 // |device_activity_fetcher_| must be destroyed last as that object is what
134 // called OnFetchDeviceActivityFailure().
135 device_activity_fetcher_.reset();
138 bool CrossDevicePromo::ShouldShowPromo() const {
139 return prefs_->GetBoolean(prefs::kCrossDevicePromoShouldBeShown);
142 void CrossDevicePromo::OptOut() {
143 VLOG(1) << "CrossDevicePromo::OptOut.";
144 UnregisterForCookieChanges();
145 prefs_->SetBoolean(prefs::kCrossDevicePromoOptedOut, true);
146 MarkPromoShouldNotBeShown();
149 void CrossDevicePromo::MaybeBrowsingSessionStarted(
150 const base::Time& previous_last_active) {
151 // In tests, or the first call for a profile, do nothing.
152 if (previous_last_active == base::Time())
153 return;
155 const base::Time time_now = base::Time::Now();
156 // Determine how often this method is called as an upper bound for how often
157 // back end servers might be called.
158 UMA_HISTOGRAM_CUSTOM_COUNTS(
159 "Signin.XDevicePromo.BrowsingSessionDurationComputed",
160 (base::Time::Now() - previous_last_active).InMinutes(), 1,
161 base::TimeDelta::FromDays(30).InMinutes(), 50);
163 // Check if this is a different browsing session since the last call.
164 if (time_now - previous_last_active <
165 inactivity_between_browsing_sessions_) {
166 VLOG(1) << "CrossDevicePromo::MaybeBrowsingSessionStarted. Same browsing "
167 "session as the last call.";
168 return;
171 if (start_last_browsing_session_ != base::Time())
172 signin_metrics::LogBrowsingSessionDuration(previous_last_active);
174 start_last_browsing_session_ = time_now;
176 if (!CheckPromoEligibility()) {
177 VLOG(1) << "CrossDevicePromo::MaybeBrowsingSessionStarted; "
178 << "Ineligible for promo.";
179 if (ShouldShowPromo())
180 MarkPromoShouldNotBeShown();
181 return;
184 // Check if there is a record of recent browsing activity on another device.
185 const base::Time device_last_active = GetTimePref(
186 prefs::kCrossDevicePromoLastDeviceActiveTime);
187 if (time_now - device_last_active < context_switch_duration_) {
188 VLOG(1) << "CrossDevicePromo::MaybeBrowsingSessionStarted; promo active.";
189 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE);
190 MarkPromoShouldBeShown();
191 return;
194 // Check for recency of device activity unless a check is already being
195 // executed because the number of devices is being updated. Use a small delay
196 // to ensure server-side data is synchronized.
197 if (!device_activity_fetcher_) {
198 VLOG(1) << "CrossDevicePromo::MaybeBrowsingSessionStarted; "
199 << "Check device activity.";
200 const int kDelayUntilGettingDeviceActivityInMS = 3000;
201 device_activity_timer_.Start(
202 FROM_HERE,
203 base::TimeDelta::FromMilliseconds(kDelayUntilGettingDeviceActivityInMS),
204 this, &CrossDevicePromo::GetDevicesActivityForGAIAAccountInCookieJar);
208 bool CrossDevicePromo::CheckPromoEligibilityForTesting() {
209 // The field trial may not have been present when Init() was called from the
210 // constructor.
211 if (!initialized_)
212 Init();
214 return CheckPromoEligibility();
217 void CrossDevicePromo::Init() {
218 DCHECK(!initialized_);
219 // We need a default value for |inactivity_between_browsing_sessions_|
220 // as it is referenced early in MaybeBrowsingSessionStarted and we want to
221 // gather as many stats about browsing sessions as possible.
222 const int kDefaultBrowsingSessionDurationInMinutes = 15;
223 inactivity_between_browsing_sessions_ =
224 base::TimeDelta::FromMinutes(kDefaultBrowsingSessionDurationInMinutes);
226 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) {
227 signin_metrics::LogXDevicePromoInitialized(
228 signin_metrics::UNINITIALIZED_OPTED_OUT);
229 return;
232 if (!SetParameterFromVariation(kParamHoursBetweenDeviceActivityChecks,
233 base::Bind(&base::TimeDelta::FromHours),
234 &delay_until_next_device_activity_fetch_) ||
235 !SetParameterFromVariation(kParamDaysToVerifySingleUserProfile,
236 base::Bind(&base::TimeDelta::FromDays),
237 &single_account_duration_threshold_) ||
238 !SetParameterFromVariation(kParamMinutesBetweenBrowsingSessions,
239 base::Bind(&base::TimeDelta::FromMinutes),
240 &inactivity_between_browsing_sessions_) ||
241 !SetParameterFromVariation(kParamMinutesMaxContextSwitchDuration,
242 base::Bind(&base::TimeDelta::FromMinutes),
243 &context_switch_duration_)) {
244 signin_metrics::LogXDevicePromoInitialized(
245 signin_metrics::NO_VARIATIONS_CONFIG);
246 return;
249 const std::string throttle = variations::GetVariationParamValue(
250 kCrossDevicePromoFieldTrial, kParamRPCThrottle);
251 int throttle_percent;
252 if (throttle.empty() || !base::StringToInt(throttle, &throttle_percent)) {
253 signin_metrics::LogXDevicePromoInitialized(
254 signin_metrics::NO_VARIATIONS_CONFIG);
255 return;
258 is_throttled_ = throttle_percent && base::RandInt(0, 99) < throttle_percent;
260 VLOG(1) << "CrossDevicePromo::Init. Service initialized. Parameters: "
261 << "Hour between RPC checks: "
262 << delay_until_next_device_activity_fetch_.InHours()
263 << " Days to verify an account in the cookie: "
264 << single_account_duration_threshold_.InDays()
265 << " Minutes between browsing sessions: "
266 << inactivity_between_browsing_sessions_.InMinutes()
267 << " Window (in minutes) for a context switch: "
268 << context_switch_duration_.InMinutes()
269 << " Throttle rate for RPC calls: " << throttle_percent
270 << " This promo is throttled: " << is_throttled_;
271 RegisterForCookieChanges();
272 initialized_ = true;
273 signin_metrics::LogXDevicePromoInitialized(signin_metrics::INITIALIZED);
274 return;
277 void CrossDevicePromo::MarkPromoShouldBeShown() {
278 VLOG(1) << "CrossDevicePromo::MarkPromoShouldBeShown.";
279 DCHECK(!prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut));
281 if (!prefs_->GetBoolean(prefs::kCrossDevicePromoShouldBeShown)) {
282 prefs_->SetBoolean(prefs::kCrossDevicePromoShouldBeShown, true);
283 FOR_EACH_OBSERVER(CrossDevicePromo::Observer, observer_list_,
284 OnPromoEligibilityChanged(true));
288 void CrossDevicePromo::MarkPromoShouldNotBeShown() {
289 VLOG(1) << "CrossDevicePromo::MarkPromoShouldNotBeShown.";
290 if (prefs_->GetBoolean(prefs::kCrossDevicePromoShouldBeShown)) {
291 prefs_->SetBoolean(prefs::kCrossDevicePromoShouldBeShown, false);
292 FOR_EACH_OBSERVER(CrossDevicePromo::Observer, observer_list_,
293 OnPromoEligibilityChanged(false));
297 bool CrossDevicePromo::CheckPromoEligibility() {
298 if (!initialized_)
299 return false;
301 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) {
302 signin_metrics::LogXDevicePromoEligible(signin_metrics::OPTED_OUT);
303 return false;
306 if (signin_manager_->IsAuthenticated()) {
307 signin_metrics::LogXDevicePromoEligible(signin_metrics::SIGNED_IN);
308 return false;
311 if (!prefs_->HasPrefPath(
312 prefs::kCrossDevicePromoObservedSingleAccountCookie) ||
313 (GetTimePref(prefs::kCrossDevicePromoObservedSingleAccountCookie) +
314 single_account_duration_threshold_ > base::Time::Now())) {
315 signin_metrics::LogXDevicePromoEligible(
316 signin_metrics::NOT_SINGLE_GAIA_ACCOUNT);
317 return false;
320 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNextFetchListDevicesTime)) {
321 // The missing preference indicates CheckPromoEligibility() has never been
322 // called. Determine when to call the DeviceActivityFetcher for the first
323 // time.
324 const uint64 milliseconds_until_next_activity_fetch = base::RandGenerator(
325 delay_until_next_device_activity_fetch_.InMilliseconds());
326 const base::Time time_of_next_device_activity_fetch = base::Time::Now() +
327 base::TimeDelta::FromMilliseconds(
328 milliseconds_until_next_activity_fetch);
329 SetTimePref(prefs::kCrossDevicePromoNextFetchListDevicesTime,
330 time_of_next_device_activity_fetch);
331 signin_metrics::LogXDevicePromoEligible(
332 signin_metrics::UNKNOWN_COUNT_DEVICES);
333 return false;
336 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNumDevices)) {
337 // The missing pref indicates no knowledge of other device activity yet.
338 const base::Time time_next_list_devices = GetTimePref(
339 prefs::kCrossDevicePromoNextFetchListDevicesTime);
340 // Not time yet to poll the list of devices.
341 if (base::Time::Now() < time_next_list_devices) {
342 signin_metrics::LogXDevicePromoEligible(
343 signin_metrics::UNKNOWN_COUNT_DEVICES);
344 return false;
346 // We're not eligible... yet! Track metrics in the results.
347 GetDevicesActivityForGAIAAccountInCookieJar();
348 return false;
351 int num_devices = prefs_->GetInteger(prefs::kCrossDevicePromoNumDevices);
352 const base::Time time_next_list_devices = GetTimePref(
353 prefs::kCrossDevicePromoNextFetchListDevicesTime);
354 if (base::Time::Now() > time_next_list_devices) {
355 GetDevicesActivityForGAIAAccountInCookieJar();
356 } else if (num_devices == 0) {
357 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES);
358 return false;
361 DCHECK(VerifyPromoEligibleReadOnly());
362 return true;
365 void CrossDevicePromo::DetermineEligibilityFromDeviceActivity(
366 const std::vector<DeviceActivityFetcher::DeviceActivity>& devices) {
368 const base::Time time_now = base::Time::Now();
369 SetTimePref(prefs::kCrossDevicePromoNextFetchListDevicesTime,
370 time_now + delay_until_next_device_activity_fetch_);
371 prefs_->SetInteger(prefs::kCrossDevicePromoNumDevices, devices.size());
373 if (devices.empty()) {
374 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES);
375 return;
378 const base::Time most_recent_last_active =
379 std::max_element(devices.begin(), devices.end(),
380 [](const DeviceActivityFetcher::DeviceActivity& first,
381 const DeviceActivityFetcher::DeviceActivity& second) {
382 return first.last_active < second.last_active;
383 })->last_active;
385 SetTimePref(prefs::kCrossDevicePromoLastDeviceActiveTime,
386 most_recent_last_active);
388 if (time_now - most_recent_last_active < context_switch_duration_) {
389 // Make sure that while the DeviceActivityFetcher was executing the promo
390 // wasn't found as ineligible to be shown.
391 if (!VerifyPromoEligibleReadOnly())
392 return;
394 // The context switch will only be valid for so long. Schedule another
395 // device activity check for when our switch would expire to check for more
396 // recent activity.
397 if (!device_activity_timer_.IsRunning()) {
398 base::TimeDelta time_to_next_check = most_recent_last_active +
399 context_switch_duration_ -
400 time_now;
401 device_activity_timer_.Start(
402 FROM_HERE, time_to_next_check, this,
403 &CrossDevicePromo::GetDevicesActivityForGAIAAccountInCookieJar);
406 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE);
407 MarkPromoShouldBeShown();
408 } else {
409 signin_metrics::LogXDevicePromoEligible(signin_metrics::NO_ACTIVE_DEVICES);
410 MarkPromoShouldNotBeShown();
414 base::Time CrossDevicePromo::GetTimePref(const std::string& pref) const {
415 return base::Time::FromInternalValue(prefs_->GetInt64(pref));
418 void CrossDevicePromo::SetTimePref(const std::string& pref,
419 const base::Time& value) {
420 prefs_->SetInt64(pref, value.ToInternalValue());
423 bool CrossDevicePromo::VerifyPromoEligibleReadOnly() const {
424 return initialized_ &&
425 !prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut) &&
426 prefs_->HasPrefPath(
427 prefs::kCrossDevicePromoObservedSingleAccountCookie) &&
428 GetTimePref(prefs::kCrossDevicePromoObservedSingleAccountCookie) +
429 single_account_duration_threshold_ <= base::Time::Now();
432 void CrossDevicePromo::GetDevicesActivityForGAIAAccountInCookieJar() {
433 // Don't start a fetch while one is processing.
434 if (device_activity_fetcher_)
435 return;
437 if (is_throttled_) {
438 signin_metrics::LogXDevicePromoEligible(
439 signin_metrics::THROTTLED_FETCHING_DEVICE_ACTIVITY);
440 return;
443 VLOG(1) << "CrossDevicePromo::GetDevicesActivityForGAIAAccountInCookieJar.";
444 DCHECK(VerifyPromoEligibleReadOnly());
445 device_activity_fetcher_.reset(
446 new DeviceActivityFetcher(signin_client_, this));
447 device_activity_fetcher_->Start();
450 void CrossDevicePromo::RegisterForCookieChanges() {
451 cookie_manager_service_->AddObserver(this);
454 void CrossDevicePromo::UnregisterForCookieChanges() {
455 cookie_manager_service_->RemoveObserver(this);