/* 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.sync.config; import java.io.FileOutputStream; import java.io.PrintStream; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.Logger; import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.sync.setup.Constants; import org.mozilla.gecko.sync.setup.SyncAccounts; import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters; import android.accounts.Account; import android.content.Context; /** * Bug 768102: 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> * Bug 769745: Work around this by pickling the current Sync account data every * sync. * <p> * Bug 735842: Work around this by un-pickling when we check if Sync 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>! */ public class AccountPickler { public static final String LOG_TAG = "AccountPickler"; public static final long VERSION = 1; /** * Remove Sync 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 Sync account to disk as a JSON object. * <p> * JSON object has keys: * <ul> * <li><code>Constants.JSON_KEY_ACCOUNT</code>: the Sync account's un-encoded username, * like "test@mozilla.com".</li> * * <li><code>Constants.JSON_KEY_PASSWORD</code>: the Sync account's password;</li> * * <li><code>Constants.JSON_KEY_SERVER</code>: the Sync account's server;</li> * * <li><code>Constants.JSON_KEY_SYNCKEY</code>: the Sync account's sync key;</li> * * <li><code>Constants.JSON_KEY_CLUSTER</code>: the Sync account's cluster (may be null);</li> * * <li><code>Constants.JSON_KEY_CLIENT_NAME</code>: the Sync account's client name (may be null);</li> * * <li><code>Constants.JSON_KEY_CLIENT_GUID</code>: the Sync account's client GUID (may be null);</li> * * <li><code>Constants.JSON_KEY_SYNC_AUTOMATICALLY</code>: true if the Android Account is syncing automically;</li> * * <li><code>Constants.JSON_KEY_VERSION</code>: version of this file;</li> * * <li><code>Constants.JSON_KEY_TIMESTAMP</code>: when this file was written.</li> * </ul> * * * @param context Android context. * @param filename name of file to persist to; must not contain path separators. * @param params the Sync account's parameters. * @param syncAutomatically whether the Android Account object is syncing automatically. */ public static void pickle(final Context context, final String filename, final SyncAccountParameters params, final boolean syncAutomatically) { final ExtendedJSONObject o = params.asJSON(); o.put(Constants.JSON_KEY_SYNC_AUTOMATICALLY, Boolean.valueOf(syncAutomatically)); o.put(Constants.JSON_KEY_VERSION, new Long(VERSION)); o.put(Constants.JSON_KEY_TIMESTAMP, new Long(System.currentTimeMillis())); PrintStream ps = null; try { final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); ps = new PrintStream(fos); ps.print(o.toJSONString()); Logger.debug(LOG_TAG, "Persisted " + o.keySet().size() + " account settings to " + filename + "."); } catch (Exception e) { Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename + "; ignoring.", e); } finally { if (ps != null) { ps.close(); } } } /** * Create Android account from saved JSON object. * * @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 Account 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; } SyncAccountParameters params = null; try { // Null checking of inputs is done in constructor. params = new SyncAccountParameters(context, null, json); } catch (IllegalArgumentException e) { Logger.warn(LOG_TAG, "Un-pickled data included null username, password, or serverURL; aborting.", e); return null; } // Default to syncing automatically. boolean syncAutomatically = true; if (json.containsKey(Constants.JSON_KEY_SYNC_AUTOMATICALLY)) { if (Boolean.FALSE.equals(json.get(Constants.JSON_KEY_SYNC_AUTOMATICALLY))) { syncAutomatically = false; } } final Account account = SyncAccounts.createSyncAccountPreservingExistingPreferences(params, syncAutomatically); if (account == null) { Logger.warn(LOG_TAG, "Failed to add Android Account; aborting."); return null; } Integer version = json.getIntegerSafely(Constants.JSON_KEY_VERSION); Integer timestamp = json.getIntegerSafely(Constants.JSON_KEY_TIMESTAMP); if (version == null || timestamp == null) { Logger.warn(LOG_TAG, "Did not find version or timestamp in pickle file; ignoring."); version = new Integer(-1); timestamp = new Integer(-1); } Logger.info(LOG_TAG, "Un-pickled Android account named " + params.username + " (version " + version + ", pickled at " + timestamp + ")."); return account; } }