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"
17 #include "net/cookies/canonical_cookie.h"
21 const int kDelayUntilGettingDeviceActivityInMS
= 3000;
22 const int kDefaultBrowsingSessionDurationInMinutes
= 15;
24 // Helper method to set a parameter based on a particular variable from the
25 // configuration. Returns false if the parameter was not found or the conversion
27 bool SetParameterFromVariation(
28 const std::string
& variation_parameter
,
29 base::TimeDelta
* local_parameter
,
30 base::Callback
<base::TimeDelta(int)> conversion
) {
31 std::string parameter_as_string
= variations::GetVariationParamValue(
32 "CrossDevicePromo", variation_parameter
);
33 if (parameter_as_string
.empty())
37 if (!base::StringToInt(parameter_as_string
, ¶meter_as_int
))
40 *local_parameter
= conversion
.Run(parameter_as_int
);
46 CrossDevicePromo::CrossDevicePromo(
47 SigninManager
* signin_manager
,
48 GaiaCookieManagerService
* cookie_manager_service
,
49 SigninClient
* signin_client
,
50 PrefService
* pref_service
)
51 : initialized_(false),
52 signin_manager_(signin_manager
),
53 cookie_manager_service_(cookie_manager_service
),
55 signin_client_(signin_client
),
57 start_last_browsing_session_(base::Time()) {
58 VLOG(1) << "CrossDevicePromo::CrossDevicePromo.";
59 DCHECK(signin_manager_
);
60 DCHECK(cookie_manager_service_
);
62 DCHECK(signin_client_
);
66 CrossDevicePromo::~CrossDevicePromo() {
69 void CrossDevicePromo::Shutdown() {
70 VLOG(1) << "CrossDevicePromo::Shutdown.";
71 UnregisterForCookieChanges();
72 if (start_last_browsing_session_
!= base::Time())
73 signin_metrics::LogBrowsingSessionDuration(start_last_browsing_session_
);
76 void CrossDevicePromo::AddObserver(CrossDevicePromo::Observer
* observer
) {
77 observer_list_
.AddObserver(observer
);
80 void CrossDevicePromo::RemoveObserver(CrossDevicePromo::Observer
* observer
) {
81 observer_list_
.RemoveObserver(observer
);
84 bool CrossDevicePromo::IsPromoActive() {
85 return prefs_
->GetBoolean(prefs::kCrossDevicePromoActive
);
88 void CrossDevicePromo::Init() {
89 DCHECK(!initialized_
);
90 // We need a default value for this as it is referenced early in
91 // UpdateLastActiveTime and we want to gather as many stats about Browsing
92 // Sessions as possible.
93 inactivity_between_browsing_sessions_
=
94 base::TimeDelta::FromMinutes(kDefaultBrowsingSessionDurationInMinutes
);
96 if (prefs_
->GetBoolean(prefs::kCrossDevicePromoOptedOut
)) {
97 signin_metrics::LogXDevicePromoInitialized(
98 signin_metrics::UNINITIALIZED_OPTED_OUT
);
102 if (!SetParameterFromVariation("HoursBetweenSyncDeviceChecks",
103 &delay_until_next_list_devices_
,
104 base::Bind(&base::TimeDelta::FromHours
)) ||
105 !SetParameterFromVariation("DaysToVerifySingleUserProfile",
106 &single_account_duration_threshold_
,
107 base::Bind(&base::TimeDelta::FromDays
)) ||
108 !SetParameterFromVariation("MinutesBetweenBrowsingSessions",
109 &inactivity_between_browsing_sessions_
,
110 base::Bind(&base::TimeDelta::FromMinutes
)) ||
111 !SetParameterFromVariation("MinutesMaxContextSwitchDuration",
112 &context_switch_duration_
,
113 base::Bind(&base::TimeDelta::FromMinutes
))) {
114 signin_metrics::LogXDevicePromoInitialized(
115 signin_metrics::NO_VARIATIONS_CONFIG
);
119 std::string throttle
=
120 variations::GetVariationParamValue("CrossDevicePromo", "RPCThrottle");
121 uint64 throttle_percent
;
122 if (throttle
.empty() || !base::StringToUint64(throttle
, &throttle_percent
)) {
123 signin_metrics::LogXDevicePromoInitialized(
124 signin_metrics::NO_VARIATIONS_CONFIG
);
129 throttle_percent
&& base::RandGenerator(100) < throttle_percent
;
131 VLOG(1) << "CrossDevicePromo::Init. Service initialized. Parameters: "
132 << "Hour between RPC checks: "
133 << delay_until_next_list_devices_
.InHours()
134 << " Days to verify an account in the cookie: "
135 << single_account_duration_threshold_
.InDays()
136 << " Minutes between browsing sessions: "
137 << inactivity_between_browsing_sessions_
.InMinutes()
138 << " Window (in minutes) for a context switch: "
139 << context_switch_duration_
.InMinutes()
140 << " Throttle rate for RPC calls: " << throttle_percent
141 << " This promo is throttled: " << is_throttled_
;
142 RegisterForCookieChanges();
144 signin_metrics::LogXDevicePromoInitialized(signin_metrics::INITIALIZED
);
148 void CrossDevicePromo::OptOut() {
149 VLOG(1) << "CrossDevicePromo::OptOut.";
150 UnregisterForCookieChanges();
151 prefs_
->SetBoolean(prefs::kCrossDevicePromoOptedOut
, true);
155 bool CrossDevicePromo::VerifyPromoEligibleReadOnly() {
159 if (prefs_
->GetBoolean(prefs::kCrossDevicePromoOptedOut
))
162 if (!prefs_
->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie
))
165 if (GetTimePref(prefs::kCrossDevicePromoObservedSingleAccountCookie
) +
166 single_account_duration_threshold_
> base::Time::Now()) {
173 bool CrossDevicePromo::CheckPromoEligibility() {
175 // In tests the variations may not be present when Init() was first called.
181 if (prefs_
->GetBoolean(prefs::kCrossDevicePromoOptedOut
)) {
182 signin_metrics::LogXDevicePromoEligible(signin_metrics::OPTED_OUT
);
186 if (signin_manager_
->IsAuthenticated()) {
187 signin_metrics::LogXDevicePromoEligible(signin_metrics::SIGNED_IN
);
191 base::Time cookie_has_one_account_since
= GetTimePref(
192 prefs::kCrossDevicePromoObservedSingleAccountCookie
);
193 if (!prefs_
->HasPrefPath(
194 prefs::kCrossDevicePromoObservedSingleAccountCookie
) ||
195 cookie_has_one_account_since
+ single_account_duration_threshold_
>
197 signin_metrics::LogXDevicePromoEligible(
198 signin_metrics::NOT_SINGLE_GAIA_ACCOUNT
);
202 // This is the first time the promo's being run; determine when to call the
203 // DeviceActivityFetcher.
204 if (!prefs_
->HasPrefPath(prefs::kCrossDevicePromoNextFetchListDevicesTime
)) {
205 const int minutes_until_next_activity_fetch
=
206 base::RandGenerator(delay_until_next_list_devices_
.InMinutes());
207 const base::Time time_of_next_device_activity_fetch
= base::Time::Now() +
208 base::TimeDelta::FromMinutes(minutes_until_next_activity_fetch
);
209 prefs_
->SetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime
,
210 time_of_next_device_activity_fetch
.ToInternalValue());
211 signin_metrics::LogXDevicePromoEligible(
212 signin_metrics::UNKNOWN_COUNT_DEVICES
);
216 // We have no knowledge of other device activity yet.
217 if (!prefs_
->HasPrefPath(prefs::kCrossDevicePromoNumDevices
)) {
218 base::Time time_next_list_devices
= GetTimePref(
219 prefs::kCrossDevicePromoNextFetchListDevicesTime
);
220 // Not time yet to poll the list of devices.
221 if (time_next_list_devices
> base::Time::Now()) {
222 signin_metrics::LogXDevicePromoEligible(
223 signin_metrics::UNKNOWN_COUNT_DEVICES
);
226 // We're not eligible... but might be! Track metrics in the results.
227 GetDevicesActivityForAccountInCookie();
231 int num_devices
= prefs_
->GetInteger(prefs::kCrossDevicePromoNumDevices
);
232 base::Time time_next_list_devices
= GetTimePref(
233 prefs::kCrossDevicePromoNextFetchListDevicesTime
);
234 if (time_next_list_devices
< base::Time::Now()) {
235 GetDevicesActivityForAccountInCookie();
236 } else if (num_devices
== 0) {
237 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES
);
241 DCHECK(VerifyPromoEligibleReadOnly());
245 void CrossDevicePromo::MaybeBrowsingSessionStarted(
246 const base::Time
& previous_last_active
) {
247 // In tests, or the first call for a profile, don't pass go.
248 if (previous_last_active
== base::Time())
251 base::Time time_now
= base::Time::Now();
252 // Determine how often this method is called. Need an estimate for QPS.
253 UMA_HISTOGRAM_CUSTOM_COUNTS(
254 "Signin.XDevicePromo.BrowsingSessionDurationComputed",
255 (base::Time::Now() - previous_last_active
).InMinutes(), 1,
256 base::TimeDelta::FromDays(30).InMinutes(), 50);
258 // Check if this is a different browsing session since the last call.
259 if (time_now
- previous_last_active
<=
260 inactivity_between_browsing_sessions_
) {
261 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime. Same browsing session "
266 if (start_last_browsing_session_
!= base::Time())
267 signin_metrics::LogBrowsingSessionDuration(previous_last_active
);
269 start_last_browsing_session_
= time_now
;
271 if (!CheckPromoEligibility()) {
272 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Ineligible for promo.";
278 // Check if there is a record of recent browsing activity on another device.
279 // If there is none, we set a timer to update the records after a small delay
280 // to ensure server-side data is synchronized.
281 base::Time device_last_active
= GetTimePref(
282 prefs::kCrossDevicePromoLastDeviceActiveTime
);
283 if (time_now
- device_last_active
< context_switch_duration_
) {
284 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; promo active.";
285 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE
);
290 // Check for recency of device activity unless a check is already being
291 // executed because the number of devices is being updated.
292 if (!device_activity_fetcher_
.get()) {
293 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Check device activity.";
294 device_activity_timer_
.Start(
296 base::TimeDelta::FromMilliseconds(kDelayUntilGettingDeviceActivityInMS
),
297 this, &CrossDevicePromo::GetDevicesActivityForAccountInCookie
);
301 void CrossDevicePromo::GetDevicesActivityForAccountInCookie() {
302 // Don't start a fetch while one is processing.
303 if (device_activity_fetcher_
.get())
307 signin_metrics::LogXDevicePromoEligible(
308 signin_metrics::THROTTLED_FETCHING_DEVICE_ACTIVITY
);
312 VLOG(1) << "CrossDevicePromo::GetDevicesActivityForAccountInCookie. Start.";
313 DCHECK(VerifyPromoEligibleReadOnly());
314 device_activity_fetcher_
.reset(
315 new DeviceActivityFetcher(signin_client_
, this));
316 device_activity_fetcher_
->Start();
319 void CrossDevicePromo::OnFetchDeviceActivitySuccess(
320 const std::vector
<DeviceActivityFetcher::DeviceActivity
>& devices
) {
321 const base::Time time_now
= base::Time::Now();
322 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivitySuccess. "
323 << devices
.size() << " devices.";
325 prefs::kCrossDevicePromoNextFetchListDevicesTime
,
326 (time_now
+ delay_until_next_list_devices_
).ToInternalValue());
327 prefs_
->SetInteger(prefs::kCrossDevicePromoNumDevices
, devices
.size());
329 if (devices
.empty()) {
330 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES
);
334 base::Time most_recent_last_active
=
335 std::max_element(devices
.begin(), devices
.end(),
336 [](const DeviceActivityFetcher::DeviceActivity
& first
,
337 const DeviceActivityFetcher::DeviceActivity
& second
) {
338 return first
.last_active
< second
.last_active
;
341 prefs_
->SetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime
,
342 most_recent_last_active
.ToInternalValue());
344 if (time_now
- most_recent_last_active
< context_switch_duration_
) {
345 // Make sure eligibility wasn't lost while executing the remote call.
346 if (!VerifyPromoEligibleReadOnly())
349 // The context switch will only be valid for so long. Schedule another
350 // DeviceActivity check for when our switch would expire to check for more
352 if (!device_activity_timer_
.IsRunning()) {
353 base::TimeDelta time_to_next_check
= most_recent_last_active
+
354 context_switch_duration_
-
356 device_activity_timer_
.Start(
357 FROM_HERE
, time_to_next_check
, this,
358 &CrossDevicePromo::GetDevicesActivityForAccountInCookie
);
361 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE
);
364 signin_metrics::LogXDevicePromoEligible(signin_metrics::NO_ACTIVE_DEVICES
);
367 device_activity_fetcher_
.reset();
370 void CrossDevicePromo::OnFetchDeviceActivityFailure() {
371 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivityFailure.";
372 signin_metrics::LogXDevicePromoEligible(
373 signin_metrics::ERROR_FETCHING_DEVICE_ACTIVITY
);
374 device_activity_fetcher_
.reset();
377 void CrossDevicePromo::RegisterForCookieChanges() {
378 cookie_manager_service_
->AddObserver(this);
381 void CrossDevicePromo::UnregisterForCookieChanges() {
382 cookie_manager_service_
->RemoveObserver(this);
385 base::Time
CrossDevicePromo::GetTimePref(const std::string
& pref
) {
386 return base::Time::FromInternalValue(prefs_
->GetInt64(pref
));
389 void CrossDevicePromo::OnGaiaAccountsInCookieUpdated(
390 const std::vector
<gaia::ListedAccount
>& accounts
,
391 const GoogleServiceAuthError
& error
) {
392 VLOG(1) << "CrossDevicePromo::OnGaiaAccountsInCookieUpdated. "
393 << accounts
.size() << " accounts with auth error " << error
.state();
394 if (error
.state() != GoogleServiceAuthError::State::NONE
)
397 // Multiple accounts seen - clear the pref.
398 bool single_account
= accounts
.size() == 1;
400 prefs_
->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie
);
401 if (!single_account
&& has_pref
) {
402 prefs_
->ClearPref(prefs::kCrossDevicePromoObservedSingleAccountCookie
);
404 } else if (single_account
&& !has_pref
) {
405 prefs_
->SetInt64(prefs::kCrossDevicePromoObservedSingleAccountCookie
,
406 base::Time::Now().ToInternalValue());
410 void CrossDevicePromo::MarkPromoActive() {
411 VLOG(1) << "CrossDevicePromo::MarkPromoActive.";
412 DCHECK(!prefs_
->GetBoolean(prefs::kCrossDevicePromoOptedOut
));
414 if (!prefs_
->GetBoolean(prefs::kCrossDevicePromoActive
)) {
415 prefs_
->SetBoolean(prefs::kCrossDevicePromoActive
, true);
416 FOR_EACH_OBSERVER(CrossDevicePromo::Observer
, observer_list_
,
417 OnPromoActivationChanged(true));
421 void CrossDevicePromo::MarkPromoInactive() {
422 VLOG(1) << "CrossDevicePromo::MarkPromoInactive.";
423 if (prefs_
->GetBoolean(prefs::kCrossDevicePromoActive
)) {
424 prefs_
->SetBoolean(prefs::kCrossDevicePromoActive
, false);
425 FOR_EACH_OBSERVER(CrossDevicePromo::Observer
, observer_list_
,
426 OnPromoActivationChanged(false));