michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.sync.config; michael@0: michael@0: import java.io.FileOutputStream; michael@0: import java.io.PrintStream; michael@0: michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.sync.ExtendedJSONObject; michael@0: import org.mozilla.gecko.sync.Utils; michael@0: import org.mozilla.gecko.sync.setup.Constants; michael@0: import org.mozilla.gecko.sync.setup.SyncAccounts; michael@0: import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters; michael@0: michael@0: import android.accounts.Account; michael@0: import android.content.Context; michael@0: michael@0: /** michael@0: * Bug 768102: Android deletes Account objects when the Authenticator that owns michael@0: * the Account disappears. This happens when an App is installed to the SD card michael@0: * and the SD card is un-mounted or the device is rebooted. michael@0: *

michael@0: * Bug 769745: Work around this by pickling the current Sync account data every michael@0: * sync. michael@0: *

michael@0: * Bug 735842: Work around this by un-pickling when we check if Sync accounts michael@0: * exist (called from Fennec). michael@0: *

michael@0: * Android just doesn't support installing Apps that define long-lived Services michael@0: * and/or own Account types onto the SD card. The documentation says not to do michael@0: * it. There are hordes of developers who want to do it, and have tried to michael@0: * register for almost every "package installation changed" broadcast intent michael@0: * that Android supports. They all explicitly state that the package that has michael@0: * changed does *not* receive the broadcast intent, thereby preventing an App michael@0: * from re-establishing its state. michael@0: *

michael@0: * Reference. michael@0: *

michael@0: * Quote: Your AbstractThreadedSyncAdapter and all its sync functionality michael@0: * will not work until external storage is remounted. michael@0: *

michael@0: * Quote: Your running Service will be killed and will not be restarted michael@0: * when external storage is remounted. You can, however, register for the michael@0: * ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify michael@0: * your application when applications installed on external storage have become michael@0: * available to the system again. At which time, you can restart your Service. michael@0: *

michael@0: * Problem: that intent doesn't work! michael@0: */ michael@0: public class AccountPickler { michael@0: public static final String LOG_TAG = "AccountPickler"; michael@0: michael@0: public static final long VERSION = 1; michael@0: michael@0: /** michael@0: * Remove Sync account persisted to disk. michael@0: * michael@0: * @param context Android context. michael@0: * @param filename name of persisted pickle file; must not contain path separators. michael@0: * @return true if given pickle existed and was successfully deleted. michael@0: */ michael@0: public static boolean deletePickle(final Context context, final String filename) { michael@0: return context.deleteFile(filename); michael@0: } michael@0: michael@0: /** michael@0: * Persist Sync account to disk as a JSON object. michael@0: *

michael@0: * JSON object has keys: michael@0: *

michael@0: * michael@0: * michael@0: * @param context Android context. michael@0: * @param filename name of file to persist to; must not contain path separators. michael@0: * @param params the Sync account's parameters. michael@0: * @param syncAutomatically whether the Android Account object is syncing automatically. michael@0: */ michael@0: public static void pickle(final Context context, final String filename, michael@0: final SyncAccountParameters params, final boolean syncAutomatically) { michael@0: final ExtendedJSONObject o = params.asJSON(); michael@0: o.put(Constants.JSON_KEY_SYNC_AUTOMATICALLY, Boolean.valueOf(syncAutomatically)); michael@0: o.put(Constants.JSON_KEY_VERSION, new Long(VERSION)); michael@0: o.put(Constants.JSON_KEY_TIMESTAMP, new Long(System.currentTimeMillis())); michael@0: michael@0: PrintStream ps = null; michael@0: try { michael@0: final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); michael@0: ps = new PrintStream(fos); michael@0: ps.print(o.toJSONString()); michael@0: Logger.debug(LOG_TAG, "Persisted " + o.keySet().size() + " account settings to " + filename + "."); michael@0: } catch (Exception e) { michael@0: Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename + "; ignoring.", e); michael@0: } finally { michael@0: if (ps != null) { michael@0: ps.close(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Create Android account from saved JSON object. michael@0: * michael@0: * @param context michael@0: * Android context. michael@0: * @param filename michael@0: * name of file to read from; must not contain path separators. michael@0: * @return created Android account, or null on error. michael@0: */ michael@0: public static Account unpickle(final Context context, final String filename) { michael@0: final String jsonString = Utils.readFile(context, filename); michael@0: if (jsonString == null) { michael@0: Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting."); michael@0: return null; michael@0: } michael@0: michael@0: ExtendedJSONObject json = null; michael@0: try { michael@0: json = ExtendedJSONObject.parseJSONObject(jsonString); michael@0: } catch (Exception e) { michael@0: Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e); michael@0: return null; michael@0: } michael@0: michael@0: SyncAccountParameters params = null; michael@0: try { michael@0: // Null checking of inputs is done in constructor. michael@0: params = new SyncAccountParameters(context, null, json); michael@0: } catch (IllegalArgumentException e) { michael@0: Logger.warn(LOG_TAG, "Un-pickled data included null username, password, or serverURL; aborting.", e); michael@0: return null; michael@0: } michael@0: michael@0: // Default to syncing automatically. michael@0: boolean syncAutomatically = true; michael@0: if (json.containsKey(Constants.JSON_KEY_SYNC_AUTOMATICALLY)) { michael@0: if (Boolean.FALSE.equals(json.get(Constants.JSON_KEY_SYNC_AUTOMATICALLY))) { michael@0: syncAutomatically = false; michael@0: } michael@0: } michael@0: michael@0: final Account account = SyncAccounts.createSyncAccountPreservingExistingPreferences(params, syncAutomatically); michael@0: if (account == null) { michael@0: Logger.warn(LOG_TAG, "Failed to add Android Account; aborting."); michael@0: return null; michael@0: } michael@0: michael@0: Integer version = json.getIntegerSafely(Constants.JSON_KEY_VERSION); michael@0: Integer timestamp = json.getIntegerSafely(Constants.JSON_KEY_TIMESTAMP); michael@0: if (version == null || timestamp == null) { michael@0: Logger.warn(LOG_TAG, "Did not find version or timestamp in pickle file; ignoring."); michael@0: version = new Integer(-1); michael@0: timestamp = new Integer(-1); michael@0: } michael@0: michael@0: Logger.info(LOG_TAG, "Un-pickled Android account named " + params.username + " (version " + version + ", pickled at " + timestamp + ")."); michael@0: michael@0: return account; michael@0: } michael@0: }