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