summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSchedulePolicy.java
blob: 708686e7260457b40cc571078cd5818f259ac5a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.fxa.sync;

import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.sync.BackoffHandler;

import android.accounts.Account;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle;

public class FxAccountSchedulePolicy implements SchedulePolicy {
  private static final String LOG_TAG = "FxAccountSchedulePolicy";

  // Our poll intervals are used to trigger automatic background syncs
  // in the absence of user activity.
  //
  // We also receive sync requests as a result of network tickles, so
  // these intervals are long, with the exception of the rapid polling
  // while we wait for verification: if we're waiting for the user to
  // click on a verification link, we sync very often in order to detect
  // a change in state.
  //
  // In the case of unverified -> unverified (no transition), this should be
  // very close to a single HTTP request (with the SyncAdapter overhead, of
  // course, but that's not wildly different from alarm manager overhead).
  //
  // The /account/status endpoint is HAWK authed by sessionToken, so we still
  // have to do some crypto no matter what.

  // TODO: only do this for a while...
  public static final long POLL_INTERVAL_PENDING_VERIFICATION = 60;         // 1 minute.

  // If we're in some kind of error state, there's no point trying often.
  // This is not the same as a server-imposed backoff, which will be
  // reflected dynamically.
  public static final long POLL_INTERVAL_ERROR_STATE_SEC = 24 * 60 * 60;    // 24 hours.

  // If we're the only device, just sync once or twice a day in case that
  // changes.
  public static final long POLL_INTERVAL_SINGLE_DEVICE_SEC = 18 * 60 * 60;  // 18 hours.

  // And if we know there are other devices, let's sync often enough that
  // we'll be more likely to be caught up (even if not completely) by the
  // time you next use this device. This is also achieved via Android's
  // network tickles.
  public static final long POLL_INTERVAL_MULTI_DEVICE_SEC = 12 * 60 * 60;   // 12 hours.

  // This is used solely as an optimization for backoff handling, so it's not
  // persisted.
  private static volatile long POLL_INTERVAL_CURRENT_SEC = POLL_INTERVAL_SINGLE_DEVICE_SEC;

  // Never sync more frequently than this, unless forced.
  // This is to avoid overly-frequent syncs during active browsing.
  public static final long RATE_LIMIT_FUNDAMENTAL_SEC = 90;                 // 90 seconds.

  /**
   * We are prompted to sync by several inputs:
   * * Periodic syncs that we schedule at long intervals. See the POLL constants.
   * * Network-tickle-based syncs that Android starts.
   * * Upload-only syncs that are caused by local database writes.
   *
   * We rate-limit periodic and network-sourced events with this constant.
   * We rate limit <b>both</b> with {@link FxAccountSchedulePolicy#RATE_LIMIT_FUNDAMENTAL_SEC}.
   */
  public static final long RATE_LIMIT_BACKGROUND_SEC = 60 * 60;             // 1 hour.

  private final AndroidFxAccount account;
  private final Context context;

  public FxAccountSchedulePolicy(Context context, AndroidFxAccount account) {
    this.account = account;
    this.context = context;
  }

  /**
   * Return a millisecond timestamp in the future, offset from the current
   * time by the provided amount.
   * @param millis the duration by which to delay
   * @return a timestamp.
   */
  private static long delay(long millis) {
    return System.currentTimeMillis() + millis;
  }

  /**
   * Updates the existing system periodic sync interval to the specified duration.
   *
   * @param intervalSeconds the requested period, which Android will vary by up to 4%.
   */
  protected void requestPeriodicSync(final long intervalSeconds) {
    final String authority = BrowserContract.AUTHORITY;
    final Account account = this.account.getAndroidAccount();
    this.context.getContentResolver();
    Logger.info(LOG_TAG, "Scheduling periodic sync for " + intervalSeconds + ".");
    ContentResolver.addPeriodicSync(account, authority, Bundle.EMPTY, intervalSeconds);
    POLL_INTERVAL_CURRENT_SEC = intervalSeconds;
  }

  @Override
  public void onSuccessfulSync(int otherClientsCount) {
    this.account.setLastSyncedTimestamp(System.currentTimeMillis());
    // This undoes the change made in observeBackoffMillis -- once we hit backoff we'll
    // periodically sync at the backoff duration, but as soon as we succeed we'll switch
    // into the client-count-dependent interval.
    long interval = (otherClientsCount > 0) ? POLL_INTERVAL_MULTI_DEVICE_SEC : POLL_INTERVAL_SINGLE_DEVICE_SEC;
    requestPeriodicSync(interval);
  }

  @Override
  public void onHandleFinal(Action needed) {
    switch (needed) {
    case NeedsPassword:
    case NeedsUpgrade:
    case NeedsFinishMigrating:
      requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
      break;
    case NeedsVerification:
      requestPeriodicSync(POLL_INTERVAL_PENDING_VERIFICATION);
      break;
    case None:
      // No action needed: we'll set the periodic sync interval
      // when the sync finishes, via the SessionCallback.
      break;
    }
  }

  @Override
  public void onUpgradeRequired() {
    // TODO: this shouldn't occur in FxA, but when we upgrade we
    // need to reduce the interval again.
    requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
  }

  @Override
  public void onUnauthorized() {
    // TODO: this shouldn't occur in FxA, but when we fix our credentials
    // we need to reduce the interval again.
    requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
  }

  @Override
  public void configureBackoffMillisOnBackoff(BackoffHandler backoffHandler, long backoffMillis, boolean onlyExtend) {
    if (onlyExtend) {
      backoffHandler.extendEarliestNextRequest(delay(backoffMillis));
    } else {
      backoffHandler.setEarliestNextRequest(delay(backoffMillis));
    }

    // Yes, we might be part-way through the interval, in which case the backoff
    // code will do its job. But we certainly don't want to reduce the interval
    // if we're given a small backoff instruction.
    // We'll reset the poll interval next time we sync without a backoff instruction.
    if (backoffMillis > (POLL_INTERVAL_CURRENT_SEC * 1000)) {
      // Slightly inflate the backoff duration to ensure that a fuzzed
      // periodic sync doesn't occur before our backoff has passed. Android
      // 19+ default to a 4% fuzz factor.
      requestPeriodicSync((long) Math.ceil((1.05 * backoffMillis) / 1000));
    }
  }

  /**
   * Accepts two {@link BackoffHandler} instances as input. These are used
   * respectively to track fundamental rate limiting, and to separately
   * rate-limit periodic and network-tickled syncs.
   */
  @Override
  public void configureBackoffMillisBeforeSyncing(BackoffHandler fundamentalRateHandler, BackoffHandler backgroundRateHandler) {
    fundamentalRateHandler.setEarliestNextRequest(delay(RATE_LIMIT_FUNDAMENTAL_SEC * 1000));
    backgroundRateHandler.setEarliestNextRequest(delay(RATE_LIMIT_BACKGROUND_SEC * 1000));
  }
}