| OLD | NEW |
| (Empty) |
| 1 // Copyright 2011 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.sync.signin; | |
| 6 | |
| 7 | |
| 8 import android.Manifest; | |
| 9 import android.accounts.Account; | |
| 10 import android.accounts.AuthenticatorDescription; | |
| 11 import android.content.Context; | |
| 12 import android.content.pm.PackageManager; | |
| 13 import android.os.AsyncTask; | |
| 14 import android.os.Process; | |
| 15 | |
| 16 import org.chromium.base.Callback; | |
| 17 import org.chromium.base.Log; | |
| 18 import org.chromium.base.VisibleForTesting; | |
| 19 import org.chromium.net.NetworkChangeNotifier; | |
| 20 | |
| 21 import java.util.ArrayList; | |
| 22 import java.util.List; | |
| 23 import java.util.Locale; | |
| 24 import java.util.concurrent.atomic.AtomicBoolean; | |
| 25 import java.util.concurrent.atomic.AtomicInteger; | |
| 26 import java.util.regex.Pattern; | |
| 27 | |
| 28 /** | |
| 29 * AccountManagerHelper wraps our access of AccountManager in Android. | |
| 30 * | |
| 31 * Use the AccountManagerHelper.get(someContext) to instantiate it | |
| 32 */ | |
| 33 public class AccountManagerHelper { | |
| 34 private static final String TAG = "Sync_Signin"; | |
| 35 | |
| 36 private static final Pattern AT_SYMBOL = Pattern.compile("@"); | |
| 37 | |
| 38 private static final String GMAIL_COM = "gmail.com"; | |
| 39 | |
| 40 private static final String GOOGLEMAIL_COM = "googlemail.com"; | |
| 41 | |
| 42 public static final String GOOGLE_ACCOUNT_TYPE = "com.google"; | |
| 43 | |
| 44 /** | |
| 45 * An account feature (corresponding to a Gaia service flag) that specifies
whether the account | |
| 46 * is a child account. | |
| 47 */ | |
| 48 @VisibleForTesting public static final String FEATURE_IS_CHILD_ACCOUNT_KEY =
"service_uca"; | |
| 49 | |
| 50 private static final Object sLock = new Object(); | |
| 51 | |
| 52 private static AccountManagerHelper sAccountManagerHelper; | |
| 53 | |
| 54 private final AccountManagerDelegate mAccountManager; | |
| 55 | |
| 56 private Context mApplicationContext; | |
| 57 | |
| 58 /** | |
| 59 * A simple callback for getAuthToken. | |
| 60 */ | |
| 61 public interface GetAuthTokenCallback { | |
| 62 /** | |
| 63 * Invoked on the UI thread if a token is provided by the AccountManager
. | |
| 64 * | |
| 65 * @param token Auth token, guaranteed not to be null. | |
| 66 */ | |
| 67 void tokenAvailable(String token); | |
| 68 | |
| 69 /** | |
| 70 * Invoked on the UI thread if no token is available. | |
| 71 * | |
| 72 * @param isTransientError Indicates if the error is transient (network
timeout or | |
| 73 * unavailable, etc) or persistent (bad credentials, permission denied,
etc). | |
| 74 */ | |
| 75 void tokenUnavailable(boolean isTransientError); | |
| 76 } | |
| 77 | |
| 78 /** | |
| 79 * @param context the Android context | |
| 80 * @param accountManager the account manager to use as a backend service | |
| 81 */ | |
| 82 private AccountManagerHelper(Context context, AccountManagerDelegate account
Manager) { | |
| 83 mApplicationContext = context.getApplicationContext(); | |
| 84 mAccountManager = accountManager; | |
| 85 } | |
| 86 | |
| 87 /** | |
| 88 * Initialize AccountManagerHelper with a custom AccountManagerDelegate. | |
| 89 * Ensures that the singleton AccountManagerHelper hasn't been created yet. | |
| 90 * This can be overriden in tests using the overrideAccountManagerHelperForT
ests method. | |
| 91 * | |
| 92 * @param context the applicationContext is retrieved from the context used
as an argument. | |
| 93 * @param delegate the custom AccountManagerDelegate to use. | |
| 94 */ | |
| 95 public static void initializeAccountManagerHelper( | |
| 96 Context context, AccountManagerDelegate delegate) { | |
| 97 synchronized (sLock) { | |
| 98 assert sAccountManagerHelper == null; | |
| 99 sAccountManagerHelper = new AccountManagerHelper(context, delegate); | |
| 100 } | |
| 101 } | |
| 102 | |
| 103 /** | |
| 104 * A getter method for AccountManagerHelper singleton which also initializes
it if not wasn't | |
| 105 * already initialized. | |
| 106 * | |
| 107 * @param context the applicationContext is retrieved from the context used
as an argument. | |
| 108 * @return a singleton instance of the AccountManagerHelper | |
| 109 */ | |
| 110 public static AccountManagerHelper get(Context context) { | |
| 111 synchronized (sLock) { | |
| 112 if (sAccountManagerHelper == null) { | |
| 113 sAccountManagerHelper = new AccountManagerHelper( | |
| 114 context, new SystemAccountManagerDelegate(context)); | |
| 115 } | |
| 116 } | |
| 117 return sAccountManagerHelper; | |
| 118 } | |
| 119 | |
| 120 /** | |
| 121 * Override AccountManagerHelper with a custom AccountManagerDelegate in tes
ts. | |
| 122 * Unlike initializeAccountManagerHelper, this will override the existing in
stance of | |
| 123 * AccountManagerHelper if any. Only for use in Tests. | |
| 124 * | |
| 125 * @param context the applicationContext is retrieved from the context used
as an argument. | |
| 126 * @param delegate the custom AccountManagerDelegate to use. | |
| 127 */ | |
| 128 @VisibleForTesting | |
| 129 public static void overrideAccountManagerHelperForTests( | |
| 130 Context context, AccountManagerDelegate delegate) { | |
| 131 synchronized (sLock) { | |
| 132 sAccountManagerHelper = new AccountManagerHelper(context, delegate); | |
| 133 } | |
| 134 } | |
| 135 | |
| 136 /** | |
| 137 * Creates an Account object for the given name. | |
| 138 */ | |
| 139 public static Account createAccountFromName(String name) { | |
| 140 return new Account(name, GOOGLE_ACCOUNT_TYPE); | |
| 141 } | |
| 142 | |
| 143 /** | |
| 144 * This method is deprecated; please use the asynchronous version below inst
ead. | |
| 145 * | |
| 146 * See http://crbug.com/517697 for details. | |
| 147 */ | |
| 148 public List<String> getGoogleAccountNames() { | |
| 149 List<String> accountNames = new ArrayList<String>(); | |
| 150 for (Account account : getGoogleAccounts()) { | |
| 151 accountNames.add(account.name); | |
| 152 } | |
| 153 return accountNames; | |
| 154 } | |
| 155 | |
| 156 /** | |
| 157 * Retrieves a list of the Google account names on the device asynchronously
. | |
| 158 */ | |
| 159 public void getGoogleAccountNames(final Callback<List<String>> callback) { | |
| 160 getGoogleAccounts(new Callback<Account[]>() { | |
| 161 @Override | |
| 162 public void onResult(Account[] accounts) { | |
| 163 List<String> accountNames = new ArrayList<String>(); | |
| 164 for (Account account : accounts) { | |
| 165 accountNames.add(account.name); | |
| 166 } | |
| 167 callback.onResult(accountNames); | |
| 168 } | |
| 169 }); | |
| 170 } | |
| 171 | |
| 172 /** | |
| 173 * This method is deprecated; please use the asynchronous version below inst
ead. | |
| 174 * | |
| 175 * See http://crbug.com/517697 for details. | |
| 176 */ | |
| 177 public Account[] getGoogleAccounts() { | |
| 178 return mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); | |
| 179 } | |
| 180 | |
| 181 /** | |
| 182 * Retrieves all Google accounts on the device asynchronously. | |
| 183 */ | |
| 184 public void getGoogleAccounts(Callback<Account[]> callback) { | |
| 185 mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE, callback); | |
| 186 } | |
| 187 | |
| 188 /** | |
| 189 * This method is deprecated; please use the asynchronous version below inst
ead. | |
| 190 * | |
| 191 * See http://crbug.com/517697 for details. | |
| 192 */ | |
| 193 public boolean hasGoogleAccounts() { | |
| 194 return getGoogleAccounts().length > 0; | |
| 195 } | |
| 196 | |
| 197 /** | |
| 198 * Asynchronously determine whether any Google accounts have been added. | |
| 199 */ | |
| 200 public void hasGoogleAccounts(final Callback<Boolean> callback) { | |
| 201 getGoogleAccounts(new Callback<Account[]>() { | |
| 202 @Override | |
| 203 public void onResult(Account[] accounts) { | |
| 204 callback.onResult(accounts.length > 0); | |
| 205 } | |
| 206 }); | |
| 207 } | |
| 208 | |
| 209 private String canonicalizeName(String name) { | |
| 210 String[] parts = AT_SYMBOL.split(name); | |
| 211 if (parts.length != 2) return name; | |
| 212 | |
| 213 if (GOOGLEMAIL_COM.equalsIgnoreCase(parts[1])) { | |
| 214 parts[1] = GMAIL_COM; | |
| 215 } | |
| 216 if (GMAIL_COM.equalsIgnoreCase(parts[1])) { | |
| 217 parts[0] = parts[0].replace(".", ""); | |
| 218 } | |
| 219 return (parts[0] + "@" + parts[1]).toLowerCase(Locale.US); | |
| 220 } | |
| 221 | |
| 222 /** | |
| 223 * This method is deprecated; please use the asynchronous version below inst
ead. | |
| 224 * | |
| 225 * See http://crbug.com/517697 for details. | |
| 226 */ | |
| 227 public Account getAccountFromName(String accountName) { | |
| 228 String canonicalName = canonicalizeName(accountName); | |
| 229 Account[] accounts = getGoogleAccounts(); | |
| 230 for (Account account : accounts) { | |
| 231 if (canonicalizeName(account.name).equals(canonicalName)) { | |
| 232 return account; | |
| 233 } | |
| 234 } | |
| 235 return null; | |
| 236 } | |
| 237 | |
| 238 /** | |
| 239 * Asynchronously returns the account if it exists; null otherwise. | |
| 240 */ | |
| 241 public void getAccountFromName(String accountName, final Callback<Account> c
allback) { | |
| 242 final String canonicalName = canonicalizeName(accountName); | |
| 243 getGoogleAccounts(new Callback<Account[]>() { | |
| 244 @Override | |
| 245 public void onResult(Account[] accounts) { | |
| 246 Account accountForName = null; | |
| 247 for (Account account : accounts) { | |
| 248 if (canonicalizeName(account.name).equals(canonicalName)) { | |
| 249 accountForName = account; | |
| 250 break; | |
| 251 } | |
| 252 } | |
| 253 callback.onResult(accountForName); | |
| 254 } | |
| 255 }); | |
| 256 } | |
| 257 | |
| 258 /** | |
| 259 * This method is deprecated; please use the asynchronous version below inst
ead. | |
| 260 * | |
| 261 * See http://crbug.com/517697 for details. | |
| 262 */ | |
| 263 public boolean hasAccountForName(String accountName) { | |
| 264 return getAccountFromName(accountName) != null; | |
| 265 } | |
| 266 | |
| 267 /** | |
| 268 * Asynchronously returns whether an account exists with the given name. | |
| 269 */ | |
| 270 // TODO(maxbogue): Remove once this function is used outside of tests. | |
| 271 @VisibleForTesting | |
| 272 public void hasAccountForName(String accountName, final Callback<Boolean> ca
llback) { | |
| 273 getAccountFromName(accountName, new Callback<Account>() { | |
| 274 @Override | |
| 275 public void onResult(Account account) { | |
| 276 callback.onResult(account != null); | |
| 277 } | |
| 278 }); | |
| 279 } | |
| 280 | |
| 281 /** | |
| 282 * @return Whether or not there is an account authenticator for Google accou
nts. | |
| 283 */ | |
| 284 public boolean hasGoogleAccountAuthenticator() { | |
| 285 AuthenticatorDescription[] descs = mAccountManager.getAuthenticatorTypes
(); | |
| 286 for (AuthenticatorDescription desc : descs) { | |
| 287 if (GOOGLE_ACCOUNT_TYPE.equals(desc.type)) return true; | |
| 288 } | |
| 289 return false; | |
| 290 } | |
| 291 | |
| 292 /** | |
| 293 * Gets the auth token and returns the response asynchronously. | |
| 294 * This should be called when we have a foreground activity that needs an au
th token. | |
| 295 * If encountered an IO error, it will attempt to retry when the network is
back. | |
| 296 * | |
| 297 * - Assumes that the account is a valid account. | |
| 298 */ | |
| 299 public void getAuthToken(final Account account, final String authTokenType, | |
| 300 final GetAuthTokenCallback callback) { | |
| 301 ConnectionRetry.runAuthTask(new AuthTask<String>() { | |
| 302 @Override | |
| 303 public String run() throws AuthException { | |
| 304 return mAccountManager.getAuthToken(account, authTokenType); | |
| 305 } | |
| 306 @Override | |
| 307 public void onSuccess(String token) { | |
| 308 callback.tokenAvailable(token); | |
| 309 } | |
| 310 @Override | |
| 311 public void onFailure(boolean isTransientError) { | |
| 312 callback.tokenUnavailable(isTransientError); | |
| 313 } | |
| 314 }); | |
| 315 } | |
| 316 | |
| 317 public boolean hasGetAccountsPermission() { | |
| 318 return mApplicationContext.checkPermission(Manifest.permission.GET_ACCOU
NTS, | |
| 319 Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_G
RANTED; | |
| 320 } | |
| 321 | |
| 322 /** | |
| 323 * Invalidates the old token (if non-null/non-empty) and asynchronously gene
rates a new one. | |
| 324 * | |
| 325 * - Assumes that the account is a valid account. | |
| 326 */ | |
| 327 public void getNewAuthToken(Account account, String authToken, String authTo
kenType, | |
| 328 GetAuthTokenCallback callback) { | |
| 329 invalidateAuthToken(authToken); | |
| 330 getAuthToken(account, authTokenType, callback); | |
| 331 } | |
| 332 | |
| 333 /** | |
| 334 * Clear an auth token from the local cache with respect to the ApplicationC
ontext. | |
| 335 */ | |
| 336 public void invalidateAuthToken(final String authToken) { | |
| 337 if (authToken == null || authToken.isEmpty()) { | |
| 338 return; | |
| 339 } | |
| 340 ConnectionRetry.runAuthTask(new AuthTask<Boolean>() { | |
| 341 @Override | |
| 342 public Boolean run() throws AuthException { | |
| 343 mAccountManager.invalidateAuthToken(authToken); | |
| 344 return true; | |
| 345 } | |
| 346 @Override | |
| 347 public void onSuccess(Boolean result) {} | |
| 348 @Override | |
| 349 public void onFailure(boolean isTransientError) { | |
| 350 Log.e(TAG, "Failed to invalidate auth token: " + authToken); | |
| 351 } | |
| 352 }); | |
| 353 } | |
| 354 | |
| 355 public void checkChildAccount(Account account, Callback<Boolean> callback) { | |
| 356 String[] features = {FEATURE_IS_CHILD_ACCOUNT_KEY}; | |
| 357 mAccountManager.hasFeatures(account, features, callback); | |
| 358 } | |
| 359 | |
| 360 private interface AuthTask<T> { | |
| 361 T run() throws AuthException; | |
| 362 void onSuccess(T result); | |
| 363 void onFailure(boolean isTransientError); | |
| 364 } | |
| 365 | |
| 366 /** | |
| 367 * A helper class to encapsulate network connection retry logic for AuthTask
s. | |
| 368 * | |
| 369 * The task will be run on the background thread. If it encounters a transie
nt error, it will | |
| 370 * wait for a network change and retry up to MAX_TRIES times. | |
| 371 */ | |
| 372 private static class ConnectionRetry<T> | |
| 373 implements NetworkChangeNotifier.ConnectionTypeObserver { | |
| 374 private static final int MAX_TRIES = 3; | |
| 375 | |
| 376 private final AuthTask<T> mAuthTask; | |
| 377 private final AtomicInteger mNumTries; | |
| 378 private final AtomicBoolean mIsTransientError; | |
| 379 | |
| 380 public static <T> void runAuthTask(AuthTask<T> authTask) { | |
| 381 new ConnectionRetry<T>(authTask).attempt(); | |
| 382 } | |
| 383 | |
| 384 private ConnectionRetry(AuthTask<T> authTask) { | |
| 385 mAuthTask = authTask; | |
| 386 mNumTries = new AtomicInteger(0); | |
| 387 mIsTransientError = new AtomicBoolean(false); | |
| 388 } | |
| 389 | |
| 390 /** | |
| 391 * Tries running the {@link AuthTask} in the background. This object is
never registered | |
| 392 * as a {@link ConnectionTypeObserver} when this method is called. | |
| 393 */ | |
| 394 private void attempt() { | |
| 395 // Clear any transient error. | |
| 396 mIsTransientError.set(false); | |
| 397 new AsyncTask<Void, Void, T>() { | |
| 398 @Override | |
| 399 public T doInBackground(Void... params) { | |
| 400 try { | |
| 401 return mAuthTask.run(); | |
| 402 } catch (AuthException ex) { | |
| 403 Log.w(TAG, "Failed to perform auth task", ex); | |
| 404 mIsTransientError.set(ex.isTransientError()); | |
| 405 } | |
| 406 return null; | |
| 407 } | |
| 408 @Override | |
| 409 public void onPostExecute(T result) { | |
| 410 if (result != null) { | |
| 411 mAuthTask.onSuccess(result); | |
| 412 } else if (!mIsTransientError.get() | |
| 413 || mNumTries.incrementAndGet() >= MAX_TRIES | |
| 414 || !NetworkChangeNotifier.isInitialized()) { | |
| 415 // Permanent error, ran out of tries, or we can't listen
for network | |
| 416 // change events; give up. | |
| 417 mAuthTask.onFailure(mIsTransientError.get()); | |
| 418 } else { | |
| 419 // Transient error with tries left; register for another
attempt. | |
| 420 NetworkChangeNotifier.addConnectionTypeObserver(Connecti
onRetry.this); | |
| 421 } | |
| 422 } | |
| 423 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); | |
| 424 } | |
| 425 | |
| 426 @Override | |
| 427 public void onConnectionTypeChanged(int connectionType) { | |
| 428 assert mNumTries.get() < MAX_TRIES; | |
| 429 if (NetworkChangeNotifier.isOnline()) { | |
| 430 // The network is back; stop listening and try again. | |
| 431 NetworkChangeNotifier.removeConnectionTypeObserver(this); | |
| 432 attempt(); | |
| 433 } | |
| 434 } | |
| 435 } | |
| 436 } | |
| OLD | NEW |