|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.fxa.sync; |
|
6 |
|
7 import java.net.URI; |
|
8 import java.net.URISyntaxException; |
|
9 import java.security.NoSuchAlgorithmException; |
|
10 import java.util.Collection; |
|
11 import java.util.Collections; |
|
12 import java.util.EnumSet; |
|
13 import java.util.concurrent.CountDownLatch; |
|
14 import java.util.concurrent.ExecutorService; |
|
15 import java.util.concurrent.Executors; |
|
16 |
|
17 import org.mozilla.gecko.background.common.log.Logger; |
|
18 import org.mozilla.gecko.background.fxa.FxAccountClient; |
|
19 import org.mozilla.gecko.background.fxa.FxAccountClient20; |
|
20 import org.mozilla.gecko.background.fxa.SkewHandler; |
|
21 import org.mozilla.gecko.browserid.BrowserIDKeyPair; |
|
22 import org.mozilla.gecko.browserid.JSONWebTokenUtils; |
|
23 import org.mozilla.gecko.fxa.FirefoxAccounts; |
|
24 import org.mozilla.gecko.fxa.FxAccountConstants; |
|
25 import org.mozilla.gecko.fxa.authenticator.AccountPickler; |
|
26 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; |
|
27 import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator; |
|
28 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; |
|
29 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; |
|
30 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; |
|
31 import org.mozilla.gecko.fxa.login.Married; |
|
32 import org.mozilla.gecko.fxa.login.State; |
|
33 import org.mozilla.gecko.fxa.login.State.StateLabel; |
|
34 import org.mozilla.gecko.fxa.login.StateFactory; |
|
35 import org.mozilla.gecko.sync.BackoffHandler; |
|
36 import org.mozilla.gecko.sync.GlobalSession; |
|
37 import org.mozilla.gecko.sync.PrefsBackoffHandler; |
|
38 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; |
|
39 import org.mozilla.gecko.sync.SyncConfiguration; |
|
40 import org.mozilla.gecko.sync.ThreadPool; |
|
41 import org.mozilla.gecko.sync.Utils; |
|
42 import org.mozilla.gecko.sync.crypto.KeyBundle; |
|
43 import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback; |
|
44 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; |
|
45 import org.mozilla.gecko.sync.net.AuthHeaderProvider; |
|
46 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider; |
|
47 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; |
|
48 import org.mozilla.gecko.tokenserver.TokenServerClient; |
|
49 import org.mozilla.gecko.tokenserver.TokenServerClientDelegate; |
|
50 import org.mozilla.gecko.tokenserver.TokenServerException; |
|
51 import org.mozilla.gecko.tokenserver.TokenServerToken; |
|
52 |
|
53 import android.accounts.Account; |
|
54 import android.content.AbstractThreadedSyncAdapter; |
|
55 import android.content.ContentProviderClient; |
|
56 import android.content.ContentResolver; |
|
57 import android.content.Context; |
|
58 import android.content.SharedPreferences; |
|
59 import android.content.SyncResult; |
|
60 import android.os.Bundle; |
|
61 import android.os.SystemClock; |
|
62 |
|
63 public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { |
|
64 private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName(); |
|
65 |
|
66 public static final String SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT = "respect_local_rate_limit"; |
|
67 public static final String SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF = "respect_remote_server_backoff"; |
|
68 |
|
69 protected static final int NOTIFICATION_ID = LOG_TAG.hashCode(); |
|
70 |
|
71 // Tracks the last seen storage hostname for backoff purposes. |
|
72 private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost"; |
|
73 |
|
74 // Used to do cheap in-memory rate limiting. Don't sync again if we |
|
75 // successfully synced within this duration. |
|
76 private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000; // 15 seconds. |
|
77 private volatile long lastSyncRealtimeMillis = 0L; |
|
78 |
|
79 protected final ExecutorService executor; |
|
80 protected final FxAccountNotificationManager notificationManager; |
|
81 |
|
82 public FxAccountSyncAdapter(Context context, boolean autoInitialize) { |
|
83 super(context, autoInitialize); |
|
84 this.executor = Executors.newSingleThreadExecutor(); |
|
85 this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID); |
|
86 } |
|
87 |
|
88 protected static class SyncDelegate { |
|
89 protected final CountDownLatch latch; |
|
90 protected final SyncResult syncResult; |
|
91 protected final AndroidFxAccount fxAccount; |
|
92 protected final Collection<String> stageNamesToSync; |
|
93 |
|
94 public SyncDelegate(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) { |
|
95 if (latch == null) { |
|
96 throw new IllegalArgumentException("latch must not be null"); |
|
97 } |
|
98 if (syncResult == null) { |
|
99 throw new IllegalArgumentException("syncResult must not be null"); |
|
100 } |
|
101 if (fxAccount == null) { |
|
102 throw new IllegalArgumentException("fxAccount must not be null"); |
|
103 } |
|
104 this.latch = latch; |
|
105 this.syncResult = syncResult; |
|
106 this.fxAccount = fxAccount; |
|
107 this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync); |
|
108 } |
|
109 |
|
110 /** |
|
111 * No error! Say that we made progress. |
|
112 */ |
|
113 protected void setSyncResultSuccess() { |
|
114 syncResult.stats.numUpdates += 1; |
|
115 } |
|
116 |
|
117 /** |
|
118 * Soft error. Say that we made progress, so that Android will sync us again |
|
119 * after exponential backoff. |
|
120 */ |
|
121 protected void setSyncResultSoftError() { |
|
122 syncResult.stats.numUpdates += 1; |
|
123 syncResult.stats.numIoExceptions += 1; |
|
124 } |
|
125 |
|
126 /** |
|
127 * Hard error. We don't want Android to sync us again, even if we make |
|
128 * progress, until the user intervenes. |
|
129 */ |
|
130 protected void setSyncResultHardError() { |
|
131 syncResult.stats.numAuthExceptions += 1; |
|
132 } |
|
133 |
|
134 public void handleSuccess() { |
|
135 Logger.info(LOG_TAG, "Sync succeeded."); |
|
136 setSyncResultSuccess(); |
|
137 latch.countDown(); |
|
138 } |
|
139 |
|
140 public void handleError(Exception e) { |
|
141 Logger.error(LOG_TAG, "Got exception syncing.", e); |
|
142 setSyncResultSoftError(); |
|
143 // This is awful, but we need to propagate bad assertions back up the |
|
144 // chain somehow, and this will do for now. |
|
145 if (e instanceof TokenServerException) { |
|
146 // We should only get here *after* we're locked into the married state. |
|
147 State state = fxAccount.getState(); |
|
148 if (state.getStateLabel() == StateLabel.Married) { |
|
149 Married married = (Married) state; |
|
150 fxAccount.setState(married.makeCohabitingState()); |
|
151 } |
|
152 } |
|
153 latch.countDown(); |
|
154 } |
|
155 |
|
156 /** |
|
157 * When the login machine terminates, we might not be in the |
|
158 * <code>Married</code> state, and therefore we can't sync. This method |
|
159 * messages as much to the user. |
|
160 * <p> |
|
161 * To avoid stopping us syncing altogether, we set a soft error rather than |
|
162 * a hard error. In future, we would like to set a hard error if we are in, |
|
163 * for example, the <code>Separated</code> state, and then have some user |
|
164 * initiated activity mark the Android account as ready to sync again. This |
|
165 * is tricky, though, so we play it safe for now. |
|
166 * |
|
167 * @param finalState |
|
168 * that login machine ended in. |
|
169 */ |
|
170 public void handleCannotSync(State finalState) { |
|
171 Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel()); |
|
172 setSyncResultSoftError(); |
|
173 latch.countDown(); |
|
174 } |
|
175 |
|
176 public void postponeSync(long millis) { |
|
177 if (millis <= 0) { |
|
178 Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay. Short-circuiting."); |
|
179 } else { |
|
180 // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669 |
|
181 // So we don't bother doing this. Instead, we rely on the periodic sync |
|
182 // we schedule, and the backoff handler for the rest. |
|
183 /* |
|
184 Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms."); |
|
185 syncResult.delayUntil = millis / 1000; |
|
186 */ |
|
187 } |
|
188 setSyncResultSoftError(); |
|
189 latch.countDown(); |
|
190 } |
|
191 |
|
192 /** |
|
193 * Simply don't sync, without setting any error flags. |
|
194 * This is the appropriate behavior when a routine backoff has not yet |
|
195 * been met. |
|
196 */ |
|
197 public void rejectSync() { |
|
198 latch.countDown(); |
|
199 } |
|
200 |
|
201 public Collection<String> getStageNamesToSync() { |
|
202 return this.stageNamesToSync; |
|
203 } |
|
204 } |
|
205 |
|
206 protected static class SessionCallback implements BaseGlobalSessionCallback { |
|
207 protected final SyncDelegate syncDelegate; |
|
208 protected final SchedulePolicy schedulePolicy; |
|
209 protected volatile BackoffHandler storageBackoffHandler; |
|
210 |
|
211 public SessionCallback(SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) { |
|
212 this.syncDelegate = syncDelegate; |
|
213 this.schedulePolicy = schedulePolicy; |
|
214 } |
|
215 |
|
216 public void setBackoffHandler(BackoffHandler backoffHandler) { |
|
217 this.storageBackoffHandler = backoffHandler; |
|
218 } |
|
219 |
|
220 @Override |
|
221 public boolean shouldBackOffStorage() { |
|
222 return storageBackoffHandler.delayMilliseconds() > 0; |
|
223 } |
|
224 |
|
225 @Override |
|
226 public void requestBackoff(long backoffMillis) { |
|
227 final boolean onlyExtend = true; // Because we trust what the storage server says. |
|
228 schedulePolicy.configureBackoffMillisOnBackoff(storageBackoffHandler, backoffMillis, onlyExtend); |
|
229 } |
|
230 |
|
231 @Override |
|
232 public void informUpgradeRequiredResponse(GlobalSession session) { |
|
233 schedulePolicy.onUpgradeRequired(); |
|
234 } |
|
235 |
|
236 @Override |
|
237 public void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL) { |
|
238 schedulePolicy.onUnauthorized(); |
|
239 } |
|
240 |
|
241 @Override |
|
242 public void handleStageCompleted(Stage currentState, GlobalSession globalSession) { |
|
243 } |
|
244 |
|
245 @Override |
|
246 public void handleSuccess(GlobalSession globalSession) { |
|
247 Logger.info(LOG_TAG, "Global session succeeded."); |
|
248 |
|
249 // Get the number of clients, so we can schedule the sync interval accordingly. |
|
250 try { |
|
251 int otherClientsCount = globalSession.getClientsDelegate().getClientsCount(); |
|
252 Logger.debug(LOG_TAG, "" + otherClientsCount + " other client(s)."); |
|
253 this.schedulePolicy.onSuccessfulSync(otherClientsCount); |
|
254 } finally { |
|
255 // Continue with the usual success flow. |
|
256 syncDelegate.handleSuccess(); |
|
257 } |
|
258 } |
|
259 |
|
260 @Override |
|
261 public void handleError(GlobalSession globalSession, Exception e) { |
|
262 Logger.warn(LOG_TAG, "Global session failed."); // Exception will be dumped by delegate below. |
|
263 syncDelegate.handleError(e); |
|
264 // TODO: should we reduce the periodic sync interval? |
|
265 } |
|
266 |
|
267 @Override |
|
268 public void handleAborted(GlobalSession globalSession, String reason) { |
|
269 Logger.warn(LOG_TAG, "Global session aborted: " + reason); |
|
270 syncDelegate.handleError(null); |
|
271 // TODO: should we reduce the periodic sync interval? |
|
272 } |
|
273 }; |
|
274 |
|
275 /** |
|
276 * Return true if the provided {@link BackoffHandler} isn't reporting that we're in |
|
277 * a backoff state, or the provided {@link Bundle} contains flags that indicate |
|
278 * we should force a sync. |
|
279 */ |
|
280 private boolean shouldPerformSync(final BackoffHandler backoffHandler, final String kind, final Bundle extras) { |
|
281 final long delay = backoffHandler.delayMilliseconds(); |
|
282 if (delay <= 0) { |
|
283 return true; |
|
284 } |
|
285 |
|
286 if (extras == null) { |
|
287 return false; |
|
288 } |
|
289 |
|
290 final boolean forced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); |
|
291 if (forced) { |
|
292 Logger.info(LOG_TAG, "Forced sync (" + kind + "): overruling remaining backoff of " + delay + "ms."); |
|
293 } else { |
|
294 Logger.info(LOG_TAG, "Not syncing (" + kind + "): must wait another " + delay + "ms."); |
|
295 } |
|
296 return forced; |
|
297 } |
|
298 |
|
299 protected void syncWithAssertion(final String audience, |
|
300 final String assertion, |
|
301 final URI tokenServerEndpointURI, |
|
302 final BackoffHandler tokenBackoffHandler, |
|
303 final SharedPreferences sharedPrefs, |
|
304 final KeyBundle syncKeyBundle, |
|
305 final String clientState, |
|
306 final SessionCallback callback, |
|
307 final Bundle extras) { |
|
308 final TokenServerClientDelegate delegate = new TokenServerClientDelegate() { |
|
309 private boolean didReceiveBackoff = false; |
|
310 |
|
311 @Override |
|
312 public String getUserAgent() { |
|
313 return FxAccountConstants.USER_AGENT; |
|
314 } |
|
315 |
|
316 @Override |
|
317 public void handleSuccess(final TokenServerToken token) { |
|
318 FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + "."); |
|
319 |
|
320 if (!didReceiveBackoff) { |
|
321 // We must be OK to touch this token server. |
|
322 tokenBackoffHandler.setEarliestNextRequest(0L); |
|
323 } |
|
324 |
|
325 final URI storageServerURI; |
|
326 try { |
|
327 storageServerURI = new URI(token.endpoint); |
|
328 } catch (URISyntaxException e) { |
|
329 handleError(e); |
|
330 return; |
|
331 } |
|
332 final String storageHostname = storageServerURI.getHost(); |
|
333 |
|
334 // We back off on a per-host basis. When we have an endpoint URI from a token, we |
|
335 // can check on the backoff status for that host. |
|
336 // If we're supposed to be backing off, we abort the not-yet-started session. |
|
337 final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "sync.storage"); |
|
338 callback.setBackoffHandler(storageBackoffHandler); |
|
339 |
|
340 String lastStorageHost = sharedPrefs.getString(PREF_BACKOFF_STORAGE_HOST, null); |
|
341 final boolean storageHostIsUnchanged = lastStorageHost != null && |
|
342 lastStorageHost.equalsIgnoreCase(storageHostname); |
|
343 if (storageHostIsUnchanged) { |
|
344 Logger.debug(LOG_TAG, "Storage host is unchanged."); |
|
345 if (!shouldPerformSync(storageBackoffHandler, "storage", extras)) { |
|
346 Logger.info(LOG_TAG, "Not syncing: storage server requested backoff."); |
|
347 callback.handleAborted(null, "Storage backoff"); |
|
348 return; |
|
349 } |
|
350 } else { |
|
351 Logger.debug(LOG_TAG, "Received new storage host."); |
|
352 } |
|
353 |
|
354 // Invalidate the previous backoff, because our storage host has changed, |
|
355 // or we never had one at all, or we're OK to sync. |
|
356 storageBackoffHandler.setEarliestNextRequest(0L); |
|
357 |
|
358 FxAccountGlobalSession globalSession = null; |
|
359 try { |
|
360 ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs); |
|
361 |
|
362 // We compute skew over time using SkewHandler. This yields an unchanging |
|
363 // skew adjustment that the HawkAuthHeaderProvider uses to adjust its |
|
364 // timestamps. Eventually we might want this to adapt within the scope of a |
|
365 // global session. |
|
366 final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname); |
|
367 final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds(); |
|
368 // We expect Sync to upload large sets of records. Calculating the |
|
369 // payload verification hash for these record sets could be expensive, |
|
370 // so we explicitly do not send payload verification hashes to the |
|
371 // Sync storage endpoint. |
|
372 final boolean includePayloadVerificationHash = false; |
|
373 final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), includePayloadVerificationHash, storageServerSkew); |
|
374 |
|
375 final Context context = getContext(); |
|
376 final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle); |
|
377 |
|
378 Collection<String> knownStageNames = SyncConfiguration.validEngineNames(); |
|
379 syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); |
|
380 syncConfig.setClusterURL(storageServerURI); |
|
381 |
|
382 globalSession = new FxAccountGlobalSession(syncConfig, callback, context, clientsDataDelegate); |
|
383 globalSession.start(); |
|
384 } catch (Exception e) { |
|
385 callback.handleError(globalSession, e); |
|
386 return; |
|
387 } |
|
388 } |
|
389 |
|
390 @Override |
|
391 public void handleFailure(TokenServerException e) { |
|
392 handleError(e); |
|
393 } |
|
394 |
|
395 @Override |
|
396 public void handleError(Exception e) { |
|
397 Logger.error(LOG_TAG, "Failed to get token.", e); |
|
398 callback.handleError(null, e); |
|
399 } |
|
400 |
|
401 @Override |
|
402 public void handleBackoff(int backoffSeconds) { |
|
403 // This is the token server telling us to back off. |
|
404 Logger.info(LOG_TAG, "Token server requesting backoff of " + backoffSeconds + "s. Backoff handler: " + tokenBackoffHandler); |
|
405 didReceiveBackoff = true; |
|
406 |
|
407 // If we've already stored a backoff, overrule it: we only use the server |
|
408 // value for token server scheduling. |
|
409 tokenBackoffHandler.setEarliestNextRequest(delay(backoffSeconds * 1000)); |
|
410 } |
|
411 |
|
412 private long delay(long delay) { |
|
413 return System.currentTimeMillis() + delay; |
|
414 } |
|
415 }; |
|
416 |
|
417 TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor); |
|
418 tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, delegate); |
|
419 } |
|
420 |
|
421 /** |
|
422 * A trivial Sync implementation that does not cache client keys, |
|
423 * certificates, or tokens. |
|
424 * |
|
425 * This should be replaced with a full {@link FxAccountAuthenticator}-based |
|
426 * token implementation. |
|
427 */ |
|
428 @Override |
|
429 public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) { |
|
430 Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); |
|
431 Logger.resetLogging(); |
|
432 |
|
433 Logger.info(LOG_TAG, "Syncing FxAccount" + |
|
434 " account named like " + Utils.obfuscateEmail(account.name) + |
|
435 " for authority " + authority + |
|
436 " with instance " + this + "."); |
|
437 |
|
438 final EnumSet<FirefoxAccounts.SyncHint> syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras); |
|
439 FirefoxAccounts.logSyncHints(syncHints); |
|
440 |
|
441 // This applies even to forced syncs, but only on success. |
|
442 if (this.lastSyncRealtimeMillis > 0L && |
|
443 (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime()) { |
|
444 Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) + |
|
445 ": minimum interval not met."); |
|
446 return; |
|
447 } |
|
448 |
|
449 final Context context = getContext(); |
|
450 final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); |
|
451 if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { |
|
452 fxAccount.dump(); |
|
453 } |
|
454 |
|
455 // Pickle in a background thread to avoid strict mode warnings. |
|
456 ThreadPool.run(new Runnable() { |
|
457 @Override |
|
458 public void run() { |
|
459 try { |
|
460 AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); |
|
461 } catch (Exception e) { |
|
462 // Should never happen, but we really don't want to die in a background thread. |
|
463 Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e); |
|
464 } |
|
465 } |
|
466 }); |
|
467 |
|
468 final CountDownLatch latch = new CountDownLatch(1); |
|
469 |
|
470 Collection<String> knownStageNames = SyncConfiguration.validEngineNames(); |
|
471 Collection<String> stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); |
|
472 |
|
473 final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync); |
|
474 |
|
475 try { |
|
476 final State state; |
|
477 try { |
|
478 state = fxAccount.getState(); |
|
479 } catch (Exception e) { |
|
480 syncDelegate.handleError(e); |
|
481 return; |
|
482 } |
|
483 |
|
484 // This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration. |
|
485 final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs(); |
|
486 |
|
487 final BackoffHandler backgroundBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "background"); |
|
488 final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate"); |
|
489 |
|
490 // If this sync was triggered by user action, this will be true. |
|
491 final boolean isImmediate = (extras != null) && |
|
492 (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) || |
|
493 extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)); |
|
494 |
|
495 // If it's not an immediate sync, it must be either periodic or tickled. |
|
496 // Check our background rate limiter. |
|
497 if (!isImmediate) { |
|
498 if (!shouldPerformSync(backgroundBackoffHandler, "background", extras)) { |
|
499 syncDelegate.rejectSync(); |
|
500 return; |
|
501 } |
|
502 } |
|
503 |
|
504 // Regardless, let's make sure we're not syncing too often. |
|
505 if (!shouldPerformSync(rateLimitBackoffHandler, "rate", extras)) { |
|
506 syncDelegate.postponeSync(rateLimitBackoffHandler.delayMilliseconds()); |
|
507 return; |
|
508 } |
|
509 |
|
510 final SchedulePolicy schedulePolicy = new FxAccountSchedulePolicy(context, fxAccount); |
|
511 |
|
512 // Set a small scheduled 'backoff' to rate-limit the next sync, |
|
513 // and extend the background delay even further into the future. |
|
514 schedulePolicy.configureBackoffMillisBeforeSyncing(rateLimitBackoffHandler, backgroundBackoffHandler); |
|
515 |
|
516 final String audience = fxAccount.getAudience(); |
|
517 final String authServerEndpoint = fxAccount.getAccountServerURI(); |
|
518 final String tokenServerEndpoint = fxAccount.getTokenServerURI(); |
|
519 final URI tokenServerEndpointURI = new URI(tokenServerEndpoint); |
|
520 |
|
521 // TODO: why doesn't the loginPolicy extract the audience from the account? |
|
522 final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor); |
|
523 final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); |
|
524 stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() { |
|
525 @Override |
|
526 public FxAccountClient getClient() { |
|
527 return client; |
|
528 } |
|
529 |
|
530 @Override |
|
531 public long getCertificateDurationInMilliseconds() { |
|
532 return 12 * 60 * 60 * 1000; |
|
533 } |
|
534 |
|
535 @Override |
|
536 public long getAssertionDurationInMilliseconds() { |
|
537 return 15 * 60 * 1000; |
|
538 } |
|
539 |
|
540 @Override |
|
541 public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { |
|
542 return StateFactory.generateKeyPair(); |
|
543 } |
|
544 |
|
545 @Override |
|
546 public void handleTransition(Transition transition, State state) { |
|
547 Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); |
|
548 } |
|
549 |
|
550 private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) { |
|
551 return shouldPerformSync(tokenBackoffHandler, "token", extras); |
|
552 } |
|
553 |
|
554 @Override |
|
555 public void handleFinal(State state) { |
|
556 Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); |
|
557 fxAccount.setState(state); |
|
558 schedulePolicy.onHandleFinal(state.getNeededAction()); |
|
559 notificationManager.update(context, fxAccount); |
|
560 try { |
|
561 if (state.getStateLabel() != StateLabel.Married) { |
|
562 syncDelegate.handleCannotSync(state); |
|
563 return; |
|
564 } |
|
565 |
|
566 final Married married = (Married) state; |
|
567 final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); |
|
568 |
|
569 /* |
|
570 * At this point we're in the correct state to sync, and we're ready to fetch |
|
571 * a token and do some work. |
|
572 * |
|
573 * But first we need to do two things: |
|
574 * 1. Check to see whether we're in a backoff situation for the token server. |
|
575 * If we are, but we're not forcing a sync, then we go no further. |
|
576 * 2. Clear an existing backoff (if we're syncing it doesn't matter, and if |
|
577 * we're forcing we'll get a new backoff if things are still bad). |
|
578 * |
|
579 * Note that we don't check the storage backoff before the token dance: the token |
|
580 * server tells us which server we're syncing to! |
|
581 * |
|
582 * That logic lives in the TokenServerClientDelegate elsewhere in this file. |
|
583 */ |
|
584 |
|
585 // Strictly speaking this backoff check could be done prior to walking through |
|
586 // the login state machine, allowing us to short-circuit sooner. |
|
587 // We don't expect many token server backoffs, and most users will be sitting |
|
588 // in the Married state, so instead we simply do this here, once. |
|
589 final BackoffHandler tokenBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "token"); |
|
590 if (!shouldRequestToken(tokenBackoffHandler, extras)) { |
|
591 Logger.info(LOG_TAG, "Not syncing (token server)."); |
|
592 syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds()); |
|
593 return; |
|
594 } |
|
595 |
|
596 final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy); |
|
597 final KeyBundle syncKeyBundle = married.getSyncKeyBundle(); |
|
598 final String clientState = married.getClientState(); |
|
599 syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras); |
|
600 } catch (Exception e) { |
|
601 syncDelegate.handleError(e); |
|
602 return; |
|
603 } |
|
604 } |
|
605 }); |
|
606 |
|
607 latch.await(); |
|
608 } catch (Exception e) { |
|
609 Logger.error(LOG_TAG, "Got error syncing.", e); |
|
610 syncDelegate.handleError(e); |
|
611 } |
|
612 |
|
613 Logger.info(LOG_TAG, "Syncing done."); |
|
614 lastSyncRealtimeMillis = SystemClock.elapsedRealtime(); |
|
615 } |
|
616 } |