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.accounts.Account; 9 import android.accounts.AccountManager; 10 import android.accounts.AccountManagerFuture; 11 import android.accounts.AuthenticatorDescription; 12 import android.accounts.AuthenticatorException; 13 import android.accounts.OperationCanceledException; 14 import android.app.Activity; 15 import android.content.Context; 16 import android.content.Intent; 17 import android.os.AsyncTask; 18 import android.os.Bundle; 19 import android.util.Log; 20 21 import com.google.common.annotations.VisibleForTesting; 22 23 import org.chromium.base.ThreadUtils; 24 import org.chromium.net.NetworkChangeNotifier; 25 26 import java.io.IOException; 27 import java.util.ArrayList; 28 import java.util.List; 29 import java.util.concurrent.atomic.AtomicBoolean; 30 import java.util.concurrent.atomic.AtomicInteger; 31 32 import javax.annotation.Nullable; 33 34 /** 35 * AccountManagerHelper wraps our access of AccountManager in Android. 36 * 37 * Use the AccountManagerHelper.get(someContext) to instantiate it 38 */ 39 public class AccountManagerHelper { 40 41 private static final String TAG = "AccountManagerHelper"; 42 43 public static final String GOOGLE_ACCOUNT_TYPE = "com.google"; 44 45 private static final Object sLock = new Object(); 46 47 private static final int MAX_TRIES = 3; 48 49 private static AccountManagerHelper sAccountManagerHelper; 50 51 private final AccountManagerDelegate mAccountManager; 52 53 private Context mApplicationContext; 54 55 public interface GetAuthTokenCallback { 56 /** 57 * Invoked on the UI thread once a token has been provided by the AccountManager. 58 * @param token Auth token, or null if no token is available (bad credentials, 59 * permission denied, etc). 60 */ tokenAvailable(String token)61 void tokenAvailable(String token); 62 } 63 64 /** 65 * @param context the Android context 66 * @param accountManager the account manager to use as a backend service 67 */ AccountManagerHelper(Context context, AccountManagerDelegate accountManager)68 private AccountManagerHelper(Context context, 69 AccountManagerDelegate accountManager) { 70 mApplicationContext = context.getApplicationContext(); 71 mAccountManager = accountManager; 72 } 73 74 /** 75 * A factory method for the AccountManagerHelper. 76 * 77 * It is possible to override the AccountManager to use in tests for the instance of the 78 * AccountManagerHelper by calling overrideAccountManagerHelperForTests(...) with 79 * your MockAccountManager. 80 * 81 * @param context the applicationContext is retrieved from the context used as an argument. 82 * @return a singleton instance of the AccountManagerHelper 83 */ get(Context context)84 public static AccountManagerHelper get(Context context) { 85 synchronized (sLock) { 86 if (sAccountManagerHelper == null) { 87 sAccountManagerHelper = new AccountManagerHelper(context, 88 new SystemAccountManagerDelegate(context)); 89 } 90 } 91 return sAccountManagerHelper; 92 } 93 94 @VisibleForTesting overrideAccountManagerHelperForTests(Context context, AccountManagerDelegate accountManager)95 public static void overrideAccountManagerHelperForTests(Context context, 96 AccountManagerDelegate accountManager) { 97 synchronized (sLock) { 98 sAccountManagerHelper = new AccountManagerHelper(context, accountManager); 99 } 100 } 101 102 /** 103 * Creates an Account object for the given name. 104 */ createAccountFromName(String name)105 public static Account createAccountFromName(String name) { 106 return new Account(name, GOOGLE_ACCOUNT_TYPE); 107 } 108 getGoogleAccountNames()109 public List<String> getGoogleAccountNames() { 110 List<String> accountNames = new ArrayList<String>(); 111 Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); 112 for (Account account : accounts) { 113 accountNames.add(account.name); 114 } 115 return accountNames; 116 } 117 getGoogleAccounts()118 public Account[] getGoogleAccounts() { 119 return mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); 120 } 121 hasGoogleAccounts()122 public boolean hasGoogleAccounts() { 123 return getGoogleAccounts().length > 0; 124 } 125 126 /** 127 * Returns the account if it exists, null otherwise. 128 */ getAccountFromName(String accountName)129 public Account getAccountFromName(String accountName) { 130 Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); 131 for (Account account : accounts) { 132 if (account.name.equals(accountName)) { 133 return account; 134 } 135 } 136 return null; 137 } 138 139 /** 140 * Returns whether the accounts exists. 141 */ hasAccountForName(String accountName)142 public boolean hasAccountForName(String accountName) { 143 return getAccountFromName(accountName) != null; 144 } 145 146 /** 147 * @return Whether or not there is an account authenticator for Google accounts. 148 */ hasGoogleAccountAuthenticator()149 public boolean hasGoogleAccountAuthenticator() { 150 AuthenticatorDescription[] descs = mAccountManager.getAuthenticatorTypes(); 151 for (AuthenticatorDescription desc : descs) { 152 if (GOOGLE_ACCOUNT_TYPE.equals(desc.type)) return true; 153 } 154 return false; 155 } 156 157 /** 158 * Gets the auth token synchronously. 159 * 160 * - Assumes that the account is a valid account. 161 * - Should not be called on the main thread. 162 */ 163 @Deprecated getAuthTokenFromBackground(Account account, String authTokenType)164 public String getAuthTokenFromBackground(Account account, String authTokenType) { 165 AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(account, 166 authTokenType, false, null, null); 167 AtomicBoolean errorEncountered = new AtomicBoolean(false); 168 return getAuthTokenInner(future, errorEncountered); 169 } 170 171 /** 172 * Gets the auth token and returns the response asynchronously. 173 * This should be called when we have a foreground activity that needs an auth token. 174 * If encountered an IO error, it will attempt to retry when the network is back. 175 * 176 * - Assumes that the account is a valid account. 177 */ getAuthTokenFromForeground(Activity activity, Account account, String authTokenType, GetAuthTokenCallback callback)178 public void getAuthTokenFromForeground(Activity activity, Account account, String authTokenType, 179 GetAuthTokenCallback callback) { 180 AtomicInteger numTries = new AtomicInteger(0); 181 AtomicBoolean errorEncountered = new AtomicBoolean(false); 182 getAuthTokenAsynchronously(activity, account, authTokenType, callback, numTries, 183 errorEncountered, null); 184 } 185 186 private class ConnectionRetry implements NetworkChangeNotifier.ConnectionTypeObserver { 187 private final Account mAccount; 188 private final String mAuthTokenType; 189 private final GetAuthTokenCallback mCallback; 190 private final AtomicInteger mNumTries; 191 private final AtomicBoolean mErrorEncountered; 192 ConnectionRetry(Account account, String authTokenType, GetAuthTokenCallback callback, AtomicInteger numTries, AtomicBoolean errorEncountered)193 ConnectionRetry(Account account, String authTokenType, GetAuthTokenCallback callback, 194 AtomicInteger numTries, AtomicBoolean errorEncountered) { 195 mAccount = account; 196 mAuthTokenType = authTokenType; 197 mCallback = callback; 198 mNumTries = numTries; 199 mErrorEncountered = errorEncountered; 200 } 201 202 @Override onConnectionTypeChanged(int connectionType)203 public void onConnectionTypeChanged(int connectionType) { 204 assert mNumTries.get() <= MAX_TRIES; 205 if (mNumTries.get() == MAX_TRIES) { 206 NetworkChangeNotifier.removeConnectionTypeObserver(this); 207 return; 208 } 209 if (NetworkChangeNotifier.isOnline()) { 210 NetworkChangeNotifier.removeConnectionTypeObserver(this); 211 getAuthTokenAsynchronously(null, mAccount, mAuthTokenType, mCallback, mNumTries, 212 mErrorEncountered, this); 213 } 214 } 215 } 216 217 // Gets the auth token synchronously getAuthTokenInner(AccountManagerFuture<Bundle> future, AtomicBoolean errorEncountered)218 private String getAuthTokenInner(AccountManagerFuture<Bundle> future, 219 AtomicBoolean errorEncountered) { 220 try { 221 Bundle result = future.getResult(); 222 if (result != null) { 223 if (result.containsKey(AccountManager.KEY_INTENT)) { 224 Log.d(TAG, "Starting intent to get auth credentials"); 225 // Need to start intent to get credentials 226 Intent intent = result.getParcelable(AccountManager.KEY_INTENT); 227 int flags = intent.getFlags(); 228 flags |= Intent.FLAG_ACTIVITY_NEW_TASK; 229 intent.setFlags(flags); 230 mApplicationContext.startActivity(intent); 231 return null; 232 } 233 return result.getString(AccountManager.KEY_AUTHTOKEN); 234 } else { 235 Log.w(TAG, "Auth token - getAuthToken returned null"); 236 } 237 } catch (OperationCanceledException e) { 238 Log.w(TAG, "Auth token - operation cancelled", e); 239 } catch (AuthenticatorException e) { 240 Log.w(TAG, "Auth token - authenticator exception", e); 241 } catch (IOException e) { 242 Log.w(TAG, "Auth token - IO exception", e); 243 errorEncountered.set(true); 244 } 245 return null; 246 } 247 getAuthTokenAsynchronously(@ullable Activity activity, final Account account, final String authTokenType, final GetAuthTokenCallback callback, final AtomicInteger numTries, final AtomicBoolean errorEncountered, final ConnectionRetry retry)248 private void getAuthTokenAsynchronously(@Nullable Activity activity, final Account account, 249 final String authTokenType, final GetAuthTokenCallback callback, 250 final AtomicInteger numTries, final AtomicBoolean errorEncountered, 251 final ConnectionRetry retry) { 252 AccountManagerFuture<Bundle> future; 253 if (numTries.get() == 0 && activity != null) { 254 future = mAccountManager.getAuthToken( 255 account, authTokenType, null, activity, null, null); 256 } else { 257 future = mAccountManager.getAuthToken( 258 account, authTokenType, false, null, null); 259 } 260 final AccountManagerFuture<Bundle> finalFuture = future; 261 errorEncountered.set(false); 262 263 // On ICS onPostExecute is never called when running an AsyncTask from a different thread 264 // than the UI thread. 265 if (ThreadUtils.runningOnUiThread()) { 266 new AsyncTask<Void, Void, String>() { 267 @Override 268 public String doInBackground(Void... params) { 269 return getAuthTokenInner(finalFuture, errorEncountered); 270 } 271 @Override 272 public void onPostExecute(String authToken) { 273 onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries, 274 errorEncountered, retry); 275 } 276 }.execute(); 277 } else { 278 String authToken = getAuthTokenInner(finalFuture, errorEncountered); 279 onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries, 280 errorEncountered, retry); 281 } 282 } 283 onGotAuthTokenResult(Account account, String authTokenType, String authToken, GetAuthTokenCallback callback, AtomicInteger numTries, AtomicBoolean errorEncountered, ConnectionRetry retry)284 private void onGotAuthTokenResult(Account account, String authTokenType, String authToken, 285 GetAuthTokenCallback callback, AtomicInteger numTries, AtomicBoolean errorEncountered, 286 ConnectionRetry retry) { 287 if (authToken != null || !errorEncountered.get() || 288 numTries.incrementAndGet() == MAX_TRIES || 289 !NetworkChangeNotifier.isInitialized()) { 290 callback.tokenAvailable(authToken); 291 return; 292 } 293 if (retry == null) { 294 ConnectionRetry newRetry = new ConnectionRetry(account, authTokenType, callback, 295 numTries, errorEncountered); 296 NetworkChangeNotifier.addConnectionTypeObserver(newRetry); 297 } else { 298 NetworkChangeNotifier.addConnectionTypeObserver(retry); 299 } 300 } 301 302 /** 303 * Invalidates the old token (if non-null/non-empty) and synchronously generates a new one. 304 * Also notifies the user (via status bar) if any user action is required. The method will 305 * return null if any user action is required to generate the new token. 306 * 307 * - Assumes that the account is a valid account. 308 * - Should not be called on the main thread. 309 */ 310 @Deprecated getNewAuthToken(Account account, String authToken, String authTokenType)311 public String getNewAuthToken(Account account, String authToken, String authTokenType) { 312 // TODO(dsmyers): consider reimplementing using an AccountManager function with an 313 // explicit timeout. 314 // Bug: https://code.google.com/p/chromium/issues/detail?id=172394. 315 if (authToken != null && !authToken.isEmpty()) { 316 mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken); 317 } 318 319 try { 320 return mAccountManager.blockingGetAuthToken(account, authTokenType, true); 321 } catch (OperationCanceledException e) { 322 Log.w(TAG, "Auth token - operation cancelled", e); 323 } catch (AuthenticatorException e) { 324 Log.w(TAG, "Auth token - authenticator exception", e); 325 } catch (IOException e) { 326 Log.w(TAG, "Auth token - IO exception", e); 327 } 328 return null; 329 } 330 331 /** 332 * Invalidates the old token (if non-null/non-empty) and asynchronously generates a new one. 333 * 334 * - Assumes that the account is a valid account. 335 */ getNewAuthTokenFromForeground(Account account, String authToken, String authTokenType, GetAuthTokenCallback callback)336 public void getNewAuthTokenFromForeground(Account account, String authToken, 337 String authTokenType, GetAuthTokenCallback callback) { 338 if (authToken != null && !authToken.isEmpty()) { 339 mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken); 340 } 341 AtomicInteger numTries = new AtomicInteger(0); 342 AtomicBoolean errorEncountered = new AtomicBoolean(false); 343 getAuthTokenAsynchronously( 344 null, account, authTokenType, callback, numTries, errorEncountered, null); 345 } 346 347 /** 348 * Removes an auth token from the AccountManager's cache. 349 */ invalidateAuthToken(String authToken)350 public void invalidateAuthToken(String authToken) { 351 mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken); 352 } 353 } 354