mobile/android/base/sync/config/AccountPickler.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/sync/config/AccountPickler.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,182 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +package org.mozilla.gecko.sync.config;
     1.9 +
    1.10 +import java.io.FileOutputStream;
    1.11 +import java.io.PrintStream;
    1.12 +
    1.13 +import org.mozilla.gecko.background.common.log.Logger;
    1.14 +import org.mozilla.gecko.sync.ExtendedJSONObject;
    1.15 +import org.mozilla.gecko.sync.Utils;
    1.16 +import org.mozilla.gecko.sync.setup.Constants;
    1.17 +import org.mozilla.gecko.sync.setup.SyncAccounts;
    1.18 +import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
    1.19 +
    1.20 +import android.accounts.Account;
    1.21 +import android.content.Context;
    1.22 +
    1.23 +/**
    1.24 + * Bug 768102: Android deletes Account objects when the Authenticator that owns
    1.25 + * the Account disappears. This happens when an App is installed to the SD card
    1.26 + * and the SD card is un-mounted or the device is rebooted.
    1.27 + * <p>
    1.28 + * Bug 769745: Work around this by pickling the current Sync account data every
    1.29 + * sync.
    1.30 + * <p>
    1.31 + * Bug 735842: Work around this by un-pickling when we check if Sync accounts
    1.32 + * exist (called from Fennec).
    1.33 + * <p>
    1.34 + * Android just doesn't support installing Apps that define long-lived Services
    1.35 + * and/or own Account types onto the SD card. The documentation says not to do
    1.36 + * it. There are hordes of developers who want to do it, and have tried to
    1.37 + * register for almost every "package installation changed" broadcast intent
    1.38 + * that Android supports. They all explicitly state that the package that has
    1.39 + * changed does *not* receive the broadcast intent, thereby preventing an App
    1.40 + * from re-establishing its state.
    1.41 + * <p>
    1.42 + * <a href="http://developer.android.com/guide/topics/data/install-location.html">Reference.</a>
    1.43 + * <p>
    1.44 + * <b>Quote</b>: Your AbstractThreadedSyncAdapter and all its sync functionality
    1.45 + * will not work until external storage is remounted.
    1.46 + * <p>
    1.47 + * <b>Quote</b>: Your running Service will be killed and will not be restarted
    1.48 + * when external storage is remounted. You can, however, register for the
    1.49 + * ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify
    1.50 + * your application when applications installed on external storage have become
    1.51 + * available to the system again. At which time, you can restart your Service.
    1.52 + * <p>
    1.53 + * Problem: <a href="http://code.google.com/p/android/issues/detail?id=8485">that intent doesn't work</a>!
    1.54 + */
    1.55 +public class AccountPickler {
    1.56 +  public static final String LOG_TAG = "AccountPickler";
    1.57 +
    1.58 +  public static final long VERSION = 1;
    1.59 +
    1.60 +  /**
    1.61 +   * Remove Sync account persisted to disk.
    1.62 +   *
    1.63 +   * @param context Android context.
    1.64 +   * @param filename name of persisted pickle file; must not contain path separators.
    1.65 +   * @return <code>true</code> if given pickle existed and was successfully deleted.
    1.66 +   */
    1.67 +  public static boolean deletePickle(final Context context, final String filename) {
    1.68 +    return context.deleteFile(filename);
    1.69 +  }
    1.70 +
    1.71 +  /**
    1.72 +   * Persist Sync account to disk as a JSON object.
    1.73 +   * <p>
    1.74 +   * JSON object has keys:
    1.75 +   * <ul>
    1.76 +   * <li><code>Constants.JSON_KEY_ACCOUNT</code>: the Sync account's un-encoded username,
    1.77 +   * like "test@mozilla.com".</li>
    1.78 +   *
    1.79 +   * <li><code>Constants.JSON_KEY_PASSWORD</code>: the Sync account's password;</li>
    1.80 +   *
    1.81 +   * <li><code>Constants.JSON_KEY_SERVER</code>: the Sync account's server;</li>
    1.82 +   *
    1.83 +   * <li><code>Constants.JSON_KEY_SYNCKEY</code>: the Sync account's sync key;</li>
    1.84 +   *
    1.85 +   * <li><code>Constants.JSON_KEY_CLUSTER</code>: the Sync account's cluster (may be null);</li>
    1.86 +   *
    1.87 +   * <li><code>Constants.JSON_KEY_CLIENT_NAME</code>: the Sync account's client name (may be null);</li>
    1.88 +   *
    1.89 +   * <li><code>Constants.JSON_KEY_CLIENT_GUID</code>: the Sync account's client GUID (may be null);</li>
    1.90 +   *
    1.91 +   * <li><code>Constants.JSON_KEY_SYNC_AUTOMATICALLY</code>: true if the Android Account is syncing automically;</li>
    1.92 +   *
    1.93 +   * <li><code>Constants.JSON_KEY_VERSION</code>: version of this file;</li>
    1.94 +   *
    1.95 +   * <li><code>Constants.JSON_KEY_TIMESTAMP</code>: when this file was written.</li>
    1.96 +   * </ul>
    1.97 +   *
    1.98 +   *
    1.99 +   * @param context Android context.
   1.100 +   * @param filename name of file to persist to; must not contain path separators.
   1.101 +   * @param params the Sync account's parameters.
   1.102 +   * @param syncAutomatically whether the Android Account object is syncing automatically.
   1.103 +   */
   1.104 +  public static void pickle(final Context context, final String filename,
   1.105 +      final SyncAccountParameters params, final boolean syncAutomatically) {
   1.106 +    final ExtendedJSONObject o = params.asJSON();
   1.107 +    o.put(Constants.JSON_KEY_SYNC_AUTOMATICALLY, Boolean.valueOf(syncAutomatically));
   1.108 +    o.put(Constants.JSON_KEY_VERSION, new Long(VERSION));
   1.109 +    o.put(Constants.JSON_KEY_TIMESTAMP, new Long(System.currentTimeMillis()));
   1.110 +
   1.111 +    PrintStream ps = null;
   1.112 +    try {
   1.113 +      final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
   1.114 +      ps = new PrintStream(fos);
   1.115 +      ps.print(o.toJSONString());
   1.116 +      Logger.debug(LOG_TAG, "Persisted " + o.keySet().size() + " account settings to " + filename + ".");
   1.117 +    } catch (Exception e) {
   1.118 +      Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename + "; ignoring.", e);
   1.119 +    } finally {
   1.120 +      if (ps != null) {
   1.121 +        ps.close();
   1.122 +      }
   1.123 +    }
   1.124 +  }
   1.125 +
   1.126 +  /**
   1.127 +   * Create Android account from saved JSON object.
   1.128 +   *
   1.129 +   * @param context
   1.130 +   *          Android context.
   1.131 +   * @param filename
   1.132 +   *          name of file to read from; must not contain path separators.
   1.133 +   * @return created Android account, or null on error.
   1.134 +   */
   1.135 +  public static Account unpickle(final Context context, final String filename) {
   1.136 +    final String jsonString = Utils.readFile(context, filename);
   1.137 +    if (jsonString == null) {
   1.138 +      Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting.");
   1.139 +      return null;
   1.140 +    }
   1.141 +
   1.142 +    ExtendedJSONObject json = null;
   1.143 +    try {
   1.144 +      json = ExtendedJSONObject.parseJSONObject(jsonString);
   1.145 +    } catch (Exception e) {
   1.146 +      Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e);
   1.147 +      return null;
   1.148 +    }
   1.149 +
   1.150 +    SyncAccountParameters params = null;
   1.151 +    try {
   1.152 +      // Null checking of inputs is done in constructor.
   1.153 +      params = new SyncAccountParameters(context, null, json);
   1.154 +    } catch (IllegalArgumentException e) {
   1.155 +      Logger.warn(LOG_TAG, "Un-pickled data included null username, password, or serverURL; aborting.", e);
   1.156 +      return null;
   1.157 +    }
   1.158 +
   1.159 +    // Default to syncing automatically.
   1.160 +    boolean syncAutomatically = true;
   1.161 +    if (json.containsKey(Constants.JSON_KEY_SYNC_AUTOMATICALLY)) {
   1.162 +      if (Boolean.FALSE.equals(json.get(Constants.JSON_KEY_SYNC_AUTOMATICALLY))) {
   1.163 +        syncAutomatically = false;
   1.164 +      }
   1.165 +    }
   1.166 +
   1.167 +    final Account account = SyncAccounts.createSyncAccountPreservingExistingPreferences(params, syncAutomatically);
   1.168 +    if (account == null) {
   1.169 +      Logger.warn(LOG_TAG, "Failed to add Android Account; aborting.");
   1.170 +      return null;
   1.171 +    }
   1.172 +
   1.173 +    Integer version   = json.getIntegerSafely(Constants.JSON_KEY_VERSION);
   1.174 +    Integer timestamp = json.getIntegerSafely(Constants.JSON_KEY_TIMESTAMP);
   1.175 +    if (version == null || timestamp == null) {
   1.176 +      Logger.warn(LOG_TAG, "Did not find version or timestamp in pickle file; ignoring.");
   1.177 +      version = new Integer(-1);
   1.178 +      timestamp = new Integer(-1);
   1.179 +    }
   1.180 +
   1.181 +    Logger.info(LOG_TAG, "Un-pickled Android account named " + params.username + " (version " + version + ", pickled at " + timestamp + ").");
   1.182 +
   1.183 +    return account;
   1.184 +  }
   1.185 +}

mercurial