OLD | NEW |
(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, ¶meter_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 : initialized_(false), |
| 52 signin_manager_(signin_manager), |
| 53 cookie_manager_service_(cookie_manager_service), |
| 54 prefs_(pref_service), |
| 55 signin_client_(signin_client), |
| 56 is_throttled_(true), |
| 57 start_last_browsing_session_(base::Time()) { |
| 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 (GetTimePref(prefs::kCrossDevicePromoObservedSingleAccountCookie) + |
| 166 single_account_duration_threshold_ > base::Time::Now()) { |
| 167 return false; |
| 168 } |
| 169 |
| 170 return true; |
| 171 } |
| 172 |
| 173 bool CrossDevicePromo::CheckPromoEligibility() { |
| 174 if (!initialized_) { |
| 175 // In tests the variations may not be present when Init() was first called. |
| 176 Init(); |
| 177 if (!initialized_) |
| 178 return false; |
| 179 } |
| 180 |
| 181 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) { |
| 182 signin_metrics::LogXDevicePromoEligible(signin_metrics::OPTED_OUT); |
| 183 return false; |
| 184 } |
| 185 |
| 186 if (signin_manager_->IsAuthenticated()) { |
| 187 signin_metrics::LogXDevicePromoEligible(signin_metrics::SIGNED_IN); |
| 188 return false; |
| 189 } |
| 190 |
| 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_ > |
| 196 base::Time::Now()) { |
| 197 signin_metrics::LogXDevicePromoEligible( |
| 198 signin_metrics::NOT_SINGLE_GAIA_ACCOUNT); |
| 199 return false; |
| 200 } |
| 201 |
| 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); |
| 213 return false; |
| 214 } |
| 215 |
| 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); |
| 224 return false; |
| 225 } |
| 226 // We're not eligible... but might be! Track metrics in the results. |
| 227 GetDevicesActivityForAccountInCookie(); |
| 228 return false; |
| 229 } |
| 230 |
| 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); |
| 238 return false; |
| 239 } |
| 240 |
| 241 DCHECK(VerifyPromoEligibleReadOnly()); |
| 242 return true; |
| 243 } |
| 244 |
| 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()) |
| 249 return; |
| 250 |
| 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); |
| 257 |
| 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 " |
| 262 "as the last call."; |
| 263 return; |
| 264 } |
| 265 |
| 266 if (start_last_browsing_session_ != base::Time()) |
| 267 signin_metrics::LogBrowsingSessionDuration(previous_last_active); |
| 268 |
| 269 start_last_browsing_session_ = time_now; |
| 270 |
| 271 if (!CheckPromoEligibility()) { |
| 272 VLOG(1) << "CrossDevicePromo::UpdateLastActiveTime; Ineligible for promo."; |
| 273 if (IsPromoActive()) |
| 274 MarkPromoInactive(); |
| 275 return; |
| 276 } |
| 277 |
| 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); |
| 286 MarkPromoActive(); |
| 287 return; |
| 288 } |
| 289 |
| 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( |
| 295 FROM_HERE, |
| 296 base::TimeDelta::FromMilliseconds(kDelayUntilGettingDeviceActivityInMS), |
| 297 this, &CrossDevicePromo::GetDevicesActivityForAccountInCookie); |
| 298 } |
| 299 } |
| 300 |
| 301 void CrossDevicePromo::GetDevicesActivityForAccountInCookie() { |
| 302 // Don't start a fetch while one is processing. |
| 303 if (device_activity_fetcher_.get()) |
| 304 return; |
| 305 |
| 306 if (is_throttled_) { |
| 307 signin_metrics::LogXDevicePromoEligible( |
| 308 signin_metrics::THROTTLED_FETCHING_DEVICE_ACTIVITY); |
| 309 return; |
| 310 } |
| 311 |
| 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(); |
| 317 } |
| 318 |
| 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."; |
| 324 prefs_->SetInt64( |
| 325 prefs::kCrossDevicePromoNextFetchListDevicesTime, |
| 326 (time_now + delay_until_next_list_devices_).ToInternalValue()); |
| 327 prefs_->SetInteger(prefs::kCrossDevicePromoNumDevices, devices.size()); |
| 328 |
| 329 if (devices.empty()) { |
| 330 signin_metrics::LogXDevicePromoEligible(signin_metrics::ZERO_DEVICES); |
| 331 return; |
| 332 } |
| 333 |
| 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; |
| 339 })->last_active; |
| 340 |
| 341 prefs_->SetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime, |
| 342 most_recent_last_active.ToInternalValue()); |
| 343 |
| 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()) |
| 347 return; |
| 348 |
| 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 |
| 351 // recent activity. |
| 352 if (!device_activity_timer_.IsRunning()) { |
| 353 base::TimeDelta time_to_next_check = most_recent_last_active + |
| 354 context_switch_duration_ - |
| 355 time_now; |
| 356 device_activity_timer_.Start( |
| 357 FROM_HERE, time_to_next_check, this, |
| 358 &CrossDevicePromo::GetDevicesActivityForAccountInCookie); |
| 359 } |
| 360 |
| 361 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE); |
| 362 MarkPromoActive(); |
| 363 } else { |
| 364 signin_metrics::LogXDevicePromoEligible(signin_metrics::NO_ACTIVE_DEVICES); |
| 365 MarkPromoInactive(); |
| 366 } |
| 367 device_activity_fetcher_.reset(); |
| 368 } |
| 369 |
| 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(); |
| 375 } |
| 376 |
| 377 void CrossDevicePromo::RegisterForCookieChanges() { |
| 378 cookie_manager_service_->AddObserver(this); |
| 379 } |
| 380 |
| 381 void CrossDevicePromo::UnregisterForCookieChanges() { |
| 382 cookie_manager_service_->RemoveObserver(this); |
| 383 } |
| 384 |
| 385 base::Time CrossDevicePromo::GetTimePref(const std::string& pref) { |
| 386 return base::Time::FromInternalValue(prefs_->GetInt64(pref)); |
| 387 } |
| 388 |
| 389 void CrossDevicePromo::OnGaiaAccountsInCookieUpdated( |
| 390 const std::vector<std::pair<std::string, bool>>& 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) |
| 395 return; |
| 396 |
| 397 // Multiple accounts seen - clear the pref. |
| 398 bool single_account = accounts.size() == 1; |
| 399 bool has_pref = |
| 400 prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie); |
| 401 if (!single_account && has_pref) { |
| 402 prefs_->ClearPref(prefs::kCrossDevicePromoObservedSingleAccountCookie); |
| 403 MarkPromoInactive(); |
| 404 } else if (single_account && !has_pref) { |
| 405 prefs_->SetInt64(prefs::kCrossDevicePromoObservedSingleAccountCookie, |
| 406 base::Time::Now().ToInternalValue()); |
| 407 } |
| 408 } |
| 409 |
| 410 void CrossDevicePromo::MarkPromoActive() { |
| 411 VLOG(1) << "CrossDevicePromo::MarkPromoActive."; |
| 412 DCHECK(!prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)); |
| 413 |
| 414 if (!prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) { |
| 415 prefs_->SetBoolean(prefs::kCrossDevicePromoActive, true); |
| 416 FOR_EACH_OBSERVER(CrossDevicePromo::Observer, observer_list_, |
| 417 OnPromoActivationChanged(true)); |
| 418 } |
| 419 } |
| 420 |
| 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)); |
| 427 } |
| 428 } |
OLD | NEW |