root/sync/android/java/src/org/chromium/sync/signin/AccountManagerHelper.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. tokenAvailable
  2. get
  3. overrideAccountManagerHelperForTests
  4. createAccountFromName
  5. getGoogleAccountNames
  6. getGoogleAccounts
  7. hasGoogleAccounts
  8. getAccountFromName
  9. hasAccountForName
  10. hasGoogleAccountAuthenticator
  11. getAuthTokenFromBackground
  12. getAuthTokenFromForeground
  13. onConnectionTypeChanged
  14. getAuthTokenInner
  15. getAuthTokenAsynchronously
  16. onGotAuthTokenResult
  17. getNewAuthToken
  18. getNewAuthTokenFromForeground
  19. invalidateAuthToken

// 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 com.google.common.annotations.VisibleForTesting;

import org.chromium.base.ThreadUtils;
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<String> getGoogleAccountNames() {
        List<String> accountNames = new ArrayList<String>();
        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<Bundle> 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<Bundle> 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<Bundle> 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<Bundle> 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<Void, Void, String>() {
                @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);
    }
}

/* [<][>][^][v][top][bottom][index][help] */