Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | package org.mozilla.gecko.sync.config; |
michael@0 | 6 | |
michael@0 | 7 | import java.io.FileOutputStream; |
michael@0 | 8 | import java.io.PrintStream; |
michael@0 | 9 | |
michael@0 | 10 | import org.mozilla.gecko.background.common.log.Logger; |
michael@0 | 11 | import org.mozilla.gecko.sync.ExtendedJSONObject; |
michael@0 | 12 | import org.mozilla.gecko.sync.Utils; |
michael@0 | 13 | import org.mozilla.gecko.sync.setup.Constants; |
michael@0 | 14 | import org.mozilla.gecko.sync.setup.SyncAccounts; |
michael@0 | 15 | import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters; |
michael@0 | 16 | |
michael@0 | 17 | import android.accounts.Account; |
michael@0 | 18 | import android.content.Context; |
michael@0 | 19 | |
michael@0 | 20 | /** |
michael@0 | 21 | * Bug 768102: Android deletes Account objects when the Authenticator that owns |
michael@0 | 22 | * the Account disappears. This happens when an App is installed to the SD card |
michael@0 | 23 | * and the SD card is un-mounted or the device is rebooted. |
michael@0 | 24 | * <p> |
michael@0 | 25 | * Bug 769745: Work around this by pickling the current Sync account data every |
michael@0 | 26 | * sync. |
michael@0 | 27 | * <p> |
michael@0 | 28 | * Bug 735842: Work around this by un-pickling when we check if Sync accounts |
michael@0 | 29 | * exist (called from Fennec). |
michael@0 | 30 | * <p> |
michael@0 | 31 | * Android just doesn't support installing Apps that define long-lived Services |
michael@0 | 32 | * and/or own Account types onto the SD card. The documentation says not to do |
michael@0 | 33 | * it. There are hordes of developers who want to do it, and have tried to |
michael@0 | 34 | * register for almost every "package installation changed" broadcast intent |
michael@0 | 35 | * that Android supports. They all explicitly state that the package that has |
michael@0 | 36 | * changed does *not* receive the broadcast intent, thereby preventing an App |
michael@0 | 37 | * from re-establishing its state. |
michael@0 | 38 | * <p> |
michael@0 | 39 | * <a href="http://developer.android.com/guide/topics/data/install-location.html">Reference.</a> |
michael@0 | 40 | * <p> |
michael@0 | 41 | * <b>Quote</b>: Your AbstractThreadedSyncAdapter and all its sync functionality |
michael@0 | 42 | * will not work until external storage is remounted. |
michael@0 | 43 | * <p> |
michael@0 | 44 | * <b>Quote</b>: Your running Service will be killed and will not be restarted |
michael@0 | 45 | * when external storage is remounted. You can, however, register for the |
michael@0 | 46 | * ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify |
michael@0 | 47 | * your application when applications installed on external storage have become |
michael@0 | 48 | * available to the system again. At which time, you can restart your Service. |
michael@0 | 49 | * <p> |
michael@0 | 50 | * Problem: <a href="http://code.google.com/p/android/issues/detail?id=8485">that intent doesn't work</a>! |
michael@0 | 51 | */ |
michael@0 | 52 | public class AccountPickler { |
michael@0 | 53 | public static final String LOG_TAG = "AccountPickler"; |
michael@0 | 54 | |
michael@0 | 55 | public static final long VERSION = 1; |
michael@0 | 56 | |
michael@0 | 57 | /** |
michael@0 | 58 | * Remove Sync account persisted to disk. |
michael@0 | 59 | * |
michael@0 | 60 | * @param context Android context. |
michael@0 | 61 | * @param filename name of persisted pickle file; must not contain path separators. |
michael@0 | 62 | * @return <code>true</code> if given pickle existed and was successfully deleted. |
michael@0 | 63 | */ |
michael@0 | 64 | public static boolean deletePickle(final Context context, final String filename) { |
michael@0 | 65 | return context.deleteFile(filename); |
michael@0 | 66 | } |
michael@0 | 67 | |
michael@0 | 68 | /** |
michael@0 | 69 | * Persist Sync account to disk as a JSON object. |
michael@0 | 70 | * <p> |
michael@0 | 71 | * JSON object has keys: |
michael@0 | 72 | * <ul> |
michael@0 | 73 | * <li><code>Constants.JSON_KEY_ACCOUNT</code>: the Sync account's un-encoded username, |
michael@0 | 74 | * like "test@mozilla.com".</li> |
michael@0 | 75 | * |
michael@0 | 76 | * <li><code>Constants.JSON_KEY_PASSWORD</code>: the Sync account's password;</li> |
michael@0 | 77 | * |
michael@0 | 78 | * <li><code>Constants.JSON_KEY_SERVER</code>: the Sync account's server;</li> |
michael@0 | 79 | * |
michael@0 | 80 | * <li><code>Constants.JSON_KEY_SYNCKEY</code>: the Sync account's sync key;</li> |
michael@0 | 81 | * |
michael@0 | 82 | * <li><code>Constants.JSON_KEY_CLUSTER</code>: the Sync account's cluster (may be null);</li> |
michael@0 | 83 | * |
michael@0 | 84 | * <li><code>Constants.JSON_KEY_CLIENT_NAME</code>: the Sync account's client name (may be null);</li> |
michael@0 | 85 | * |
michael@0 | 86 | * <li><code>Constants.JSON_KEY_CLIENT_GUID</code>: the Sync account's client GUID (may be null);</li> |
michael@0 | 87 | * |
michael@0 | 88 | * <li><code>Constants.JSON_KEY_SYNC_AUTOMATICALLY</code>: true if the Android Account is syncing automically;</li> |
michael@0 | 89 | * |
michael@0 | 90 | * <li><code>Constants.JSON_KEY_VERSION</code>: version of this file;</li> |
michael@0 | 91 | * |
michael@0 | 92 | * <li><code>Constants.JSON_KEY_TIMESTAMP</code>: when this file was written.</li> |
michael@0 | 93 | * </ul> |
michael@0 | 94 | * |
michael@0 | 95 | * |
michael@0 | 96 | * @param context Android context. |
michael@0 | 97 | * @param filename name of file to persist to; must not contain path separators. |
michael@0 | 98 | * @param params the Sync account's parameters. |
michael@0 | 99 | * @param syncAutomatically whether the Android Account object is syncing automatically. |
michael@0 | 100 | */ |
michael@0 | 101 | public static void pickle(final Context context, final String filename, |
michael@0 | 102 | final SyncAccountParameters params, final boolean syncAutomatically) { |
michael@0 | 103 | final ExtendedJSONObject o = params.asJSON(); |
michael@0 | 104 | o.put(Constants.JSON_KEY_SYNC_AUTOMATICALLY, Boolean.valueOf(syncAutomatically)); |
michael@0 | 105 | o.put(Constants.JSON_KEY_VERSION, new Long(VERSION)); |
michael@0 | 106 | o.put(Constants.JSON_KEY_TIMESTAMP, new Long(System.currentTimeMillis())); |
michael@0 | 107 | |
michael@0 | 108 | PrintStream ps = null; |
michael@0 | 109 | try { |
michael@0 | 110 | final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); |
michael@0 | 111 | ps = new PrintStream(fos); |
michael@0 | 112 | ps.print(o.toJSONString()); |
michael@0 | 113 | Logger.debug(LOG_TAG, "Persisted " + o.keySet().size() + " account settings to " + filename + "."); |
michael@0 | 114 | } catch (Exception e) { |
michael@0 | 115 | Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename + "; ignoring.", e); |
michael@0 | 116 | } finally { |
michael@0 | 117 | if (ps != null) { |
michael@0 | 118 | ps.close(); |
michael@0 | 119 | } |
michael@0 | 120 | } |
michael@0 | 121 | } |
michael@0 | 122 | |
michael@0 | 123 | /** |
michael@0 | 124 | * Create Android account from saved JSON object. |
michael@0 | 125 | * |
michael@0 | 126 | * @param context |
michael@0 | 127 | * Android context. |
michael@0 | 128 | * @param filename |
michael@0 | 129 | * name of file to read from; must not contain path separators. |
michael@0 | 130 | * @return created Android account, or null on error. |
michael@0 | 131 | */ |
michael@0 | 132 | public static Account unpickle(final Context context, final String filename) { |
michael@0 | 133 | final String jsonString = Utils.readFile(context, filename); |
michael@0 | 134 | if (jsonString == null) { |
michael@0 | 135 | Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting."); |
michael@0 | 136 | return null; |
michael@0 | 137 | } |
michael@0 | 138 | |
michael@0 | 139 | ExtendedJSONObject json = null; |
michael@0 | 140 | try { |
michael@0 | 141 | json = ExtendedJSONObject.parseJSONObject(jsonString); |
michael@0 | 142 | } catch (Exception e) { |
michael@0 | 143 | Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e); |
michael@0 | 144 | return null; |
michael@0 | 145 | } |
michael@0 | 146 | |
michael@0 | 147 | SyncAccountParameters params = null; |
michael@0 | 148 | try { |
michael@0 | 149 | // Null checking of inputs is done in constructor. |
michael@0 | 150 | params = new SyncAccountParameters(context, null, json); |
michael@0 | 151 | } catch (IllegalArgumentException e) { |
michael@0 | 152 | Logger.warn(LOG_TAG, "Un-pickled data included null username, password, or serverURL; aborting.", e); |
michael@0 | 153 | return null; |
michael@0 | 154 | } |
michael@0 | 155 | |
michael@0 | 156 | // Default to syncing automatically. |
michael@0 | 157 | boolean syncAutomatically = true; |
michael@0 | 158 | if (json.containsKey(Constants.JSON_KEY_SYNC_AUTOMATICALLY)) { |
michael@0 | 159 | if (Boolean.FALSE.equals(json.get(Constants.JSON_KEY_SYNC_AUTOMATICALLY))) { |
michael@0 | 160 | syncAutomatically = false; |
michael@0 | 161 | } |
michael@0 | 162 | } |
michael@0 | 163 | |
michael@0 | 164 | final Account account = SyncAccounts.createSyncAccountPreservingExistingPreferences(params, syncAutomatically); |
michael@0 | 165 | if (account == null) { |
michael@0 | 166 | Logger.warn(LOG_TAG, "Failed to add Android Account; aborting."); |
michael@0 | 167 | return null; |
michael@0 | 168 | } |
michael@0 | 169 | |
michael@0 | 170 | Integer version = json.getIntegerSafely(Constants.JSON_KEY_VERSION); |
michael@0 | 171 | Integer timestamp = json.getIntegerSafely(Constants.JSON_KEY_TIMESTAMP); |
michael@0 | 172 | if (version == null || timestamp == null) { |
michael@0 | 173 | Logger.warn(LOG_TAG, "Did not find version or timestamp in pickle file; ignoring."); |
michael@0 | 174 | version = new Integer(-1); |
michael@0 | 175 | timestamp = new Integer(-1); |
michael@0 | 176 | } |
michael@0 | 177 | |
michael@0 | 178 | Logger.info(LOG_TAG, "Un-pickled Android account named " + params.username + " (version " + version + ", pickled at " + timestamp + ")."); |
michael@0 | 179 | |
michael@0 | 180 | return account; |
michael@0 | 181 | } |
michael@0 | 182 | } |