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 +}