mobile/android/base/sync/setup/SyncAccounts.java

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:0ed005f21cd5
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 package org.mozilla.gecko.sync.setup;
6
7 import java.io.File;
8 import java.io.UnsupportedEncodingException;
9 import java.security.NoSuchAlgorithmException;
10
11 import org.mozilla.gecko.background.common.GlobalConstants;
12 import org.mozilla.gecko.background.common.log.Logger;
13 import org.mozilla.gecko.db.BrowserContract;
14 import org.mozilla.gecko.sync.CredentialException;
15 import org.mozilla.gecko.sync.ExtendedJSONObject;
16 import org.mozilla.gecko.sync.SyncConfiguration;
17 import org.mozilla.gecko.sync.SyncConstants;
18 import org.mozilla.gecko.sync.ThreadPool;
19 import org.mozilla.gecko.sync.Utils;
20 import org.mozilla.gecko.sync.config.AccountPickler;
21 import org.mozilla.gecko.sync.repositories.android.RepoUtils;
22
23 import android.accounts.Account;
24 import android.accounts.AccountManager;
25 import android.content.ActivityNotFoundException;
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.os.Bundle;
32 import android.provider.Settings;
33 import android.util.Log;
34
35 /**
36 * This class contains utilities that are of use to Fennec
37 * and Sync setup activities.
38 * <p>
39 * Do not break these APIs without correcting upstream code!
40 */
41 public class SyncAccounts {
42 private static final String LOG_TAG = "SyncAccounts";
43
44 private static final String MOTO_BLUR_SETTINGS_ACTIVITY = "com.motorola.blur.settings.AccountsAndServicesPreferenceActivity";
45 private static final String MOTO_BLUR_PACKAGE = "com.motorola.blur.setup";
46
47 /**
48 * Return Sync accounts.
49 *
50 * @param c
51 * Android context.
52 * @return Sync accounts.
53 */
54 public static Account[] syncAccounts(final Context c) {
55 return AccountManager.get(c).getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC);
56 }
57
58 /**
59 * Returns true if a Sync account is set up, or we have a pickled Sync account
60 * on disk that should be un-pickled (Bug 769745). If we have a pickled Sync
61 * account, try to un-pickle it and create the corresponding Sync account.
62 * <p>
63 * Do not call this method from the main thread.
64 */
65 public static boolean syncAccountsExist(Context c) {
66 final boolean accountsExist = AccountManager.get(c).getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC).length > 0;
67 if (accountsExist) {
68 return true;
69 }
70
71 final File file = c.getFileStreamPath(Constants.ACCOUNT_PICKLE_FILENAME);
72 if (!file.exists()) {
73 return false;
74 }
75
76 // There is a small race window here: if the user creates a new Sync account
77 // between our checks, this could erroneously report that no Sync accounts
78 // exist.
79 final Account account = AccountPickler.unpickle(c, Constants.ACCOUNT_PICKLE_FILENAME);
80 return (account != null);
81 }
82
83 /**
84 * This class encapsulates the parameters needed to create a new Firefox Sync
85 * account.
86 */
87 public static class SyncAccountParameters {
88 public final Context context;
89 public final AccountManager accountManager;
90
91
92 public final String username; // services.sync.account
93 public final String syncKey; // in password manager: "chrome://weave (Mozilla Services Encryption Passphrase)"
94 public final String password; // in password manager: "chrome://weave (Mozilla Services Password)"
95 public final String serverURL; // services.sync.serverURL
96 public final String clusterURL; // services.sync.clusterURL
97 public final String clientName; // services.sync.client.name
98 public final String clientGuid; // services.sync.client.GUID
99
100 /**
101 * Encapsulate the parameters needed to create a new Firefox Sync account.
102 *
103 * @param context
104 * the current <code>Context</code>; cannot be null.
105 * @param accountManager
106 * an <code>AccountManager</code> instance to use; if null, get it
107 * from <code>context</code>.
108 * @param username
109 * the desired username; cannot be null.
110 * @param syncKey
111 * the desired sync key; cannot be null.
112 * @param password
113 * the desired password; cannot be null.
114 * @param serverURL
115 * the server URL to use; if null, use the default.
116 * @param clusterURL
117 * the cluster URL to use; if null, a fresh cluster URL will be
118 * retrieved from the server during the next sync.
119 * @param clientName
120 * the client name; if null, a fresh client record will be uploaded
121 * to the server during the next sync.
122 * @param clientGuid
123 * the client GUID; if null, a fresh client record will be uploaded
124 * to the server during the next sync.
125 */
126 public SyncAccountParameters(Context context, AccountManager accountManager,
127 String username, String syncKey, String password,
128 String serverURL, String clusterURL,
129 String clientName, String clientGuid) {
130 if (context == null) {
131 throw new IllegalArgumentException("Null context passed to SyncAccountParameters constructor.");
132 }
133 if (username == null) {
134 throw new IllegalArgumentException("Null username passed to SyncAccountParameters constructor.");
135 }
136 if (syncKey == null) {
137 throw new IllegalArgumentException("Null syncKey passed to SyncAccountParameters constructor.");
138 }
139 if (password == null) {
140 throw new IllegalArgumentException("Null password passed to SyncAccountParameters constructor.");
141 }
142 this.context = context;
143 this.accountManager = accountManager;
144 this.username = username;
145 this.syncKey = syncKey;
146 this.password = password;
147 this.serverURL = serverURL;
148 this.clusterURL = clusterURL;
149 this.clientName = clientName;
150 this.clientGuid = clientGuid;
151 }
152
153 public SyncAccountParameters(Context context, AccountManager accountManager,
154 String username, String syncKey, String password, String serverURL) {
155 this(context, accountManager, username, syncKey, password, serverURL, null, null, null);
156 }
157
158 public SyncAccountParameters(final Context context, final AccountManager accountManager, final ExtendedJSONObject o) {
159 this(context, accountManager,
160 o.getString(Constants.JSON_KEY_ACCOUNT),
161 o.getString(Constants.JSON_KEY_SYNCKEY),
162 o.getString(Constants.JSON_KEY_PASSWORD),
163 o.getString(Constants.JSON_KEY_SERVER),
164 o.getString(Constants.JSON_KEY_CLUSTER),
165 o.getString(Constants.JSON_KEY_CLIENT_NAME),
166 o.getString(Constants.JSON_KEY_CLIENT_GUID));
167 }
168
169 public ExtendedJSONObject asJSON() {
170 final ExtendedJSONObject o = new ExtendedJSONObject();
171 o.put(Constants.JSON_KEY_ACCOUNT, username);
172 o.put(Constants.JSON_KEY_PASSWORD, password);
173 o.put(Constants.JSON_KEY_SERVER, serverURL);
174 o.put(Constants.JSON_KEY_SYNCKEY, syncKey);
175 o.put(Constants.JSON_KEY_CLUSTER, clusterURL);
176 o.put(Constants.JSON_KEY_CLIENT_NAME, clientName);
177 o.put(Constants.JSON_KEY_CLIENT_GUID, clientGuid);
178 return o;
179 }
180 }
181
182 /**
183 * Create a sync account, clearing any existing preferences, and set it to
184 * sync automatically.
185 * <p>
186 * Do not call this method from the main thread.
187 *
188 * @param syncAccount
189 * parameters of the account to be created.
190 * @return created <code>Account</code>, or null if an error occurred and the
191 * account could not be added.
192 */
193 public static Account createSyncAccount(SyncAccountParameters syncAccount) {
194 return createSyncAccount(syncAccount, true, true);
195 }
196
197 /**
198 * Create a sync account, clearing any existing preferences.
199 * <p>
200 * Do not call this method from the main thread.
201 * <p>
202 * Intended for testing; use
203 * <code>createSyncAccount(SyncAccountParameters)</code> instead.
204 *
205 * @param syncAccount
206 * parameters of the account to be created.
207 * @param syncAutomatically
208 * whether to start syncing this Account automatically (
209 * <code>false</code> for test accounts).
210 * @return created Android <code>Account</code>, or null if an error occurred
211 * and the account could not be added.
212 */
213 public static Account createSyncAccount(SyncAccountParameters syncAccount,
214 boolean syncAutomatically) {
215 return createSyncAccount(syncAccount, syncAutomatically, true);
216 }
217
218 public static Account createSyncAccountPreservingExistingPreferences(SyncAccountParameters syncAccount,
219 boolean syncAutomatically) {
220 return createSyncAccount(syncAccount, syncAutomatically, false);
221 }
222
223 /**
224 * Create a sync account.
225 * <p>
226 * Do not call this method from the main thread.
227 * <p>
228 * Intended for testing; use
229 * <code>createSyncAccount(SyncAccountParameters)</code> instead.
230 *
231 * @param syncAccount
232 * parameters of the account to be created.
233 * @param syncAutomatically
234 * whether to start syncing this Account automatically (
235 * <code>false</code> for test accounts).
236 * @param clearPreferences
237 * <code>true</code> to clear existing preferences before creating.
238 * @return created Android <code>Account</code>, or null if an error occurred
239 * and the account could not be added.
240 */
241 protected static Account createSyncAccount(SyncAccountParameters syncAccount,
242 boolean syncAutomatically, boolean clearPreferences) {
243 final Context context = syncAccount.context;
244 final AccountManager accountManager = (syncAccount.accountManager == null) ?
245 AccountManager.get(syncAccount.context) : syncAccount.accountManager;
246 final String username = syncAccount.username;
247 final String syncKey = syncAccount.syncKey;
248 final String password = syncAccount.password;
249 final String serverURL = (syncAccount.serverURL == null) ?
250 SyncConstants.DEFAULT_AUTH_SERVER : syncAccount.serverURL;
251
252 Logger.debug(LOG_TAG, "Using account manager " + accountManager);
253 if (!RepoUtils.stringsEqual(syncAccount.serverURL, SyncConstants.DEFAULT_AUTH_SERVER)) {
254 Logger.info(LOG_TAG, "Setting explicit server URL: " + serverURL);
255 }
256
257 final Account account = new Account(username, SyncConstants.ACCOUNTTYPE_SYNC);
258 final Bundle userbundle = new Bundle();
259
260 // Add sync key and server URL.
261 userbundle.putString(Constants.OPTION_SYNCKEY, syncKey);
262 userbundle.putString(Constants.OPTION_SERVER, serverURL);
263 Logger.debug(LOG_TAG, "Adding account for " + SyncConstants.ACCOUNTTYPE_SYNC);
264 boolean result = false;
265 try {
266 result = accountManager.addAccountExplicitly(account, password, userbundle);
267 } catch (SecurityException e) {
268 // We use Log rather than Logger here to avoid possibly hiding these errors.
269 final String message = e.getMessage();
270 if (message != null && (message.indexOf("is different than the authenticator's uid") > 0)) {
271 Log.wtf(SyncConstants.GLOBAL_LOG_TAG,
272 "Unable to create account. " +
273 "If you have more than one version of " +
274 "Firefox/Beta/Aurora/Nightly/Fennec installed, that's why.",
275 e);
276 } else {
277 Log.e(SyncConstants.GLOBAL_LOG_TAG, "Unable to create account.", e);
278 }
279 }
280
281 if (!result) {
282 Logger.error(LOG_TAG, "Failed to add account " + account + "!");
283 return null;
284 }
285 Logger.debug(LOG_TAG, "Account " + account + " added successfully.");
286
287 setSyncAutomatically(account, syncAutomatically);
288 setIsSyncable(account, syncAutomatically);
289 Logger.debug(LOG_TAG, "Set account to sync automatically? " + syncAutomatically + ".");
290
291 try {
292 final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
293 final String profile = Constants.DEFAULT_PROFILE;
294 final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
295
296 final SharedPreferences.Editor editor = Utils.getSharedPreferences(context, product, username, serverURL, profile, version).edit();
297 if (clearPreferences) {
298 final String prefsPath = Utils.getPrefsPath(product, username, serverURL, profile, version);
299 Logger.info(LOG_TAG, "Clearing preferences path " + prefsPath + " for this account.");
300 editor.clear();
301 }
302
303 if (syncAccount.clusterURL != null) {
304 editor.putString(SyncConfiguration.PREF_CLUSTER_URL, syncAccount.clusterURL);
305 }
306
307 if (syncAccount.clientName != null && syncAccount.clientGuid != null) {
308 Logger.debug(LOG_TAG, "Setting client name to " + syncAccount.clientName + " and client GUID to " + syncAccount.clientGuid + ".");
309 editor.putString(SyncConfiguration.PREF_CLIENT_NAME, syncAccount.clientName);
310 editor.putString(SyncConfiguration.PREF_ACCOUNT_GUID, syncAccount.clientGuid);
311 } else {
312 Logger.debug(LOG_TAG, "Client name and guid not both non-null, so not setting client data.");
313 }
314
315 editor.commit();
316 } catch (Exception e) {
317 Logger.error(LOG_TAG, "Could not clear prefs path!", e);
318 }
319 return account;
320 }
321
322 public static void setIsSyncable(Account account, boolean isSyncable) {
323 String authority = BrowserContract.AUTHORITY;
324 ContentResolver.setIsSyncable(account, authority, isSyncable ? 1 : 0);
325 }
326
327 public static void setSyncAutomatically(Account account, boolean syncAutomatically) {
328 if (syncAutomatically) {
329 ContentResolver.setMasterSyncAutomatically(true);
330 }
331
332 String authority = BrowserContract.AUTHORITY;
333 Logger.debug(LOG_TAG, "Setting authority " + authority + " to " +
334 (syncAutomatically ? "" : "not ") + "sync automatically.");
335 ContentResolver.setSyncAutomatically(account, authority, syncAutomatically);
336 }
337
338 public static void backgroundSetSyncAutomatically(final Account account, final boolean syncAutomatically) {
339 ThreadPool.run(new Runnable() {
340 @Override
341 public void run() {
342 setSyncAutomatically(account, syncAutomatically);
343 }
344 });
345 }
346 /**
347 * Bug 721760: try to start a vendor-specific Accounts & Sync activity on Moto
348 * Blur devices.
349 * <p>
350 * Bug 773562: actually start and catch <code>ActivityNotFoundException</code>,
351 * rather than just returning the <code>Intent</code> only, because some
352 * Moto devices fail to start the activity.
353 *
354 * @param context
355 * current Android context.
356 * @param vendorPackage
357 * vendor specific package name.
358 * @param vendorClass
359 * vendor specific class name.
360 * @return null on failure, otherwise the <code>Intent</code> started.
361 */
362 protected static Intent openVendorSyncSettings(Context context, final String vendorPackage, final String vendorClass) {
363 try {
364 final int contextFlags = Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY;
365 Context foreignContext = context.createPackageContext(vendorPackage, contextFlags);
366 Class<?> klass = foreignContext.getClassLoader().loadClass(vendorClass);
367
368 final Intent intent = new Intent(foreignContext, klass);
369 context.startActivity(intent);
370 Logger.info(LOG_TAG, "Vendor package " + vendorPackage + " and class " +
371 vendorClass + " found, and activity launched.");
372 return intent;
373 } catch (NameNotFoundException e) {
374 Logger.debug(LOG_TAG, "Vendor package " + vendorPackage + " not found. Skipping.");
375 } catch (ClassNotFoundException e) {
376 Logger.debug(LOG_TAG, "Vendor package " + vendorPackage + " found but class " +
377 vendorClass + " not found. Skipping.", e);
378 } catch (ActivityNotFoundException e) {
379 // Bug 773562 - android.content.ActivityNotFoundException on Motorola devices.
380 Logger.warn(LOG_TAG, "Vendor package " + vendorPackage + " and class " +
381 vendorClass + " found, but activity not launched. Skipping.", e);
382 } catch (Exception e) {
383 // Just in case.
384 Logger.warn(LOG_TAG, "Caught exception launching activity from vendor package " + vendorPackage +
385 " and class " + vendorClass + ". Ignoring.", e);
386 }
387 return null;
388 }
389
390 /**
391 * Start Sync settings activity.
392 *
393 * @param context
394 * current Android context.
395 * @return the <code>Intent</code> started.
396 */
397 public static Intent openSyncSettings(Context context) {
398 // Bug 721760 - opening Sync settings takes user to Battery & Data Manager
399 // on a variety of Motorola devices. This work around tries to load the
400 // correct Intent by hand. Oh, Android.
401 Intent intent = openVendorSyncSettings(context, MOTO_BLUR_PACKAGE, MOTO_BLUR_SETTINGS_ACTIVITY);
402 if (intent != null) {
403 return intent;
404 }
405
406 // Open default Sync settings activity.
407 intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
408 // Bug 774233: do not start activity as a new task (second run fails on some HTC devices).
409 context.startActivity(intent); // We should always find this Activity.
410 return intent;
411 }
412
413 /**
414 * Synchronously extract Sync account parameters from Android account version
415 * 0, using plain auth token type.
416 * <p>
417 * Safe to call from main thread.
418 *
419 * @param context
420 * Android context.
421 * @param accountManager
422 * Android account manager.
423 * @param account
424 * Android Account.
425 * @return Sync account parameters, always non-null; fields username,
426 * password, serverURL, and syncKey always non-null.
427 */
428 public static SyncAccountParameters blockingFromAndroidAccountV0(final Context context, final AccountManager accountManager, final Account account)
429 throws CredentialException {
430 String username;
431 try {
432 username = Utils.usernameFromAccount(account.name);
433 } catch (NoSuchAlgorithmException e) {
434 throw new CredentialException.MissingCredentialException("username");
435 } catch (UnsupportedEncodingException e) {
436 throw new CredentialException.MissingCredentialException("username");
437 }
438
439 /*
440 * If we are accessing an Account that we don't own, Android will throw an
441 * unchecked <code>SecurityException</code> saying
442 * "W FxSync(XXXX) java.lang.SecurityException: caller uid XXXXX is different than the authenticator's uid".
443 * We catch that error and throw accordingly.
444 */
445 String password;
446 String syncKey;
447 String serverURL;
448 try {
449 password = accountManager.getPassword(account);
450 syncKey = accountManager.getUserData(account, Constants.OPTION_SYNCKEY);
451 serverURL = accountManager.getUserData(account, Constants.OPTION_SERVER);
452 } catch (SecurityException e) {
453 Logger.warn(LOG_TAG, "Got security exception fetching Sync account parameters; throwing.");
454 throw new CredentialException.MissingAllCredentialsException(e);
455 }
456
457 if (password == null &&
458 username == null &&
459 syncKey == null &&
460 serverURL == null) {
461 throw new CredentialException.MissingAllCredentialsException();
462 }
463
464 if (password == null) {
465 throw new CredentialException.MissingCredentialException("password");
466 }
467
468 if (syncKey == null) {
469 throw new CredentialException.MissingCredentialException("syncKey");
470 }
471
472 if (serverURL == null) {
473 throw new CredentialException.MissingCredentialException("serverURL");
474 }
475
476 try {
477 // SyncAccountParameters constructor throws on null inputs. This shouldn't
478 // happen, but let's be safe.
479 return new SyncAccountParameters(context, accountManager, username, syncKey, password, serverURL);
480 } catch (Exception e) {
481 Logger.warn(LOG_TAG, "Got exception fetching Sync account parameters; throwing.");
482 throw new CredentialException.MissingAllCredentialsException(e);
483 }
484 }
485
486 /**
487 * Bug 790931: create an intent announcing that a Sync account will be
488 * deleted.
489 * <p>
490 * This intent <b>must</b> be broadcast with secure permissions, because it
491 * contains sensitive user information including the Sync account password and
492 * Sync key.
493 * <p>
494 * Version 1 of the created intent includes extras with keys
495 * <code>Constants.JSON_KEY_VERSION</code>,
496 * <code>Constants.JSON_KEY_TIMESTAMP</code>, and
497 * <code>Constants.JSON_KEY_ACCOUNT</code> (which is the Android Account name,
498 * not the encoded Sync Account name).
499 * <p>
500 * If possible, it contains the key <code>Constants.JSON_KEY_PAYLOAD</code>
501 * with value the Sync account parameters as JSON, <b>except the Sync key has
502 * been replaced with the empty string</b>. (We replace, rather than remove,
503 * the Sync key because SyncAccountParameters expects a non-null Sync key.)
504 *
505 * @see SyncAccountParameters#asJSON
506 *
507 * @param context
508 * Android context.
509 * @param accountManager
510 * Android account manager.
511 * @param account
512 * Android account being removed.
513 * @return <code>Intent</code> to broadcast.
514 */
515 public static Intent makeSyncAccountDeletedIntent(final Context context, final AccountManager accountManager, final Account account) {
516 final Intent intent = new Intent(SyncConstants.SYNC_ACCOUNT_DELETED_ACTION);
517
518 intent.putExtra(Constants.JSON_KEY_VERSION, Long.valueOf(SyncConstants.SYNC_ACCOUNT_DELETED_INTENT_VERSION));
519 intent.putExtra(Constants.JSON_KEY_TIMESTAMP, Long.valueOf(System.currentTimeMillis()));
520 intent.putExtra(Constants.JSON_KEY_ACCOUNT, account.name);
521
522 SyncAccountParameters accountParameters = null;
523 try {
524 accountParameters = SyncAccounts.blockingFromAndroidAccountV0(context, accountManager, account);
525 } catch (Exception e) {
526 Logger.warn(LOG_TAG, "Caught exception fetching account parameters.", e);
527 }
528
529 if (accountParameters != null) {
530 ExtendedJSONObject json = accountParameters.asJSON();
531 json.put(Constants.JSON_KEY_SYNCKEY, ""); // Reduce attack surface area by removing Sync key.
532 intent.putExtra(Constants.JSON_KEY_PAYLOAD, json.toJSONString());
533 }
534
535 return intent;
536 }
537
538 /**
539 * Synchronously fetch SharedPreferences of a profile associated with a Sync
540 * account.
541 * <p>
542 * Safe to call from main thread.
543 *
544 * @param context
545 * Android context.
546 * @param accountManager
547 * Android account manager.
548 * @param account
549 * Android Account.
550 * @param product
551 * package.
552 * @param profile
553 * of account.
554 * @param version
555 * number.
556 * @return SharedPreferences associated with Sync account.
557 * @throws CredentialException
558 * @throws NoSuchAlgorithmException
559 * @throws UnsupportedEncodingException
560 */
561 public static SharedPreferences blockingPrefsFromAndroidAccountV0(final Context context, final AccountManager accountManager, final Account account,
562 final String product, final String profile, final long version)
563 throws CredentialException, NoSuchAlgorithmException, UnsupportedEncodingException {
564 SyncAccountParameters params = SyncAccounts.blockingFromAndroidAccountV0(context, accountManager, account);
565 String prefsPath = Utils.getPrefsPath(product, params.username, params.serverURL, profile, version);
566
567 return context.getSharedPreferences(prefsPath, Utils.SHARED_PREFERENCES_MODE);
568 }
569
570 /**
571 * Synchronously fetch SharedPreferences of a profile associated with the
572 * default Firefox profile of a Sync Account.
573 * <p>
574 * Uses the default package, default profile, and current version.
575 * <p>
576 * Safe to call from main thread.
577 *
578 * @param context
579 * Android context.
580 * @param accountManager
581 * Android account manager.
582 * @param account
583 * Android Account.
584 * @return SharedPreferences associated with Sync account.
585 * @throws CredentialException
586 * @throws NoSuchAlgorithmException
587 * @throws UnsupportedEncodingException
588 */
589 public static SharedPreferences blockingPrefsFromDefaultProfileV0(final Context context, final AccountManager accountManager, final Account account)
590 throws CredentialException, NoSuchAlgorithmException, UnsupportedEncodingException {
591 final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
592 final String profile = Constants.DEFAULT_PROFILE;
593 final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
594
595 return blockingPrefsFromAndroidAccountV0(context, accountManager, account, product, profile, version);
596 }
597 }

mercurial