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