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"
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
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())
35 if (!base::StringToInt(parameter_as_string
, ¶meter_as_int
))
38 *local_parameter
= conversion
.Run(parameter_as_int
);
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
[] =
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
),
66 signin_client_(signin_client
),
68 start_last_browsing_session_(base::Time()) {
69 VLOG(1) << "CrossDevicePromo::CrossDevicePromo.";
70 DCHECK(signin_manager_
);
71 DCHECK(cookie_manager_service_
);
73 DCHECK(signin_client_
);
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
)
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
,
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())
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.";
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();
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();
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(
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
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
);
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
);
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
);
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();
273 signin_metrics::LogXDevicePromoInitialized(signin_metrics::INITIALIZED
);
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() {
301 if (prefs_
->GetBoolean(prefs::kCrossDevicePromoOptedOut
)) {
302 signin_metrics::LogXDevicePromoEligible(signin_metrics::OPTED_OUT
);
306 if (signin_manager_
->IsAuthenticated()) {
307 signin_metrics::LogXDevicePromoEligible(signin_metrics::SIGNED_IN
);
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
);
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
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
);
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
);
346 // We're not eligible... yet! Track metrics in the results.
347 GetDevicesActivityForGAIAAccountInCookieJar();
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
);
361 DCHECK(VerifyPromoEligibleReadOnly());
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
);
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
;
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())
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
397 if (!device_activity_timer_
.IsRunning()) {
398 base::TimeDelta time_to_next_check
= most_recent_last_active
+
399 context_switch_duration_
-
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();
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
) &&
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_
)
438 signin_metrics::LogXDevicePromoEligible(
439 signin_metrics::THROTTLED_FETCHING_DEVICE_ACTIVITY
);
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);