1 // Copyright 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 android.accounts.Account;
9 import android.accounts.AccountManager;
10 import android.accounts.AccountManagerFuture;
11 import android.accounts.AuthenticatorDescription;
12 import android.accounts.AuthenticatorException;
13 import android.accounts.OperationCanceledException;
14 import android.app.Activity;
15 import android.content.Context;
16 import android.os.AsyncTask;
17 import android.os.Bundle;
18 import android.util.Log;
20 import org.chromium.base.ThreadUtils;
21 import org.chromium.base.VisibleForTesting;
22 import org.chromium.net.NetworkChangeNotifier;
24 import java.io.IOException;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.concurrent.atomic.AtomicBoolean;
29 import java.util.concurrent.atomic.AtomicInteger;
30 import java.util.regex.Pattern;
32 import javax.annotation.Nullable;
35 * AccountManagerHelper wraps our access of AccountManager in Android.
37 * Use the AccountManagerHelper.get(someContext) to instantiate it
39 public class AccountManagerHelper {
41 private static final String TAG = "AccountManagerHelper";
43 private static final Pattern AT_SYMBOL = Pattern.compile("@");
45 private static final String GMAIL_COM = "gmail.com";
47 private static final String GOOGLEMAIL_COM = "googlemail.com";
49 public static final String GOOGLE_ACCOUNT_TYPE = "com.google";
51 private static final Object sLock = new Object();
53 private static final int MAX_TRIES = 3;
55 private static AccountManagerHelper sAccountManagerHelper;
57 private final AccountManagerDelegate mAccountManager;
59 private Context mApplicationContext;
62 * A simple callback for getAuthToken.
64 public interface GetAuthTokenCallback {
66 * Invoked on the UI thread once a token has been provided by the AccountManager.
67 * @param token Auth token, or null if no token is available (bad credentials,
68 * permission denied, etc).
70 void tokenAvailable(String token);
74 * @param context the Android context
75 * @param accountManager the account manager to use as a backend service
77 private AccountManagerHelper(Context context,
78 AccountManagerDelegate accountManager) {
79 mApplicationContext = context.getApplicationContext();
80 mAccountManager = accountManager;
84 * A factory method for the AccountManagerHelper.
86 * It is possible to override the AccountManager to use in tests for the instance of the
87 * AccountManagerHelper by calling overrideAccountManagerHelperForTests(...) with
88 * your MockAccountManager.
90 * @param context the applicationContext is retrieved from the context used as an argument.
91 * @return a singleton instance of the AccountManagerHelper
93 public static AccountManagerHelper get(Context context) {
94 synchronized (sLock) {
95 if (sAccountManagerHelper == null) {
96 sAccountManagerHelper = new AccountManagerHelper(context,
97 new SystemAccountManagerDelegate(context));
100 return sAccountManagerHelper;
104 public static void overrideAccountManagerHelperForTests(Context context,
105 AccountManagerDelegate accountManager) {
106 synchronized (sLock) {
107 sAccountManagerHelper = new AccountManagerHelper(context, accountManager);
112 * Creates an Account object for the given name.
114 public static Account createAccountFromName(String name) {
115 return new Account(name, GOOGLE_ACCOUNT_TYPE);
118 public List<String> getGoogleAccountNames() {
119 List<String> accountNames = new ArrayList<String>();
120 for (Account account : getGoogleAccounts()) {
121 accountNames.add(account.name);
126 public Account[] getGoogleAccounts() {
127 return mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE);
130 public boolean hasGoogleAccounts() {
131 return getGoogleAccounts().length > 0;
134 private String canonicalizeName(String name) {
135 String[] parts = AT_SYMBOL.split(name);
136 if (parts.length != 2) return name;
138 if (GOOGLEMAIL_COM.equalsIgnoreCase(parts[1])) {
139 parts[1] = GMAIL_COM;
141 if (GMAIL_COM.equalsIgnoreCase(parts[1])) {
142 parts[0] = parts[0].replace(".", "");
144 return (parts[0] + "@" + parts[1]).toLowerCase(Locale.US);
148 * Returns the account if it exists, null otherwise.
150 public Account getAccountFromName(String accountName) {
151 String canonicalName = canonicalizeName(accountName);
152 Account[] accounts = getGoogleAccounts();
153 for (Account account : accounts) {
154 if (canonicalizeName(account.name).equals(canonicalName)) {
162 * Returns whether the accounts exists.
164 public boolean hasAccountForName(String accountName) {
165 return getAccountFromName(accountName) != null;
169 * @return Whether or not there is an account authenticator for Google accounts.
171 public boolean hasGoogleAccountAuthenticator() {
172 AuthenticatorDescription[] descs = mAccountManager.getAuthenticatorTypes();
173 for (AuthenticatorDescription desc : descs) {
174 if (GOOGLE_ACCOUNT_TYPE.equals(desc.type)) return true;
180 * Gets the auth token synchronously.
182 * - Assumes that the account is a valid account.
183 * - Should not be called on the main thread.
186 public String getAuthTokenFromBackground(Account account, String authTokenType) {
187 AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(
188 account, authTokenType, true, null, null);
189 AtomicBoolean errorEncountered = new AtomicBoolean(false);
190 return getAuthTokenInner(future, errorEncountered);
194 * Gets the auth token and returns the response asynchronously.
195 * This should be called when we have a foreground activity that needs an auth token.
196 * If encountered an IO error, it will attempt to retry when the network is back.
198 * - Assumes that the account is a valid account.
200 public void getAuthTokenFromForeground(Activity activity, Account account, String authTokenType,
201 GetAuthTokenCallback callback) {
202 AtomicInteger numTries = new AtomicInteger(0);
203 AtomicBoolean errorEncountered = new AtomicBoolean(false);
204 getAuthTokenAsynchronously(activity, account, authTokenType, callback, numTries,
205 errorEncountered, null);
208 private class ConnectionRetry implements NetworkChangeNotifier.ConnectionTypeObserver {
209 private final Account mAccount;
210 private final String mAuthTokenType;
211 private final GetAuthTokenCallback mCallback;
212 private final AtomicInteger mNumTries;
213 private final AtomicBoolean mErrorEncountered;
215 ConnectionRetry(Account account, String authTokenType, GetAuthTokenCallback callback,
216 AtomicInteger numTries, AtomicBoolean errorEncountered) {
218 mAuthTokenType = authTokenType;
219 mCallback = callback;
220 mNumTries = numTries;
221 mErrorEncountered = errorEncountered;
225 public void onConnectionTypeChanged(int connectionType) {
226 assert mNumTries.get() <= MAX_TRIES;
227 if (mNumTries.get() == MAX_TRIES) {
228 NetworkChangeNotifier.removeConnectionTypeObserver(this);
231 if (NetworkChangeNotifier.isOnline()) {
232 NetworkChangeNotifier.removeConnectionTypeObserver(this);
233 getAuthTokenAsynchronously(null, mAccount, mAuthTokenType, mCallback, mNumTries,
234 mErrorEncountered, this);
239 // Gets the auth token synchronously
240 private String getAuthTokenInner(AccountManagerFuture<Bundle> future,
241 AtomicBoolean errorEncountered) {
243 Bundle result = future.getResult();
244 if (result != null) {
245 return result.getString(AccountManager.KEY_AUTHTOKEN);
247 Log.w(TAG, "Auth token - getAuthToken returned null");
249 } catch (OperationCanceledException e) {
250 Log.w(TAG, "Auth token - operation cancelled", e);
251 } catch (AuthenticatorException e) {
252 Log.w(TAG, "Auth token - authenticator exception", e);
253 } catch (IOException e) {
254 Log.w(TAG, "Auth token - IO exception", e);
255 errorEncountered.set(true);
260 private void getAuthTokenAsynchronously(@Nullable Activity activity, final Account account,
261 final String authTokenType, final GetAuthTokenCallback callback,
262 final AtomicInteger numTries, final AtomicBoolean errorEncountered,
263 final ConnectionRetry retry) {
264 final AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(
265 account, authTokenType, true, null, null);
266 errorEncountered.set(false);
268 // On ICS onPostExecute is never called when running an AsyncTask from a different thread
269 // than the UI thread.
270 if (ThreadUtils.runningOnUiThread()) {
271 new AsyncTask<Void, Void, String>() {
273 public String doInBackground(Void... params) {
274 return getAuthTokenInner(future, errorEncountered);
277 public void onPostExecute(String authToken) {
278 onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries,
279 errorEncountered, retry);
283 String authToken = getAuthTokenInner(future, errorEncountered);
284 onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries,
285 errorEncountered, retry);
289 private void onGotAuthTokenResult(Account account, String authTokenType, String authToken,
290 GetAuthTokenCallback callback, AtomicInteger numTries, AtomicBoolean errorEncountered,
291 ConnectionRetry retry) {
292 if (authToken != null || !errorEncountered.get() ||
293 numTries.incrementAndGet() == MAX_TRIES ||
294 !NetworkChangeNotifier.isInitialized()) {
295 callback.tokenAvailable(authToken);
299 ConnectionRetry newRetry = new ConnectionRetry(account, authTokenType, callback,
300 numTries, errorEncountered);
301 NetworkChangeNotifier.addConnectionTypeObserver(newRetry);
303 NetworkChangeNotifier.addConnectionTypeObserver(retry);
308 * Invalidates the old token (if non-null/non-empty) and synchronously generates a new one.
309 * Also notifies the user (via status bar) if any user action is required. The method will
310 * return null if any user action is required to generate the new token.
312 * - Assumes that the account is a valid account.
313 * - Should not be called on the main thread.
316 public String getNewAuthToken(Account account, String authToken, String authTokenType) {
317 invalidateAuthToken(authToken);
319 // TODO(dsmyers): consider reimplementing using an AccountManager function with an
321 // Bug: https://code.google.com/p/chromium/issues/detail?id=172394.
323 return mAccountManager.blockingGetAuthToken(account, authTokenType, true);
324 } catch (OperationCanceledException e) {
325 Log.w(TAG, "Auth token - operation cancelled", e);
326 } catch (AuthenticatorException e) {
327 Log.w(TAG, "Auth token - authenticator exception", e);
328 } catch (IOException e) {
329 Log.w(TAG, "Auth token - IO exception", e);
335 * Invalidates the old token (if non-null/non-empty) and asynchronously generates a new one.
337 * - Assumes that the account is a valid account.
339 public void getNewAuthTokenFromForeground(Account account, String authToken,
340 String authTokenType, GetAuthTokenCallback callback) {
341 invalidateAuthToken(authToken);
342 AtomicInteger numTries = new AtomicInteger(0);
343 AtomicBoolean errorEncountered = new AtomicBoolean(false);
344 getAuthTokenAsynchronously(
345 null, account, authTokenType, callback, numTries, errorEncountered, null);
349 * Removes an auth token from the AccountManager's cache.
351 public void invalidateAuthToken(String authToken) {
352 if (authToken != null && !authToken.isEmpty()) {
353 mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken);