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