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