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

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: git cl format 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
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_);
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_ =
94 base::TimeDelta::FromMinutes(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 =
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);
125 return;
126 }
127
128 is_throttled_ =
129 throttle_percent && base::RandGenerator(100) < throttle_percent;
130
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();
143 initialized_ = true;
144 signin_metrics::LogXDevicePromoInitialized(signin_metrics::INITIALIZED);
145 return;
146 }
147
148 void CrossDevicePromo::OptOut() {
149 VLOG(1) << "CrossDevicePromo::OptOut.";
150 UnregisterForCookieChanges();
151 prefs_->SetBoolean(prefs::kCrossDevicePromoOptedOut, true);
152 MarkPromoInactive();
153 }
154
155 bool CrossDevicePromo::VerifyPromoEligibleReadOnly() {
156 if (!initialized_)
157 return false;
158
159 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut))
160 return false;
161
162 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie))
163 return false;
164
165 if (base::Time::FromInternalValue(prefs_->GetInt64(
166 prefs::kCrossDevicePromoObservedSingleAccountCookie)) +
167 single_account_duration_threshold_ >
168 base::Time::Now()) {
Alexei Svitkine (slow) 2015/05/19 17:10:37 Formatting seems off. But perhaps this is what git
Mike Lerman 2015/05/19 18:17:34 I just reformatted it manually.
169 return false;
170 }
171
172 return true;
173 }
174
175 bool CrossDevicePromo::CheckPromoEligibility() {
176 if (!initialized_) {
177 // In tests the variations may not be present when Init() was first called.
178 Init();
179 if (!initialized_)
180 return false;
181 }
182
183 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) {
184 signin_metrics::LogXDevicePromoEligible(signin_metrics::OPTED_OUT);
185 return false;
186 }
187
188 if (signin_manager_->IsAuthenticated()) {
189 signin_metrics::LogXDevicePromoEligible(signin_metrics::SIGNED_IN);
190 return false;
191 }
192
193 base::Time cookie_has_one_account_since = base::Time::FromInternalValue(
194 prefs_->GetInt64(prefs::kCrossDevicePromoObservedSingleAccountCookie));
195 if (!prefs_->HasPrefPath(
196 prefs::kCrossDevicePromoObservedSingleAccountCookie) ||
197 cookie_has_one_account_since + single_account_duration_threshold_ >
198 base::Time::Now()) {
199 signin_metrics::LogXDevicePromoEligible(
200 signin_metrics::NOT_SINGLE_GAIA_ACCOUNT);
201 return false;
202 }
203
204 // This is the first time the promo's being run; determine when to call the
205 // DeviceActivityFetcher.
206 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNextFetchListDevicesTime)) {
207 int minutes_until_next_list_devices =
208 base::RandGenerator(delay_until_next_list_devices_.InMinutes());
Alexei Svitkine (slow) 2015/05/19 17:10:37 Is there a reason to clamp to minute values, as op
Mike Lerman 2015/05/19 18:17:34 It seemed the appropriate level, given this value
209 prefs_->SetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime,
210 (base::Time::Now() + base::TimeDelta::FromMinutes(
211 minutes_until_next_list_devices))
Alexei Svitkine (slow) 2015/05/19 17:10:37 Nit: Can you make a local var for some of this inn
Mike Lerman 2015/05/19 18:17:34 Done.
212 .ToInternalValue());
213 signin_metrics::LogXDevicePromoEligible(
214 signin_metrics::UNKNOWN_COUNT_DEVICES);
215 return false;
216 }
217
218 // We have no knowledge of other device activity yet.
219 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNumDevices)) {
220 base::Time time_next_list_devices = base::Time::FromInternalValue(
221 prefs_->GetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime));
222 // Not time yet to poll the list of devices.
223 if (time_next_list_devices > base::Time::Now()) {
224 signin_metrics::LogXDevicePromoEligible(
225 signin_metrics::UNKNOWN_COUNT_DEVICES);
226 return false;
227 }
228 // We're not eligible... but might be! Track metrics in the results.
229 GetDevicesActivityForAccountInCookie();
230 return false;
231 }
232
233 int num_devices = prefs_->GetInteger(prefs::kCrossDevicePromoNumDevices);
234 base::Time time_next_list_devices = base::Time::FromInternalValue(
235 prefs_->GetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime));
Alexei Svitkine (slow) 2015/05/19 17:10:37 Make maybe make a helper - GetTimePref(pref_name)
Mike Lerman 2015/05/19 18:17:34 I do, and I like. thanks!
236 if (time_next_list_devices < base::Time::Now()) {
237 GetDevicesActivityForAccountInCookie();
238 } else if (num_devices == 0) {
239 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES);
240 return false;
241 }
242
243 DCHECK(VerifyPromoEligibleReadOnly());
244 return true;
245 }
246
247 void CrossDevicePromo::MaybeBrowsingSessionStarted(
248 const base::Time& previous_last_active) {
249 // In tests, or the first call for a profile, don't pass go.
250 if (previous_last_active == base::Time())
251 return;
252
253 base::Time time_now = base::Time::Now();
254 // Determine how often this method is called. Need an estimate for QPS.
255 UMA_HISTOGRAM_CUSTOM_COUNTS(
256 "Signin.XDevicePromo.BrowsingSessionDurationComputed",
257 (base::Time::Now() - previous_last_active).InMinutes(), 1,
258 base::TimeDelta::FromDays(30).InMinutes(), 50);
259
260 // Check if this is a different browsing session since the last call.
261 if (time_now - previous_last_active <=
262 inactivity_between_browsing_sessions_) {
263 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime. Same browsing session "
264 "as the last call.";
265 return;
266 }
267
268 if (start_last_browsing_session_ != base::Time())
269 signin_metrics::LogBrowsingSessionDuration(previous_last_active);
270
271 start_last_browsing_session_ = time_now;
272
273 if (!CheckPromoEligibility()) {
274 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Ineligible for promo.";
275 if (IsPromoActive())
276 MarkPromoInactive();
277 return;
278 }
279
280 // Check if there is a record of recent browsing activity on another device.
281 // If there is none, we set a timer to update the records after a small delay
282 // to ensure server-side data is synchronized.
283 base::Time device_last_active = base::Time::FromInternalValue(
284 prefs_->GetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime));
285 if (time_now - device_last_active < context_switch_duration_) {
286 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; promo active.";
287 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE);
288 MarkPromoActive();
289 return;
290 }
291
292 // Check for recency of device activity unless a check is already being
293 // executed because the number of devices is being updated.
294 if (!device_activity_fetcher_.get()) {
295 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Check device activity.";
296 device_activity_timer_.Start(
297 FROM_HERE,
298 base::TimeDelta::FromMilliseconds(kDelayUntilGettingDeviceActivityInMS),
299 this, &CrossDevicePromo::GetDevicesActivityForAccountInCookie);
300 }
301 }
302
303 void CrossDevicePromo::GetDevicesActivityForAccountInCookie() {
304 // Don't start a fetch while one is processing.
305 if (device_activity_fetcher_.get())
306 return;
307
308 if (is_throttled_) {
309 signin_metrics::LogXDevicePromoEligible(
310 signin_metrics::THROTTLED_FETCHING_DEVICE_ACTIVITY);
311 return;
312 }
313
314 VLOG(1) << "CrossDevicePromo::GetDevicesActivityForAccountInCookie. Start.";
315 DCHECK(VerifyPromoEligibleReadOnly());
316 device_activity_fetcher_.reset(
317 new DeviceActivityFetcher(signin_client_, this));
318 device_activity_fetcher_->Start();
319 }
320
321 void CrossDevicePromo::OnFetchDeviceActivitySuccess(
322 const std::vector<DeviceActivityFetcher::DeviceActivity>& devices) {
323 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivitySuccess. "
324 << devices.size() << " devices.";
325 prefs_->SetInt64(
326 prefs::kCrossDevicePromoNextFetchListDevicesTime,
327 (base::Time::Now() + delay_until_next_list_devices_).ToInternalValue());
328 prefs_->SetInteger(prefs::kCrossDevicePromoNumDevices, devices.size());
329
330 if (devices.empty()) {
331 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES);
332 return;
333 }
334
335 base::Time most_recent_last_active =
336 std::max_element(devices.begin(), devices.end(),
337 [](const DeviceActivityFetcher::DeviceActivity& first,
338 const DeviceActivityFetcher::DeviceActivity& second) {
Alexei Svitkine (slow) 2015/05/19 17:10:37 Can you use const auto& first here?
Mike Lerman 2015/05/19 18:17:34 MSVC compiler (I'm working on windows) didn't like
anthonyvd 2015/05/19 18:57:50 Indeed, auto type deduction in lambda parameters i
339 return first.last_active < second.last_active;
340 })->last_active;
341
342 prefs_->SetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime,
343 most_recent_last_active.ToInternalValue());
344
345 if (base::Time::Now() - most_recent_last_active < context_switch_duration_) {
346 // Make sure eligibility wasn't lost while executing the remote call.
347 if (!VerifyPromoEligibleReadOnly())
348 return;
349
350 // The context switch will only be valid for so long. Schedule another
351 // DeviceActivity check for when our switch would expire to check for more
352 // recent activity.
353 if (!device_activity_timer_.IsRunning()) {
354 base::TimeDelta time_to_next_check = most_recent_last_active +
355 context_switch_duration_ -
356 base::Time::Now();
Alexei Svitkine (slow) 2015/05/19 17:10:37 Nit: Make a local var for Now() at the top of the
Mike Lerman 2015/05/19 18:17:34 Done.
357 device_activity_timer_.Start(
358 FROM_HERE, time_to_next_check, this,
359 &CrossDevicePromo::GetDevicesActivityForAccountInCookie);
360 }
361
362 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE);
363 MarkPromoActive();
364 } else {
365 signin_metrics::LogXDevicePromoEligible(signin_metrics::NO_ACTIVE_DEVICES);
366 MarkPromoInactive();
367 }
368 device_activity_fetcher_.reset();
369 }
370
371 void CrossDevicePromo::OnFetchDeviceActivityFailure() {
372 VLOG(1) << "CrossDevicePromo::OnFetchDeviceActivityFailure.";
373 signin_metrics::LogXDevicePromoEligible(
374 signin_metrics::ERROR_FETCHING_DEVICE_ACTIVITY);
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) {
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 bool single_account = accounts.size() == 1;
396 bool has_pref =
397 prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie);
398 if (!single_account && has_pref) {
399 prefs_->ClearPref(prefs::kCrossDevicePromoObservedSingleAccountCookie);
400 MarkPromoInactive();
401 } else if (single_account && !has_pref) {
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, observer_list_,
414 OnPromoActivationChanged(true));
415 }
416 }
417
418 void CrossDevicePromo::MarkPromoInactive() {
419 VLOG(1) << "CrossDevicePromo::MarkPromoInactive.";
420 if (prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) {
421 prefs_->SetBoolean(prefs::kCrossDevicePromoActive, false);
422 FOR_EACH_OBSERVER(CrossDevicePromo::Observer, observer_list_,
423 OnPromoActivationChanged(false));
424 }
425 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698