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

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

Powered by Google App Engine
This is Rietveld 408576698