| 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 |
| 23 // Helper method to set a parameter based on a particular variable from the |
| 24 // configuration. Returns false if the parameter was not found or the |
| 25 // converstion could not succeed. |
| 26 bool SetParameterFromVariation( |
| 27 std::string variation_parameter, |
| 28 base::TimeDelta& local_parameter, |
| 29 base::Callback<base::TimeDelta(int)> conversion) { |
| 30 std::string parameter_as_string = variations::GetVariationParamValue( |
| 31 "CrossDevicePromo", variation_parameter); |
| 32 if (parameter_as_string.empty()) |
| 33 return false; |
| 34 |
| 35 int parameter_as_int; |
| 36 if (!base::StringToInt(parameter_as_string, ¶meter_as_int)) |
| 37 return false; |
| 38 |
| 39 local_parameter = conversion.Run(parameter_as_int); |
| 40 return true; |
| 41 } |
| 42 |
| 43 } // namespace |
| 44 |
| 45 CrossDevicePromo::CrossDevicePromo( |
| 46 SigninManager* signin_manager, |
| 47 GaiaCookieManagerService* cookie_manager_service, |
| 48 SigninClient* signin_client, |
| 49 PrefService* pref_service) |
| 50 : signin_manager_(signin_manager), |
| 51 cookie_manager_service_(cookie_manager_service), |
| 52 prefs_(pref_service), |
| 53 signin_client_(signin_client), |
| 54 start_last_browsing_session_(base::Time()), |
| 55 initialized_(false), |
| 56 track_metrics_(true) { |
| 57 Init(); |
| 58 } |
| 59 |
| 60 CrossDevicePromo::~CrossDevicePromo() {} |
| 61 |
| 62 void CrossDevicePromo::Shutdown() { |
| 63 UnregisterForCookieChanges(); |
| 64 UMA_HISTOGRAM_TIMES("Signin.XDevicePromo.BrowsingSessionDuration", |
| 65 base::Time::Now() - start_last_browsing_session_); |
| 66 } |
| 67 |
| 68 |
| 69 void CrossDevicePromo::AddObserver(CrossDevicePromo::Observer* observer) { |
| 70 observer_list_.AddObserver(observer); |
| 71 } |
| 72 |
| 73 void CrossDevicePromo::RemoveObserver(CrossDevicePromo::Observer* observer) { |
| 74 observer_list_.RemoveObserver(observer); |
| 75 } |
| 76 |
| 77 bool CrossDevicePromo::IsPromoActive() { |
| 78 return prefs_->GetBoolean(prefs::kCrossDevicePromoActive); |
| 79 } |
| 80 |
| 81 void CrossDevicePromo::Init() { |
| 82 if (!prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut) && |
| 83 SetParameterFromVariation( |
| 84 "HoursBetweenSyncDeviceChecks", |
| 85 delay_until_next_list_devices_, |
| 86 base::Bind(&base::TimeDelta::FromHours)) && |
| 87 SetParameterFromVariation( |
| 88 "DaysToVerifySingleUserProfile", |
| 89 single_account_in_profile_, |
| 90 base::Bind(&base::TimeDelta::FromDays)) && |
| 91 SetParameterFromVariation( |
| 92 "MinutesBetweenBrowsingSessions", |
| 93 inactivity_between_browsing_sessions_, |
| 94 base::Bind(&base::TimeDelta::FromMinutes)) && |
| 95 SetParameterFromVariation( |
| 96 "MinutesMaxContextSwitchDuration", |
| 97 context_switch_duration_, |
| 98 base::Bind(&base::TimeDelta::FromMinutes))) { |
| 99 RegisterForCookieChanges(); |
| 100 initialized_ = true; |
| 101 UMA_HISTOGRAM_BOOLEAN("Signin.XDevicePromo.Initialized", true); |
| 102 } else { |
| 103 UMA_HISTOGRAM_BOOLEAN("Signin.XDevicePromo.Initialized", false); |
| 104 } |
| 105 } |
| 106 |
| 107 void CrossDevicePromo::OptOut() { |
| 108 UnregisterForCookieChanges(); |
| 109 prefs_->SetBoolean(prefs::kCrossDevicePromoOptedOut, true); |
| 110 MarkPromoInactive(); |
| 111 } |
| 112 |
| 113 bool CrossDevicePromo::CheckPromoEligibility() { |
| 114 if (!initialized_) { |
| 115 Init(); |
| 116 if (!initialized_) |
| 117 return false; |
| 118 } |
| 119 |
| 120 if (prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)) { |
| 121 signin_metrics::LogXDevicePromoEligible(signin_metrics::OPTED_OUT); |
| 122 return false; |
| 123 } |
| 124 |
| 125 if (signin_manager_->GetAuthenticatedUsername().empty()) { |
| 126 signin_metrics::LogXDevicePromoEligible(signin_metrics::NOT_SIGNED_IN); |
| 127 return false; |
| 128 } |
| 129 |
| 130 base::Time cookie_has_one_account_since = base::Time::FromInternalValue( |
| 131 prefs_->GetInt64( |
| 132 prefs::kCrossDevicePromoObservedSingleAccountCookie)); |
| 133 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoObservedSingleAccountCookie) |
| 134 || cookie_has_one_account_since + single_account_in_profile_ > |
| 135 base::Time::Now()) { |
| 136 signin_metrics::LogXDevicePromoEligible( |
| 137 signin_metrics::NOT_SINGLE_GAIA_ACCOUNT); |
| 138 return false; |
| 139 } |
| 140 |
| 141 // This is the first time the promos being run; determine when to call the |
| 142 // ListDevices endpoint. |
| 143 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNextFetchListDevicesTime)) { |
| 144 int minutes_until_next_list_devices = base::RandGenerator( |
| 145 delay_until_next_list_devices_.InMinutes()); |
| 146 prefs_->SetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime, |
| 147 (base::Time::Now() + |
| 148 base::TimeDelta::FromMinutes(minutes_until_next_list_devices)). |
| 149 ToInternalValue()); |
| 150 signin_metrics::LogXDevicePromoEligible( |
| 151 signin_metrics::UNKNOWN_COUNT_SYNC_DEVICES); |
| 152 return false; |
| 153 } |
| 154 |
| 155 // We have no knowledge of other sync devices yet. |
| 156 if (!prefs_->HasPrefPath(prefs::kCrossDevicePromoNumDevices)) { |
| 157 base::Time time_next_list_devices = base::Time::FromInternalValue( |
| 158 prefs_->GetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime)); |
| 159 // Not time yet to poll the list of devices. |
| 160 if (time_next_list_devices > base::Time::Now()) { |
| 161 signin_metrics::LogXDevicePromoEligible( |
| 162 signin_metrics::UNKNOWN_COUNT_SYNC_DEVICES); |
| 163 return false; |
| 164 } else { |
| 165 // We're not eligible... but might be! Track metrics in the results. |
| 166 GetSyncDevicesForAccountInCookie(); |
| 167 return false; |
| 168 } |
| 169 } |
| 170 |
| 171 int num_devices = prefs_->GetInt64( |
| 172 prefs::kCrossDevicePromoNumDevices); |
| 173 if (num_devices > 0) { |
| 174 base::Time time_next_list_devices = base::Time::FromInternalValue( |
| 175 prefs_->GetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime)); |
| 176 if (time_next_list_devices < base::Time::Now()) { |
| 177 // Log this execution through the flow as ZERO_SYNC_DEVICES; do not log |
| 178 // the result calculated after GetSyncDevices. We want each check for |
| 179 // promo eligibility to show up exactly once. |
| 180 track_metrics_ = false; |
| 181 GetSyncDevicesForAccountInCookie(); |
| 182 } |
| 183 |
| 184 signin_metrics::LogXDevicePromoEligible( |
| 185 signin_metrics::ZERO_SYNC_DEVICES); |
| 186 return false; |
| 187 } |
| 188 |
| 189 return true; |
| 190 } |
| 191 |
| 192 void CrossDevicePromo::UpdateLastActiveTime( |
| 193 const base::Time& previous_last_active) { |
| 194 UMA_HISTOGRAM_TIMES("Signin.XDevicePromo.BrowsingSessionDurationComputed", |
| 195 base::Time::Now() - previous_last_active); |
| 196 // Check if this is a different browsing session since the last call. |
| 197 if (base::Time::Now() - previous_last_active <= |
| 198 inactivity_between_browsing_sessions_) { |
| 199 return; |
| 200 } |
| 201 |
| 202 if (start_last_browsing_session_ != base::Time()) { |
| 203 UMA_HISTOGRAM_TIMES("Signin.XDevicePromo.BrowsingSessionDuration", |
| 204 previous_last_active - start_last_browsing_session_); |
| 205 } |
| 206 |
| 207 start_last_browsing_session_ = base::Time::Now(); |
| 208 |
| 209 if (!CheckPromoEligibility()) |
| 210 return; |
| 211 |
| 212 // Check if there is a record of recent browsing activity on another device. |
| 213 // If there is none, we set a timer to update the records after a small delay |
| 214 // to ensure server-side data is synchronized. |
| 215 base::Time device_last_active = base::Time::FromInternalValue( |
| 216 prefs_->GetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime)); |
| 217 if (base::Time::Now() - device_last_active < context_switch_duration_) { |
| 218 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE); |
| 219 MarkPromoActive(); |
| 220 return; |
| 221 } |
| 222 |
| 223 device_activity_timer_.Start( |
| 224 FROM_HERE, |
| 225 base::TimeDelta::FromMilliseconds(kDelayUntilGettingDeviceActivityInMS), |
| 226 this, |
| 227 &CrossDevicePromo::GetSyncDevicesForAccountInCookie); |
| 228 } |
| 229 |
| 230 void CrossDevicePromo::GetSyncDevicesForAccountInCookie() { |
| 231 // Don't start a fetch while one is processing. |
| 232 if (device_activity_fetcher_.get()) |
| 233 return; |
| 234 |
| 235 device_activity_fetcher_.reset( |
| 236 new DeviceActivityFetcher(signin_client_, this)); |
| 237 device_activity_fetcher_->Start(); |
| 238 } |
| 239 |
| 240 void CrossDevicePromo::OnFetchDeviceActivitySuccess( |
| 241 const std::vector<DeviceActivityFetcher::DeviceActivity>& devices) { |
| 242 prefs_->SetInt64(prefs::kCrossDevicePromoNextFetchListDevicesTime, |
| 243 (base::Time::Now() + |
| 244 delay_until_next_list_devices_).ToInternalValue()); |
| 245 prefs_->SetInt64(prefs::kCrossDevicePromoNumDevices, devices.size()); |
| 246 |
| 247 if (devices.empty()) { |
| 248 if (track_metrics_) { |
| 249 signin_metrics::LogXDevicePromoEligible( |
| 250 signin_metrics::ZERO_SYNC_DEVICES); |
| 251 } |
| 252 return; |
| 253 } |
| 254 |
| 255 base::Time most_recent_last_active = base::Time(); |
| 256 for (size_t i = 0; i < devices.size(); i++) { |
| 257 if (devices[i].last_active > most_recent_last_active) |
| 258 most_recent_last_active = devices[i].last_active; |
| 259 } |
| 260 |
| 261 prefs_->SetInt64(prefs::kCrossDevicePromoLastDeviceActiveTime, |
| 262 most_recent_last_active.ToInternalValue()); |
| 263 |
| 264 if (base::Time::Now() - most_recent_last_active < context_switch_duration_) { |
| 265 if (track_metrics_) |
| 266 signin_metrics::LogXDevicePromoEligible(signin_metrics::ELIGIBLE); |
| 267 MarkPromoActive(); |
| 268 } else { |
| 269 if (track_metrics_) { |
| 270 signin_metrics::LogXDevicePromoEligible( |
| 271 signin_metrics::NO_ACTIVE_SYNC_DEVICES); |
| 272 } |
| 273 MarkPromoInactive(); |
| 274 } |
| 275 track_metrics_ = true; |
| 276 device_activity_fetcher_.reset(); |
| 277 } |
| 278 |
| 279 void CrossDevicePromo::OnFetchDeviceActivityFailure() { |
| 280 if (track_metrics_) { |
| 281 signin_metrics::LogXDevicePromoEligible( |
| 282 signin_metrics::ERROR_FETCHING_SYNC_DEVICES); |
| 283 } |
| 284 device_activity_fetcher_.reset(); |
| 285 } |
| 286 |
| 287 void CrossDevicePromo::RegisterForCookieChanges() { |
| 288 cookie_manager_service_->AddObserver(this); |
| 289 } |
| 290 |
| 291 void CrossDevicePromo::UnregisterForCookieChanges() { |
| 292 cookie_manager_service_->RemoveObserver(this); |
| 293 } |
| 294 |
| 295 void CrossDevicePromo::OnGaiaAccountsInCookieUpdated( |
| 296 const std::vector<std::pair<std::string, bool> >& accounts, |
| 297 const GoogleServiceAuthError& error) { |
| 298 VLOG(1) << "CrossDevicePromo::OnGaiaAccountsInCookieUpdated. " |
| 299 << accounts.size() << " accounts with auth error " << error.state(); |
| 300 if (error.state() != GoogleServiceAuthError::State::NONE) |
| 301 return; |
| 302 |
| 303 // Multiple accounts seen - clear the pref. |
| 304 if (accounts.size() != 1 && prefs_->HasPrefPath( |
| 305 prefs::kCrossDevicePromoObservedSingleAccountCookie)) { |
| 306 UMA_HISTOGRAM_TIMES("Signin.XDevicePromo.NoLongerSingleAccount", |
| 307 base::Time::Now() - |
| 308 base::Time::FromInternalValue(prefs_->GetInt64( |
| 309 prefs::kCrossDevicePromoObservedSingleAccountCookie))); |
| 310 prefs_->ClearPref(prefs::kCrossDevicePromoObservedSingleAccountCookie); |
| 311 // Single account seen. Note if this is the first time we've seen this. |
| 312 } else if (accounts.size() == 1 && !prefs_->HasPrefPath( |
| 313 prefs::kCrossDevicePromoObservedSingleAccountCookie)){ |
| 314 prefs_->SetInt64(prefs::kCrossDevicePromoObservedSingleAccountCookie, |
| 315 base::Time::Now().ToInternalValue()); |
| 316 } |
| 317 } |
| 318 |
| 319 void CrossDevicePromo::MarkPromoActive() { |
| 320 DCHECK(!prefs_->GetBoolean(prefs::kCrossDevicePromoOptedOut)); |
| 321 |
| 322 if (!prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) { |
| 323 prefs_->SetBoolean(prefs::kCrossDevicePromoActive, true); |
| 324 FOR_EACH_OBSERVER(CrossDevicePromo::Observer, |
| 325 observer_list_, |
| 326 OnPromoActivationChanged(true)); |
| 327 } |
| 328 } |
| 329 |
| 330 void CrossDevicePromo::MarkPromoInactive() { |
| 331 if (prefs_->GetBoolean(prefs::kCrossDevicePromoActive)) { |
| 332 prefs_->SetBoolean(prefs::kCrossDevicePromoActive, false); |
| 333 FOR_EACH_OBSERVER(CrossDevicePromo::Observer, |
| 334 observer_list_, |
| 335 OnPromoActivationChanged(false)); |
| 336 } |
| 337 } |
| OLD | NEW |