mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
276 lines
11 KiB
Java
276 lines
11 KiB
Java
/* 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);
|
|
}
|
|
}
|
|
}
|
|
}
|