OLD | NEW |
| (Empty) |
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.Manifest; | |
9 import android.accounts.Account; | |
10 import android.accounts.AuthenticatorDescription; | |
11 import android.content.Context; | |
12 import android.content.pm.PackageManager; | |
13 import android.os.AsyncTask; | |
14 import android.os.Process; | |
15 | |
16 import org.chromium.base.Callback; | |
17 import org.chromium.base.Log; | |
18 import org.chromium.base.VisibleForTesting; | |
19 import org.chromium.net.NetworkChangeNotifier; | |
20 | |
21 import java.util.ArrayList; | |
22 import java.util.List; | |
23 import java.util.Locale; | |
24 import java.util.concurrent.atomic.AtomicBoolean; | |
25 import java.util.concurrent.atomic.AtomicInteger; | |
26 import java.util.regex.Pattern; | |
27 | |
28 /** | |
29 * AccountManagerHelper wraps our access of AccountManager in Android. | |
30 * | |
31 * Use the AccountManagerHelper.get(someContext) to instantiate it | |
32 */ | |
33 public class AccountManagerHelper { | |
34 private static final String TAG = "Sync_Signin"; | |
35 | |
36 private static final Pattern AT_SYMBOL = Pattern.compile("@"); | |
37 | |
38 private static final String GMAIL_COM = "gmail.com"; | |
39 | |
40 private static final String GOOGLEMAIL_COM = "googlemail.com"; | |
41 | |
42 public static final String GOOGLE_ACCOUNT_TYPE = "com.google"; | |
43 | |
44 /** | |
45 * An account feature (corresponding to a Gaia service flag) that specifies
whether the account | |
46 * is a child account. | |
47 */ | |
48 @VisibleForTesting public static final String FEATURE_IS_CHILD_ACCOUNT_KEY =
"service_uca"; | |
49 | |
50 private static final Object sLock = new Object(); | |
51 | |
52 private static AccountManagerHelper sAccountManagerHelper; | |
53 | |
54 private final AccountManagerDelegate mAccountManager; | |
55 | |
56 private Context mApplicationContext; | |
57 | |
58 /** | |
59 * A simple callback for getAuthToken. | |
60 */ | |
61 public interface GetAuthTokenCallback { | |
62 /** | |
63 * Invoked on the UI thread if a token is provided by the AccountManager
. | |
64 * | |
65 * @param token Auth token, guaranteed not to be null. | |
66 */ | |
67 void tokenAvailable(String token); | |
68 | |
69 /** | |
70 * Invoked on the UI thread if no token is available. | |
71 * | |
72 * @param isTransientError Indicates if the error is transient (network
timeout or | |
73 * unavailable, etc) or persistent (bad credentials, permission denied,
etc). | |
74 */ | |
75 void tokenUnavailable(boolean isTransientError); | |
76 } | |
77 | |
78 /** | |
79 * @param context the Android context | |
80 * @param accountManager the account manager to use as a backend service | |
81 */ | |
82 private AccountManagerHelper(Context context, AccountManagerDelegate account
Manager) { | |
83 mApplicationContext = context.getApplicationContext(); | |
84 mAccountManager = accountManager; | |
85 } | |
86 | |
87 /** | |
88 * Initialize AccountManagerHelper with a custom AccountManagerDelegate. | |
89 * Ensures that the singleton AccountManagerHelper hasn't been created yet. | |
90 * This can be overriden in tests using the overrideAccountManagerHelperForT
ests method. | |
91 * | |
92 * @param context the applicationContext is retrieved from the context used
as an argument. | |
93 * @param delegate the custom AccountManagerDelegate to use. | |
94 */ | |
95 public static void initializeAccountManagerHelper( | |
96 Context context, AccountManagerDelegate delegate) { | |
97 synchronized (sLock) { | |
98 assert sAccountManagerHelper == null; | |
99 sAccountManagerHelper = new AccountManagerHelper(context, delegate); | |
100 } | |
101 } | |
102 | |
103 /** | |
104 * A getter method for AccountManagerHelper singleton which also initializes
it if not wasn't | |
105 * already initialized. | |
106 * | |
107 * @param context the applicationContext is retrieved from the context used
as an argument. | |
108 * @return a singleton instance of the AccountManagerHelper | |
109 */ | |
110 public static AccountManagerHelper get(Context context) { | |
111 synchronized (sLock) { | |
112 if (sAccountManagerHelper == null) { | |
113 sAccountManagerHelper = new AccountManagerHelper( | |
114 context, new SystemAccountManagerDelegate(context)); | |
115 } | |
116 } | |
117 return sAccountManagerHelper; | |
118 } | |
119 | |
120 /** | |
121 * Override AccountManagerHelper with a custom AccountManagerDelegate in tes
ts. | |
122 * Unlike initializeAccountManagerHelper, this will override the existing in
stance of | |
123 * AccountManagerHelper if any. Only for use in Tests. | |
124 * | |
125 * @param context the applicationContext is retrieved from the context used
as an argument. | |
126 * @param delegate the custom AccountManagerDelegate to use. | |
127 */ | |
128 @VisibleForTesting | |
129 public static void overrideAccountManagerHelperForTests( | |
130 Context context, AccountManagerDelegate delegate) { | |
131 synchronized (sLock) { | |
132 sAccountManagerHelper = new AccountManagerHelper(context, delegate); | |
133 } | |
134 } | |
135 | |
136 /** | |
137 * Creates an Account object for the given name. | |
138 */ | |
139 public static Account createAccountFromName(String name) { | |
140 return new Account(name, GOOGLE_ACCOUNT_TYPE); | |
141 } | |
142 | |
143 /** | |
144 * This method is deprecated; please use the asynchronous version below inst
ead. | |
145 * | |
146 * See http://crbug.com/517697 for details. | |
147 */ | |
148 public List<String> getGoogleAccountNames() { | |
149 List<String> accountNames = new ArrayList<String>(); | |
150 for (Account account : getGoogleAccounts()) { | |
151 accountNames.add(account.name); | |
152 } | |
153 return accountNames; | |
154 } | |
155 | |
156 /** | |
157 * Retrieves a list of the Google account names on the device asynchronously
. | |
158 */ | |
159 public void getGoogleAccountNames(final Callback<List<String>> callback) { | |
160 getGoogleAccounts(new Callback<Account[]>() { | |
161 @Override | |
162 public void onResult(Account[] accounts) { | |
163 List<String> accountNames = new ArrayList<String>(); | |
164 for (Account account : accounts) { | |
165 accountNames.add(account.name); | |
166 } | |
167 callback.onResult(accountNames); | |
168 } | |
169 }); | |
170 } | |
171 | |
172 /** | |
173 * This method is deprecated; please use the asynchronous version below inst
ead. | |
174 * | |
175 * See http://crbug.com/517697 for details. | |
176 */ | |
177 public Account[] getGoogleAccounts() { | |
178 return mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); | |
179 } | |
180 | |
181 /** | |
182 * Retrieves all Google accounts on the device asynchronously. | |
183 */ | |
184 public void getGoogleAccounts(Callback<Account[]> callback) { | |
185 mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE, callback); | |
186 } | |
187 | |
188 /** | |
189 * This method is deprecated; please use the asynchronous version below inst
ead. | |
190 * | |
191 * See http://crbug.com/517697 for details. | |
192 */ | |
193 public boolean hasGoogleAccounts() { | |
194 return getGoogleAccounts().length > 0; | |
195 } | |
196 | |
197 /** | |
198 * Asynchronously determine whether any Google accounts have been added. | |
199 */ | |
200 public void hasGoogleAccounts(final Callback<Boolean> callback) { | |
201 getGoogleAccounts(new Callback<Account[]>() { | |
202 @Override | |
203 public void onResult(Account[] accounts) { | |
204 callback.onResult(accounts.length > 0); | |
205 } | |
206 }); | |
207 } | |
208 | |
209 private String canonicalizeName(String name) { | |
210 String[] parts = AT_SYMBOL.split(name); | |
211 if (parts.length != 2) return name; | |
212 | |
213 if (GOOGLEMAIL_COM.equalsIgnoreCase(parts[1])) { | |
214 parts[1] = GMAIL_COM; | |
215 } | |
216 if (GMAIL_COM.equalsIgnoreCase(parts[1])) { | |
217 parts[0] = parts[0].replace(".", ""); | |
218 } | |
219 return (parts[0] + "@" + parts[1]).toLowerCase(Locale.US); | |
220 } | |
221 | |
222 /** | |
223 * This method is deprecated; please use the asynchronous version below inst
ead. | |
224 * | |
225 * See http://crbug.com/517697 for details. | |
226 */ | |
227 public Account getAccountFromName(String accountName) { | |
228 String canonicalName = canonicalizeName(accountName); | |
229 Account[] accounts = getGoogleAccounts(); | |
230 for (Account account : accounts) { | |
231 if (canonicalizeName(account.name).equals(canonicalName)) { | |
232 return account; | |
233 } | |
234 } | |
235 return null; | |
236 } | |
237 | |
238 /** | |
239 * Asynchronously returns the account if it exists; null otherwise. | |
240 */ | |
241 public void getAccountFromName(String accountName, final Callback<Account> c
allback) { | |
242 final String canonicalName = canonicalizeName(accountName); | |
243 getGoogleAccounts(new Callback<Account[]>() { | |
244 @Override | |
245 public void onResult(Account[] accounts) { | |
246 Account accountForName = null; | |
247 for (Account account : accounts) { | |
248 if (canonicalizeName(account.name).equals(canonicalName)) { | |
249 accountForName = account; | |
250 break; | |
251 } | |
252 } | |
253 callback.onResult(accountForName); | |
254 } | |
255 }); | |
256 } | |
257 | |
258 /** | |
259 * This method is deprecated; please use the asynchronous version below inst
ead. | |
260 * | |
261 * See http://crbug.com/517697 for details. | |
262 */ | |
263 public boolean hasAccountForName(String accountName) { | |
264 return getAccountFromName(accountName) != null; | |
265 } | |
266 | |
267 /** | |
268 * Asynchronously returns whether an account exists with the given name. | |
269 */ | |
270 // TODO(maxbogue): Remove once this function is used outside of tests. | |
271 @VisibleForTesting | |
272 public void hasAccountForName(String accountName, final Callback<Boolean> ca
llback) { | |
273 getAccountFromName(accountName, new Callback<Account>() { | |
274 @Override | |
275 public void onResult(Account account) { | |
276 callback.onResult(account != null); | |
277 } | |
278 }); | |
279 } | |
280 | |
281 /** | |
282 * @return Whether or not there is an account authenticator for Google accou
nts. | |
283 */ | |
284 public boolean hasGoogleAccountAuthenticator() { | |
285 AuthenticatorDescription[] descs = mAccountManager.getAuthenticatorTypes
(); | |
286 for (AuthenticatorDescription desc : descs) { | |
287 if (GOOGLE_ACCOUNT_TYPE.equals(desc.type)) return true; | |
288 } | |
289 return false; | |
290 } | |
291 | |
292 /** | |
293 * Gets the auth token and returns the response asynchronously. | |
294 * This should be called when we have a foreground activity that needs an au
th token. | |
295 * If encountered an IO error, it will attempt to retry when the network is
back. | |
296 * | |
297 * - Assumes that the account is a valid account. | |
298 */ | |
299 public void getAuthToken(final Account account, final String authTokenType, | |
300 final GetAuthTokenCallback callback) { | |
301 ConnectionRetry.runAuthTask(new AuthTask<String>() { | |
302 @Override | |
303 public String run() throws AuthException { | |
304 return mAccountManager.getAuthToken(account, authTokenType); | |
305 } | |
306 @Override | |
307 public void onSuccess(String token) { | |
308 callback.tokenAvailable(token); | |
309 } | |
310 @Override | |
311 public void onFailure(boolean isTransientError) { | |
312 callback.tokenUnavailable(isTransientError); | |
313 } | |
314 }); | |
315 } | |
316 | |
317 public boolean hasGetAccountsPermission() { | |
318 return mApplicationContext.checkPermission(Manifest.permission.GET_ACCOU
NTS, | |
319 Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_G
RANTED; | |
320 } | |
321 | |
322 /** | |
323 * Invalidates the old token (if non-null/non-empty) and asynchronously gene
rates a new one. | |
324 * | |
325 * - Assumes that the account is a valid account. | |
326 */ | |
327 public void getNewAuthToken(Account account, String authToken, String authTo
kenType, | |
328 GetAuthTokenCallback callback) { | |
329 invalidateAuthToken(authToken); | |
330 getAuthToken(account, authTokenType, callback); | |
331 } | |
332 | |
333 /** | |
334 * Clear an auth token from the local cache with respect to the ApplicationC
ontext. | |
335 */ | |
336 public void invalidateAuthToken(final String authToken) { | |
337 if (authToken == null || authToken.isEmpty()) { | |
338 return; | |
339 } | |
340 ConnectionRetry.runAuthTask(new AuthTask<Boolean>() { | |
341 @Override | |
342 public Boolean run() throws AuthException { | |
343 mAccountManager.invalidateAuthToken(authToken); | |
344 return true; | |
345 } | |
346 @Override | |
347 public void onSuccess(Boolean result) {} | |
348 @Override | |
349 public void onFailure(boolean isTransientError) { | |
350 Log.e(TAG, "Failed to invalidate auth token: " + authToken); | |
351 } | |
352 }); | |
353 } | |
354 | |
355 public void checkChildAccount(Account account, Callback<Boolean> callback) { | |
356 String[] features = {FEATURE_IS_CHILD_ACCOUNT_KEY}; | |
357 mAccountManager.hasFeatures(account, features, callback); | |
358 } | |
359 | |
360 private interface AuthTask<T> { | |
361 T run() throws AuthException; | |
362 void onSuccess(T result); | |
363 void onFailure(boolean isTransientError); | |
364 } | |
365 | |
366 /** | |
367 * A helper class to encapsulate network connection retry logic for AuthTask
s. | |
368 * | |
369 * The task will be run on the background thread. If it encounters a transie
nt error, it will | |
370 * wait for a network change and retry up to MAX_TRIES times. | |
371 */ | |
372 private static class ConnectionRetry<T> | |
373 implements NetworkChangeNotifier.ConnectionTypeObserver { | |
374 private static final int MAX_TRIES = 3; | |
375 | |
376 private final AuthTask<T> mAuthTask; | |
377 private final AtomicInteger mNumTries; | |
378 private final AtomicBoolean mIsTransientError; | |
379 | |
380 public static <T> void runAuthTask(AuthTask<T> authTask) { | |
381 new ConnectionRetry<T>(authTask).attempt(); | |
382 } | |
383 | |
384 private ConnectionRetry(AuthTask<T> authTask) { | |
385 mAuthTask = authTask; | |
386 mNumTries = new AtomicInteger(0); | |
387 mIsTransientError = new AtomicBoolean(false); | |
388 } | |
389 | |
390 /** | |
391 * Tries running the {@link AuthTask} in the background. This object is
never registered | |
392 * as a {@link ConnectionTypeObserver} when this method is called. | |
393 */ | |
394 private void attempt() { | |
395 // Clear any transient error. | |
396 mIsTransientError.set(false); | |
397 new AsyncTask<Void, Void, T>() { | |
398 @Override | |
399 public T doInBackground(Void... params) { | |
400 try { | |
401 return mAuthTask.run(); | |
402 } catch (AuthException ex) { | |
403 Log.w(TAG, "Failed to perform auth task", ex); | |
404 mIsTransientError.set(ex.isTransientError()); | |
405 } | |
406 return null; | |
407 } | |
408 @Override | |
409 public void onPostExecute(T result) { | |
410 if (result != null) { | |
411 mAuthTask.onSuccess(result); | |
412 } else if (!mIsTransientError.get() | |
413 || mNumTries.incrementAndGet() >= MAX_TRIES | |
414 || !NetworkChangeNotifier.isInitialized()) { | |
415 // Permanent error, ran out of tries, or we can't listen
for network | |
416 // change events; give up. | |
417 mAuthTask.onFailure(mIsTransientError.get()); | |
418 } else { | |
419 // Transient error with tries left; register for another
attempt. | |
420 NetworkChangeNotifier.addConnectionTypeObserver(Connecti
onRetry.this); | |
421 } | |
422 } | |
423 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); | |
424 } | |
425 | |
426 @Override | |
427 public void onConnectionTypeChanged(int connectionType) { | |
428 assert mNumTries.get() < MAX_TRIES; | |
429 if (NetworkChangeNotifier.isOnline()) { | |
430 // The network is back; stop listening and try again. | |
431 NetworkChangeNotifier.removeConnectionTypeObserver(this); | |
432 attempt(); | |
433 } | |
434 } | |
435 } | |
436 } | |
OLD | NEW |