Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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; |
michael@0 | 6 | |
michael@0 | 7 | import java.io.IOException; |
michael@0 | 8 | import java.net.URI; |
michael@0 | 9 | import java.net.URISyntaxException; |
michael@0 | 10 | import java.util.ArrayList; |
michael@0 | 11 | import java.util.Collection; |
michael@0 | 12 | import java.util.Collections; |
michael@0 | 13 | import java.util.HashMap; |
michael@0 | 14 | import java.util.HashSet; |
michael@0 | 15 | import java.util.List; |
michael@0 | 16 | import java.util.Map; |
michael@0 | 17 | import java.util.Map.Entry; |
michael@0 | 18 | import java.util.Set; |
michael@0 | 19 | import java.util.concurrent.atomic.AtomicLong; |
michael@0 | 20 | |
michael@0 | 21 | import org.json.simple.JSONArray; |
michael@0 | 22 | import org.json.simple.parser.ParseException; |
michael@0 | 23 | import org.mozilla.gecko.background.common.log.Logger; |
michael@0 | 24 | import org.mozilla.gecko.sync.crypto.CryptoException; |
michael@0 | 25 | import org.mozilla.gecko.sync.crypto.KeyBundle; |
michael@0 | 26 | import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback; |
michael@0 | 27 | import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; |
michael@0 | 28 | import org.mozilla.gecko.sync.delegates.FreshStartDelegate; |
michael@0 | 29 | import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate; |
michael@0 | 30 | import org.mozilla.gecko.sync.delegates.KeyUploadDelegate; |
michael@0 | 31 | import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate; |
michael@0 | 32 | import org.mozilla.gecko.sync.delegates.NodeAssignmentCallback; |
michael@0 | 33 | import org.mozilla.gecko.sync.delegates.WipeServerDelegate; |
michael@0 | 34 | import org.mozilla.gecko.sync.net.AuthHeaderProvider; |
michael@0 | 35 | import org.mozilla.gecko.sync.net.BaseResource; |
michael@0 | 36 | import org.mozilla.gecko.sync.net.HttpResponseObserver; |
michael@0 | 37 | import org.mozilla.gecko.sync.net.SyncResponse; |
michael@0 | 38 | import org.mozilla.gecko.sync.net.SyncStorageRecordRequest; |
michael@0 | 39 | import org.mozilla.gecko.sync.net.SyncStorageRequest; |
michael@0 | 40 | import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; |
michael@0 | 41 | import org.mozilla.gecko.sync.net.SyncStorageResponse; |
michael@0 | 42 | import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage; |
michael@0 | 43 | import org.mozilla.gecko.sync.stage.AndroidBrowserHistoryServerSyncStage; |
michael@0 | 44 | import org.mozilla.gecko.sync.stage.CheckPreconditionsStage; |
michael@0 | 45 | import org.mozilla.gecko.sync.stage.CompletedStage; |
michael@0 | 46 | import org.mozilla.gecko.sync.stage.EnsureClusterURLStage; |
michael@0 | 47 | import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage; |
michael@0 | 48 | import org.mozilla.gecko.sync.stage.FennecTabsServerSyncStage; |
michael@0 | 49 | import org.mozilla.gecko.sync.stage.FetchInfoCollectionsStage; |
michael@0 | 50 | import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage; |
michael@0 | 51 | import org.mozilla.gecko.sync.stage.FormHistoryServerSyncStage; |
michael@0 | 52 | import org.mozilla.gecko.sync.stage.GlobalSyncStage; |
michael@0 | 53 | import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; |
michael@0 | 54 | import org.mozilla.gecko.sync.stage.NoSuchStageException; |
michael@0 | 55 | import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage; |
michael@0 | 56 | import org.mozilla.gecko.sync.stage.SyncClientsEngineStage; |
michael@0 | 57 | import org.mozilla.gecko.sync.stage.UploadMetaGlobalStage; |
michael@0 | 58 | |
michael@0 | 59 | import android.content.Context; |
michael@0 | 60 | import ch.boye.httpclientandroidlib.HttpResponse; |
michael@0 | 61 | |
michael@0 | 62 | public class GlobalSession implements HttpResponseObserver { |
michael@0 | 63 | private static final String LOG_TAG = "GlobalSession"; |
michael@0 | 64 | |
michael@0 | 65 | public static final long STORAGE_VERSION = 5; |
michael@0 | 66 | |
michael@0 | 67 | public SyncConfiguration config = null; |
michael@0 | 68 | |
michael@0 | 69 | protected Map<Stage, GlobalSyncStage> stages; |
michael@0 | 70 | public Stage currentState = Stage.idle; |
michael@0 | 71 | |
michael@0 | 72 | public final BaseGlobalSessionCallback callback; |
michael@0 | 73 | protected final Context context; |
michael@0 | 74 | protected final ClientsDataDelegate clientsDelegate; |
michael@0 | 75 | protected final NodeAssignmentCallback nodeAssignmentCallback; |
michael@0 | 76 | |
michael@0 | 77 | /** |
michael@0 | 78 | * Map from engine name to new settings for an updated meta/global record. |
michael@0 | 79 | * Engines to remove will have <code>null</code> EngineSettings. |
michael@0 | 80 | */ |
michael@0 | 81 | public final Map<String, EngineSettings> enginesToUpdate = new HashMap<String, EngineSettings>(); |
michael@0 | 82 | |
michael@0 | 83 | /* |
michael@0 | 84 | * Key accessors. |
michael@0 | 85 | */ |
michael@0 | 86 | public KeyBundle keyBundleForCollection(String collection) throws NoCollectionKeysSetException { |
michael@0 | 87 | return config.getCollectionKeys().keyBundleForCollection(collection); |
michael@0 | 88 | } |
michael@0 | 89 | |
michael@0 | 90 | /* |
michael@0 | 91 | * Config passthrough for convenience. |
michael@0 | 92 | */ |
michael@0 | 93 | public AuthHeaderProvider getAuthHeaderProvider() { |
michael@0 | 94 | return config.getAuthHeaderProvider(); |
michael@0 | 95 | } |
michael@0 | 96 | |
michael@0 | 97 | public URI wboURI(String collection, String id) throws URISyntaxException { |
michael@0 | 98 | return config.wboURI(collection, id); |
michael@0 | 99 | } |
michael@0 | 100 | |
michael@0 | 101 | public GlobalSession(SyncConfiguration config, |
michael@0 | 102 | BaseGlobalSessionCallback callback, |
michael@0 | 103 | Context context, |
michael@0 | 104 | ClientsDataDelegate clientsDelegate, NodeAssignmentCallback nodeAssignmentCallback) |
michael@0 | 105 | throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException { |
michael@0 | 106 | |
michael@0 | 107 | if (callback == null) { |
michael@0 | 108 | throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor."); |
michael@0 | 109 | } |
michael@0 | 110 | |
michael@0 | 111 | this.callback = callback; |
michael@0 | 112 | this.context = context; |
michael@0 | 113 | this.clientsDelegate = clientsDelegate; |
michael@0 | 114 | this.nodeAssignmentCallback = nodeAssignmentCallback; |
michael@0 | 115 | |
michael@0 | 116 | this.config = config; |
michael@0 | 117 | registerCommands(); |
michael@0 | 118 | prepareStages(); |
michael@0 | 119 | |
michael@0 | 120 | if (config.stagesToSync == null) { |
michael@0 | 121 | Logger.info(LOG_TAG, "No stages to sync specified; defaulting to all valid engine names."); |
michael@0 | 122 | config.stagesToSync = Collections.unmodifiableCollection(SyncConfiguration.validEngineNames()); |
michael@0 | 123 | } |
michael@0 | 124 | |
michael@0 | 125 | // TODO: data-driven plan for the sync, referring to prepareStages. |
michael@0 | 126 | } |
michael@0 | 127 | |
michael@0 | 128 | /** |
michael@0 | 129 | * Register commands this global session knows how to process. |
michael@0 | 130 | * <p> |
michael@0 | 131 | * Re-registering a command overwrites any existing registration. |
michael@0 | 132 | */ |
michael@0 | 133 | protected static void registerCommands() { |
michael@0 | 134 | final CommandProcessor processor = CommandProcessor.getProcessor(); |
michael@0 | 135 | |
michael@0 | 136 | processor.registerCommand("resetEngine", new CommandRunner(1) { |
michael@0 | 137 | @Override |
michael@0 | 138 | public void executeCommand(final GlobalSession session, List<String> args) { |
michael@0 | 139 | HashSet<String> names = new HashSet<String>(); |
michael@0 | 140 | names.add(args.get(0)); |
michael@0 | 141 | session.resetStagesByName(names); |
michael@0 | 142 | } |
michael@0 | 143 | }); |
michael@0 | 144 | |
michael@0 | 145 | processor.registerCommand("resetAll", new CommandRunner(0) { |
michael@0 | 146 | @Override |
michael@0 | 147 | public void executeCommand(final GlobalSession session, List<String> args) { |
michael@0 | 148 | session.resetAllStages(); |
michael@0 | 149 | } |
michael@0 | 150 | }); |
michael@0 | 151 | |
michael@0 | 152 | processor.registerCommand("wipeEngine", new CommandRunner(1) { |
michael@0 | 153 | @Override |
michael@0 | 154 | public void executeCommand(final GlobalSession session, List<String> args) { |
michael@0 | 155 | HashSet<String> names = new HashSet<String>(); |
michael@0 | 156 | names.add(args.get(0)); |
michael@0 | 157 | session.wipeStagesByName(names); |
michael@0 | 158 | } |
michael@0 | 159 | }); |
michael@0 | 160 | |
michael@0 | 161 | processor.registerCommand("wipeAll", new CommandRunner(0) { |
michael@0 | 162 | @Override |
michael@0 | 163 | public void executeCommand(final GlobalSession session, List<String> args) { |
michael@0 | 164 | session.wipeAllStages(); |
michael@0 | 165 | } |
michael@0 | 166 | }); |
michael@0 | 167 | |
michael@0 | 168 | processor.registerCommand("displayURI", new CommandRunner(3) { |
michael@0 | 169 | @Override |
michael@0 | 170 | public void executeCommand(final GlobalSession session, List<String> args) { |
michael@0 | 171 | CommandProcessor.displayURI(args, session.getContext()); |
michael@0 | 172 | } |
michael@0 | 173 | }); |
michael@0 | 174 | } |
michael@0 | 175 | |
michael@0 | 176 | protected void prepareStages() { |
michael@0 | 177 | HashMap<Stage, GlobalSyncStage> stages = new HashMap<Stage, GlobalSyncStage>(); |
michael@0 | 178 | |
michael@0 | 179 | stages.put(Stage.checkPreconditions, new CheckPreconditionsStage()); |
michael@0 | 180 | stages.put(Stage.ensureClusterURL, new EnsureClusterURLStage(nodeAssignmentCallback)); |
michael@0 | 181 | stages.put(Stage.fetchInfoCollections, new FetchInfoCollectionsStage()); |
michael@0 | 182 | stages.put(Stage.fetchMetaGlobal, new FetchMetaGlobalStage()); |
michael@0 | 183 | stages.put(Stage.ensureKeysStage, new EnsureCrypto5KeysStage()); |
michael@0 | 184 | stages.put(Stage.syncClientsEngine, new SyncClientsEngineStage()); |
michael@0 | 185 | |
michael@0 | 186 | stages.put(Stage.syncTabs, new FennecTabsServerSyncStage()); |
michael@0 | 187 | stages.put(Stage.syncPasswords, new PasswordsServerSyncStage()); |
michael@0 | 188 | stages.put(Stage.syncBookmarks, new AndroidBrowserBookmarksServerSyncStage()); |
michael@0 | 189 | stages.put(Stage.syncHistory, new AndroidBrowserHistoryServerSyncStage()); |
michael@0 | 190 | stages.put(Stage.syncFormHistory, new FormHistoryServerSyncStage()); |
michael@0 | 191 | |
michael@0 | 192 | stages.put(Stage.uploadMetaGlobal, new UploadMetaGlobalStage()); |
michael@0 | 193 | stages.put(Stage.completed, new CompletedStage()); |
michael@0 | 194 | |
michael@0 | 195 | this.stages = Collections.unmodifiableMap(stages); |
michael@0 | 196 | } |
michael@0 | 197 | |
michael@0 | 198 | public GlobalSyncStage getSyncStageByName(String name) throws NoSuchStageException { |
michael@0 | 199 | return getSyncStageByName(Stage.byName(name)); |
michael@0 | 200 | } |
michael@0 | 201 | |
michael@0 | 202 | public GlobalSyncStage getSyncStageByName(Stage next) throws NoSuchStageException { |
michael@0 | 203 | GlobalSyncStage stage = stages.get(next); |
michael@0 | 204 | if (stage == null) { |
michael@0 | 205 | throw new NoSuchStageException(next); |
michael@0 | 206 | } |
michael@0 | 207 | return stage; |
michael@0 | 208 | } |
michael@0 | 209 | |
michael@0 | 210 | public Collection<GlobalSyncStage> getSyncStagesByEnum(Collection<Stage> enums) { |
michael@0 | 211 | ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>(); |
michael@0 | 212 | for (Stage name : enums) { |
michael@0 | 213 | try { |
michael@0 | 214 | GlobalSyncStage stage = this.getSyncStageByName(name); |
michael@0 | 215 | out.add(stage); |
michael@0 | 216 | } catch (NoSuchStageException e) { |
michael@0 | 217 | Logger.warn(LOG_TAG, "Unable to find stage with name " + name); |
michael@0 | 218 | } |
michael@0 | 219 | } |
michael@0 | 220 | return out; |
michael@0 | 221 | } |
michael@0 | 222 | |
michael@0 | 223 | public Collection<GlobalSyncStage> getSyncStagesByName(Collection<String> names) { |
michael@0 | 224 | ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>(); |
michael@0 | 225 | for (String name : names) { |
michael@0 | 226 | try { |
michael@0 | 227 | GlobalSyncStage stage = this.getSyncStageByName(name); |
michael@0 | 228 | out.add(stage); |
michael@0 | 229 | } catch (NoSuchStageException e) { |
michael@0 | 230 | Logger.warn(LOG_TAG, "Unable to find stage with name " + name); |
michael@0 | 231 | } |
michael@0 | 232 | } |
michael@0 | 233 | return out; |
michael@0 | 234 | } |
michael@0 | 235 | |
michael@0 | 236 | /** |
michael@0 | 237 | * Advance and loop around the stages of a sync. |
michael@0 | 238 | * @param current |
michael@0 | 239 | * @return |
michael@0 | 240 | * The next stage to execute. |
michael@0 | 241 | */ |
michael@0 | 242 | public static Stage nextStage(Stage current) { |
michael@0 | 243 | int index = current.ordinal() + 1; |
michael@0 | 244 | int max = Stage.completed.ordinal() + 1; |
michael@0 | 245 | return Stage.values()[index % max]; |
michael@0 | 246 | } |
michael@0 | 247 | |
michael@0 | 248 | /** |
michael@0 | 249 | * Move to the next stage in the syncing process. |
michael@0 | 250 | */ |
michael@0 | 251 | public void advance() { |
michael@0 | 252 | // If we have a backoff, request a backoff and don't advance to next stage. |
michael@0 | 253 | long existingBackoff = largestBackoffObserved.get(); |
michael@0 | 254 | if (existingBackoff > 0) { |
michael@0 | 255 | this.abort(null, "Aborting sync because of backoff of " + existingBackoff + " milliseconds."); |
michael@0 | 256 | return; |
michael@0 | 257 | } |
michael@0 | 258 | |
michael@0 | 259 | this.callback.handleStageCompleted(this.currentState, this); |
michael@0 | 260 | Stage next = nextStage(this.currentState); |
michael@0 | 261 | GlobalSyncStage nextStage; |
michael@0 | 262 | try { |
michael@0 | 263 | nextStage = this.getSyncStageByName(next); |
michael@0 | 264 | } catch (NoSuchStageException e) { |
michael@0 | 265 | this.abort(e, "No such stage " + next); |
michael@0 | 266 | return; |
michael@0 | 267 | } |
michael@0 | 268 | this.currentState = next; |
michael@0 | 269 | Logger.info(LOG_TAG, "Running next stage " + next + " (" + nextStage + ")..."); |
michael@0 | 270 | try { |
michael@0 | 271 | nextStage.execute(this); |
michael@0 | 272 | } catch (Exception ex) { |
michael@0 | 273 | Logger.warn(LOG_TAG, "Caught exception " + ex + " running stage " + next); |
michael@0 | 274 | this.abort(ex, "Uncaught exception in stage."); |
michael@0 | 275 | return; |
michael@0 | 276 | } |
michael@0 | 277 | } |
michael@0 | 278 | |
michael@0 | 279 | public Context getContext() { |
michael@0 | 280 | return this.context; |
michael@0 | 281 | } |
michael@0 | 282 | |
michael@0 | 283 | /** |
michael@0 | 284 | * Begin a sync. |
michael@0 | 285 | * <p> |
michael@0 | 286 | * The caller is responsible for: |
michael@0 | 287 | * <ul> |
michael@0 | 288 | * <li>Verifying that any backoffs/minimum next sync requests are respected.</li> |
michael@0 | 289 | * <li>Ensuring that the device is online.</li> |
michael@0 | 290 | * <li>Ensuring that dependencies are ready.</li> |
michael@0 | 291 | * </ul> |
michael@0 | 292 | * |
michael@0 | 293 | * @throws AlreadySyncingException |
michael@0 | 294 | */ |
michael@0 | 295 | public void start() throws AlreadySyncingException { |
michael@0 | 296 | if (this.currentState != GlobalSyncStage.Stage.idle) { |
michael@0 | 297 | throw new AlreadySyncingException(this.currentState); |
michael@0 | 298 | } |
michael@0 | 299 | installAsHttpResponseObserver(); // Uninstalled by completeSync or abort. |
michael@0 | 300 | this.advance(); |
michael@0 | 301 | } |
michael@0 | 302 | |
michael@0 | 303 | /** |
michael@0 | 304 | * Stop this sync and start again. |
michael@0 | 305 | * @throws AlreadySyncingException |
michael@0 | 306 | */ |
michael@0 | 307 | protected void restart() throws AlreadySyncingException { |
michael@0 | 308 | this.currentState = GlobalSyncStage.Stage.idle; |
michael@0 | 309 | if (callback.shouldBackOffStorage()) { |
michael@0 | 310 | this.callback.handleAborted(this, "Told to back off."); |
michael@0 | 311 | return; |
michael@0 | 312 | } |
michael@0 | 313 | this.start(); |
michael@0 | 314 | } |
michael@0 | 315 | |
michael@0 | 316 | /** |
michael@0 | 317 | * We're finished (aborted or succeeded): release resources. |
michael@0 | 318 | */ |
michael@0 | 319 | protected void cleanUp() { |
michael@0 | 320 | uninstallAsHttpResponseObserver(); |
michael@0 | 321 | this.stages = null; |
michael@0 | 322 | } |
michael@0 | 323 | |
michael@0 | 324 | public void completeSync() { |
michael@0 | 325 | cleanUp(); |
michael@0 | 326 | this.currentState = GlobalSyncStage.Stage.idle; |
michael@0 | 327 | this.callback.handleSuccess(this); |
michael@0 | 328 | } |
michael@0 | 329 | |
michael@0 | 330 | /** |
michael@0 | 331 | * Record that an updated meta/global record should be uploaded with the given |
michael@0 | 332 | * settings for the given engine. |
michael@0 | 333 | * |
michael@0 | 334 | * @param engineName engine to update. |
michael@0 | 335 | * @param engineSettings new syncID and version. |
michael@0 | 336 | */ |
michael@0 | 337 | public void recordForMetaGlobalUpdate(String engineName, EngineSettings engineSettings) { |
michael@0 | 338 | enginesToUpdate.put(engineName, engineSettings); |
michael@0 | 339 | } |
michael@0 | 340 | |
michael@0 | 341 | /** |
michael@0 | 342 | * Record that an updated meta/global record should be uploaded without the |
michael@0 | 343 | * given engine name. |
michael@0 | 344 | * |
michael@0 | 345 | * @param engineName |
michael@0 | 346 | * engine to remove. |
michael@0 | 347 | */ |
michael@0 | 348 | public void removeEngineFromMetaGlobal(String engineName) { |
michael@0 | 349 | enginesToUpdate.put(engineName, null); |
michael@0 | 350 | } |
michael@0 | 351 | |
michael@0 | 352 | public boolean hasUpdatedMetaGlobal() { |
michael@0 | 353 | if (enginesToUpdate.isEmpty()) { |
michael@0 | 354 | Logger.info(LOG_TAG, "Not uploading updated meta/global record since there are no engines requesting upload."); |
michael@0 | 355 | return false; |
michael@0 | 356 | } |
michael@0 | 357 | |
michael@0 | 358 | if (Logger.shouldLogVerbose(LOG_TAG)) { |
michael@0 | 359 | Logger.trace(LOG_TAG, "Uploading updated meta/global record since there are engine changes to meta/global."); |
michael@0 | 360 | Logger.trace(LOG_TAG, "Engines requesting update [" + Utils.toCommaSeparatedString(enginesToUpdate.keySet()) + "]"); |
michael@0 | 361 | } |
michael@0 | 362 | |
michael@0 | 363 | return true; |
michael@0 | 364 | } |
michael@0 | 365 | |
michael@0 | 366 | public void updateMetaGlobalInPlace() { |
michael@0 | 367 | config.metaGlobal.declined = this.declinedEngineNames(); |
michael@0 | 368 | ExtendedJSONObject engines = config.metaGlobal.getEngines(); |
michael@0 | 369 | for (Entry<String, EngineSettings> pair : enginesToUpdate.entrySet()) { |
michael@0 | 370 | if (pair.getValue() == null) { |
michael@0 | 371 | engines.remove(pair.getKey()); |
michael@0 | 372 | } else { |
michael@0 | 373 | engines.put(pair.getKey(), pair.getValue().toJSONObject()); |
michael@0 | 374 | } |
michael@0 | 375 | } |
michael@0 | 376 | |
michael@0 | 377 | enginesToUpdate.clear(); |
michael@0 | 378 | } |
michael@0 | 379 | |
michael@0 | 380 | /** |
michael@0 | 381 | * Synchronously upload an updated meta/global. |
michael@0 | 382 | * <p> |
michael@0 | 383 | * All problems are logged and ignored. |
michael@0 | 384 | */ |
michael@0 | 385 | public void uploadUpdatedMetaGlobal() { |
michael@0 | 386 | updateMetaGlobalInPlace(); |
michael@0 | 387 | |
michael@0 | 388 | Logger.debug(LOG_TAG, "Uploading updated meta/global record."); |
michael@0 | 389 | final Object monitor = new Object(); |
michael@0 | 390 | |
michael@0 | 391 | Runnable doUpload = new Runnable() { |
michael@0 | 392 | @Override |
michael@0 | 393 | public void run() { |
michael@0 | 394 | config.metaGlobal.upload(new MetaGlobalDelegate() { |
michael@0 | 395 | @Override |
michael@0 | 396 | public void handleSuccess(MetaGlobal global, SyncStorageResponse response) { |
michael@0 | 397 | Logger.info(LOG_TAG, "Successfully uploaded updated meta/global record."); |
michael@0 | 398 | // Engine changes are stored as diffs, so update enabled engines in config to match uploaded meta/global. |
michael@0 | 399 | config.enabledEngineNames = config.metaGlobal.getEnabledEngineNames(); |
michael@0 | 400 | // Clear userSelectedEngines because they are updated in config and meta/global. |
michael@0 | 401 | config.userSelectedEngines = null; |
michael@0 | 402 | |
michael@0 | 403 | synchronized (monitor) { |
michael@0 | 404 | monitor.notify(); |
michael@0 | 405 | } |
michael@0 | 406 | } |
michael@0 | 407 | |
michael@0 | 408 | @Override |
michael@0 | 409 | public void handleMissing(MetaGlobal global, SyncStorageResponse response) { |
michael@0 | 410 | Logger.warn(LOG_TAG, "Got 404 missing uploading updated meta/global record; shouldn't happen. Ignoring."); |
michael@0 | 411 | synchronized (monitor) { |
michael@0 | 412 | monitor.notify(); |
michael@0 | 413 | } |
michael@0 | 414 | } |
michael@0 | 415 | |
michael@0 | 416 | @Override |
michael@0 | 417 | public void handleFailure(SyncStorageResponse response) { |
michael@0 | 418 | Logger.warn(LOG_TAG, "Failed to upload updated meta/global record; ignoring."); |
michael@0 | 419 | synchronized (monitor) { |
michael@0 | 420 | monitor.notify(); |
michael@0 | 421 | } |
michael@0 | 422 | } |
michael@0 | 423 | |
michael@0 | 424 | @Override |
michael@0 | 425 | public void handleError(Exception e) { |
michael@0 | 426 | Logger.warn(LOG_TAG, "Got exception trying to upload updated meta/global record; ignoring.", e); |
michael@0 | 427 | synchronized (monitor) { |
michael@0 | 428 | monitor.notify(); |
michael@0 | 429 | } |
michael@0 | 430 | } |
michael@0 | 431 | }); |
michael@0 | 432 | } |
michael@0 | 433 | }; |
michael@0 | 434 | |
michael@0 | 435 | final Thread upload = new Thread(doUpload); |
michael@0 | 436 | synchronized (monitor) { |
michael@0 | 437 | try { |
michael@0 | 438 | upload.start(); |
michael@0 | 439 | monitor.wait(); |
michael@0 | 440 | Logger.debug(LOG_TAG, "Uploaded updated meta/global record."); |
michael@0 | 441 | } catch (InterruptedException e) { |
michael@0 | 442 | Logger.error(LOG_TAG, "Uploading updated meta/global interrupted; continuing."); |
michael@0 | 443 | } |
michael@0 | 444 | } |
michael@0 | 445 | } |
michael@0 | 446 | |
michael@0 | 447 | |
michael@0 | 448 | public void abort(Exception e, String reason) { |
michael@0 | 449 | Logger.warn(LOG_TAG, "Aborting sync: " + reason, e); |
michael@0 | 450 | cleanUp(); |
michael@0 | 451 | long existingBackoff = largestBackoffObserved.get(); |
michael@0 | 452 | if (existingBackoff > 0) { |
michael@0 | 453 | callback.requestBackoff(existingBackoff); |
michael@0 | 454 | } |
michael@0 | 455 | if (!(e instanceof HTTPFailureException)) { |
michael@0 | 456 | // e is null, or we aborted for a non-HTTP reason; okay to upload new meta/global record. |
michael@0 | 457 | if (this.hasUpdatedMetaGlobal()) { |
michael@0 | 458 | this.uploadUpdatedMetaGlobal(); // Only logs errors; does not call abort. |
michael@0 | 459 | } |
michael@0 | 460 | } |
michael@0 | 461 | this.callback.handleError(this, e); |
michael@0 | 462 | } |
michael@0 | 463 | |
michael@0 | 464 | public void handleHTTPError(SyncStorageResponse response, String reason) { |
michael@0 | 465 | // TODO: handling of 50x (backoff), 401 (node reassignment or auth error). |
michael@0 | 466 | // Fall back to aborting. |
michael@0 | 467 | Logger.warn(LOG_TAG, "Aborting sync due to HTTP " + response.getStatusCode()); |
michael@0 | 468 | this.interpretHTTPFailure(response.httpResponse()); |
michael@0 | 469 | this.abort(new HTTPFailureException(response), reason); |
michael@0 | 470 | } |
michael@0 | 471 | |
michael@0 | 472 | /** |
michael@0 | 473 | * Perform appropriate backoff etc. extraction. |
michael@0 | 474 | */ |
michael@0 | 475 | public void interpretHTTPFailure(HttpResponse response) { |
michael@0 | 476 | // TODO: handle permanent rejection. |
michael@0 | 477 | long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds(); |
michael@0 | 478 | if (responseBackoff > 0) { |
michael@0 | 479 | callback.requestBackoff(responseBackoff); |
michael@0 | 480 | } |
michael@0 | 481 | |
michael@0 | 482 | if (response.getStatusLine() != null) { |
michael@0 | 483 | final int statusCode = response.getStatusLine().getStatusCode(); |
michael@0 | 484 | switch(statusCode) { |
michael@0 | 485 | |
michael@0 | 486 | case 400: |
michael@0 | 487 | SyncStorageResponse storageResponse = new SyncStorageResponse(response); |
michael@0 | 488 | this.interpretHTTPBadRequestBody(storageResponse); |
michael@0 | 489 | break; |
michael@0 | 490 | |
michael@0 | 491 | case 401: |
michael@0 | 492 | /* |
michael@0 | 493 | * Alert our callback we have a 401 on a cluster URL. This GlobalSession |
michael@0 | 494 | * will fail, but the next one will fetch a new cluster URL and will |
michael@0 | 495 | * distinguish between "node reassignment" and "user password changed". |
michael@0 | 496 | */ |
michael@0 | 497 | callback.informUnauthorizedResponse(this, config.getClusterURL()); |
michael@0 | 498 | break; |
michael@0 | 499 | } |
michael@0 | 500 | } |
michael@0 | 501 | } |
michael@0 | 502 | |
michael@0 | 503 | protected void interpretHTTPBadRequestBody(final SyncStorageResponse storageResponse) { |
michael@0 | 504 | try { |
michael@0 | 505 | final String body = storageResponse.body(); |
michael@0 | 506 | if (body == null) { |
michael@0 | 507 | return; |
michael@0 | 508 | } |
michael@0 | 509 | if (SyncStorageResponse.RESPONSE_CLIENT_UPGRADE_REQUIRED.equals(body)) { |
michael@0 | 510 | callback.informUpgradeRequiredResponse(this); |
michael@0 | 511 | return; |
michael@0 | 512 | } |
michael@0 | 513 | } catch (Exception e) { |
michael@0 | 514 | Logger.warn(LOG_TAG, "Exception parsing HTTP 400 body.", e); |
michael@0 | 515 | } |
michael@0 | 516 | } |
michael@0 | 517 | |
michael@0 | 518 | public void fetchInfoCollections(JSONRecordFetchDelegate callback) throws URISyntaxException { |
michael@0 | 519 | final JSONRecordFetcher fetcher = new JSONRecordFetcher(config.infoCollectionsURL(), getAuthHeaderProvider()); |
michael@0 | 520 | fetcher.fetch(callback); |
michael@0 | 521 | } |
michael@0 | 522 | |
michael@0 | 523 | /** |
michael@0 | 524 | * Upload new crypto/keys. |
michael@0 | 525 | * |
michael@0 | 526 | * @param keys |
michael@0 | 527 | * new keys. |
michael@0 | 528 | * @param keyUploadDelegate |
michael@0 | 529 | * a delegate. |
michael@0 | 530 | */ |
michael@0 | 531 | public void uploadKeys(final CollectionKeys keys, |
michael@0 | 532 | final KeyUploadDelegate keyUploadDelegate) { |
michael@0 | 533 | SyncStorageRecordRequest request; |
michael@0 | 534 | try { |
michael@0 | 535 | request = new SyncStorageRecordRequest(this.config.keysURI()); |
michael@0 | 536 | } catch (URISyntaxException e) { |
michael@0 | 537 | keyUploadDelegate.onKeyUploadFailed(e); |
michael@0 | 538 | return; |
michael@0 | 539 | } |
michael@0 | 540 | |
michael@0 | 541 | request.delegate = new SyncStorageRequestDelegate() { |
michael@0 | 542 | |
michael@0 | 543 | @Override |
michael@0 | 544 | public String ifUnmodifiedSince() { |
michael@0 | 545 | return null; |
michael@0 | 546 | } |
michael@0 | 547 | |
michael@0 | 548 | @Override |
michael@0 | 549 | public void handleRequestSuccess(SyncStorageResponse response) { |
michael@0 | 550 | Logger.debug(LOG_TAG, "Keys uploaded."); |
michael@0 | 551 | BaseResource.consumeEntity(response); // We don't need the response at all. |
michael@0 | 552 | keyUploadDelegate.onKeysUploaded(); |
michael@0 | 553 | } |
michael@0 | 554 | |
michael@0 | 555 | @Override |
michael@0 | 556 | public void handleRequestFailure(SyncStorageResponse response) { |
michael@0 | 557 | Logger.debug(LOG_TAG, "Failed to upload keys."); |
michael@0 | 558 | GlobalSession.this.interpretHTTPFailure(response.httpResponse()); |
michael@0 | 559 | BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response. |
michael@0 | 560 | keyUploadDelegate.onKeyUploadFailed(new HTTPFailureException(response)); |
michael@0 | 561 | } |
michael@0 | 562 | |
michael@0 | 563 | @Override |
michael@0 | 564 | public void handleRequestError(Exception ex) { |
michael@0 | 565 | Logger.warn(LOG_TAG, "Got exception trying to upload keys", ex); |
michael@0 | 566 | keyUploadDelegate.onKeyUploadFailed(ex); |
michael@0 | 567 | } |
michael@0 | 568 | |
michael@0 | 569 | @Override |
michael@0 | 570 | public AuthHeaderProvider getAuthHeaderProvider() { |
michael@0 | 571 | return GlobalSession.this.getAuthHeaderProvider(); |
michael@0 | 572 | } |
michael@0 | 573 | }; |
michael@0 | 574 | |
michael@0 | 575 | // Convert keys to an encrypted crypto record. |
michael@0 | 576 | CryptoRecord keysRecord; |
michael@0 | 577 | try { |
michael@0 | 578 | keysRecord = keys.asCryptoRecord(); |
michael@0 | 579 | keysRecord.setKeyBundle(config.syncKeyBundle); |
michael@0 | 580 | keysRecord.encrypt(); |
michael@0 | 581 | } catch (Exception e) { |
michael@0 | 582 | Logger.warn(LOG_TAG, "Got exception trying creating crypto record from keys", e); |
michael@0 | 583 | keyUploadDelegate.onKeyUploadFailed(e); |
michael@0 | 584 | return; |
michael@0 | 585 | } |
michael@0 | 586 | |
michael@0 | 587 | request.put(keysRecord); |
michael@0 | 588 | } |
michael@0 | 589 | |
michael@0 | 590 | /* |
michael@0 | 591 | * meta/global callbacks. |
michael@0 | 592 | */ |
michael@0 | 593 | public void processMetaGlobal(MetaGlobal global) { |
michael@0 | 594 | config.metaGlobal = global; |
michael@0 | 595 | |
michael@0 | 596 | Long storageVersion = global.getStorageVersion(); |
michael@0 | 597 | if (storageVersion == null) { |
michael@0 | 598 | Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote storage version."); |
michael@0 | 599 | freshStart(); |
michael@0 | 600 | return; |
michael@0 | 601 | } |
michael@0 | 602 | if (storageVersion < STORAGE_VERSION) { |
michael@0 | 603 | Logger.warn(LOG_TAG, "Outdated server: reported " + |
michael@0 | 604 | "remote storage version " + storageVersion + " < " + |
michael@0 | 605 | "local storage version " + STORAGE_VERSION); |
michael@0 | 606 | freshStart(); |
michael@0 | 607 | return; |
michael@0 | 608 | } |
michael@0 | 609 | if (storageVersion > STORAGE_VERSION) { |
michael@0 | 610 | Logger.warn(LOG_TAG, "Outdated client: reported " + |
michael@0 | 611 | "remote storage version " + storageVersion + " > " + |
michael@0 | 612 | "local storage version " + STORAGE_VERSION); |
michael@0 | 613 | requiresUpgrade(); |
michael@0 | 614 | return; |
michael@0 | 615 | } |
michael@0 | 616 | String remoteSyncID = global.getSyncID(); |
michael@0 | 617 | if (remoteSyncID == null) { |
michael@0 | 618 | Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote syncID."); |
michael@0 | 619 | freshStart(); |
michael@0 | 620 | return; |
michael@0 | 621 | } |
michael@0 | 622 | String localSyncID = config.syncID; |
michael@0 | 623 | if (!remoteSyncID.equals(localSyncID)) { |
michael@0 | 624 | Logger.warn(LOG_TAG, "Remote syncID different from local syncID: resetting client and assuming remote syncID."); |
michael@0 | 625 | resetAllStages(); |
michael@0 | 626 | config.purgeCryptoKeys(); |
michael@0 | 627 | config.syncID = remoteSyncID; |
michael@0 | 628 | } |
michael@0 | 629 | // Compare lastModified timestamps for remote/local engine selection times. |
michael@0 | 630 | Logger.debug(LOG_TAG, "Comparing local engine selection timestamp [" + config.userSelectedEnginesTimestamp + "] to server meta/global timestamp [" + config.persistedMetaGlobal().lastModified() + "]."); |
michael@0 | 631 | if (config.userSelectedEnginesTimestamp < config.persistedMetaGlobal().lastModified()) { |
michael@0 | 632 | // Remote has later meta/global timestamp. Don't upload engine changes. |
michael@0 | 633 | config.userSelectedEngines = null; |
michael@0 | 634 | } |
michael@0 | 635 | // Persist enabled engine names. |
michael@0 | 636 | config.enabledEngineNames = global.getEnabledEngineNames(); |
michael@0 | 637 | if (config.enabledEngineNames == null) { |
michael@0 | 638 | Logger.warn(LOG_TAG, "meta/global reported no enabled engine names!"); |
michael@0 | 639 | } else { |
michael@0 | 640 | if (Logger.shouldLogVerbose(LOG_TAG)) { |
michael@0 | 641 | Logger.trace(LOG_TAG, "Persisting enabled engine names '" + |
michael@0 | 642 | Utils.toCommaSeparatedString(config.enabledEngineNames) + "' from meta/global."); |
michael@0 | 643 | } |
michael@0 | 644 | } |
michael@0 | 645 | |
michael@0 | 646 | // Persist declined. |
michael@0 | 647 | // Our declined engines at any point are: |
michael@0 | 648 | // Whatever they were remotely, plus whatever they were locally, less any |
michael@0 | 649 | // engines that were just enabled locally or remotely. |
michael@0 | 650 | // If remote just 'won', our recently enabled list just got cleared. |
michael@0 | 651 | final HashSet<String> allDeclined = new HashSet<String>(); |
michael@0 | 652 | |
michael@0 | 653 | final Set<String> newRemoteDeclined = global.getDeclinedEngineNames(); |
michael@0 | 654 | final Set<String> oldLocalDeclined = config.declinedEngineNames; |
michael@0 | 655 | |
michael@0 | 656 | allDeclined.addAll(newRemoteDeclined); |
michael@0 | 657 | allDeclined.addAll(oldLocalDeclined); |
michael@0 | 658 | |
michael@0 | 659 | if (config.userSelectedEngines != null) { |
michael@0 | 660 | for (Entry<String, Boolean> selection : config.userSelectedEngines.entrySet()) { |
michael@0 | 661 | if (selection.getValue()) { |
michael@0 | 662 | allDeclined.remove(selection.getKey()); |
michael@0 | 663 | } |
michael@0 | 664 | } |
michael@0 | 665 | } |
michael@0 | 666 | |
michael@0 | 667 | config.declinedEngineNames = allDeclined; |
michael@0 | 668 | if (config.declinedEngineNames.isEmpty()) { |
michael@0 | 669 | Logger.debug(LOG_TAG, "meta/global reported no declined engine names, and we have none declined locally."); |
michael@0 | 670 | } else { |
michael@0 | 671 | if (Logger.shouldLogVerbose(LOG_TAG)) { |
michael@0 | 672 | Logger.trace(LOG_TAG, "Persisting declined engine names '" + |
michael@0 | 673 | Utils.toCommaSeparatedString(config.declinedEngineNames) + "' from meta/global."); |
michael@0 | 674 | } |
michael@0 | 675 | } |
michael@0 | 676 | |
michael@0 | 677 | config.persistToPrefs(); |
michael@0 | 678 | advance(); |
michael@0 | 679 | } |
michael@0 | 680 | |
michael@0 | 681 | public void processMissingMetaGlobal(MetaGlobal global) { |
michael@0 | 682 | freshStart(); |
michael@0 | 683 | } |
michael@0 | 684 | |
michael@0 | 685 | /** |
michael@0 | 686 | * Do a fresh start then quietly finish the sync, starting another. |
michael@0 | 687 | */ |
michael@0 | 688 | public void freshStart() { |
michael@0 | 689 | final GlobalSession globalSession = this; |
michael@0 | 690 | freshStart(this, new FreshStartDelegate() { |
michael@0 | 691 | |
michael@0 | 692 | @Override |
michael@0 | 693 | public void onFreshStartFailed(Exception e) { |
michael@0 | 694 | globalSession.abort(e, "Fresh start failed."); |
michael@0 | 695 | } |
michael@0 | 696 | |
michael@0 | 697 | @Override |
michael@0 | 698 | public void onFreshStart() { |
michael@0 | 699 | try { |
michael@0 | 700 | Logger.warn(LOG_TAG, "Fresh start succeeded; restarting global session."); |
michael@0 | 701 | globalSession.config.persistToPrefs(); |
michael@0 | 702 | globalSession.restart(); |
michael@0 | 703 | } catch (Exception e) { |
michael@0 | 704 | Logger.warn(LOG_TAG, "Got exception when restarting sync after freshStart.", e); |
michael@0 | 705 | globalSession.abort(e, "Got exception after freshStart."); |
michael@0 | 706 | } |
michael@0 | 707 | } |
michael@0 | 708 | }); |
michael@0 | 709 | } |
michael@0 | 710 | |
michael@0 | 711 | /** |
michael@0 | 712 | * Clean the server, aborting the current sync. |
michael@0 | 713 | * <p> |
michael@0 | 714 | * <ol> |
michael@0 | 715 | * <li>Wipe the server storage.</li> |
michael@0 | 716 | * <li>Reset all stages and purge cached state: (meta/global and crypto/keys records).</li> |
michael@0 | 717 | * <li>Upload fresh meta/global record.</li> |
michael@0 | 718 | * <li>Upload fresh crypto/keys record.</li> |
michael@0 | 719 | * <li>Restart the sync entirely in order to re-download meta/global and crypto/keys record.</li> |
michael@0 | 720 | * </ol> |
michael@0 | 721 | * @param session the current session. |
michael@0 | 722 | * @param freshStartDelegate delegate to notify on fresh start or failure. |
michael@0 | 723 | */ |
michael@0 | 724 | protected static void freshStart(final GlobalSession session, final FreshStartDelegate freshStartDelegate) { |
michael@0 | 725 | Logger.debug(LOG_TAG, "Fresh starting."); |
michael@0 | 726 | |
michael@0 | 727 | final MetaGlobal mg = session.generateNewMetaGlobal(); |
michael@0 | 728 | |
michael@0 | 729 | session.wipeServer(session.getAuthHeaderProvider(), new WipeServerDelegate() { |
michael@0 | 730 | |
michael@0 | 731 | @Override |
michael@0 | 732 | public void onWiped(long timestamp) { |
michael@0 | 733 | Logger.debug(LOG_TAG, "Successfully wiped server. Resetting all stages and purging cached meta/global and crypto/keys records."); |
michael@0 | 734 | |
michael@0 | 735 | session.resetAllStages(); |
michael@0 | 736 | session.config.purgeMetaGlobal(); |
michael@0 | 737 | session.config.purgeCryptoKeys(); |
michael@0 | 738 | session.config.persistToPrefs(); |
michael@0 | 739 | |
michael@0 | 740 | Logger.info(LOG_TAG, "Uploading new meta/global with sync ID " + mg.syncID + "."); |
michael@0 | 741 | |
michael@0 | 742 | // It would be good to set the X-If-Unmodified-Since header to `timestamp` |
michael@0 | 743 | // for this PUT to ensure at least some level of transactionality. |
michael@0 | 744 | // Unfortunately, the servers don't support it after a wipe right now |
michael@0 | 745 | // (bug 693893), so we're going to defer this until bug 692700. |
michael@0 | 746 | mg.upload(new MetaGlobalDelegate() { |
michael@0 | 747 | @Override |
michael@0 | 748 | public void handleSuccess(MetaGlobal uploadedGlobal, SyncStorageResponse uploadResponse) { |
michael@0 | 749 | Logger.info(LOG_TAG, "Uploaded new meta/global with sync ID " + uploadedGlobal.syncID + "."); |
michael@0 | 750 | |
michael@0 | 751 | // Generate new keys. |
michael@0 | 752 | CollectionKeys keys = null; |
michael@0 | 753 | try { |
michael@0 | 754 | keys = session.generateNewCryptoKeys(); |
michael@0 | 755 | } catch (CryptoException e) { |
michael@0 | 756 | Logger.warn(LOG_TAG, "Got exception generating new keys; failing fresh start.", e); |
michael@0 | 757 | freshStartDelegate.onFreshStartFailed(e); |
michael@0 | 758 | } |
michael@0 | 759 | if (keys == null) { |
michael@0 | 760 | Logger.warn(LOG_TAG, "Got null keys from generateNewKeys; failing fresh start."); |
michael@0 | 761 | freshStartDelegate.onFreshStartFailed(null); |
michael@0 | 762 | } |
michael@0 | 763 | |
michael@0 | 764 | // Upload new keys. |
michael@0 | 765 | Logger.info(LOG_TAG, "Uploading new crypto/keys."); |
michael@0 | 766 | session.uploadKeys(keys, new KeyUploadDelegate() { |
michael@0 | 767 | @Override |
michael@0 | 768 | public void onKeysUploaded() { |
michael@0 | 769 | Logger.info(LOG_TAG, "Uploaded new crypto/keys."); |
michael@0 | 770 | freshStartDelegate.onFreshStart(); |
michael@0 | 771 | } |
michael@0 | 772 | |
michael@0 | 773 | @Override |
michael@0 | 774 | public void onKeyUploadFailed(Exception e) { |
michael@0 | 775 | Logger.warn(LOG_TAG, "Got exception uploading new keys.", e); |
michael@0 | 776 | freshStartDelegate.onFreshStartFailed(e); |
michael@0 | 777 | } |
michael@0 | 778 | }); |
michael@0 | 779 | } |
michael@0 | 780 | |
michael@0 | 781 | @Override |
michael@0 | 782 | public void handleMissing(MetaGlobal global, SyncStorageResponse response) { |
michael@0 | 783 | // Shouldn't happen on upload. |
michael@0 | 784 | Logger.warn(LOG_TAG, "Got 'missing' response uploading new meta/global."); |
michael@0 | 785 | freshStartDelegate.onFreshStartFailed(new Exception("meta/global missing while uploading.")); |
michael@0 | 786 | } |
michael@0 | 787 | |
michael@0 | 788 | @Override |
michael@0 | 789 | public void handleFailure(SyncStorageResponse response) { |
michael@0 | 790 | Logger.warn(LOG_TAG, "Got failure " + response.getStatusCode() + " uploading new meta/global."); |
michael@0 | 791 | session.interpretHTTPFailure(response.httpResponse()); |
michael@0 | 792 | freshStartDelegate.onFreshStartFailed(new HTTPFailureException(response)); |
michael@0 | 793 | } |
michael@0 | 794 | |
michael@0 | 795 | @Override |
michael@0 | 796 | public void handleError(Exception e) { |
michael@0 | 797 | Logger.warn(LOG_TAG, "Got error uploading new meta/global.", e); |
michael@0 | 798 | freshStartDelegate.onFreshStartFailed(e); |
michael@0 | 799 | } |
michael@0 | 800 | }); |
michael@0 | 801 | } |
michael@0 | 802 | |
michael@0 | 803 | @Override |
michael@0 | 804 | public void onWipeFailed(Exception e) { |
michael@0 | 805 | Logger.warn(LOG_TAG, "Wipe failed."); |
michael@0 | 806 | freshStartDelegate.onFreshStartFailed(e); |
michael@0 | 807 | } |
michael@0 | 808 | }); |
michael@0 | 809 | } |
michael@0 | 810 | |
michael@0 | 811 | // Note that we do not yet implement wipeRemote: it's only necessary for |
michael@0 | 812 | // first sync options. |
michael@0 | 813 | // -- reset local stages, wipe server for each stage *except* clients |
michael@0 | 814 | // (stages only, not whole server!), send wipeEngine commands to each client. |
michael@0 | 815 | // |
michael@0 | 816 | // Similarly for startOver (because we don't receive that notification). |
michael@0 | 817 | // -- remove client data from server, reset local stages, clear keys, reset |
michael@0 | 818 | // backoff, clear all prefs, discard credentials. |
michael@0 | 819 | // |
michael@0 | 820 | // Change passphrase: wipe entire server, reset client to force upload, sync. |
michael@0 | 821 | // |
michael@0 | 822 | // When an engine is disabled: wipe its collections on the server, reupload |
michael@0 | 823 | // meta/global. |
michael@0 | 824 | // |
michael@0 | 825 | // On syncing each stage: if server has engine version 0 or old, wipe server, |
michael@0 | 826 | // reset client to prompt reupload. |
michael@0 | 827 | // If sync ID mismatch: take that syncID and reset client. |
michael@0 | 828 | |
michael@0 | 829 | protected void wipeServer(final AuthHeaderProvider authHeaderProvider, final WipeServerDelegate wipeDelegate) { |
michael@0 | 830 | SyncStorageRequest request; |
michael@0 | 831 | final GlobalSession self = this; |
michael@0 | 832 | |
michael@0 | 833 | try { |
michael@0 | 834 | request = new SyncStorageRequest(config.storageURL()); |
michael@0 | 835 | } catch (URISyntaxException ex) { |
michael@0 | 836 | Logger.warn(LOG_TAG, "Invalid URI in wipeServer."); |
michael@0 | 837 | wipeDelegate.onWipeFailed(ex); |
michael@0 | 838 | return; |
michael@0 | 839 | } |
michael@0 | 840 | |
michael@0 | 841 | request.delegate = new SyncStorageRequestDelegate() { |
michael@0 | 842 | |
michael@0 | 843 | @Override |
michael@0 | 844 | public String ifUnmodifiedSince() { |
michael@0 | 845 | return null; |
michael@0 | 846 | } |
michael@0 | 847 | |
michael@0 | 848 | @Override |
michael@0 | 849 | public void handleRequestSuccess(SyncStorageResponse response) { |
michael@0 | 850 | BaseResource.consumeEntity(response); |
michael@0 | 851 | wipeDelegate.onWiped(response.normalizedWeaveTimestamp()); |
michael@0 | 852 | } |
michael@0 | 853 | |
michael@0 | 854 | @Override |
michael@0 | 855 | public void handleRequestFailure(SyncStorageResponse response) { |
michael@0 | 856 | Logger.warn(LOG_TAG, "Got request failure " + response.getStatusCode() + " in wipeServer."); |
michael@0 | 857 | // Process HTTP failures here to pick up backoffs, etc. |
michael@0 | 858 | self.interpretHTTPFailure(response.httpResponse()); |
michael@0 | 859 | BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response. |
michael@0 | 860 | wipeDelegate.onWipeFailed(new HTTPFailureException(response)); |
michael@0 | 861 | } |
michael@0 | 862 | |
michael@0 | 863 | @Override |
michael@0 | 864 | public void handleRequestError(Exception ex) { |
michael@0 | 865 | Logger.warn(LOG_TAG, "Got exception in wipeServer.", ex); |
michael@0 | 866 | wipeDelegate.onWipeFailed(ex); |
michael@0 | 867 | } |
michael@0 | 868 | |
michael@0 | 869 | @Override |
michael@0 | 870 | public AuthHeaderProvider getAuthHeaderProvider() { |
michael@0 | 871 | return GlobalSession.this.getAuthHeaderProvider(); |
michael@0 | 872 | } |
michael@0 | 873 | }; |
michael@0 | 874 | request.delete(); |
michael@0 | 875 | } |
michael@0 | 876 | |
michael@0 | 877 | public void wipeAllStages() { |
michael@0 | 878 | Logger.info(LOG_TAG, "Wiping all stages."); |
michael@0 | 879 | // Includes "clients". |
michael@0 | 880 | this.wipeStagesByEnum(Stage.getNamedStages()); |
michael@0 | 881 | } |
michael@0 | 882 | |
michael@0 | 883 | public void wipeStages(Collection<GlobalSyncStage> stages) { |
michael@0 | 884 | for (GlobalSyncStage stage : stages) { |
michael@0 | 885 | try { |
michael@0 | 886 | Logger.info(LOG_TAG, "Wiping " + stage); |
michael@0 | 887 | stage.wipeLocal(this); |
michael@0 | 888 | } catch (Exception e) { |
michael@0 | 889 | Logger.error(LOG_TAG, "Ignoring wipe failure for stage " + stage, e); |
michael@0 | 890 | } |
michael@0 | 891 | } |
michael@0 | 892 | } |
michael@0 | 893 | |
michael@0 | 894 | public void wipeStagesByEnum(Collection<Stage> stages) { |
michael@0 | 895 | wipeStages(this.getSyncStagesByEnum(stages)); |
michael@0 | 896 | } |
michael@0 | 897 | |
michael@0 | 898 | public void wipeStagesByName(Collection<String> names) { |
michael@0 | 899 | wipeStages(this.getSyncStagesByName(names)); |
michael@0 | 900 | } |
michael@0 | 901 | |
michael@0 | 902 | public void resetAllStages() { |
michael@0 | 903 | Logger.info(LOG_TAG, "Resetting all stages."); |
michael@0 | 904 | // Includes "clients". |
michael@0 | 905 | this.resetStagesByEnum(Stage.getNamedStages()); |
michael@0 | 906 | } |
michael@0 | 907 | |
michael@0 | 908 | public void resetStages(Collection<GlobalSyncStage> stages) { |
michael@0 | 909 | for (GlobalSyncStage stage : stages) { |
michael@0 | 910 | try { |
michael@0 | 911 | Logger.info(LOG_TAG, "Resetting " + stage); |
michael@0 | 912 | stage.resetLocal(this); |
michael@0 | 913 | } catch (Exception e) { |
michael@0 | 914 | Logger.error(LOG_TAG, "Ignoring reset failure for stage " + stage, e); |
michael@0 | 915 | } |
michael@0 | 916 | } |
michael@0 | 917 | } |
michael@0 | 918 | |
michael@0 | 919 | public void resetStagesByEnum(Collection<Stage> stages) { |
michael@0 | 920 | resetStages(this.getSyncStagesByEnum(stages)); |
michael@0 | 921 | } |
michael@0 | 922 | |
michael@0 | 923 | public void resetStagesByName(Collection<String> names) { |
michael@0 | 924 | resetStages(this.getSyncStagesByName(names)); |
michael@0 | 925 | } |
michael@0 | 926 | |
michael@0 | 927 | /** |
michael@0 | 928 | * Engines to explicitly mark as declined in a fresh meta/global record. |
michael@0 | 929 | * <p> |
michael@0 | 930 | * Returns an empty array if the user hasn't elected to customize data types, |
michael@0 | 931 | * or an array of engines that the user un-checked during customization. |
michael@0 | 932 | * <p> |
michael@0 | 933 | * Engines that Android Sync doesn't recognize are <b>not</b> included in |
michael@0 | 934 | * the returned array. |
michael@0 | 935 | * |
michael@0 | 936 | * @return a new JSONArray of engine names. |
michael@0 | 937 | */ |
michael@0 | 938 | @SuppressWarnings("unchecked") |
michael@0 | 939 | protected JSONArray declinedEngineNames() { |
michael@0 | 940 | final JSONArray declined = new JSONArray(); |
michael@0 | 941 | for (String engine : config.declinedEngineNames) { |
michael@0 | 942 | declined.add(engine); |
michael@0 | 943 | }; |
michael@0 | 944 | |
michael@0 | 945 | return declined; |
michael@0 | 946 | } |
michael@0 | 947 | |
michael@0 | 948 | /** |
michael@0 | 949 | * Engines to include in a fresh meta/global record. |
michael@0 | 950 | * <p> |
michael@0 | 951 | * Returns either the persisted engine names (perhaps we have been node |
michael@0 | 952 | * re-assigned and are initializing a clean server: we want to upload the |
michael@0 | 953 | * persisted engine names so that we don't accidentally disable engines that |
michael@0 | 954 | * Android Sync doesn't recognize), or the set of engines names that Android |
michael@0 | 955 | * Sync implements. |
michael@0 | 956 | * |
michael@0 | 957 | * @return set of engine names. |
michael@0 | 958 | */ |
michael@0 | 959 | protected Set<String> enabledEngineNames() { |
michael@0 | 960 | if (config.enabledEngineNames != null) { |
michael@0 | 961 | return config.enabledEngineNames; |
michael@0 | 962 | } |
michael@0 | 963 | |
michael@0 | 964 | // These are the default set of engine names. |
michael@0 | 965 | Set<String> validEngineNames = SyncConfiguration.validEngineNames(); |
michael@0 | 966 | |
michael@0 | 967 | // If the user hasn't set any selected engines, that's okay -- default to |
michael@0 | 968 | // everything. |
michael@0 | 969 | if (config.userSelectedEngines == null) { |
michael@0 | 970 | return validEngineNames; |
michael@0 | 971 | } |
michael@0 | 972 | |
michael@0 | 973 | // userSelectedEngines has keys that are engine names, and boolean values |
michael@0 | 974 | // corresponding to whether the user asked for the engine to sync or not. If |
michael@0 | 975 | // an engine is not present, that means the user didn't change its sync |
michael@0 | 976 | // setting. Since we default to everything on, that means the user didn't |
michael@0 | 977 | // turn it off; therefore, it's included in the set of engines to sync. |
michael@0 | 978 | Set<String> validAndSelectedEngineNames = new HashSet<String>(); |
michael@0 | 979 | for (String engineName : validEngineNames) { |
michael@0 | 980 | if (config.userSelectedEngines.containsKey(engineName) && |
michael@0 | 981 | !config.userSelectedEngines.get(engineName)) { |
michael@0 | 982 | continue; |
michael@0 | 983 | } |
michael@0 | 984 | validAndSelectedEngineNames.add(engineName); |
michael@0 | 985 | } |
michael@0 | 986 | return validAndSelectedEngineNames; |
michael@0 | 987 | } |
michael@0 | 988 | |
michael@0 | 989 | /** |
michael@0 | 990 | * Generate fresh crypto/keys collection. |
michael@0 | 991 | * @return crypto/keys collection. |
michael@0 | 992 | * @throws CryptoException |
michael@0 | 993 | */ |
michael@0 | 994 | @SuppressWarnings("static-method") |
michael@0 | 995 | public CollectionKeys generateNewCryptoKeys() throws CryptoException { |
michael@0 | 996 | return CollectionKeys.generateCollectionKeys(); |
michael@0 | 997 | } |
michael@0 | 998 | |
michael@0 | 999 | /** |
michael@0 | 1000 | * Generate a fresh meta/global record. |
michael@0 | 1001 | * @return meta/global record. |
michael@0 | 1002 | */ |
michael@0 | 1003 | public MetaGlobal generateNewMetaGlobal() { |
michael@0 | 1004 | final String newSyncID = Utils.generateGuid(); |
michael@0 | 1005 | final String metaURL = this.config.metaURL(); |
michael@0 | 1006 | |
michael@0 | 1007 | ExtendedJSONObject engines = new ExtendedJSONObject(); |
michael@0 | 1008 | for (String engineName : enabledEngineNames()) { |
michael@0 | 1009 | EngineSettings engineSettings = null; |
michael@0 | 1010 | try { |
michael@0 | 1011 | GlobalSyncStage globalStage = this.getSyncStageByName(engineName); |
michael@0 | 1012 | Integer version = globalStage.getStorageVersion(); |
michael@0 | 1013 | if (version == null) { |
michael@0 | 1014 | continue; // Don't want this stage to be included in meta/global. |
michael@0 | 1015 | } |
michael@0 | 1016 | engineSettings = new EngineSettings(Utils.generateGuid(), version.intValue()); |
michael@0 | 1017 | } catch (NoSuchStageException e) { |
michael@0 | 1018 | // No trouble; Android Sync might not recognize this engine yet. |
michael@0 | 1019 | // By default, version 0. Other clients will see the 0 version and reset/wipe accordingly. |
michael@0 | 1020 | engineSettings = new EngineSettings(Utils.generateGuid(), 0); |
michael@0 | 1021 | } |
michael@0 | 1022 | engines.put(engineName, engineSettings.toJSONObject()); |
michael@0 | 1023 | } |
michael@0 | 1024 | |
michael@0 | 1025 | MetaGlobal metaGlobal = new MetaGlobal(metaURL, this.getAuthHeaderProvider()); |
michael@0 | 1026 | metaGlobal.setSyncID(newSyncID); |
michael@0 | 1027 | metaGlobal.setStorageVersion(STORAGE_VERSION); |
michael@0 | 1028 | metaGlobal.setEngines(engines); |
michael@0 | 1029 | |
michael@0 | 1030 | // We assume that the config's declined engines have been updated |
michael@0 | 1031 | // according to the user's selections. |
michael@0 | 1032 | metaGlobal.setDeclinedEngineNames(this.declinedEngineNames()); |
michael@0 | 1033 | |
michael@0 | 1034 | return metaGlobal; |
michael@0 | 1035 | } |
michael@0 | 1036 | |
michael@0 | 1037 | /** |
michael@0 | 1038 | * Suggest that your Sync client needs to be upgraded to work |
michael@0 | 1039 | * with this server. |
michael@0 | 1040 | */ |
michael@0 | 1041 | public void requiresUpgrade() { |
michael@0 | 1042 | Logger.info(LOG_TAG, "Client outdated storage version; requires update."); |
michael@0 | 1043 | // TODO: notify UI. |
michael@0 | 1044 | this.abort(null, "Requires upgrade"); |
michael@0 | 1045 | } |
michael@0 | 1046 | |
michael@0 | 1047 | /** |
michael@0 | 1048 | * If meta/global is missing or malformed, throws a MetaGlobalException. |
michael@0 | 1049 | * Otherwise, returns true if there is an entry for this engine in the |
michael@0 | 1050 | * meta/global "engines" object. |
michael@0 | 1051 | * <p> |
michael@0 | 1052 | * This is a global/permanent setting, not a local/temporary setting. For the |
michael@0 | 1053 | * latter, see {@link GlobalSession#isEngineLocallyEnabled(String)}. |
michael@0 | 1054 | * |
michael@0 | 1055 | * @param engineName the name to check (e.g., "bookmarks"). |
michael@0 | 1056 | * @param engineSettings |
michael@0 | 1057 | * if non-null, verify that the server engine settings are congruent |
michael@0 | 1058 | * with this, throwing the appropriate MetaGlobalException if not. |
michael@0 | 1059 | * @return |
michael@0 | 1060 | * true if the engine with the provided name is present in the |
michael@0 | 1061 | * meta/global "engines" object, and verification passed. |
michael@0 | 1062 | * |
michael@0 | 1063 | * @throws MetaGlobalException |
michael@0 | 1064 | */ |
michael@0 | 1065 | public boolean isEngineRemotelyEnabled(String engineName, EngineSettings engineSettings) throws MetaGlobalException { |
michael@0 | 1066 | if (this.config.metaGlobal == null) { |
michael@0 | 1067 | throw new MetaGlobalNotSetException(); |
michael@0 | 1068 | } |
michael@0 | 1069 | |
michael@0 | 1070 | // This should not occur. |
michael@0 | 1071 | if (this.config.enabledEngineNames == null) { |
michael@0 | 1072 | Logger.error(LOG_TAG, "No enabled engines in config. Giving up."); |
michael@0 | 1073 | throw new MetaGlobalMissingEnginesException(); |
michael@0 | 1074 | } |
michael@0 | 1075 | |
michael@0 | 1076 | if (!(this.config.enabledEngineNames.contains(engineName))) { |
michael@0 | 1077 | Logger.debug(LOG_TAG, "Engine " + engineName + " not enabled: no meta/global entry."); |
michael@0 | 1078 | return false; |
michael@0 | 1079 | } |
michael@0 | 1080 | |
michael@0 | 1081 | // If we have a meta/global, check that it's safe for us to sync. |
michael@0 | 1082 | // (If we don't, we'll create one later, which is why we return `true` above.) |
michael@0 | 1083 | if (engineSettings != null) { |
michael@0 | 1084 | // Throws if there's a problem. |
michael@0 | 1085 | this.config.metaGlobal.verifyEngineSettings(engineName, engineSettings); |
michael@0 | 1086 | } |
michael@0 | 1087 | |
michael@0 | 1088 | return true; |
michael@0 | 1089 | } |
michael@0 | 1090 | |
michael@0 | 1091 | |
michael@0 | 1092 | /** |
michael@0 | 1093 | * Return true if the named stage should be synced this session. |
michael@0 | 1094 | * <p> |
michael@0 | 1095 | * This is a local/temporary setting, in contrast to the meta/global record, |
michael@0 | 1096 | * which is a global/permanent setting. For the latter, see |
michael@0 | 1097 | * {@link GlobalSession#isEngineRemotelyEnabled(String, EngineSettings)}. |
michael@0 | 1098 | * |
michael@0 | 1099 | * @param stageName |
michael@0 | 1100 | * to query. |
michael@0 | 1101 | * @return true if named stage is enabled for this sync. |
michael@0 | 1102 | */ |
michael@0 | 1103 | public boolean isEngineLocallyEnabled(String stageName) { |
michael@0 | 1104 | if (config.stagesToSync == null) { |
michael@0 | 1105 | return true; |
michael@0 | 1106 | } |
michael@0 | 1107 | return config.stagesToSync.contains(stageName); |
michael@0 | 1108 | } |
michael@0 | 1109 | |
michael@0 | 1110 | public ClientsDataDelegate getClientsDelegate() { |
michael@0 | 1111 | return this.clientsDelegate; |
michael@0 | 1112 | } |
michael@0 | 1113 | |
michael@0 | 1114 | /** |
michael@0 | 1115 | * The longest backoff observed to date; -1 means no backoff observed. |
michael@0 | 1116 | */ |
michael@0 | 1117 | protected final AtomicLong largestBackoffObserved = new AtomicLong(-1); |
michael@0 | 1118 | |
michael@0 | 1119 | /** |
michael@0 | 1120 | * Reset any observed backoff and start observing HTTP responses for backoff |
michael@0 | 1121 | * requests. |
michael@0 | 1122 | */ |
michael@0 | 1123 | protected void installAsHttpResponseObserver() { |
michael@0 | 1124 | Logger.debug(LOG_TAG, "Installing " + this + " as BaseResource HttpResponseObserver."); |
michael@0 | 1125 | BaseResource.setHttpResponseObserver(this); |
michael@0 | 1126 | largestBackoffObserved.set(-1); |
michael@0 | 1127 | } |
michael@0 | 1128 | |
michael@0 | 1129 | /** |
michael@0 | 1130 | * Stop observing HttpResponses for backoff requests. |
michael@0 | 1131 | */ |
michael@0 | 1132 | protected void uninstallAsHttpResponseObserver() { |
michael@0 | 1133 | Logger.debug(LOG_TAG, "Uninstalling " + this + " as BaseResource HttpResponseObserver."); |
michael@0 | 1134 | BaseResource.setHttpResponseObserver(null); |
michael@0 | 1135 | } |
michael@0 | 1136 | |
michael@0 | 1137 | /** |
michael@0 | 1138 | * Observe all HTTP response for backoff requests on all status codes, not just errors. |
michael@0 | 1139 | */ |
michael@0 | 1140 | @Override |
michael@0 | 1141 | public void observeHttpResponse(HttpResponse response) { |
michael@0 | 1142 | long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds(); // TODO: don't allocate object? |
michael@0 | 1143 | if (responseBackoff <= 0) { |
michael@0 | 1144 | return; |
michael@0 | 1145 | } |
michael@0 | 1146 | |
michael@0 | 1147 | Logger.debug(LOG_TAG, "Observed " + responseBackoff + " millisecond backoff request."); |
michael@0 | 1148 | while (true) { |
michael@0 | 1149 | long existingBackoff = largestBackoffObserved.get(); |
michael@0 | 1150 | if (existingBackoff >= responseBackoff) { |
michael@0 | 1151 | return; |
michael@0 | 1152 | } |
michael@0 | 1153 | if (largestBackoffObserved.compareAndSet(existingBackoff, responseBackoff)) { |
michael@0 | 1154 | return; |
michael@0 | 1155 | } |
michael@0 | 1156 | } |
michael@0 | 1157 | } |
michael@0 | 1158 | } |