- add sources.
[platform/framework/web/crosswalk.git] / src / sync / android / java / src / org / chromium / sync / signin / AccountManagerHelper.java
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.
4
5 package org.chromium.sync.signin;
6
7
8 import com.google.common.annotations.VisibleForTesting;
9
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;
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.concurrent.atomic.AtomicBoolean;
29 import java.util.concurrent.atomic.AtomicInteger;
30 import java.util.List;
31 import javax.annotation.Nullable;
32
33 /**
34  * AccountManagerHelper wraps our access of AccountManager in Android.
35  *
36  * Use the AccountManagerHelper.get(someContext) to instantiate it
37  */
38 public class AccountManagerHelper {
39
40     private static final String TAG = "AccountManagerHelper";
41
42     public static final String GOOGLE_ACCOUNT_TYPE = "com.google";
43
44     private static final Object lock = new Object();
45
46     private static final int MAX_TRIES = 3;
47
48     private static AccountManagerHelper sAccountManagerHelper;
49
50     private final AccountManagerDelegate mAccountManager;
51
52     private Context mApplicationContext;
53
54     public interface GetAuthTokenCallback {
55         /**
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).
59          */
60         void tokenAvailable(String token);
61     }
62
63     /**
64      * @param context the Android context
65      * @param accountManager the account manager to use as a backend service
66      */
67     private AccountManagerHelper(Context context,
68                                  AccountManagerDelegate accountManager) {
69         mApplicationContext = context.getApplicationContext();
70         mAccountManager = accountManager;
71     }
72
73     /**
74      * A factory method for the AccountManagerHelper.
75      *
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.
79      *
80      * @param context the applicationContext is retrieved from the context used as an argument.
81      * @return a singleton instance of the AccountManagerHelper
82      */
83     public static AccountManagerHelper get(Context context) {
84         synchronized (lock) {
85             if (sAccountManagerHelper == null) {
86                 sAccountManagerHelper = new AccountManagerHelper(context,
87                         new SystemAccountManagerDelegate(context));
88             }
89         }
90         return sAccountManagerHelper;
91     }
92
93     @VisibleForTesting
94     public static void overrideAccountManagerHelperForTests(Context context,
95             AccountManagerDelegate accountManager) {
96         synchronized (lock) {
97             sAccountManagerHelper = new AccountManagerHelper(context, accountManager);
98         }
99     }
100
101     /**
102      * Creates an Account object for the given name.
103      */
104     public static Account createAccountFromName(String name) {
105         return new Account(name, GOOGLE_ACCOUNT_TYPE);
106     }
107
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);
113         }
114         return accountNames;
115     }
116
117     public Account[] getGoogleAccounts() {
118         return mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE);
119     }
120
121     public boolean hasGoogleAccounts() {
122         return getGoogleAccounts().length > 0;
123     }
124
125     /**
126      * Returns the account if it exists, null otherwise.
127      */
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)) {
132                 return account;
133             }
134         }
135         return null;
136     }
137
138     /**
139      * Returns whether the accounts exists.
140      */
141     public boolean hasAccountForName(String accountName) {
142         return getAccountFromName(accountName) != null;
143     }
144
145     /**
146      * @return Whether or not there is an account authenticator for Google accounts.
147      */
148     public boolean hasGoogleAccountAuthenticator() {
149         AuthenticatorDescription[] descs = mAccountManager.getAuthenticatorTypes();
150         for (AuthenticatorDescription desc : descs) {
151             if (GOOGLE_ACCOUNT_TYPE.equals(desc.type)) return true;
152         }
153         return false;
154     }
155
156     /**
157      * Gets the auth token synchronously.
158      *
159      * - Assumes that the account is a valid account.
160      * - Should not be called on the main thread.
161      */
162     @Deprecated
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);
168     }
169
170     /**
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.
174      *
175      * - Assumes that the account is a valid account.
176      */
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);
183     }
184
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;
191
192         ConnectionRetry(Account account, String authTokenType, GetAuthTokenCallback callback,
193                 AtomicInteger numTries, AtomicBoolean errorEncountered) {
194             mAccount = account;
195             mAuthTokenType = authTokenType;
196             mCallback = callback;
197             mNumTries = numTries;
198             mErrorEncountered = errorEncountered;
199         }
200
201         @Override
202         public void onConnectionTypeChanged(int connectionType) {
203             assert mNumTries.get() <= MAX_TRIES;
204             if (mNumTries.get() == MAX_TRIES) {
205                 NetworkChangeNotifier.removeConnectionTypeObserver(this);
206                 return;
207             }
208             if (NetworkChangeNotifier.isOnline()) {
209                 NetworkChangeNotifier.removeConnectionTypeObserver(this);
210                 getAuthTokenAsynchronously(null, mAccount, mAuthTokenType, mCallback, mNumTries,
211                         mErrorEncountered, this);
212             }
213         }
214     }
215
216     // Gets the auth token synchronously
217     private String getAuthTokenInner(AccountManagerFuture<Bundle> future,
218             AtomicBoolean errorEncountered) {
219         try {
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);
230                     return null;
231                 }
232                 return result.getString(AccountManager.KEY_AUTHTOKEN);
233             } else {
234                 Log.w(TAG, "Auth token - getAuthToken returned null");
235             }
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);
243         }
244         return null;
245     }
246
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);
255         } else {
256             future = mAccountManager.getAuthToken(
257                     account, authTokenType, false, null, null);
258         }
259         final AccountManagerFuture<Bundle> finalFuture = future;
260         errorEncountered.set(false);
261
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>() {
266                 @Override
267                 public String doInBackground(Void... params) {
268                     return getAuthTokenInner(finalFuture, errorEncountered);
269                 }
270                 @Override
271                 public void onPostExecute(String authToken) {
272                     onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries,
273                             errorEncountered, retry);
274                 }
275             }.execute();
276         } else {
277             String authToken = getAuthTokenInner(finalFuture, errorEncountered);
278             onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries,
279                     errorEncountered, retry);
280         }
281     }
282
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);
290             return;
291         }
292         if (retry == null) {
293             ConnectionRetry newRetry = new ConnectionRetry(account, authTokenType, callback,
294                     numTries, errorEncountered);
295             NetworkChangeNotifier.addConnectionTypeObserver(newRetry);
296         } else {
297             NetworkChangeNotifier.addConnectionTypeObserver(retry);
298         }
299     }
300
301     /**
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.
305      *
306      * - Assumes that the account is a valid account.
307      * - Should not be called on the main thread.
308      */
309     @Deprecated
310     public String getNewAuthToken(Account account, String authToken, String authTokenType) {
311         // TODO(dsmyers): consider reimplementing using an AccountManager function with an
312         // explicit timeout.
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);
316         }
317
318         try {
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);
326         }
327         return null;
328     }
329
330     /**
331      * Invalidates the old token (if non-null/non-empty) and asynchronously generates a new one.
332      *
333      * - Assumes that the account is a valid account.
334      */
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);
339         }
340         AtomicInteger numTries = new AtomicInteger(0);
341         AtomicBoolean errorEncountered = new AtomicBoolean(false);
342         getAuthTokenAsynchronously(
343             null, account, authTokenType, callback, numTries, errorEncountered, null);
344     }
345
346     /**
347      * Removes an auth token from the AccountManager's cache.
348      */
349     public void invalidateAuthToken(String authToken) {
350         mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken);
351     }
352 }