// Copyright 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.sync.signin; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorDescription; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import org.chromium.net.NetworkChangeNotifier; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; /** * AccountManagerHelper wraps our access of AccountManager in Android. * * Use the AccountManagerHelper.get(someContext) to instantiate it */ public class AccountManagerHelper { private static final String TAG = "AccountManagerHelper"; public static final String GOOGLE_ACCOUNT_TYPE = "com.google"; private static final Object sLock = new Object(); private static final int MAX_TRIES = 3; private static AccountManagerHelper sAccountManagerHelper; private final AccountManagerDelegate mAccountManager; private Context mApplicationContext; public interface GetAuthTokenCallback { /** * Invoked on the UI thread once a token has been provided by the AccountManager. * @param token Auth token, or null if no token is available (bad credentials, * permission denied, etc). */ void tokenAvailable(String token); } /** * @param context the Android context * @param accountManager the account manager to use as a backend service */ private AccountManagerHelper(Context context, AccountManagerDelegate accountManager) { mApplicationContext = context.getApplicationContext(); mAccountManager = accountManager; } /** * A factory method for the AccountManagerHelper. * * It is possible to override the AccountManager to use in tests for the instance of the * AccountManagerHelper by calling overrideAccountManagerHelperForTests(...) with * your MockAccountManager. * * @param context the applicationContext is retrieved from the context used as an argument. * @return a singleton instance of the AccountManagerHelper */ public static AccountManagerHelper get(Context context) { synchronized (sLock) { if (sAccountManagerHelper == null) { sAccountManagerHelper = new AccountManagerHelper(context, new SystemAccountManagerDelegate(context)); } } return sAccountManagerHelper; } @VisibleForTesting public static void overrideAccountManagerHelperForTests(Context context, AccountManagerDelegate accountManager) { synchronized (sLock) { sAccountManagerHelper = new AccountManagerHelper(context, accountManager); } } /** * Creates an Account object for the given name. */ public static Account createAccountFromName(String name) { return new Account(name, GOOGLE_ACCOUNT_TYPE); } public List getGoogleAccountNames() { List accountNames = new ArrayList(); Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); for (Account account : accounts) { accountNames.add(account.name); } return accountNames; } public Account[] getGoogleAccounts() { return mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); } public boolean hasGoogleAccounts() { return getGoogleAccounts().length > 0; } /** * Returns the account if it exists, null otherwise. */ public Account getAccountFromName(String accountName) { Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); for (Account account : accounts) { if (account.name.equals(accountName)) { return account; } } return null; } /** * Returns whether the accounts exists. */ public boolean hasAccountForName(String accountName) { return getAccountFromName(accountName) != null; } /** * @return Whether or not there is an account authenticator for Google accounts. */ public boolean hasGoogleAccountAuthenticator() { AuthenticatorDescription[] descs = mAccountManager.getAuthenticatorTypes(); for (AuthenticatorDescription desc : descs) { if (GOOGLE_ACCOUNT_TYPE.equals(desc.type)) return true; } return false; } /** * Gets the auth token synchronously. * * - Assumes that the account is a valid account. * - Should not be called on the main thread. */ @Deprecated public String getAuthTokenFromBackground(Account account, String authTokenType) { AccountManagerFuture future = mAccountManager.getAuthToken(account, authTokenType, false, null, null); AtomicBoolean errorEncountered = new AtomicBoolean(false); return getAuthTokenInner(future, errorEncountered); } /** * Gets the auth token and returns the response asynchronously. * This should be called when we have a foreground activity that needs an auth token. * If encountered an IO error, it will attempt to retry when the network is back. * * - Assumes that the account is a valid account. */ public void getAuthTokenFromForeground(Activity activity, Account account, String authTokenType, GetAuthTokenCallback callback) { AtomicInteger numTries = new AtomicInteger(0); AtomicBoolean errorEncountered = new AtomicBoolean(false); getAuthTokenAsynchronously(activity, account, authTokenType, callback, numTries, errorEncountered, null); } private class ConnectionRetry implements NetworkChangeNotifier.ConnectionTypeObserver { private final Account mAccount; private final String mAuthTokenType; private final GetAuthTokenCallback mCallback; private final AtomicInteger mNumTries; private final AtomicBoolean mErrorEncountered; ConnectionRetry(Account account, String authTokenType, GetAuthTokenCallback callback, AtomicInteger numTries, AtomicBoolean errorEncountered) { mAccount = account; mAuthTokenType = authTokenType; mCallback = callback; mNumTries = numTries; mErrorEncountered = errorEncountered; } @Override public void onConnectionTypeChanged(int connectionType) { assert mNumTries.get() <= MAX_TRIES; if (mNumTries.get() == MAX_TRIES) { NetworkChangeNotifier.removeConnectionTypeObserver(this); return; } if (NetworkChangeNotifier.isOnline()) { NetworkChangeNotifier.removeConnectionTypeObserver(this); getAuthTokenAsynchronously(null, mAccount, mAuthTokenType, mCallback, mNumTries, mErrorEncountered, this); } } } // Gets the auth token synchronously private String getAuthTokenInner(AccountManagerFuture future, AtomicBoolean errorEncountered) { try { Bundle result = future.getResult(); if (result != null) { if (result.containsKey(AccountManager.KEY_INTENT)) { Log.d(TAG, "Starting intent to get auth credentials"); // Need to start intent to get credentials Intent intent = result.getParcelable(AccountManager.KEY_INTENT); int flags = intent.getFlags(); flags |= Intent.FLAG_ACTIVITY_NEW_TASK; intent.setFlags(flags); mApplicationContext.startActivity(intent); return null; } return result.getString(AccountManager.KEY_AUTHTOKEN); } else { Log.w(TAG, "Auth token - getAuthToken returned null"); } } catch (OperationCanceledException e) { Log.w(TAG, "Auth token - operation cancelled", e); } catch (AuthenticatorException e) { Log.w(TAG, "Auth token - authenticator exception", e); } catch (IOException e) { Log.w(TAG, "Auth token - IO exception", e); errorEncountered.set(true); } return null; } private void getAuthTokenAsynchronously(@Nullable Activity activity, final Account account, final String authTokenType, final GetAuthTokenCallback callback, final AtomicInteger numTries, final AtomicBoolean errorEncountered, final ConnectionRetry retry) { AccountManagerFuture future; if (numTries.get() == 0 && activity != null) { future = mAccountManager.getAuthToken( account, authTokenType, null, activity, null, null); } else { future = mAccountManager.getAuthToken( account, authTokenType, false, null, null); } final AccountManagerFuture finalFuture = future; errorEncountered.set(false); // On ICS onPostExecute is never called when running an AsyncTask from a different thread // than the UI thread. if (ThreadUtils.runningOnUiThread()) { new AsyncTask() { @Override public String doInBackground(Void... params) { return getAuthTokenInner(finalFuture, errorEncountered); } @Override public void onPostExecute(String authToken) { onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries, errorEncountered, retry); } }.execute(); } else { String authToken = getAuthTokenInner(finalFuture, errorEncountered); onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries, errorEncountered, retry); } } private void onGotAuthTokenResult(Account account, String authTokenType, String authToken, GetAuthTokenCallback callback, AtomicInteger numTries, AtomicBoolean errorEncountered, ConnectionRetry retry) { if (authToken != null || !errorEncountered.get() || numTries.incrementAndGet() == MAX_TRIES || !NetworkChangeNotifier.isInitialized()) { callback.tokenAvailable(authToken); return; } if (retry == null) { ConnectionRetry newRetry = new ConnectionRetry(account, authTokenType, callback, numTries, errorEncountered); NetworkChangeNotifier.addConnectionTypeObserver(newRetry); } else { NetworkChangeNotifier.addConnectionTypeObserver(retry); } } /** * Invalidates the old token (if non-null/non-empty) and synchronously generates a new one. * Also notifies the user (via status bar) if any user action is required. The method will * return null if any user action is required to generate the new token. * * - Assumes that the account is a valid account. * - Should not be called on the main thread. */ @Deprecated public String getNewAuthToken(Account account, String authToken, String authTokenType) { // TODO(dsmyers): consider reimplementing using an AccountManager function with an // explicit timeout. // Bug: https://code.google.com/p/chromium/issues/detail?id=172394. if (authToken != null && !authToken.isEmpty()) { mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken); } try { return mAccountManager.blockingGetAuthToken(account, authTokenType, true); } catch (OperationCanceledException e) { Log.w(TAG, "Auth token - operation cancelled", e); } catch (AuthenticatorException e) { Log.w(TAG, "Auth token - authenticator exception", e); } catch (IOException e) { Log.w(TAG, "Auth token - IO exception", e); } return null; } /** * Invalidates the old token (if non-null/non-empty) and asynchronously generates a new one. * * - Assumes that the account is a valid account. */ public void getNewAuthTokenFromForeground(Account account, String authToken, String authTokenType, GetAuthTokenCallback callback) { if (authToken != null && !authToken.isEmpty()) { mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken); } AtomicInteger numTries = new AtomicInteger(0); AtomicBoolean errorEncountered = new AtomicBoolean(false); getAuthTokenAsynchronously( null, account, authTokenType, callback, numTries, errorEncountered, null); } /** * Removes an auth token from the AccountManager's cache. */ public void invalidateAuthToken(String authToken) { mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken); } }