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 package org.chromium.chrome.browser.signin; |
| 6 |
| 7 import android.accounts.Account; |
| 8 import android.content.Context; |
| 9 import android.content.SharedPreferences; |
| 10 import android.os.AsyncTask; |
| 11 import android.preference.PreferenceManager; |
| 12 |
| 13 import com.google.android.gms.auth.AccountChangeEvent; |
| 14 import com.google.android.gms.auth.GoogleAuthException; |
| 15 import com.google.android.gms.auth.GoogleAuthUtil; |
| 16 |
| 17 import org.chromium.base.Log; |
| 18 import org.chromium.base.VisibleForTesting; |
| 19 import org.chromium.chrome.browser.invalidation.InvalidationController; |
| 20 import org.chromium.chrome.browser.invalidation.InvalidationServiceFactory; |
| 21 import org.chromium.chrome.browser.preferences.ChromePreferenceManager; |
| 22 import org.chromium.chrome.browser.preferences.PrefServiceBridge; |
| 23 import org.chromium.chrome.browser.profiles.Profile; |
| 24 import org.chromium.chrome.browser.signin.SigninManager.SignInFlowObserver; |
| 25 import org.chromium.chrome.browser.sync.ProfileSyncService; |
| 26 import org.chromium.chrome.browser.sync.SyncController; |
| 27 import org.chromium.sync.AndroidSyncSettings; |
| 28 import org.chromium.sync.internal_api.pub.base.ModelType; |
| 29 import org.chromium.sync.signin.AccountManagerHelper; |
| 30 import org.chromium.sync.signin.ChromeSigninController; |
| 31 |
| 32 import java.io.IOException; |
| 33 import java.util.ArrayList; |
| 34 import java.util.HashSet; |
| 35 import java.util.List; |
| 36 import java.util.Set; |
| 37 |
| 38 /** |
| 39 * A helper for tasks like re-signin. |
| 40 * |
| 41 * This should be merged into SigninManager when it is upstreamed. |
| 42 */ |
| 43 public class SigninHelper { |
| 44 |
| 45 private static final String TAG = "SigninHelper"; |
| 46 |
| 47 private static final Object LOCK = new Object(); |
| 48 |
| 49 private static final String ACCOUNTS_CHANGED_PREFS_KEY = "prefs_sync_account
s_changed"; |
| 50 |
| 51 // Key to the shared pref that holds the new account's name if the currently
signed |
| 52 // in account has been renamed. |
| 53 private static final String ACCOUNT_RENAMED_PREFS_KEY = "prefs_sync_account_
renamed"; |
| 54 |
| 55 // Key to the shared pref that holds the last read index of all the account
changed |
| 56 // events of the current signed in account. |
| 57 private static final String ACCOUNT_RENAME_EVENT_INDEX_PREFS_KEY = |
| 58 "prefs_sync_account_rename_event_index"; |
| 59 |
| 60 private static final String ANDROID_ACCOUNTS_PREFS_KEY = "prefs_sync_android
_accounts"; |
| 61 |
| 62 private static SigninHelper sInstance; |
| 63 |
| 64 /** |
| 65 * Retrieve more detailed information from account changed intents. |
| 66 */ |
| 67 public static interface AccountChangeEventChecker { |
| 68 public List<String> getAccountChangeEvents( |
| 69 Context context, int index, String accountName); |
| 70 } |
| 71 |
| 72 /** |
| 73 * Uses GoogleAuthUtil.getAccountChangeEvents to detect if account |
| 74 * renaming has occured. |
| 75 */ |
| 76 public static final class SystemAccountChangeEventChecker |
| 77 implements SigninHelper.AccountChangeEventChecker { |
| 78 @Override |
| 79 public List<String> getAccountChangeEvents( |
| 80 Context context, int index, String accountName) { |
| 81 try { |
| 82 List<AccountChangeEvent> list = GoogleAuthUtil.getAccountChangeE
vents( |
| 83 context, index, accountName); |
| 84 List<String> result = new ArrayList<String>(list.size()); |
| 85 for (AccountChangeEvent e : list) { |
| 86 if (e.getChangeType() == GoogleAuthUtil.CHANGE_TYPE_ACCOUNT_
RENAMED_TO) { |
| 87 result.add(e.getChangeData()); |
| 88 } else { |
| 89 result.add(null); |
| 90 } |
| 91 } |
| 92 return result; |
| 93 } catch (IOException e) { |
| 94 Log.w(TAG, "Failed to get change events", e); |
| 95 } catch (GoogleAuthException e) { |
| 96 Log.w(TAG, "Failed to get change events", e); |
| 97 } |
| 98 return new ArrayList<String>(0); |
| 99 } |
| 100 } |
| 101 |
| 102 |
| 103 @VisibleForTesting |
| 104 protected final Context mContext; |
| 105 |
| 106 private final ChromeSigninController mChromeSigninController; |
| 107 |
| 108 private final ProfileSyncService mProfileSyncService; |
| 109 |
| 110 private final SigninManager mSigninManager; |
| 111 |
| 112 private final OAuth2TokenService mOAuth2TokenService; |
| 113 |
| 114 private final SyncController mSyncController; |
| 115 |
| 116 public static SigninHelper get(Context context) { |
| 117 synchronized (LOCK) { |
| 118 if (sInstance == null) { |
| 119 sInstance = new SigninHelper(context.getApplicationContext()); |
| 120 } |
| 121 } |
| 122 return sInstance; |
| 123 } |
| 124 |
| 125 private SigninHelper(Context context) { |
| 126 mContext = context; |
| 127 mProfileSyncService = ProfileSyncService.get(mContext); |
| 128 mSigninManager = SigninManager.get(mContext); |
| 129 mOAuth2TokenService = OAuth2TokenService.getForProfile(Profile.getLastUs
edProfile()); |
| 130 mSyncController = SyncController.get(context); |
| 131 mChromeSigninController = ChromeSigninController.get(mContext); |
| 132 } |
| 133 |
| 134 public void validateAccountSettings(boolean accountsChanged) { |
| 135 Account syncAccount = mChromeSigninController.getSignedInUser(); |
| 136 if (syncAccount == null) { |
| 137 if (SigninManager.getAndroidSigninPromoExperimentGroup() < 0) return
; |
| 138 |
| 139 // Never shows a signin promo if user has manually disconnected. |
| 140 String lastSyncAccountName = |
| 141 PrefServiceBridge.getInstance().getSyncLastAccountName(); |
| 142 if (lastSyncAccountName != null && !lastSyncAccountName.isEmpty()) r
eturn; |
| 143 |
| 144 SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPr
eferences(mContext); |
| 145 boolean hasKnownAccountKeys = sharedPrefs.contains(ANDROID_ACCOUNTS_
PREFS_KEY); |
| 146 // Nothing to do if Android accounts are not changed and already kno
wn to Chrome. |
| 147 if (hasKnownAccountKeys && !accountsChanged) return; |
| 148 |
| 149 List<String> currentAccountNames = |
| 150 AccountManagerHelper.get(mContext).getGoogleAccountNames(); |
| 151 if (hasKnownAccountKeys) { |
| 152 ChromePreferenceManager chromePreferenceManager = |
| 153 ChromePreferenceManager.getInstance(mContext); |
| 154 if (!chromePreferenceManager.getSigninPromoShown()) { |
| 155 Set<String> lastKnownAccountNames = sharedPrefs.getStringSet
( |
| 156 ANDROID_ACCOUNTS_PREFS_KEY, new HashSet<String>()); |
| 157 Set<String> newAccountNames = new HashSet<String>(currentAcc
ountNames); |
| 158 newAccountNames.removeAll(lastKnownAccountNames); |
| 159 if (!newAccountNames.isEmpty()) { |
| 160 chromePreferenceManager.setShowSigninPromo(true); |
| 161 } |
| 162 } |
| 163 } |
| 164 |
| 165 sharedPrefs.edit().putStringSet( |
| 166 ANDROID_ACCOUNTS_PREFS_KEY, new HashSet<String>(currentAccou
ntNames)).apply(); |
| 167 return; |
| 168 } |
| 169 |
| 170 String renamedAccount = getNewSignedInAccountName(mContext); |
| 171 if (accountsChanged && renamedAccount != null) { |
| 172 handleAccountRename(ChromeSigninController.get(mContext).getSignedIn
AccountName(), |
| 173 renamedAccount); |
| 174 return; |
| 175 } |
| 176 |
| 177 // Always check for account deleted. |
| 178 if (!accountExists(syncAccount)) { |
| 179 // It is possible that Chrome got to this point without account |
| 180 // rename notification. Let us signout before doing a rename. |
| 181 // updateAccountRenameData(mContext, new SystemAccountChangeEventChe
cker()); |
| 182 AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>()
{ |
| 183 @Override |
| 184 protected Void doInBackground(Void... params) { |
| 185 updateAccountRenameData(mContext, new SystemAccountChangeEve
ntChecker()); |
| 186 return null; |
| 187 } |
| 188 |
| 189 @Override |
| 190 protected void onPostExecute(Void result) { |
| 191 String renamedAccount = getNewSignedInAccountName(mContext); |
| 192 if (renamedAccount == null) { |
| 193 mSigninManager.signOut(null, null); |
| 194 } else { |
| 195 validateAccountSettings(true); |
| 196 } |
| 197 } |
| 198 }; |
| 199 task.execute(); |
| 200 return; |
| 201 } |
| 202 |
| 203 if (accountsChanged) { |
| 204 // Account details have changed so inform the token service that cre
dentials |
| 205 // should now be available. |
| 206 mOAuth2TokenService.validateAccounts(mContext, false); |
| 207 } |
| 208 |
| 209 if (AndroidSyncSettings.isSyncEnabled(mContext)) { |
| 210 if (mProfileSyncService.hasSyncSetupCompleted()) { |
| 211 if (accountsChanged) { |
| 212 // Nudge the syncer to ensure it does a full sync. |
| 213 InvalidationServiceFactory.getForProfile(Profile.getLastUsed
Profile()) |
| 214 .requestSyncFromNativeChromeForAllTypes(
); |
| 215 } |
| 216 } else { |
| 217 // We should have set up sync but for some reason it's not enabl
ed. Tell the sync |
| 218 // engine to start. |
| 219 mProfileSyncService.syncSignIn(); |
| 220 } |
| 221 } |
| 222 } |
| 223 |
| 224 /** |
| 225 * Deal with account rename. The current approach is to sign out and then si
gn back in. |
| 226 * In the (near) future, we should just be clearing all the cached email add
ress here |
| 227 * and have the UI re-fetch the emailing address based on the ID. |
| 228 */ |
| 229 private void handleAccountRename(final String oldName, final String newName)
{ |
| 230 Log.i(TAG, "handleAccountRename from: " + oldName + " to " + newName); |
| 231 |
| 232 // TODO(acleung): I think most of the operations need to run on the main |
| 233 // thread. May be we should have a progress Dialog? |
| 234 |
| 235 // Before signing out, remember the current sync state and data types. |
| 236 final boolean isSyncWanted = AndroidSyncSettings.isChromeSyncEnabled(mCo
ntext); |
| 237 final Set<ModelType> dataTypes = mProfileSyncService.getPreferredDataTyp
es(); |
| 238 |
| 239 // TODO(acleung): Deal with passphrase or just prompt user to re-enter i
t? |
| 240 |
| 241 // Perform a sign-out with a callback to sign-in again. |
| 242 mSigninManager.signOut(null, new Runnable() { |
| 243 @Override |
| 244 public void run() { |
| 245 // Clear the shared perf only after signOut is successful. |
| 246 // If Chrome dies, we can try it again on next run. |
| 247 // Otherwise, if re-sign-in fails, we'll just leave chrome |
| 248 // signed-out. |
| 249 clearNewSignedInAccountName(mContext); |
| 250 performResignin(newName, isSyncWanted, dataTypes); |
| 251 } |
| 252 }); |
| 253 } |
| 254 |
| 255 private void performResignin(String newName, |
| 256 final boolean isSyncWanted, |
| 257 final Set<ModelType> dataTypes) { |
| 258 // This is the correct account now. |
| 259 final Account account = AccountManagerHelper.createAccountFromName(newNa
me); |
| 260 |
| 261 mSigninManager.startSignIn(null, account, true, new SignInFlowObserver()
{ |
| 262 @Override |
| 263 public void onSigninComplete() { |
| 264 mProfileSyncService.setSetupInProgress(false); |
| 265 |
| 266 if (isSyncWanted) { |
| 267 mSyncController.start(); |
| 268 InvalidationController controller = InvalidationController.g
et(mContext); |
| 269 controller.refreshRegisteredTypes(dataTypes); |
| 270 } else { |
| 271 mSyncController.stop(); |
| 272 } |
| 273 |
| 274 validateAccountSettings(true); |
| 275 } |
| 276 |
| 277 @Override |
| 278 public void onSigninCancelled() { |
| 279 } |
| 280 }); |
| 281 } |
| 282 |
| 283 private boolean accountExists(Account account) { |
| 284 Account[] accounts = AccountManagerHelper.get(mContext).getGoogleAccount
s(); |
| 285 for (Account a : accounts) { |
| 286 if (a.equals(account)) { |
| 287 return true; |
| 288 } |
| 289 } |
| 290 return false; |
| 291 } |
| 292 |
| 293 /** |
| 294 * Sets the ACCOUNTS_CHANGED_PREFS_KEY to true. |
| 295 */ |
| 296 public static void markAccountsChangedPref(Context context) { |
| 297 // The process may go away as soon as we return from onReceive but Andro
id makes sure |
| 298 // that in-flight disk writes from apply() complete before changing comp
onent states. |
| 299 PreferenceManager.getDefaultSharedPreferences(context) |
| 300 .edit().putBoolean(ACCOUNTS_CHANGED_PREFS_KEY, true).apply(); |
| 301 } |
| 302 |
| 303 /** |
| 304 * @return The new account name of the current user. Null if it wasn't renam
ed. |
| 305 */ |
| 306 public static String getNewSignedInAccountName(Context context) { |
| 307 return (PreferenceManager.getDefaultSharedPreferences(context) |
| 308 .getString(ACCOUNT_RENAMED_PREFS_KEY, null)); |
| 309 } |
| 310 |
| 311 private static void clearNewSignedInAccountName(Context context) { |
| 312 PreferenceManager.getDefaultSharedPreferences(context) |
| 313 .edit() |
| 314 .putString(ACCOUNT_RENAMED_PREFS_KEY, null) |
| 315 .apply(); |
| 316 } |
| 317 |
| 318 private static String getLastKnownAccountName(Context context) { |
| 319 // This is the last known name of the currently signed in user. |
| 320 // It can be: |
| 321 // 1. The signed in account name known to the ChromeSigninController. |
| 322 // 2. A pending newly choosen name that is differed from the one known
to |
| 323 // ChromeSigninController but is stored in ACCOUNT_RENAMED_PREFS_KEY
. |
| 324 String name = PreferenceManager.getDefaultSharedPreferences(context).get
String( |
| 325 ACCOUNT_RENAMED_PREFS_KEY, null); |
| 326 |
| 327 // If there is no pending rename, take the name known to ChromeSigninCon
troller. |
| 328 return name == null ? ChromeSigninController.get(context).getSignedInAcc
ountName() : name; |
| 329 } |
| 330 |
| 331 public static void updateAccountRenameData(Context context) { |
| 332 updateAccountRenameData(context, new SystemAccountChangeEventChecker()); |
| 333 } |
| 334 |
| 335 @VisibleForTesting |
| 336 public static void updateAccountRenameData(Context context, AccountChangeEve
ntChecker checker) { |
| 337 String curName = getLastKnownAccountName(context); |
| 338 |
| 339 // Skip the search if there is no signed in account. |
| 340 if (curName == null) return; |
| 341 |
| 342 String newName = curName; |
| 343 |
| 344 // This is the last read index of all the account change event. |
| 345 int eventIndex = PreferenceManager.getDefaultSharedPreferences(context).
getInt( |
| 346 ACCOUNT_RENAME_EVENT_INDEX_PREFS_KEY, 0); |
| 347 |
| 348 int newIndex = eventIndex; |
| 349 |
| 350 try { |
| 351 outerLoop: while (true) { |
| 352 List<String> nameChanges = checker.getAccountChangeEvents(contex
t, |
| 353 newIndex, newName); |
| 354 |
| 355 for (String name : nameChanges) { |
| 356 if (name != null) { |
| 357 // We have found a rename event of the current account. |
| 358 // We need to check if that account is further renamed. |
| 359 newName = name; |
| 360 newIndex = 0; // Start from the beginning of the new acc
ount. |
| 361 continue outerLoop; |
| 362 } |
| 363 } |
| 364 |
| 365 // If there is no rename event pending. Update the last read ind
ex to avoid |
| 366 // re-reading them in the future. |
| 367 newIndex = nameChanges.size(); |
| 368 break; |
| 369 } |
| 370 } catch (Exception e) { |
| 371 Log.w(TAG, "Error while looking for rename events.", e); |
| 372 } |
| 373 |
| 374 if (!curName.equals(newName)) { |
| 375 PreferenceManager.getDefaultSharedPreferences(context) |
| 376 .edit().putString(ACCOUNT_RENAMED_PREFS_KEY, newName).apply(
); |
| 377 } |
| 378 |
| 379 if (newIndex != eventIndex) { |
| 380 PreferenceManager.getDefaultSharedPreferences(context) |
| 381 .edit().putInt(ACCOUNT_RENAME_EVENT_INDEX_PREFS_KEY, newInde
x).apply(); |
| 382 } |
| 383 } |
| 384 |
| 385 public static boolean checkAndClearAccountsChangedPref(Context context) { |
| 386 if (PreferenceManager.getDefaultSharedPreferences(context) |
| 387 .getBoolean(ACCOUNTS_CHANGED_PREFS_KEY, false)) { |
| 388 // Clear the value in prefs. |
| 389 PreferenceManager.getDefaultSharedPreferences(context) |
| 390 .edit().putBoolean(ACCOUNTS_CHANGED_PREFS_KEY, false).apply(
); |
| 391 return true; |
| 392 } else { |
| 393 return false; |
| 394 } |
| 395 } |
| 396 } |
OLD | NEW |