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.
5 package org.chromium.sync.signin;
8 import com.google.common.annotations.VisibleForTesting;
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;
23 import org.chromium.base.ThreadUtils;
24 import org.chromium.net.NetworkChangeNotifier;
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;
34 * AccountManagerHelper wraps our access of AccountManager in Android.
36 * Use the AccountManagerHelper.get(someContext) to instantiate it
38 public class AccountManagerHelper {
40 private static final String TAG = "AccountManagerHelper";
42 public static final String GOOGLE_ACCOUNT_TYPE = "com.google";
44 private static final Object lock = new Object();
46 private static final int MAX_TRIES = 3;
48 private static AccountManagerHelper sAccountManagerHelper;
50 private final AccountManagerDelegate mAccountManager;
52 private Context mApplicationContext;
54 public interface GetAuthTokenCallback {
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).
60 void tokenAvailable(String token);
64 * @param context the Android context
65 * @param accountManager the account manager to use as a backend service
67 private AccountManagerHelper(Context context,
68 AccountManagerDelegate accountManager) {
69 mApplicationContext = context.getApplicationContext();
70 mAccountManager = accountManager;
74 * A factory method for the AccountManagerHelper.
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.
80 * @param context the applicationContext is retrieved from the context used as an argument.
81 * @return a singleton instance of the AccountManagerHelper
83 public static AccountManagerHelper get(Context context) {
85 if (sAccountManagerHelper == null) {
86 sAccountManagerHelper = new AccountManagerHelper(context,
87 new SystemAccountManagerDelegate(context));
90 return sAccountManagerHelper;
94 public static void overrideAccountManagerHelperForTests(Context context,
95 AccountManagerDelegate accountManager) {
97 sAccountManagerHelper = new AccountManagerHelper(context, accountManager);
102 * Creates an Account object for the given name.
104 public static Account createAccountFromName(String name) {
105 return new Account(name, GOOGLE_ACCOUNT_TYPE);
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);
117 public Account[] getGoogleAccounts() {
118 return mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE);
121 public boolean hasGoogleAccounts() {
122 return getGoogleAccounts().length > 0;
126 * Returns the account if it exists, null otherwise.
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)) {
139 * Returns whether the accounts exists.
141 public boolean hasAccountForName(String accountName) {
142 return getAccountFromName(accountName) != null;
146 * @return Whether or not there is an account authenticator for Google accounts.
148 public boolean hasGoogleAccountAuthenticator() {
149 AuthenticatorDescription[] descs = mAccountManager.getAuthenticatorTypes();
150 for (AuthenticatorDescription desc : descs) {
151 if (GOOGLE_ACCOUNT_TYPE.equals(desc.type)) return true;
157 * Gets the auth token synchronously.
159 * - Assumes that the account is a valid account.
160 * - Should not be called on the main thread.
163 public String getAuthTokenFromBackground(Account account, String authTokenType) {
164 AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(account,
165 authTokenType, false, null, null);
166 AtomicBoolean errorEncountered = new AtomicBoolean(false);
167 return getAuthTokenInner(future, errorEncountered);
171 * Gets the auth token and returns the response asynchronously.
172 * This should be called when we have a foreground activity that needs an auth token.
173 * If encountered an IO error, it will attempt to retry when the network is back.
175 * - Assumes that the account is a valid account.
177 public void getAuthTokenFromForeground(Activity activity, Account account, String authTokenType,
178 GetAuthTokenCallback callback) {
179 AtomicInteger numTries = new AtomicInteger(0);
180 AtomicBoolean errorEncountered = new AtomicBoolean(false);
181 getAuthTokenAsynchronously(activity, account, authTokenType, callback, numTries,
182 errorEncountered, null);
185 private class ConnectionRetry implements NetworkChangeNotifier.ConnectionTypeObserver {
186 private final Account mAccount;
187 private final String mAuthTokenType;
188 private final GetAuthTokenCallback mCallback;
189 private final AtomicInteger mNumTries;
190 private final AtomicBoolean mErrorEncountered;
192 ConnectionRetry(Account account, String authTokenType, GetAuthTokenCallback callback,
193 AtomicInteger numTries, AtomicBoolean errorEncountered) {
195 mAuthTokenType = authTokenType;
196 mCallback = callback;
197 mNumTries = numTries;
198 mErrorEncountered = errorEncountered;
202 public void onConnectionTypeChanged(int connectionType) {
203 assert mNumTries.get() <= MAX_TRIES;
204 if (mNumTries.get() == MAX_TRIES) {
205 NetworkChangeNotifier.removeConnectionTypeObserver(this);
208 if (NetworkChangeNotifier.isOnline()) {
209 NetworkChangeNotifier.removeConnectionTypeObserver(this);
210 getAuthTokenAsynchronously(null, mAccount, mAuthTokenType, mCallback, mNumTries,
211 mErrorEncountered, this);
216 // Gets the auth token synchronously
217 private String getAuthTokenInner(AccountManagerFuture<Bundle> future,
218 AtomicBoolean errorEncountered) {
220 Bundle result = future.getResult();
221 if (result != null) {
222 if (result.containsKey(AccountManager.KEY_INTENT)) {
223 Log.d(TAG, "Starting intent to get auth credentials");
224 // Need to start intent to get credentials
225 Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
226 int flags = intent.getFlags();
227 flags |= Intent.FLAG_ACTIVITY_NEW_TASK;
228 intent.setFlags(flags);
229 mApplicationContext.startActivity(intent);
232 return result.getString(AccountManager.KEY_AUTHTOKEN);
234 Log.w(TAG, "Auth token - getAuthToken returned null");
236 } catch (OperationCanceledException e) {
237 Log.w(TAG, "Auth token - operation cancelled", e);
238 } catch (AuthenticatorException e) {
239 Log.w(TAG, "Auth token - authenticator exception", e);
240 } catch (IOException e) {
241 Log.w(TAG, "Auth token - IO exception", e);
242 errorEncountered.set(true);
247 private void getAuthTokenAsynchronously(@Nullable Activity activity, final Account account,
248 final String authTokenType, final GetAuthTokenCallback callback,
249 final AtomicInteger numTries, final AtomicBoolean errorEncountered,
250 final ConnectionRetry retry) {
251 AccountManagerFuture<Bundle> future;
252 if (numTries.get() == 0 && activity != null) {
253 future = mAccountManager.getAuthToken(
254 account, authTokenType, null, activity, null, null);
256 future = mAccountManager.getAuthToken(
257 account, authTokenType, false, null, null);
259 final AccountManagerFuture<Bundle> finalFuture = future;
260 errorEncountered.set(false);
262 // On ICS onPostExecute is never called when running an AsyncTask from a different thread
263 // than the UI thread.
264 if (ThreadUtils.runningOnUiThread()) {
265 new AsyncTask<Void, Void, String>() {
267 public String doInBackground(Void... params) {
268 return getAuthTokenInner(finalFuture, errorEncountered);
271 public void onPostExecute(String authToken) {
272 onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries,
273 errorEncountered, retry);
277 String authToken = getAuthTokenInner(finalFuture, errorEncountered);
278 onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries,
279 errorEncountered, retry);
283 private void onGotAuthTokenResult(Account account, String authTokenType, String authToken,
284 GetAuthTokenCallback callback, AtomicInteger numTries, AtomicBoolean errorEncountered,
285 ConnectionRetry retry) {
286 if (authToken != null || !errorEncountered.get() ||
287 numTries.incrementAndGet() == MAX_TRIES ||
288 !NetworkChangeNotifier.isInitialized()) {
289 callback.tokenAvailable(authToken);
293 ConnectionRetry newRetry = new ConnectionRetry(account, authTokenType, callback,
294 numTries, errorEncountered);
295 NetworkChangeNotifier.addConnectionTypeObserver(newRetry);
297 NetworkChangeNotifier.addConnectionTypeObserver(retry);
302 * Invalidates the old token (if non-null/non-empty) and synchronously generates a new one.
303 * Also notifies the user (via status bar) if any user action is required. The method will
304 * return null if any user action is required to generate the new token.
306 * - Assumes that the account is a valid account.
307 * - Should not be called on the main thread.
310 public String getNewAuthToken(Account account, String authToken, String authTokenType) {
311 // TODO(dsmyers): consider reimplementing using an AccountManager function with an
313 // Bug: https://code.google.com/p/chromium/issues/detail?id=172394.
314 if (authToken != null && !authToken.isEmpty()) {
315 mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken);
319 return mAccountManager.blockingGetAuthToken(account, authTokenType, true);
320 } catch (OperationCanceledException e) {
321 Log.w(TAG, "Auth token - operation cancelled", e);
322 } catch (AuthenticatorException e) {
323 Log.w(TAG, "Auth token - authenticator exception", e);
324 } catch (IOException e) {
325 Log.w(TAG, "Auth token - IO exception", e);
331 * Invalidates the old token (if non-null/non-empty) and asynchronously generates a new one.
333 * - Assumes that the account is a valid account.
335 public void getNewAuthTokenFromForeground(Account account, String authToken,
336 String authTokenType, GetAuthTokenCallback callback) {
337 if (authToken != null && !authToken.isEmpty()) {
338 mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken);
340 AtomicInteger numTries = new AtomicInteger(0);
341 AtomicBoolean errorEncountered = new AtomicBoolean(false);
342 getAuthTokenAsynchronously(
343 null, account, authTokenType, callback, numTries, errorEncountered, null);
347 * Removes an auth token from the AccountManager's cache.
349 public void invalidateAuthToken(String authToken) {
350 mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken);