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

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: first set of anthony's comments 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.
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:39 typo: converstion
Mike Lerman 2015/05/15 20:47:34 Done.
27 bool SetParameterFromVariation(
28 std::string variation_parameter,
29 base::TimeDelta& local_parameter,
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 const & for line 28, pointer for out arg on line 2
Mike Lerman 2015/05/15 20:47:34 Done.
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.";
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 nit: DCHECK pointers are good?
Mike Lerman 2015/05/15 20:47:33 Done.
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 // We need a default value for this as it is referenced early in
87 // UpdateLastActiveTime and we want to gather as many stats about Browsing
88 // Sessions as possible.
89 inactivity_between_browsing_sessions_ = base::TimeDelta::FromMinutes(
90 kDefaultBrowsingSessionDurationInMinutes);
91
92 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) {
93 UMA_HISTOGRAM_BOOLEAN("Signin.XDevicePromo.Initialized", false);
94 return;
95 }
96
97 if (!SetParameterFromVariation("HoursBetweenSyncDeviceChecks",
98 delay_until_next_list_devices_,
99 base::Bind(&base::TimeDelta::FromHours)) ||
100 !SetParameterFromVariation("DaysToVerifySingleUserProfile",
101 single_account_duration_threshold_,
102 base::Bind(&base::TimeDelta::FromDays)) ||
103 !SetParameterFromVariation("MinutesBetweenBrowsingSessions",
104 inactivity_between_browsing_sessions_,
105 base::Bind(&base::TimeDelta::FromMinutes)) ||
106 !SetParameterFromVariation("MinutesMaxContextSwitchDuration",
107 context_switch_duration_,
108 base::Bind(&base::TimeDelta::FromMinutes))) {
109 UMA_HISTOGRAM_BOOLEAN("Signin.XDevicePromo.Initialized", false);
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 Is it worth making this histogram an enum to diffe
Mike Lerman 2015/05/15 20:47:34 I'll split it into three: OPTED_OUT, NO_VARIATIONS
110 return;
111 }
112
113 std::string throttle = variations::GetVariationParamValue(
114 "CrossDevicePromo", "RPCThrottle");
115 if (throttle.empty() || !base::StringToUint64(throttle, &rpc_throttle_)) {
116 UMA_HISTOGRAM_BOOLEAN("Signin.XDevicePromo.Initialized", false);
117 return;
118 }
119
120 VLOG(1) << "CrossDevicePromo::Init. Service initialized. Parameters: "
121 << "Hour between RPC checks: "
122 << delay_until_next_list_devices_.InHours()
123 << "Days to verify an account in the cookie: "
124 << single_account_duration_threshold_.InDays()
125 << "Minutes between browsing sessions: "
126 << inactivity_between_browsing_sessions_.InMinutes()
127 << "Window (in minutes) for a context switch: "
128 << context_switch_duration_.InMinutes()
129 << "Throttle rate for RPC calls: "
130 << rpc_throttle_;
131 RegisterForCookieChanges();
132 initialized_ = true;
133 UMA_HISTOGRAM_BOOLEAN("Signin.XDevicePromo.Initialized", true);
134 return;
135 }
136
137 void CrossDevicePromo::OptOut() {
138 VLOG(1) << "CrossDevicePromo::OptOut.";
139 UnregisterForCookieChanges();
140 prefs_->SetBoolean(prefs::kCrossDevicePromoOptedOut, true);
141 MarkPromoInactive();
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 Stop any fetchers too?
Mike Lerman 2015/05/15 20:47:34 Yes. Added a Stop method to the DeviceActivityFetc
142 }
143
144 bool CrossDevicePromo::VerifyPromoEligibleReadOnly() {
145 if (!initialized_)
146 return false;
147
148 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut))
149 return false;
150
151 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie))
152 return false;
153
154 if (base::Time::FromInternalValue(prefs_->GetInt64(
155 prefs::kCrossDevicePromoObservedSingleAccountCookie)) +
156 single_account_duration_threshold_ >
157 base::Time::Now()) {
158 return false;
159 }
160
161 return true;
162 }
163
164 bool CrossDevicePromo::CheckPromoEligibility() {
165 if (!initialized_) {
166 // In tests the variations may not be present when Init() was first called.
167 Init();
168 if (!initialized_)
169 return false;
170 }
171
172 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) {
173 signin_metrics::LogXDevicePromoEligible(signin_metrics::OPTED_OUT);
174 return false;
175 }
176
177 if (!signin_manager_->GetAuthenticatedUsername().empty()) {
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 Use IsAuthenticated() instead of !GetAuthenticated
Mike Lerman 2015/05/15 20:47:34 Done.
178 signin_metrics::LogXDevicePromoEligible(signin_metrics::SIGNED_IN);
179 return false;
180 }
181
182 base::Time cookie_has_one_account_since = base::Time::FromInternalValue(
183 prefs_->GetInt64(
184 prefs::kCrossDevicePromoObservedSingleAccountCookie));
185 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie)
186 || cookie_has_one_account_since + single_account_duration_threshold_ >
187 base::Time::Now()) {
188 signin_metrics::LogXDevicePromoEligible(
189 signin_metrics::NOT_SINGLE_GAIA_ACCOUNT);
190 return false;
191 }
192
193 // This is the first time the promo's being run; determine when to call the
194 // DeviceActivityFetcher.
195 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNextFetchListDevicesTime)) {
196 int minutes_until_next_list_devices = base::RandGenerator(
197 delay_until_next_list_devices_.InMinutes());
198 prefs_->SetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime,
199 (base::Time::Now() +
200 base::TimeDelta::FromMinutes(minutes_until_next_list_devices)).
201 ToInternalValue());
202 signin_metrics::LogXDevicePromoEligible(
203 signin_metrics::UNKNOWN_COUNT_DEVICES);
204 return false;
205 }
206
207 // We have no knowledge of other device activity yet.
208 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNumDevices)) {
209 base::Time time_next_list_devices = base::Time::FromInternalValue(
210 prefs_->GetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime));
211 // Not time yet to poll the list of devices.
212 if (time_next_list_devices > base::Time::Now()) {
213 signin_metrics::LogXDevicePromoEligible(
214 signin_metrics::UNKNOWN_COUNT_DEVICES);
215 return false;
216 } else {
217 // We're not eligible... but might be! Track metrics in the results.
218 GetDevicesActivityForAccountInCookie();
219 return false;
220 }
221 }
222
223 int num_devices = prefs_->GetInteger(prefs::kCrossDevicePromoNumDevices);
224 base::Time time_next_list_devices = base::Time::FromInternalValue(
225 prefs_->GetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime));
226 if (time_next_list_devices < base::Time::Now()) {
227 GetDevicesActivityForAccountInCookie();
228 } else if (num_devices == 0) {
229 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES);
230 return false;
231 }
232
233 DCHECK(VerifyPromoEligibleReadOnly());
234 return true;
235 }
236
237 void CrossDevicePromo::UpdateLastActiveTime(
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 nit: should this be called something like UpdateSt
Mike Lerman 2015/05/15 20:47:34 I opted for: MaybeBrowsingSessionStarted
238 const base::Time& previous_last_active) {
239 // In tests, or the first call for a profile, don't pass go.
240 if (previous_last_active == base::Time())
241 return;
242
243 // Determine how often this method is called. Need an estimate for QPS.
244 UMA_HISTOGRAM_CUSTOM_COUNTS(
245 "Signin.XDevicePromo.BrowsingSessionDurationComputed",
246 (base::Time::Now() - previous_last_active).InMinutes(),
247 1, base::TimeDelta::FromDays(30).InMinutes(), 50);
248
249 // Check if this is a different browsing session since the last call.
250 if (base::Time::Now() - previous_last_active <=
251 inactivity_between_browsing_sessions_) {
252 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime. Same browsing session "
253 "as the last call.";
254 return;
255 }
256
257 if (start_last_browsing_session_ != base::Time())
258 signin_metrics::LogBrowsingSessionDuration(previous_last_active);
259
260 start_last_browsing_session_ = base::Time::Now();
261
262 if (!CheckPromoEligibility()) {
263 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Ineligible for promo.";
264 if (IsPromoActive())
265 MarkPromoInactive();
266 return;
267 }
268
269 // Check if there is a record of recent browsing activity on another device.
270 // If there is none, we set a timer to update the records after a small delay
271 // to ensure server-side data is synchronized.
272 base::Time device_last_active = base::Time::FromInternalValue(
273 prefs_->GetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime));
274 if (base::Time::Now() - device_last_active < context_switch_duration_) {
275 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; promo active.";
276 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE);
277 MarkPromoActive();
278 return;
279 }
280
281 // Check for recency of device activity unless a check is already being
282 // executed because the number of devices is being updated.
283 if (!device_activity_fetcher_.get()) {
284 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Check device activity.";
285 device_activity_timer_.Start(
286 FROM_HERE,
287 base::TimeDelta::FromMilliseconds(kDelayUntilGettingDeviceActivityInMS),
288 this,
289 &CrossDevicePromo::GetDevicesActivityForAccountInCookie);
290 }
291 }
292
293 void CrossDevicePromo::GetDevicesActivityForAccountInCookie() {
294 // Don't start a fetch while one is processing.
295 if (device_activity_fetcher_.get())
296 return;
297
298 if (rpc_throttle_ && base::RandGenerator(100) < rpc_throttle_) {
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 Should be calculated each time or only once per ch
Mike Lerman 2015/05/15 20:47:34 Once per Chrome/per client makes sense, that way w
299 if (track_metrics_) {
300 signin_metrics::LogXDevicePromoEligible(
301 signin_metrics::THROTTLED_FETCHING_DEVICE_ACTIVITY);
302 }
303 return;
304 }
305
306 VLOG(1) << "CrossDevicePromo::GetDevicesActivityForAccountInCookie. Start.";
307 DCHECK(VerifyPromoEligibleReadOnly());
308 device_activity_fetcher_.reset(
309 new DeviceActivityFetcher(signin_client_, this));
310 device_activity_fetcher_->Start();
311 }
312
313 void CrossDevicePromo::OnFetchDeviceActivitySuccess(
314 const std::vector<DeviceActivityFetcher::DeviceActivity>& devices) {
315 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivitySuccess. "
316 << devices.size() << " devices.";
317 prefs_->SetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime,
318 (base::Time::Now() +
319 delay_until_next_list_devices_).ToInternalValue());
320 prefs_->SetInteger(prefs::kCrossDevicePromoNumDevices, devices.size());
321
322 if (devices.empty()) {
323 if (track_metrics_)
324 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES);
325 return;
326 }
327
328 base::Time most_recent_last_active = base::Time();
329 for (size_t i = 0; i < devices.size(); i++) {
330 if (devices[i].last_active > most_recent_last_active)
331 most_recent_last_active = devices[i].last_active;
332 }
333
334 prefs_->SetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime,
335 most_recent_last_active.ToInternalValue());
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 nit: extra space.
Mike Lerman 2015/05/15 20:47:34 Done.
336
337 if (base::Time::Now() - most_recent_last_active < context_switch_duration_) {
338 // Make sure eligibility wasn't lost while executing the remote call.
339 if (!VerifyPromoEligibleReadOnly())
340 return;
341
342 // The context switch will only be valid for so long. Schedule another
343 // DeviceActivity check for when our switch would expire to check for more
344 // recent activity.
345 if (!device_activity_timer_.IsRunning()) {
346 base::TimeDelta time_to_next_check = most_recent_last_active +
347 context_switch_duration_ - base::Time::Now();
348 device_activity_timer_.Start(
349 FROM_HERE,
350 time_to_next_check,
351 this,
352 &CrossDevicePromo::GetDevicesActivityForAccountInCookie);
353 }
354
355 if (track_metrics_)
356 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE);
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 It seems that not all calls to uma logs are protec
Mike Lerman 2015/05/15 20:47:34 Ah, I don't need track_metrics_ anymore! It's neve
357 MarkPromoActive();
358 } else {
359 if (track_metrics_) {
360 signin_metrics::LogXDevicePromoEligible(
361 signin_metrics::NO_ACTIVE_DEVICES);
362 }
363 MarkPromoInactive();
364 }
365 track_metrics_ = true;
366 device_activity_fetcher_.reset();
367 }
368
369 void CrossDevicePromo::OnFetchDeviceActivityFailure() {
370 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivityFailure.";
371 if (track_metrics_) {
372 signin_metrics::LogXDevicePromoEligible(
373 signin_metrics::ERROR_FETCHING_DEVICE_ACTIVITY);
374 }
375 device_activity_fetcher_.reset();
376 }
377
378 void CrossDevicePromo::RegisterForCookieChanges() {
379 cookie_manager_service_->AddObserver(this);
380 }
381
382 void CrossDevicePromo::UnregisterForCookieChanges() {
383 cookie_manager_service_->RemoveObserver(this);
384 }
385
386 void CrossDevicePromo::OnGaiaAccountsInCookieUpdated(
387 const std::vector<std::pair<std::string, bool> >& accounts,
388 const GoogleServiceAuthError& error) {
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 Too many spaces?
Mike Lerman 2015/05/15 20:47:34 Done.
389 VLOG(1) << "CrossDevicePromo::OnGaiaAccountsInCookieUpdated. "
390 << accounts.size() << " accounts with auth error " << error.state();
391 if (error.state() != GoogleServiceAuthError::State::NONE)
392 return;
393
394 // Multiple accounts seen - clear the pref.
395 if (accounts.size() != 1 && prefs_->HasPrefPath(
396 prefs::kCrossDevicePromoObservedSingleAccountCookie)) {
397 prefs_->ClearPref(prefs::kCrossDevicePromoObservedSingleAccountCookie);
398 MarkPromoInactive();
399 // Single account seen. Note if this is the first time we've seen this.
400 } else if (accounts.size() == 1 && !prefs_->HasPrefPath(
401 prefs::kCrossDevicePromoObservedSingleAccountCookie)){
Roger Tawa OOO till Jul 10th 2015/05/15 16:01:40 nit: space before {
Mike Lerman 2015/05/15 20:47:34 Done.
402 prefs_->SetInt64(prefs::kCrossDevicePromoObservedSingleAccountCookie,
403 base::Time::Now().ToInternalValue());
404 }
405 }
406
407 void CrossDevicePromo::MarkPromoActive() {
408 VLOG(1) << "CrossDevicePromo::MarkPromoActive.";
409 DCHECK(!prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut));
410
411 if (!prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) {
412 prefs_->SetBoolean(prefs::kCrossDevicePromoActive, true);
413 FOR_EACH_OBSERVER(CrossDevicePromo::Observer,
414 observer_list_,
415 OnPromoActivationChanged(true));
416 }
417 }
418
419 void CrossDevicePromo::MarkPromoInactive() {
420 VLOG(1) << "CrossDevicePromo::MarkPromoInactive.";
421 if (prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) {
422 prefs_->SetBoolean(prefs::kCrossDevicePromoActive, false);
423 FOR_EACH_OBSERVER(CrossDevicePromo::Observer,
424 observer_list_,
425 OnPromoActivationChanged(false));
426 }
427 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698