PageRenderTime 38ms CodeModel.GetById 25ms app.highlight 10ms RepoModel.GetById 1ms app.codeStats 0ms

/sync/android/java/src/org/chromium/sync/signin/AccountManagerHelper.java

https://gitlab.com/jonnialva90/iridium-browser
Java | 381 lines | 249 code | 45 blank | 87 comment | 37 complexity | e37fb93d6a5dedcb450500287dec1695 MD5 | raw file
  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
  5package org.chromium.sync.signin;
  6
  7
  8import android.Manifest;
  9import android.accounts.Account;
 10import android.accounts.AccountManager;
 11import android.accounts.AccountManagerFuture;
 12import android.accounts.AuthenticatorDescription;
 13import android.accounts.AuthenticatorException;
 14import android.accounts.OperationCanceledException;
 15import android.content.Context;
 16import android.content.pm.PackageManager;
 17import android.os.AsyncTask;
 18import android.os.Bundle;
 19import android.os.Process;
 20import android.util.Log;
 21
 22import org.chromium.base.BuildInfo;
 23import org.chromium.base.ThreadUtils;
 24import org.chromium.base.VisibleForTesting;
 25import org.chromium.net.NetworkChangeNotifier;
 26
 27import java.io.IOException;
 28import java.util.ArrayList;
 29import java.util.List;
 30import java.util.Locale;
 31import java.util.concurrent.atomic.AtomicBoolean;
 32import java.util.concurrent.atomic.AtomicInteger;
 33import java.util.regex.Pattern;
 34
 35/**
 36 * AccountManagerHelper wraps our access of AccountManager in Android.
 37 *
 38 * Use the AccountManagerHelper.get(someContext) to instantiate it
 39 */
 40public class AccountManagerHelper {
 41    private static final String TAG = "cr.Sync.Signin";
 42
 43    private static final Pattern AT_SYMBOL = Pattern.compile("@");
 44
 45    private static final String GMAIL_COM = "gmail.com";
 46
 47    private static final String GOOGLEMAIL_COM = "googlemail.com";
 48
 49    public static final String GOOGLE_ACCOUNT_TYPE = "com.google";
 50
 51    /**
 52     * An account feature (corresponding to a Gaia service flag) that specifies whether the account
 53     * is a child account.
 54     */
 55    @VisibleForTesting public static final String FEATURE_IS_CHILD_ACCOUNT_KEY = "service_uca";
 56
 57    private static final Object sLock = new Object();
 58
 59    private static final int MAX_TRIES = 3;
 60
 61    private static AccountManagerHelper sAccountManagerHelper;
 62
 63    private final AccountManagerDelegate mAccountManager;
 64
 65    private Context mApplicationContext;
 66
 67    /**
 68     * A simple callback for getAuthToken.
 69     */
 70    public interface GetAuthTokenCallback {
 71        /**
 72         * Invoked on the UI thread if a token is provided by the AccountManager.
 73         *
 74         * @param token Auth token, guaranteed not to be null.
 75         */
 76        void tokenAvailable(String token);
 77
 78        /**
 79         * Invoked on the UI thread if no token is available.
 80         *
 81         * @param isTransientError Indicates if the error is transient (network timeout or
 82         * unavailable, etc) or persistent (bad credentials, permission denied, etc).
 83         */
 84        void tokenUnavailable(boolean isTransientError);
 85    }
 86
 87    /**
 88     * @param context the Android context
 89     * @param accountManager the account manager to use as a backend service
 90     */
 91    private AccountManagerHelper(Context context, AccountManagerDelegate accountManager) {
 92        mApplicationContext = context.getApplicationContext();
 93        mAccountManager = accountManager;
 94    }
 95
 96    /**
 97     * Initialize AccountManagerHelper with a custom AccountManagerDelegate.
 98     * Ensures that the singleton AccountManagerHelper hasn't been created yet.
 99     * This can be overriden in tests using the overrideAccountManagerHelperForTests method.
100     *
101     * @param context the applicationContext is retrieved from the context used as an argument.
102     * @param delegate the custom AccountManagerDelegate to use.
103     */
104    public static void initializeAccountManagerHelper(
105            Context context, AccountManagerDelegate delegate) {
106        synchronized (sLock) {
107            assert sAccountManagerHelper == null;
108            sAccountManagerHelper = new AccountManagerHelper(context, delegate);
109        }
110    }
111
112    /**
113     * A getter method for AccountManagerHelper singleton which also initializes it if not wasn't
114     * already initialized.
115     *
116     * @param context the applicationContext is retrieved from the context used as an argument.
117     * @return a singleton instance of the AccountManagerHelper
118     */
119    public static AccountManagerHelper get(Context context) {
120        synchronized (sLock) {
121            if (sAccountManagerHelper == null) {
122                sAccountManagerHelper = new AccountManagerHelper(
123                        context, new SystemAccountManagerDelegate(context));
124            }
125        }
126        return sAccountManagerHelper;
127    }
128
129    /**
130     * Override AccountManagerHelper with a custom AccountManagerDelegate in tests.
131     * Unlike initializeAccountManagerHelper, this will override the existing instance of
132     * AccountManagerHelper if any. Only for use in Tests.
133     *
134     * @param context the applicationContext is retrieved from the context used as an argument.
135     * @param delegate the custom AccountManagerDelegate to use.
136     */
137    @VisibleForTesting
138    public static void overrideAccountManagerHelperForTests(
139            Context context, AccountManagerDelegate delegate) {
140        synchronized (sLock) {
141            sAccountManagerHelper = new AccountManagerHelper(context, delegate);
142        }
143    }
144
145    /**
146     * Creates an Account object for the given name.
147     */
148    public static Account createAccountFromName(String name) {
149        return new Account(name, GOOGLE_ACCOUNT_TYPE);
150    }
151
152    public List<String> getGoogleAccountNames() {
153        List<String> accountNames = new ArrayList<String>();
154        for (Account account : getGoogleAccounts()) {
155            accountNames.add(account.name);
156        }
157        return accountNames;
158    }
159
160    /**
161     * Returns all Google accounts on the device.
162     * @return an array of accounts.
163     */
164    public Account[] getGoogleAccounts() {
165        return mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE);
166    }
167
168    public void getGoogleAccounts(AccountManagerDelegate.Callback<Account[]> callback) {
169        mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE, callback);
170    }
171
172    public boolean hasGoogleAccounts() {
173        return getGoogleAccounts().length > 0;
174    }
175
176    private String canonicalizeName(String name) {
177        String[] parts = AT_SYMBOL.split(name);
178        if (parts.length != 2) return name;
179
180        if (GOOGLEMAIL_COM.equalsIgnoreCase(parts[1])) {
181            parts[1] = GMAIL_COM;
182        }
183        if (GMAIL_COM.equalsIgnoreCase(parts[1])) {
184            parts[0] = parts[0].replace(".", "");
185        }
186        return (parts[0] + "@" + parts[1]).toLowerCase(Locale.US);
187    }
188
189    /**
190     * Returns the account if it exists, null otherwise.
191     */
192    public Account getAccountFromName(String accountName) {
193        String canonicalName = canonicalizeName(accountName);
194        Account[] accounts = getGoogleAccounts();
195        for (Account account : accounts) {
196            if (canonicalizeName(account.name).equals(canonicalName)) {
197                return account;
198            }
199        }
200        return null;
201    }
202
203    /**
204     * Returns whether the accounts exists.
205     */
206    public boolean hasAccountForName(String accountName) {
207        return getAccountFromName(accountName) != null;
208    }
209
210    /**
211     * @return Whether or not there is an account authenticator for Google accounts.
212     */
213    public boolean hasGoogleAccountAuthenticator() {
214        AuthenticatorDescription[] descs = mAccountManager.getAuthenticatorTypes();
215        for (AuthenticatorDescription desc : descs) {
216            if (GOOGLE_ACCOUNT_TYPE.equals(desc.type)) return true;
217        }
218        return false;
219    }
220
221    /**
222     * Gets the auth token and returns the response asynchronously.
223     * This should be called when we have a foreground activity that needs an auth token.
224     * If encountered an IO error, it will attempt to retry when the network is back.
225     *
226     * - Assumes that the account is a valid account.
227     */
228    public void getAuthToken(Account account, String authTokenType, GetAuthTokenCallback callback) {
229        AtomicInteger numTries = new AtomicInteger(0);
230        AtomicBoolean isTransientError = new AtomicBoolean(false);
231        getAuthTokenAsynchronously(
232                account, authTokenType, callback, numTries, isTransientError, null);
233    }
234
235    private class ConnectionRetry implements NetworkChangeNotifier.ConnectionTypeObserver {
236        private final Account mAccount;
237        private final String mAuthTokenType;
238        private final GetAuthTokenCallback mCallback;
239        private final AtomicInteger mNumTries;
240        private final AtomicBoolean mIsTransientError;
241
242        ConnectionRetry(Account account, String authTokenType, GetAuthTokenCallback callback,
243                AtomicInteger numTries, AtomicBoolean isTransientError) {
244            mAccount = account;
245            mAuthTokenType = authTokenType;
246            mCallback = callback;
247            mNumTries = numTries;
248            mIsTransientError = isTransientError;
249        }
250
251        @Override
252        public void onConnectionTypeChanged(int connectionType) {
253            assert mNumTries.get() <= MAX_TRIES;
254            if (mNumTries.get() == MAX_TRIES) {
255                NetworkChangeNotifier.removeConnectionTypeObserver(this);
256                return;
257            }
258            if (NetworkChangeNotifier.isOnline()) {
259                NetworkChangeNotifier.removeConnectionTypeObserver(this);
260                getAuthTokenAsynchronously(
261                        mAccount, mAuthTokenType, mCallback, mNumTries, mIsTransientError, this);
262            }
263        }
264    }
265
266    private boolean hasUseCredentialsPermission() {
267        return BuildInfo.isMncOrLater()
268                || mApplicationContext.checkPermission("android.permission.USE_CREDENTIALS",
269                Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED;
270    }
271
272    public boolean hasGetAccountsPermission() {
273        return mApplicationContext.checkPermission(Manifest.permission.GET_ACCOUNTS,
274                Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED;
275    }
276
277    // Gets the auth token synchronously
278    private String getAuthTokenInner(AccountManagerFuture<Bundle> future,
279            AtomicBoolean isTransientError) {
280        try {
281            Bundle result = future.getResult();
282            if (result != null) {
283                return result.getString(AccountManager.KEY_AUTHTOKEN);
284            } else {
285                Log.w(TAG, "Auth token - getAuthToken returned null");
286            }
287        } catch (OperationCanceledException e) {
288            Log.w(TAG, "Auth token - operation cancelled", e);
289        } catch (AuthenticatorException e) {
290            Log.w(TAG, "Auth token - authenticator exception", e);
291        } catch (IOException e) {
292            Log.w(TAG, "Auth token - IO exception", e);
293            isTransientError.set(true);
294        }
295        return null;
296    }
297
298    private void getAuthTokenAsynchronously(final Account account, final String authTokenType,
299            final GetAuthTokenCallback callback, final AtomicInteger numTries,
300            final AtomicBoolean isTransientError, final ConnectionRetry retry) {
301        // Return null token for no USE_CREDENTIALS permission.
302        if (!hasUseCredentialsPermission()) {
303            ThreadUtils.runOnUiThread(new Runnable() {
304                @Override
305                public void run() {
306                    callback.tokenUnavailable(false);
307                }
308            });
309            return;
310        }
311        final AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(
312                account, authTokenType, true, null, null);
313        isTransientError.set(false);
314
315        new AsyncTask<Void, Void, String>() {
316            @Override
317            public String doInBackground(Void... params) {
318                return getAuthTokenInner(future, isTransientError);
319            }
320            @Override
321            public void onPostExecute(String authToken) {
322                onGotAuthTokenResult(account, authTokenType, authToken, callback, numTries,
323                        isTransientError, retry);
324            }
325        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
326    }
327
328    private void onGotAuthTokenResult(Account account, String authTokenType, String authToken,
329            GetAuthTokenCallback callback, AtomicInteger numTries, AtomicBoolean isTransientError,
330            ConnectionRetry retry) {
331        if (authToken != null) {
332            callback.tokenAvailable(authToken);
333            return;
334        } else if (!isTransientError.get()
335                || numTries.incrementAndGet() == MAX_TRIES
336                || !NetworkChangeNotifier.isInitialized()) {
337            callback.tokenUnavailable(isTransientError.get());
338            return;
339        }
340        if (retry == null) {
341            ConnectionRetry newRetry = new ConnectionRetry(account, authTokenType, callback,
342                    numTries, isTransientError);
343            NetworkChangeNotifier.addConnectionTypeObserver(newRetry);
344        } else {
345            NetworkChangeNotifier.addConnectionTypeObserver(retry);
346        }
347    }
348
349    /**
350     * Invalidates the old token (if non-null/non-empty) and asynchronously generates a new one.
351     *
352     * - Assumes that the account is a valid account.
353     */
354    public void getNewAuthToken(Account account, String authToken, String authTokenType,
355            GetAuthTokenCallback callback) {
356        invalidateAuthToken(authToken);
357        AtomicInteger numTries = new AtomicInteger(0);
358        AtomicBoolean isTransientError = new AtomicBoolean(false);
359        getAuthTokenAsynchronously(
360                account, authTokenType, callback, numTries, isTransientError, null);
361    }
362
363    /**
364     * Removes an auth token from the AccountManager's cache.
365     */
366    public void invalidateAuthToken(String authToken) {
367        // Cancel operation for no USE_CREDENTIALS permission.
368        if (!hasUseCredentialsPermission()) {
369            return;
370        }
371        if (authToken != null && !authToken.isEmpty()) {
372            mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken);
373        }
374    }
375
376    public void checkChildAccount(
377            Account account, AccountManagerDelegate.Callback<Boolean> callback) {
378        String[] features = {FEATURE_IS_CHILD_ACCOUNT_KEY};
379        mAccountManager.hasFeatures(account, features, callback);
380    }
381}