mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 957894: Update account pickling code for Firefox Accounts. r=nalexander
This commit is contained in:
parent
96d36654c2
commit
a302af9ad4
@ -561,6 +561,7 @@ sync_java_files = [
|
||||
'fxa/activities/FxAccountStatusFragment.java',
|
||||
'fxa/activities/FxAccountUpdateCredentialsActivity.java',
|
||||
'fxa/activities/FxAccountVerifiedAccountActivity.java',
|
||||
'fxa/authenticator/AccountPickler.java',
|
||||
'fxa/authenticator/AndroidFxAccount.java',
|
||||
'fxa/authenticator/FxAccountAuthenticator.java',
|
||||
'fxa/authenticator/FxAccountAuthenticatorService.java',
|
||||
@ -578,6 +579,8 @@ sync_java_files = [
|
||||
'fxa/login/State.java',
|
||||
'fxa/login/StateFactory.java',
|
||||
'fxa/login/TokensAndKeysState.java',
|
||||
'fxa/receivers/FxAccountDeletedReceiver.java',
|
||||
'fxa/receivers/FxAccountDeletedService.java',
|
||||
'fxa/sync/FxAccountGlobalSession.java',
|
||||
'fxa/sync/FxAccountNotificationManager.java',
|
||||
'fxa/sync/FxAccountSchedulePolicy.java',
|
||||
|
@ -4,6 +4,14 @@
|
||||
|
||||
package org.mozilla.gecko.fxa;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.fxa.authenticator.AccountPickler;
|
||||
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
|
||||
import org.mozilla.gecko.sync.ThreadPool;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.Context;
|
||||
@ -12,8 +20,10 @@ import android.content.Context;
|
||||
* Simple public accessors for Firefox account objects.
|
||||
*/
|
||||
public class FirefoxAccounts {
|
||||
private static final String LOG_TAG = FirefoxAccounts.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Return true if at least one Firefox account exists.
|
||||
* Returns true if a FirefoxAccount exists, false otherwise.
|
||||
*
|
||||
* @param context Android context.
|
||||
* @return true if at least one Firefox account exists.
|
||||
@ -24,12 +34,63 @@ public class FirefoxAccounts {
|
||||
|
||||
/**
|
||||
* Return Firefox accounts.
|
||||
* <p>
|
||||
* If no accounts exist in the AccountManager, one may be created
|
||||
* via a pickled FirefoxAccount, if available, and that account
|
||||
* will be added to the AccountManager and returned.
|
||||
* <p>
|
||||
* Note that this can be called from any thread.
|
||||
*
|
||||
* @param context Android context.
|
||||
* @return Firefox account objects.
|
||||
*/
|
||||
public static Account[] getFirefoxAccounts(final Context context) {
|
||||
return AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
|
||||
final Account[] accounts =
|
||||
AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
|
||||
if (accounts.length > 0) {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
final Account pickledAccount = getPickledAccount(context);
|
||||
return (pickledAccount != null) ? new Account[] {pickledAccount} : new Account[0];
|
||||
}
|
||||
|
||||
private static Account getPickledAccount(final Context context) {
|
||||
// To avoid a StrictMode violation for disk access, we call this from a background thread.
|
||||
// We do this every time, so the caller doesn't have to care.
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final Account[] accounts = new Account[1];
|
||||
ThreadPool.run(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final File file = context.getFileStreamPath(FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
|
||||
if (!file.exists()) {
|
||||
accounts[0] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// There is a small race window here: if the user creates a new Firefox account
|
||||
// between our checks, this could erroneously report that no Firefox accounts
|
||||
// exist.
|
||||
final AndroidFxAccount fxAccount =
|
||||
AccountPickler.unpickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
|
||||
accounts[0] = fxAccount.getAndroidAccount();
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await(); // Wait for the background thread to return.
|
||||
} catch (InterruptedException e) {
|
||||
Logger.warn(LOG_TAG,
|
||||
"Foreground thread unexpectedly interrupted while getting pickled account", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return accounts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,4 +104,4 @@ public class FirefoxAccounts {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,4 +34,36 @@ public class FxAccountConstants {
|
||||
public static final long MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS = 15 * 60 * 1000;
|
||||
|
||||
public static final String USER_AGENT = "Firefox-Android-FxAccounts/ (" + GlobalConstants.MOZ_APP_DISPLAYNAME + " " + GlobalConstants.MOZ_APP_VERSION + ")";
|
||||
|
||||
public static final String ACCOUNT_PICKLE_FILENAME = "fxa.account.json";
|
||||
|
||||
/**
|
||||
* This action is broadcast when an Android Firefox Account is deleted.
|
||||
* This allows each installed Firefox to delete any Firefox Account pickle
|
||||
* file.
|
||||
* <p>
|
||||
* It is protected by signing-level permission PER_ACCOUNT_TYPE_PERMISSION and
|
||||
* can be received only by Firefox channels sharing the same Android Firefox
|
||||
* Account type.
|
||||
* <p>
|
||||
* See {@link org.mozilla.gecko.fxa.AndroidFxAccount#makeDeletedAccountIntent(android.content.Context, android.accounts.Account)}
|
||||
* for contents of the intent.
|
||||
*
|
||||
* See bug 790931 for additional information in the context of Sync.
|
||||
*/
|
||||
public static final String ACCOUNT_DELETED_ACTION = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@.accounts.ACCOUNT_DELETED_ACTION";
|
||||
|
||||
/**
|
||||
* Version number of contents of SYNC_ACCOUNT_DELETED_ACTION intent.
|
||||
*/
|
||||
public static final long ACCOUNT_DELETED_INTENT_VERSION = 1;
|
||||
|
||||
public static final String ACCOUNT_DELETED_INTENT_VERSION_KEY = "account_deleted_intent_version";
|
||||
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_KEY = "account_deleted_intent_account";
|
||||
|
||||
/**
|
||||
* This signing-level permission protects broadcast intents that should be
|
||||
* received only by Firefox channels sharing the same Android Firefox Account type.
|
||||
*/
|
||||
public static final String PER_ACCOUNT_TYPE_PERMISSION = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE";
|
||||
}
|
||||
|
@ -67,15 +67,15 @@ public class FxAccountGetStartedActivity extends AccountAuthenticatorActivity {
|
||||
} else if (FirefoxAccounts.firefoxAccountsExist(this)) {
|
||||
intent = new Intent(this, FxAccountStatusActivity.class);
|
||||
}
|
||||
|
||||
if (intent != null) {
|
||||
this.setAccountAuthenticatorResult(null);
|
||||
setResult(RESULT_CANCELED);
|
||||
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
|
||||
// the soft keyboard not being shown for the started activity. Why, Android, why?
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
return;
|
||||
this.startActivity(intent);
|
||||
this.finish();
|
||||
}
|
||||
}
|
||||
|
||||
|
275
mobile/android/base/fxa/authenticator/AccountPickler.java
Normal file
275
mobile/android/base/fxa/authenticator/AccountPickler.java
Normal file
@ -0,0 +1,275 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.fxa.authenticator;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.fxa.login.State;
|
||||
import org.mozilla.gecko.fxa.login.State.StateLabel;
|
||||
import org.mozilla.gecko.fxa.login.StateFactory;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.NonObjectJSONException;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Android deletes Account objects when the Authenticator that owns the Account
|
||||
* disappears. This happens when an App is installed to the SD card and the SD
|
||||
* card is un-mounted or the device is rebooted.
|
||||
* <p>
|
||||
* We work around this by pickling the current Firefox account data every sync
|
||||
* and unpickling when we check if Firefox accounts exist (called from Fennec).
|
||||
* <p>
|
||||
* Android just doesn't support installing Apps that define long-lived Services
|
||||
* and/or own Account types onto the SD card. The documentation says not to do
|
||||
* it. There are hordes of developers who want to do it, and have tried to
|
||||
* register for almost every "package installation changed" broadcast intent
|
||||
* that Android supports. They all explicitly state that the package that has
|
||||
* changed does *not* receive the broadcast intent, thereby preventing an App
|
||||
* from re-establishing its state.
|
||||
* <p>
|
||||
* <a href="http://developer.android.com/guide/topics/data/install-location.html">Reference.</a>
|
||||
* <p>
|
||||
* <b>Quote</b>: Your AbstractThreadedSyncAdapter and all its sync functionality
|
||||
* will not work until external storage is remounted.
|
||||
* <p>
|
||||
* <b>Quote</b>: Your running Service will be killed and will not be restarted
|
||||
* when external storage is remounted. You can, however, register for the
|
||||
* ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify
|
||||
* your application when applications installed on external storage have become
|
||||
* available to the system again. At which time, you can restart your Service.
|
||||
* <p>
|
||||
* Problem: <a href="http://code.google.com/p/android/issues/detail?id=8485">that intent doesn't work</a>!
|
||||
* <p>
|
||||
* See bug 768102 for more information in the context of Sync.
|
||||
*/
|
||||
public class AccountPickler {
|
||||
public static final String LOG_TAG = AccountPickler.class.getSimpleName();
|
||||
|
||||
public static final long PICKLE_VERSION = 1;
|
||||
|
||||
private static final String KEY_PICKLE_VERSION = "pickle_version";
|
||||
private static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp";
|
||||
|
||||
private static final String KEY_ACCOUNT_VERSION = "account_version";
|
||||
private static final String KEY_ACCOUNT_TYPE = "account_type";
|
||||
private static final String KEY_EMAIL = "email";
|
||||
private static final String KEY_PROFILE = "profile";
|
||||
private static final String KEY_IDP_SERVER_URI = "idpServerURI";
|
||||
private static final String KEY_TOKEN_SERVER_URI = "tokenServerURI";
|
||||
private static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled";
|
||||
|
||||
private static final String KEY_BUNDLE = "bundle";
|
||||
|
||||
/**
|
||||
* Remove Firefox account persisted to disk.
|
||||
*
|
||||
* @param context Android context.
|
||||
* @param filename name of persisted pickle file; must not contain path separators.
|
||||
* @return <code>true</code> if given pickle existed and was successfully deleted.
|
||||
*/
|
||||
public static boolean deletePickle(final Context context, final String filename) {
|
||||
return context.deleteFile(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist Firefox account to disk as a JSON object.
|
||||
*
|
||||
* @param AndroidFxAccount the account to persist to disk
|
||||
* @param filename name of file to persist to; must not contain path separators.
|
||||
*/
|
||||
public static void pickle(final AndroidFxAccount account, final String filename) {
|
||||
final ExtendedJSONObject o = new ExtendedJSONObject();
|
||||
o.put(KEY_PICKLE_VERSION, Long.valueOf(PICKLE_VERSION));
|
||||
o.put(KEY_PICKLE_TIMESTAMP, Long.valueOf(System.currentTimeMillis()));
|
||||
|
||||
o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION);
|
||||
o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
|
||||
o.put(KEY_EMAIL, account.getEmail());
|
||||
o.put(KEY_PROFILE, account.getProfile());
|
||||
o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI());
|
||||
o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI());
|
||||
o.put(KEY_IS_SYNCING_ENABLED, account.isSyncingEnabled());
|
||||
|
||||
// TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs.
|
||||
|
||||
final ExtendedJSONObject bundle = account.unbundle();
|
||||
if (bundle == null) {
|
||||
Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting.");
|
||||
return;
|
||||
}
|
||||
o.put(KEY_BUNDLE, bundle);
|
||||
|
||||
writeToDisk(account.context, filename, o);
|
||||
}
|
||||
|
||||
private static void writeToDisk(final Context context, final String filename,
|
||||
final ExtendedJSONObject pickle) {
|
||||
try {
|
||||
final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
|
||||
try {
|
||||
final PrintStream ps = new PrintStream(fos);
|
||||
try {
|
||||
ps.print(pickle.toJSONString());
|
||||
Logger.debug(LOG_TAG, "Persisted " + pickle.keySet().size() +
|
||||
" account settings to " + filename + ".");
|
||||
} finally {
|
||||
ps.close();
|
||||
}
|
||||
} finally {
|
||||
fos.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename +
|
||||
"; ignoring.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Android account from saved JSON object. Assumes that an account does not exist.
|
||||
*
|
||||
* @param context
|
||||
* Android context.
|
||||
* @param filename
|
||||
* name of file to read from; must not contain path separators.
|
||||
* @return created Android account, or null on error.
|
||||
*/
|
||||
public static AndroidFxAccount unpickle(final Context context, final String filename) {
|
||||
final String jsonString = Utils.readFile(context, filename);
|
||||
if (jsonString == null) {
|
||||
Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting.");
|
||||
return null;
|
||||
}
|
||||
|
||||
ExtendedJSONObject json = null;
|
||||
try {
|
||||
json = ExtendedJSONObject.parseJSONObject(jsonString);
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
final UnpickleParams params;
|
||||
try {
|
||||
params = UnpickleParams.fromJSON(json);
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Got exception extracting unpickle json; aborting.", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
final AndroidFxAccount account;
|
||||
try {
|
||||
account = AndroidFxAccount.addAndroidAccount(context, params.email, params.profile,
|
||||
params.idpServerURI, params.tokenServerURI, params.state, params.accountVersion,
|
||||
params.isSyncingEnabled, true, params.bundle);
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Exception when adding Android Account; aborting.", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (account == null) {
|
||||
Logger.warn(LOG_TAG, "Failed to add Android Account; aborting.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Long timestamp = json.getLong(KEY_PICKLE_TIMESTAMP);
|
||||
if (timestamp == null) {
|
||||
Logger.warn(LOG_TAG, "Did not find timestamp in pickle file; ignoring.");
|
||||
timestamp = Long.valueOf(-1);
|
||||
}
|
||||
|
||||
Logger.info(LOG_TAG, "Un-pickled Android account named " + params.email + " (version " +
|
||||
params.pickleVersion + ", pickled at " + timestamp + ").");
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
private static class UnpickleParams {
|
||||
private Long pickleVersion;
|
||||
|
||||
private int accountVersion;
|
||||
private String email;
|
||||
private String profile;
|
||||
private String idpServerURI;
|
||||
private String tokenServerURI;
|
||||
private boolean isSyncingEnabled;
|
||||
|
||||
private ExtendedJSONObject bundle;
|
||||
private State state;
|
||||
|
||||
private UnpickleParams() {
|
||||
}
|
||||
|
||||
private static UnpickleParams fromJSON(final ExtendedJSONObject json)
|
||||
throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
|
||||
final UnpickleParams params = new UnpickleParams();
|
||||
params.pickleVersion = json.getLong(KEY_PICKLE_VERSION);
|
||||
if (params.pickleVersion == null) {
|
||||
throw new IllegalStateException("Pickle version not found.");
|
||||
}
|
||||
|
||||
switch (params.pickleVersion.intValue()) {
|
||||
case 1:
|
||||
params.unpickleV1(json);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unknown pickle version, " + params.pickleVersion + ".");
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
private void unpickleV1(final ExtendedJSONObject json)
|
||||
throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
// Sanity check.
|
||||
final String accountType = json.getString(KEY_ACCOUNT_TYPE);
|
||||
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
|
||||
throw new IllegalStateException("Account type has changed from, " + accountType +
|
||||
", to, " + FxAccountConstants.ACCOUNT_TYPE + ".");
|
||||
}
|
||||
|
||||
this.accountVersion = json.getIntegerSafely(KEY_ACCOUNT_VERSION);
|
||||
this.email = json.getString(KEY_EMAIL);
|
||||
this.profile = json.getString(KEY_PROFILE);
|
||||
this.idpServerURI = json.getString(KEY_IDP_SERVER_URI);
|
||||
this.tokenServerURI = json.getString(KEY_TOKEN_SERVER_URI);
|
||||
this.isSyncingEnabled = json.getBoolean(KEY_IS_SYNCING_ENABLED);
|
||||
|
||||
this.bundle = json.getObject(KEY_BUNDLE);
|
||||
if (bundle == null) {
|
||||
throw new IllegalStateException("Pickle bundle is null.");
|
||||
}
|
||||
this.state = getState(bundle);
|
||||
}
|
||||
|
||||
private State getState(final ExtendedJSONObject bundle) throws InvalidKeySpecException,
|
||||
NonObjectJSONException, NoSuchAlgorithmException {
|
||||
// TODO: Should copy-pasta BUNDLE_KEY_STATE & LABEL to this file to ensure we maintain
|
||||
// old versions?
|
||||
final StateLabel stateLabel = StateLabel.valueOf(
|
||||
bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE_LABEL));
|
||||
final String stateString = bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE);
|
||||
if (stateLabel == null) {
|
||||
throw new IllegalStateException("stateLabel must not be null");
|
||||
}
|
||||
if (stateString == null) {
|
||||
throw new IllegalStateException("stateString must not be null");
|
||||
}
|
||||
|
||||
try {
|
||||
return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("could not get state", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
|
||||
@ -41,6 +42,7 @@ public class AndroidFxAccount {
|
||||
|
||||
public static final int CURRENT_PREFS_VERSION = 1;
|
||||
|
||||
// When updating the account, do not forget to update AccountPickler.
|
||||
public static final int CURRENT_ACCOUNT_VERSION = 3;
|
||||
public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
|
||||
public static final String ACCOUNT_KEY_PROFILE = "profile";
|
||||
@ -82,6 +84,18 @@ public class AndroidFxAccount {
|
||||
this.accountManager = AccountManager.get(this.context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the Firefox account to disk as a JSON object. Note that this is a wrapper around
|
||||
* {@link AccountPickler#pickle}, and is identical to calling it directly.
|
||||
* <p>
|
||||
* Note that pickling is different from bundling, which involves operations on a
|
||||
* {@link android.os.Bundle Bundle} object of miscellaenous data associated with the account.
|
||||
* See {@link #persistBundle} and {@link #unbundle} for more.
|
||||
*/
|
||||
public void pickle(final String filename) {
|
||||
AccountPickler.pickle(this, filename);
|
||||
}
|
||||
|
||||
public Account getAndroidAccount() {
|
||||
return this.account;
|
||||
}
|
||||
@ -99,10 +113,18 @@ public class AndroidFxAccount {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given data as the internal bundle associated with this account.
|
||||
* @param bundle to write to account.
|
||||
*/
|
||||
protected void persistBundle(ExtendedJSONObject bundle) {
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the internal bundle associated with this account.
|
||||
* @return bundle associated with account.
|
||||
*/
|
||||
protected ExtendedJSONObject unbundle() {
|
||||
final int version = getAccountVersion();
|
||||
if (version < CURRENT_ACCOUNT_VERSION) {
|
||||
@ -275,6 +297,22 @@ public class AndroidFxAccount {
|
||||
String tokenServerURI,
|
||||
State state)
|
||||
throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
|
||||
return addAndroidAccount(context, email, profile, idpServerURI, tokenServerURI, state,
|
||||
CURRENT_ACCOUNT_VERSION, true, false, null);
|
||||
}
|
||||
|
||||
public static AndroidFxAccount addAndroidAccount(
|
||||
Context context,
|
||||
String email,
|
||||
String profile,
|
||||
String idpServerURI,
|
||||
String tokenServerURI,
|
||||
State state,
|
||||
final int accountVersion,
|
||||
final boolean syncEnabled,
|
||||
final boolean fromPickle,
|
||||
ExtendedJSONObject bundle)
|
||||
throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
|
||||
if (email == null) {
|
||||
throw new IllegalArgumentException("email must not be null");
|
||||
}
|
||||
@ -288,6 +326,12 @@ public class AndroidFxAccount {
|
||||
throw new IllegalArgumentException("state must not be null");
|
||||
}
|
||||
|
||||
// TODO: Add migration code.
|
||||
if (accountVersion != CURRENT_ACCOUNT_VERSION) {
|
||||
throw new IllegalStateException("Could not create account of version " + accountVersion +
|
||||
". Current version is " + CURRENT_ACCOUNT_VERSION + ".");
|
||||
}
|
||||
|
||||
// Android has internal restrictions that require all values in this
|
||||
// bundle to be strings. *sigh*
|
||||
Bundle userdata = new Bundle();
|
||||
@ -297,13 +341,15 @@ public class AndroidFxAccount {
|
||||
userdata.putString(ACCOUNT_KEY_AUDIENCE, FxAccountUtils.getAudienceForURL(tokenServerURI));
|
||||
userdata.putString(ACCOUNT_KEY_PROFILE, profile);
|
||||
|
||||
ExtendedJSONObject descriptor = new ExtendedJSONObject();
|
||||
if (bundle == null) {
|
||||
bundle = new ExtendedJSONObject();
|
||||
// TODO: How to upgrade?
|
||||
bundle.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
|
||||
}
|
||||
bundle.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
|
||||
bundle.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
|
||||
|
||||
descriptor.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
|
||||
descriptor.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
|
||||
|
||||
descriptor.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
|
||||
userdata.putString(ACCOUNT_KEY_DESCRIPTOR, descriptor.toJSONString());
|
||||
userdata.putString(ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
|
||||
|
||||
Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
|
||||
AccountManager accountManager = AccountManager.get(context);
|
||||
@ -316,8 +362,16 @@ public class AndroidFxAccount {
|
||||
}
|
||||
|
||||
AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
|
||||
fxAccount.clearSyncPrefs();
|
||||
fxAccount.enableSyncing();
|
||||
|
||||
if (!fromPickle) {
|
||||
fxAccount.clearSyncPrefs();
|
||||
}
|
||||
|
||||
if (syncEnabled) {
|
||||
fxAccount.enableSyncing();
|
||||
} else {
|
||||
fxAccount.disableSyncing();
|
||||
}
|
||||
|
||||
return fxAccount;
|
||||
}
|
||||
@ -326,6 +380,19 @@ public class AndroidFxAccount {
|
||||
getSyncPrefs().edit().clear().commit();
|
||||
}
|
||||
|
||||
public boolean isSyncingEnabled() {
|
||||
// TODO: Authority will be static in PR 426.
|
||||
final int result = ContentResolver.getIsSyncable(account, BrowserContract.AUTHORITY);
|
||||
if (result > 0) {
|
||||
return true;
|
||||
} else if (result == 0) {
|
||||
return false;
|
||||
} else {
|
||||
// This should not happen.
|
||||
throw new IllegalStateException("Sync enabled state unknown.");
|
||||
}
|
||||
}
|
||||
|
||||
public void enableSyncing() {
|
||||
Logger.info(LOG_TAG, "Disabling sync for account named like " + Utils.obfuscateEmail(getEmail()));
|
||||
for (String authority : new String[] { BrowserContract.AUTHORITY }) {
|
||||
@ -403,4 +470,22 @@ public class AndroidFxAccount {
|
||||
public String getEmail() {
|
||||
return account.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an intent announcing that a Firefox account will be deleted.
|
||||
*
|
||||
* @param context
|
||||
* Android context.
|
||||
* @param account
|
||||
* Android account being removed.
|
||||
* @return <code>Intent</code> to broadcast.
|
||||
*/
|
||||
public static Intent makeDeletedAccountIntent(final Context context, final Account account) {
|
||||
final Intent intent = new Intent(FxAccountConstants.ACCOUNT_DELETED_ACTION);
|
||||
|
||||
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY,
|
||||
Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION));
|
||||
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name);
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
|
@ -98,4 +98,47 @@ public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the account is going to be removed, broadcast an "account deleted"
|
||||
* intent. This allows us to clean up the account.
|
||||
* <p>
|
||||
* It is preferable to receive Android's LOGIN_ACCOUNTS_CHANGED_ACTION broadcast
|
||||
* than to create our own hacky broadcast here, but that doesn't include enough
|
||||
* information about which Accounts changed to correctly identify whether a Sync
|
||||
* account has been removed (when some Firefox channels are installed on the SD
|
||||
* card). We can work around this by storing additional state but it's both messy
|
||||
* and expensive because the broadcast is noisy.
|
||||
* <p>
|
||||
* Note that this is <b>not</b> called when an Android Account is blown away
|
||||
* due to the SD card being unmounted.
|
||||
*/
|
||||
@Override
|
||||
public Bundle getAccountRemovalAllowed(final AccountAuthenticatorResponse response, Account account)
|
||||
throws NetworkErrorException {
|
||||
Bundle result = super.getAccountRemovalAllowed(response, account);
|
||||
|
||||
if (result == null ||
|
||||
!result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) ||
|
||||
result.containsKey(AccountManager.KEY_INTENT)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
|
||||
if (!removalAllowed) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Broadcast a message to all Firefox channels sharing this Android
|
||||
// Account type telling that this Firefox account has been deleted.
|
||||
//
|
||||
// Broadcast intents protected with permissions are secure, so it's okay
|
||||
// to include private information such as a password.
|
||||
final Intent intent = AndroidFxAccount.makeDeletedAccountIntent(context, account);
|
||||
Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " +
|
||||
"broadcasting secure intent " + intent.getAction() + ".");
|
||||
context.sendBroadcast(intent, FxAccountConstants.PER_ACCOUNT_TYPE_PERMISSION);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.fxa.receivers;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class FxAccountDeletedReceiver extends BroadcastReceiver {
|
||||
public static final String LOG_TAG = FxAccountDeletedReceiver.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* This receiver can be killed as soon as it returns, but we have things to do
|
||||
* that can't be done on the main thread (network activity). Therefore we
|
||||
* start a service to do our clean up work for us, with Android doing the
|
||||
* heavy lifting for the service's lifecycle.
|
||||
* <p>
|
||||
* See <a href="http://developer.android.com/reference/android/content/BroadcastReceiver.html#ReceiverLifecycle">the Android documentation</a>
|
||||
* for details.
|
||||
*/
|
||||
@Override
|
||||
public void onReceive(final Context context, Intent broadcastIntent) {
|
||||
Logger.debug(LOG_TAG, "FxAccount deleted broadcast received.");
|
||||
|
||||
Intent serviceIntent = new Intent(context, FxAccountDeletedService.class);
|
||||
serviceIntent.putExtras(broadcastIntent);
|
||||
context.startService(serviceIntent);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.fxa.receivers;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.sync.config.AccountPickler;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* A background service to clean up after a Firefox Account is deleted.
|
||||
* <p>
|
||||
* Note that we specifically handle deleting the pickle file using a Service and a
|
||||
* BroadcastReceiver, rather than a background thread, to allow channels sharing a Firefox account
|
||||
* to delete their respective pickle files (since, if one remains, the account will be restored
|
||||
* when that channel is used).
|
||||
*/
|
||||
public class FxAccountDeletedService extends IntentService {
|
||||
public static final String LOG_TAG = FxAccountDeletedService.class.getSimpleName();
|
||||
|
||||
public FxAccountDeletedService() {
|
||||
super(LOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(final Intent intent) {
|
||||
final Context context = this;
|
||||
|
||||
long intentVersion = intent.getLongExtra(
|
||||
FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY, 0);
|
||||
long expectedVersion = FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION;
|
||||
if (intentVersion != expectedVersion) {
|
||||
Logger.warn(LOG_TAG, "Intent malformed: version " + intentVersion + " given but " +
|
||||
"version " + expectedVersion + "expected. Not cleaning up after deleted Account.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Android Account name, not Sync encoded account name.
|
||||
final String accountName = intent.getStringExtra(
|
||||
FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY);
|
||||
if (accountName == null) {
|
||||
Logger.warn(LOG_TAG, "Intent malformed: no account name given. Not cleaning up after " +
|
||||
"deleted Account.");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info(LOG_TAG, "Firefox account named " + accountName + " being removed; " +
|
||||
"deleting saved pickle file '" + FxAccountConstants.ACCOUNT_PICKLE_FILENAME + "'.");
|
||||
deletePickle(context);
|
||||
}
|
||||
|
||||
public static void deletePickle(final Context context) {
|
||||
try {
|
||||
AccountPickler.deletePickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
|
||||
} catch (Exception e) {
|
||||
// This should never happen, but we really don't want to die in a background thread.
|
||||
Logger.warn(LOG_TAG, "Got exception deleting saved pickle file; ignoring.", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import org.mozilla.gecko.browserid.RSACryptoImplementation;
|
||||
import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient;
|
||||
import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.fxa.authenticator.AccountPickler;
|
||||
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
|
||||
import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
|
||||
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
|
||||
@ -35,6 +36,7 @@ import org.mozilla.gecko.sync.GlobalSession;
|
||||
import org.mozilla.gecko.sync.PrefsBackoffHandler;
|
||||
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
|
||||
import org.mozilla.gecko.sync.SyncConfiguration;
|
||||
import org.mozilla.gecko.sync.ThreadPool;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
import org.mozilla.gecko.sync.crypto.KeyBundle;
|
||||
import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
|
||||
@ -440,6 +442,19 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
fxAccount.dump();
|
||||
}
|
||||
|
||||
// Pickle in a background thread to avoid strict mode warnings.
|
||||
ThreadPool.run(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
|
||||
} catch (Exception e) {
|
||||
// Should never happen, but we really don't want to die in a background thread.
|
||||
Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final SyncDelegate syncDelegate = new SyncDelegate(context, latch, syncResult, fxAccount, notificationManager);
|
||||
|
||||
|
@ -66,3 +66,11 @@
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedReceiver"
|
||||
android:permission="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE">
|
||||
<intent-filter>
|
||||
<action android:name="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@.accounts.ACCOUNT_DELETED_ACTION"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
@ -7,3 +7,12 @@
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
|
||||
<!-- A signature level permission granted only to the Firefox
|
||||
channels sharing an Android Account type. -->
|
||||
<permission
|
||||
android:name="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE"
|
||||
android:protectionLevel="signature">
|
||||
</permission>
|
||||
|
||||
<uses-permission android:name="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE" />
|
||||
|
@ -20,3 +20,7 @@
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/fxaccount_syncadapter" />
|
||||
</service>
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" >
|
||||
</service>
|
||||
|
@ -69,8 +69,6 @@
|
||||
android:name="org.mozilla.gecko.sync.receivers.SyncAccountDeletedReceiver"
|
||||
android:permission="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE">
|
||||
<intent-filter>
|
||||
<!-- This needs to be kept the same as
|
||||
GlobalConstants.SYNC_ACCOUNT_DELETED_ACTION. -->
|
||||
<action android:name="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.accounts.SYNC_ACCOUNT_DELETED_ACTION"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
@ -9,8 +9,7 @@
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
|
||||
<!-- A signature level permission granted only to the Firefox
|
||||
versions sharing an Android Account type. This needs to
|
||||
agree with GlobalConstants.PER_ACCOUNT_TYPE_PERMISSION. -->
|
||||
versions sharing an Android Account type. -->
|
||||
<permission
|
||||
android:name="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE"
|
||||
android:protectionLevel="signature">
|
||||
|
@ -22,6 +22,7 @@ BACKGROUND_TESTS_JAVA_FILES := \
|
||||
src/db/TestFennecTabsStorage.java \
|
||||
src/db/TestFormHistoryRepositorySession.java \
|
||||
src/db/TestPasswordsRepository.java \
|
||||
src/fxa/authenticator/TestAccountPickler.java \
|
||||
src/fxa/TestBrowserIDKeyPairGeneration.java \
|
||||
src/healthreport/MockDatabaseEnvironment.java \
|
||||
src/healthreport/MockHealthReportDatabaseStorage.java \
|
||||
|
@ -0,0 +1,119 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko.background.fxa.authenticator;
|
||||
|
||||
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.fxa.authenticator.AccountPickler;
|
||||
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
|
||||
import org.mozilla.gecko.fxa.login.Separated;
|
||||
import org.mozilla.gecko.fxa.login.State;
|
||||
import org.mozilla.gecko.background.sync.TestSyncAccounts;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.test.RenamingDelegatingContext;
|
||||
|
||||
public class TestAccountPickler extends AndroidSyncTestCase {
|
||||
private final static String FILENAME_PREFIX = "TestAccountPickler-";
|
||||
private final static String PICKLE_FILENAME = "pickle";
|
||||
|
||||
public Account account;
|
||||
public RenamingDelegatingContext context;
|
||||
public AccountManager accountManager;
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
this.account = null;
|
||||
// Randomize the filename prefix in case we don't clean up correctly.
|
||||
this.context = new RenamingDelegatingContext(getApplicationContext(), FILENAME_PREFIX +
|
||||
Math.random() * 1000001 + "-");
|
||||
this.accountManager = AccountManager.get(context);
|
||||
}
|
||||
|
||||
public void tearDown() {
|
||||
if (this.account != null) {
|
||||
deleteAccount(this, this.accountManager, this.account);
|
||||
this.account = null;
|
||||
}
|
||||
this.context.deleteFile(PICKLE_FILENAME);
|
||||
}
|
||||
|
||||
public static void deleteAccount(final InstrumentationTestCase test,
|
||||
final AccountManager accountManager, final Account account) {
|
||||
TestSyncAccounts.deleteAccount(test, accountManager, account);
|
||||
}
|
||||
|
||||
private boolean accountsExist() {
|
||||
// Note that we don't use FirefoxAccounts.firefoxAccountsExist because it unpickles.
|
||||
return AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE).length > 0;
|
||||
}
|
||||
|
||||
public AndroidFxAccount addDummyAccount() throws Exception {
|
||||
final String email = "iu@fakedomain.io";
|
||||
final State state = new Separated(email, "uid", false); // State choice is arbitrary.
|
||||
final AndroidFxAccount account = AndroidFxAccount.addAndroidAccount(context, email,
|
||||
"profile", "serverURI", "tokenServerURI", state);
|
||||
assertNotNull(account);
|
||||
assertTrue(accountsExist()); // Sanity check.
|
||||
this.account = account.getAndroidAccount(); // To remove in tearDown() if we throw.
|
||||
return account;
|
||||
}
|
||||
|
||||
public void testPickleAndUnpickle() throws Exception {
|
||||
final AndroidFxAccount inputAccount = addDummyAccount();
|
||||
// Sync is enabled by default so we do a more thorough test by disabling it.
|
||||
inputAccount.disableSyncing();
|
||||
|
||||
AccountPickler.pickle(inputAccount, PICKLE_FILENAME);
|
||||
|
||||
// unpickle adds an account to the AccountManager so delete it first.
|
||||
deleteAccount(this, this.accountManager, inputAccount.getAndroidAccount());
|
||||
assertFalse(accountsExist());
|
||||
|
||||
final AndroidFxAccount unpickledAccount =
|
||||
AccountPickler.unpickle(context, PICKLE_FILENAME);
|
||||
assertNotNull(unpickledAccount);
|
||||
this.account = unpickledAccount.getAndroidAccount(); // To remove in tearDown().
|
||||
assertAccountsEquals(inputAccount, unpickledAccount);
|
||||
}
|
||||
|
||||
public void testDeletePickle() throws Exception {
|
||||
final AndroidFxAccount account = addDummyAccount();
|
||||
AccountPickler.pickle(account, PICKLE_FILENAME);
|
||||
|
||||
final String s = Utils.readFile(context, PICKLE_FILENAME);
|
||||
assertNotNull(s);
|
||||
assertTrue(s.length() > 0);
|
||||
|
||||
AccountPickler.deletePickle(context, PICKLE_FILENAME);
|
||||
org.mozilla.gecko.background.sync.TestAccountPickler.assertFileNotPresent(
|
||||
context, PICKLE_FILENAME);
|
||||
}
|
||||
|
||||
private void assertAccountsEquals(final AndroidFxAccount expected,
|
||||
final AndroidFxAccount actual) throws Exception {
|
||||
// TODO: Write and use AndroidFxAccount.equals
|
||||
// TODO: protected.
|
||||
//assertEquals(expected.getAccountVersion(), actual.getAccountVersion());
|
||||
assertEquals(expected.getProfile(), actual.getProfile());
|
||||
assertEquals(expected.getAccountServerURI(), actual.getAccountServerURI());
|
||||
assertEquals(expected.getAudience(), actual.getAudience());
|
||||
assertEquals(expected.getTokenServerURI(), actual.getTokenServerURI());
|
||||
assertEquals(expected.getSyncPrefsPath(), actual.getSyncPrefsPath());
|
||||
assertEquals(expected.isSyncingEnabled(), actual.isSyncingEnabled());
|
||||
assertEquals(expected.getEmail(), actual.getEmail());
|
||||
assertStateEquals(expected.getState(), actual.getState());
|
||||
}
|
||||
|
||||
private void assertStateEquals(final State expected, final State actual) throws Exception {
|
||||
// TODO: Write and use State.equals. Thus, this is only thorough for the State base class.
|
||||
assertEquals(expected.getStateLabel(), actual.getStateLabel());
|
||||
assertEquals(expected.email, actual.email);
|
||||
assertEquals(expected.uid, actual.uid);
|
||||
assertEquals(expected.verified, actual.verified);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user