--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
@@ -55,30 +55,36 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName();
public static final int NOTIFICATION_ID = LOG_TAG.hashCode();
// Tracks the last seen storage hostname for backoff purposes.
private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost";
// Used to do cheap in-memory rate limiting. Don't sync again if we
// successfully synced within this duration.
private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000; // 15 seconds.
private volatile long lastSyncRealtimeMillis;
+ // Non-user initiated sync can't take longer than 30 minutes.
+ // To ensure we're not churning through device's battery/resources, we limit sync to 10 minutes,
+ // and request a re-sync if we hit that deadline.
+ private static final long SYNC_DEADLINE_DELTA_MILLIS = TimeUnit.MINUTES.toMillis(10);
+
protected final ExecutorService executor;
protected final FxAccountNotificationManager notificationManager;
public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
this.executor = Executors.newSingleThreadExecutor();
this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID);
}
@@ -226,26 +232,26 @@ public class FxAccountSyncAdapter extend
if (forced) {
Logger.info(LOG_TAG, "Forced sync (" + kind + "): overruling remaining backoff of " + delay + "ms.");
} else {
Logger.info(LOG_TAG, "Not syncing (" + kind + "): must wait another " + delay + "ms.");
}
return forced;
}
- protected void syncWithAssertion(final String audience,
- final String assertion,
+ protected void syncWithAssertion(final String assertion,
final URI tokenServerEndpointURI,
final BackoffHandler tokenBackoffHandler,
final SharedPreferences sharedPrefs,
final KeyBundle syncKeyBundle,
final String clientState,
final SessionCallback callback,
final Bundle extras,
- final AndroidFxAccount fxAccount) {
+ final AndroidFxAccount fxAccount,
+ final long syncDeadline) {
final TokenServerClientDelegate delegate = new TokenServerClientDelegate() {
private boolean didReceiveBackoff = false;
@Override
public String getUserAgent() {
return FxAccountConstants.USER_AGENT;
}
@@ -316,17 +322,17 @@ public class FxAccountSyncAdapter extend
final Context context = getContext();
final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle);
Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
syncConfig.setClusterURL(storageServerURI);
globalSession = new GlobalSession(syncConfig, callback, context, clientsDataDelegate);
- globalSession.start();
+ globalSession.start(syncDeadline);
} catch (Exception e) {
callback.handleError(globalSession, e);
return;
}
}
@Override
public void handleFailure(TokenServerException e) {
@@ -381,16 +387,20 @@ public class FxAccountSyncAdapter extend
@Override
public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) {
Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
Logger.resetLogging();
final Context context = getContext();
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ // NB: we use elapsedRealtime which is time since boot, to ensure our clock is monotonic and isn't
+ // paused while CPU is in the power-saving mode.
+ final long syncDeadline = SystemClock.elapsedRealtime() + SYNC_DEADLINE_DELTA_MILLIS;
+
Logger.info(LOG_TAG, "Syncing FxAccount" +
" account named like " + Utils.obfuscateEmail(account.name) +
" for authority " + authority +
" with instance " + this + ".");
Logger.info(LOG_TAG, "Account last synced at: " + fxAccount.getLastSyncedTimestamp());
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
@@ -531,17 +541,19 @@ public class FxAccountSyncAdapter extend
Logger.info(LOG_TAG, "Not syncing (token server).");
syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds());
return;
}
final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy);
final KeyBundle syncKeyBundle = married.getSyncKeyBundle();
final String clientState = married.getClientState();
- syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras, fxAccount);
+ syncWithAssertion(
+ assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs,
+ syncKeyBundle, clientState, sessionCallback, extras, fxAccount, syncDeadline);
// Register the device if necessary (asynchronous, in another thread)
if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION
|| TextUtils.isEmpty(fxAccount.getDeviceId())) {
FxAccountDeviceRegistrator.register(context);
}
// Force fetch the profile avatar information. (asynchronous, in another thread)
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
@@ -69,16 +69,18 @@ public class GlobalSession implements Ht
protected Map<Stage, GlobalSyncStage> stages;
public Stage currentState = Stage.idle;
public final GlobalSessionCallback callback;
protected final Context context;
protected final ClientsDataDelegate clientsDelegate;
+ private long syncDeadline;
+
/**
* Map from engine name to new settings for an updated meta/global record.
* Engines to remove will have <code>null</code> EngineSettings.
*/
public final Map<String, EngineSettings> enginesToUpdate = new HashMap<String, EngineSettings>();
/*
* Key accessors.
@@ -229,16 +231,20 @@ public class GlobalSession implements Ht
out.add(stage);
} catch (NoSuchStageException e) {
Logger.warn(LOG_TAG, "Unable to find stage with name " + name);
}
}
return out;
}
+ public long getSyncDeadline() {
+ return syncDeadline;
+ }
+
/**
* Advance and loop around the stages of a sync.
* @param current
* @return
* The next stage to execute.
*/
public static Stage nextStage(Stage current) {
int index = current.ordinal() + 1;
@@ -288,35 +294,40 @@ public class GlobalSession implements Ht
* <ul>
* <li>Verifying that any backoffs/minimum next sync requests are respected.</li>
* <li>Ensuring that the device is online.</li>
* <li>Ensuring that dependencies are ready.</li>
* </ul>
*
* @throws AlreadySyncingException
*/
- public void start() throws AlreadySyncingException {
+ public void start(final long syncDeadline) throws AlreadySyncingException {
if (this.currentState != GlobalSyncStage.Stage.idle) {
throw new AlreadySyncingException(this.currentState);
}
+
+ // Make the deadline value available to stages via its getter.
+ this.syncDeadline = syncDeadline;
+
installAsHttpResponseObserver(); // Uninstalled by completeSync or abort.
this.advance();
}
/**
* Stop this sync and start again.
* @throws AlreadySyncingException
*/
protected void restart() throws AlreadySyncingException {
this.currentState = GlobalSyncStage.Stage.idle;
if (callback.shouldBackOffStorage()) {
this.callback.handleAborted(this, "Told to back off.");
return;
}
- this.start();
+ // Restart with the same deadline as before.
+ this.start(syncDeadline);
}
/**
* We're finished (aborted or succeeded): release resources.
*/
protected void cleanUp() {
uninstallAsHttpResponseObserver();
this.stages = null;