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

Side by Side 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: pre-review 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 unified diff | Download patch
OLDNEW
(Empty)
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.
4
5 #include "chrome/browser/signin/cross_device_promo.h"
6
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"
18
19 namespace {
20
21 const int kDelayUntilGettingDeviceActivityInMS = 3000;
22 const int kDefaultBrowsingSessionDurationInMinutes = 15;
23
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
26 // converstion could not succeed.
27 bool SetParameterFromVariation(
28 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())
34 return false;
35
36 int parameter_as_int;
37 if (!base::StringToInt(parameter_as_string, &parameter_as_int))
38 return false;
39
40 local_parameter = conversion.Run(parameter_as_int);
41 return true;
42 }
43
44 } // namespace
45
46 CrossDevicePromo::CrossDevicePromo(
47 SigninManager* signin_manager,
48 GaiaCookieManagerService* cookie_manager_service,
49 SigninClient* signin_client,
50 PrefService* pref_service)
51 : signin_manager_(signin_manager),
52 cookie_manager_service_(cookie_manager_service),
53 prefs_(pref_service),
54 signin_client_(signin_client),
55 start_last_browsing_session_(base::Time()),
56 initialized_(false),
57 track_metrics_(true) {
58 VLOG(1) << "CrossDevicePromo::CrossDevicePromo.";
59 Init();
60 }
61
62 CrossDevicePromo::~CrossDevicePromo() {}
63
64 void CrossDevicePromo::Shutdown() {
65 VLOG(1) << "CrossDevicePromo::Shutdown.";
66 UnregisterForCookieChanges();
67 if (start_last_browsing_session_ != base::Time())
68 signin_metrics::LogBrowsingSessionDuration(start_last_browsing_session_);
69 }
70
71
72 void CrossDevicePromo::AddObserver(CrossDevicePromo::Observer* observer) {
73 observer_list_.AddObserver(observer);
74 }
75
76 void CrossDevicePromo::RemoveObserver(CrossDevicePromo::Observer* observer) {
77 observer_list_.RemoveObserver(observer);
78 }
79
80 bool CrossDevicePromo::IsPromoActive() {
81 return prefs_->GetBoolean(prefs::kCrossDevicePromoActive);
82 }
83
84 void CrossDevicePromo::Init() {
85 DCHECK(!initialized_);
86 if (!prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut) &&
87 SetParameterFromVariation(
88 "HoursBetweenSyncDeviceChecks",
89 delay_until_next_list_devices_,
90 base::Bind(&base::TimeDelta::FromHours)) &&
91 SetParameterFromVariation(
92 "DaysToVerifySingleUserProfile",
93 single_account_in_profile_,
94 base::Bind(&base::TimeDelta::FromDays)) &&
95 SetParameterFromVariation(
96 "MinutesBetweenBrowsingSessions",
97 inactivity_between_browsing_sessions_,
98 base::Bind(&base::TimeDelta::FromMinutes)) &&
99 SetParameterFromVariation(
100 "MinutesMaxContextSwitchDuration",
101 context_switch_duration_,
102 base::Bind(&base::TimeDelta::FromMinutes))) {
103
104 std::string throttle = variations::GetVariationParamValue(
105 "CrossDevicePromo", "RPCThrottle");
106 if (throttle.empty() ||
107 !base::StringToUint64(throttle, &rpc_throttle_)) {
108 UMA_HISTOGRAM_BOOLEAN("Signin.XDevicePromo.Initialized", false);
109 return;
110 }
111
112 VLOG(1) << "CrossDevicePromo::Init. Service initialized. Parameters: "
113 << "Hour between RPC checks: "
114 << delay_until_next_list_devices_.InHours()
115 << "Days to verify an account in the cookie: "
116 << single_account_in_profile_.InDays()
117 << "Minutes between browsing sessions: "
118 << inactivity_between_browsing_sessions_.InMinutes()
119 << "Window (in minutes) for a context switch: "
120 << context_switch_duration_.InMinutes()
121 << "Throttle rate for RPC calls: "
122 << rpc_throttle_;
123 RegisterForCookieChanges();
124 initialized_ = true;
125 UMA_HISTOGRAM_BOOLEAN("Signin.XDevicePromo.Initialized", true);
126 } else {
127 // We need a default value for this as it is referenced early in
128 // UpdateLastActiveTime and we want to gather as many stats about Browsing
129 // Sessions as possible.
130 inactivity_between_browsing_sessions_ = base::TimeDelta::FromMinutes(
131 kDefaultBrowsingSessionDurationInMinutes);
132 UMA_HISTOGRAM_BOOLEAN("Signin.XDevicePromo.Initialized", false);
133 }
134 }
135
136 void CrossDevicePromo::OptOut() {
137 VLOG(1) << "CrossDevicePromo::OptOut.";
138 UnregisterForCookieChanges();
139 prefs_->SetBoolean(prefs::kCrossDevicePromoOptedOut, true);
140 MarkPromoInactive();
141 }
142
143 bool CrossDevicePromo::VerifyPromoEligibleReadOnly() {
144 if (!initialized_)
145 return false;
146
147 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut))
148 return false;
149
150 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie))
151 return false;
152
153 if (base::Time::FromInternalValue(prefs_->GetInt64(
154 prefs::kCrossDevicePromoObservedSingleAccountCookie)) +
155 single_account_in_profile_ >
156 base::Time::Now()) {
157 return false;
158 }
159
160 return true;
161 }
162
163 bool CrossDevicePromo::CheckPromoEligibility() {
164 if (!initialized_) {
165 Init();
166 if (!initialized_)
167 return false;
168 }
169
170 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) {
171 signin_metrics::LogXDevicePromoEligible(signin_metrics::OPTED_OUT);
172 return false;
173 }
174
175 if (!signin_manager_->GetAuthenticatedUsername().empty()) {
176 signin_metrics::LogXDevicePromoEligible(signin_metrics::SIGNED_IN);
177 return false;
178 }
179
180 base::Time cookie_has_one_account_since = base::Time::FromInternalValue(
181 prefs_->GetInt64(
182 prefs::kCrossDevicePromoObservedSingleAccountCookie));
183 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie)
184 || cookie_has_one_account_since + single_account_in_profile_ >
185 base::Time::Now()) {
186 signin_metrics::LogXDevicePromoEligible(
187 signin_metrics::NOT_SINGLE_GAIA_ACCOUNT);
188 return false;
189 }
190
191 // This is the first time the promo's being run; determine when to call the
192 // DeviceActivityFetcher.
193 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNextFetchListDevicesTime)) {
194 int minutes_until_next_list_devices = base::RandGenerator(
195 delay_until_next_list_devices_.InMinutes());
196 prefs_->SetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime,
197 (base::Time::Now() +
198 base::TimeDelta::FromMinutes(minutes_until_next_list_devices)).
199 ToInternalValue());
200 signin_metrics::LogXDevicePromoEligible(
201 signin_metrics::UNKNOWN_COUNT_DEVICES);
202 return false;
203 }
204
205 // We have no knowledge of other device activity yet.
206 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNumDevices)) {
207 base::Time time_next_list_devices = base::Time::FromInternalValue(
208 prefs_->GetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime));
209 // Not time yet to poll the list of devices.
210 if (time_next_list_devices > base::Time::Now()) {
211 signin_metrics::LogXDevicePromoEligible(
212 signin_metrics::UNKNOWN_COUNT_DEVICES);
213 return false;
214 } else {
215 // We're not eligible... but might be! Track metrics in the results.
216 GetDevicesActivityForAccountInCookie();
217 return false;
218 }
219 }
220
221 int num_devices = prefs_->GetInteger(prefs::kCrossDevicePromoNumDevices);
222 base::Time time_next_list_devices = base::Time::FromInternalValue(
223 prefs_->GetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime));
224 if (time_next_list_devices < base::Time::Now()) {
225 GetDevicesActivityForAccountInCookie();
226 } else if (num_devices == 0) {
227 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES);
228 return false;
229 }
230
231 DCHECK(VerifyPromoEligibleReadOnly());
232 return true;
233 }
234
235 void CrossDevicePromo::UpdateLastActiveTime(
236 const base::Time& previous_last_active) {
237 // In tests, or the first call for a profile, don't pass go.
238 if (previous_last_active == base::Time())
239 return;
240
241 // Determine how often this method is called. Need an estimate for QPS.
242 UMA_HISTOGRAM_CUSTOM_COUNTS(
243 "Signin.XDevicePromo.BrowsingSessionDurationComputed",
244 (base::Time::Now() - previous_last_active).InMinutes(),
245 1, base::TimeDelta::FromDays(30).InMinutes(), 50);
246
247 // Check if this is a different browsing session since the last call.
248 if (base::Time::Now() - previous_last_active <=
249 inactivity_between_browsing_sessions_) {
250 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime. Same browsing session "
251 "as the last call.";
252 return;
253 }
254
255 if (start_last_browsing_session_ != base::Time())
256 signin_metrics::LogBrowsingSessionDuration(previous_last_active);
257
258 start_last_browsing_session_ = base::Time::Now();
259
260 if (!CheckPromoEligibility()) {
261 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Ineligible for promo.";
262 if (IsPromoActive())
263 MarkPromoInactive();
264 return;
265 }
266
267 // Check if there is a record of recent browsing activity on another device.
268 // If there is none, we set a timer to update the records after a small delay
269 // to ensure server-side data is synchronized.
270 base::Time device_last_active = base::Time::FromInternalValue(
271 prefs_->GetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime));
272 if (base::Time::Now() - device_last_active < context_switch_duration_) {
273 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; promo active.";
274 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE);
275 MarkPromoActive();
276 return;
277 }
278
279 // Check for recency of device activity unless a check is already being
280 // executed because the number of devices is being updated.
281 if (!device_activity_fetcher_.get()) {
282 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Check device activity.";
283 device_activity_timer_.Start(
284 FROM_HERE,
285 base::TimeDelta::FromMilliseconds(kDelayUntilGettingDeviceActivityInMS),
286 this,
287 &CrossDevicePromo::GetDevicesActivityForAccountInCookie);
288 }
289 }
290
291 void CrossDevicePromo::GetDevicesActivityForAccountInCookie() {
292 // Don't start a fetch while one is processing.
293 if (device_activity_fetcher_.get())
294 return;
295
296 if (rpc_throttle_ && base::RandGenerator(100) < rpc_throttle_) {
297 if (track_metrics_) {
298 signin_metrics::LogXDevicePromoEligible(
299 signin_metrics::THROTTLED_FETCHING_DEVICE_ACTIVITY);
300 }
301 return;
302 }
303
304 VLOG(1) << "CrossDevicePromo::GetDevicesActivityForAccountInCookie. Start.";
305 DCHECK(VerifyPromoEligibleReadOnly());
306 device_activity_fetcher_.reset(
307 new DeviceActivityFetcher(signin_client_, this));
308 device_activity_fetcher_->Start();
309 }
310
311 void CrossDevicePromo::OnFetchDeviceActivitySuccess(
312 const std::vector<DeviceActivityFetcher::DeviceActivity>& devices) {
313 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivitySuccess. "
314 << devices.size() << " devices.";
315 prefs_->SetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime,
316 (base::Time::Now() +
317 delay_until_next_list_devices_).ToInternalValue());
318 prefs_->SetInteger(prefs::kCrossDevicePromoNumDevices, devices.size());
319
320 if (devices.empty()) {
321 if (track_metrics_)
322 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES);
323 return;
324 }
325
326 base::Time most_recent_last_active = base::Time();
327 for (size_t i = 0; i < devices.size(); i++) {
328 if (devices[i].last_active > most_recent_last_active)
329 most_recent_last_active = devices[i].last_active;
330 }
331
332 prefs_->SetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime,
333 most_recent_last_active.ToInternalValue());
334
335 if (base::Time::Now() - most_recent_last_active < context_switch_duration_) {
336 // Make sure eligibility wasn't lost while executing the remote call.
337 if (!VerifyPromoEligibleReadOnly())
338 return;
339
340 // The context switch will only be valid for so long. Schedule another
341 // DeviceActivity check for when our switch would expire to check for more
342 // recent activity.
343 if (!device_activity_timer_.IsRunning()) {
344 base::TimeDelta time_to_next_check = most_recent_last_active +
345 context_switch_duration_ - base::Time::Now();
346 device_activity_timer_.Start(
347 FROM_HERE,
348 time_to_next_check,
349 this,
350 &CrossDevicePromo::GetDevicesActivityForAccountInCookie);
351 }
352
353 if (track_metrics_)
354 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE);
355 MarkPromoActive();
356 } else {
357 if (track_metrics_) {
358 signin_metrics::LogXDevicePromoEligible(
359 signin_metrics::NO_ACTIVE_DEVICES);
360 }
361 MarkPromoInactive();
362 }
363 track_metrics_ = true;
364 device_activity_fetcher_.reset();
365 }
366
367 void CrossDevicePromo::OnFetchDeviceActivityFailure() {
368 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivityFailure.";
369 if (track_metrics_) {
370 signin_metrics::LogXDevicePromoEligible(
371 signin_metrics::ERROR_FETCHING_DEVICE_ACTIVITY);
372 }
373 device_activity_fetcher_.reset();
374 }
375
376 void CrossDevicePromo::RegisterForCookieChanges() {
377 cookie_manager_service_->AddObserver(this);
378 }
379
380 void CrossDevicePromo::UnregisterForCookieChanges() {
381 cookie_manager_service_->RemoveObserver(this);
382 }
383
384 void CrossDevicePromo::OnGaiaAccountsInCookieUpdated(
385 const std::vector<std::pair<std::string, bool> >& accounts,
386 const GoogleServiceAuthError& error) {
387 VLOG(1) << "CrossDevicePromo::OnGaiaAccountsInCookieUpdated. "
388 << accounts.size() << " accounts with auth error " << error.state();
389 if (error.state() != GoogleServiceAuthError::State::NONE)
390 return;
391
392 // Multiple accounts seen - clear the pref.
393 if (accounts.size() != 1 && prefs_->HasPrefPath(
394 prefs::kCrossDevicePromoObservedSingleAccountCookie)) {
395 prefs_->ClearPref(prefs::kCrossDevicePromoObservedSingleAccountCookie);
396 MarkPromoInactive();
397 // Single account seen. Note if this is the first time we've seen this.
398 } else if (accounts.size() == 1 && !prefs_->HasPrefPath(
399 prefs::kCrossDevicePromoObservedSingleAccountCookie)){
400 prefs_->SetInt64(prefs::kCrossDevicePromoObservedSingleAccountCookie,
401 base::Time::Now().ToInternalValue());
402 }
403 }
404
405 void CrossDevicePromo::MarkPromoActive() {
406 VLOG(1) << "CrossDevicePromo::MarkPromoActive.";
407 DCHECK(!prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut));
408
409 if (!prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) {
410 prefs_->SetBoolean(prefs::kCrossDevicePromoActive, true);
411 FOR_EACH_OBSERVER(CrossDevicePromo::Observer,
412 observer_list_,
413 OnPromoActivationChanged(true));
414 }
415 }
416
417 void CrossDevicePromo::MarkPromoInactive() {
418 VLOG(1) << "CrossDevicePromo::MarkPromoInactive.";
419 if (prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) {
420 prefs_->SetBoolean(prefs::kCrossDevicePromoActive, false);
421 FOR_EACH_OBSERVER(CrossDevicePromo::Observer,
422 observer_list_,
423 OnPromoActivationChanged(false));
424 }
425 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698