2011-12-21 08:44:08 -08:00
|
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
|
|
*
|
|
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
|
|
* the License. You may obtain a copy of the License at
|
|
|
|
* http://www.mozilla.org/MPL/
|
|
|
|
*
|
|
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
|
|
* for the specific language governing rights and limitations under the
|
|
|
|
* License.
|
|
|
|
*
|
|
|
|
* The Original Code is Android Sync Client.
|
|
|
|
*
|
|
|
|
* The Initial Developer of the Original Code is
|
|
|
|
* the Mozilla Foundation.
|
|
|
|
* Portions created by the Initial Developer are Copyright (C) 2011
|
|
|
|
* the Initial Developer. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Contributor(s):
|
2012-01-14 09:20:31 -08:00
|
|
|
* Chenxia Liu <liuche@mozilla.com>
|
|
|
|
* Richard Newman <rnewman@mozilla.com>
|
2011-12-21 08:44:08 -08:00
|
|
|
*
|
|
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
|
|
* the provisions above, a recipient may use your version of this file under
|
|
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
|
|
*
|
|
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
|
|
|
|
package org.mozilla.gecko.sync.syncadapter;
|
|
|
|
|
|
|
|
import java.io.IOException;
|
2012-01-14 09:20:31 -08:00
|
|
|
import java.io.UnsupportedEncodingException;
|
2011-12-21 08:44:08 -08:00
|
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
import org.json.simple.parser.ParseException;
|
|
|
|
import org.mozilla.gecko.sync.AlreadySyncingException;
|
|
|
|
import org.mozilla.gecko.sync.GlobalSession;
|
|
|
|
import org.mozilla.gecko.sync.NonObjectJSONException;
|
|
|
|
import org.mozilla.gecko.sync.SyncConfiguration;
|
|
|
|
import org.mozilla.gecko.sync.SyncConfigurationException;
|
|
|
|
import org.mozilla.gecko.sync.SyncException;
|
2012-01-14 09:20:31 -08:00
|
|
|
import org.mozilla.gecko.sync.Utils;
|
|
|
|
import org.mozilla.gecko.sync.crypto.Cryptographer;
|
|
|
|
import org.mozilla.gecko.sync.crypto.KeyBundle;
|
2011-12-21 08:44:08 -08:00
|
|
|
import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
|
|
|
|
import org.mozilla.gecko.sync.setup.Constants;
|
|
|
|
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
|
|
|
|
|
|
|
|
import android.accounts.Account;
|
|
|
|
import android.accounts.AccountManager;
|
|
|
|
import android.accounts.AccountManagerCallback;
|
|
|
|
import android.accounts.AccountManagerFuture;
|
|
|
|
import android.accounts.AuthenticatorException;
|
|
|
|
import android.accounts.OperationCanceledException;
|
|
|
|
import android.content.AbstractThreadedSyncAdapter;
|
|
|
|
import android.content.ContentProviderClient;
|
|
|
|
import android.content.Context;
|
2012-01-14 09:20:31 -08:00
|
|
|
import android.content.SharedPreferences;
|
|
|
|
import android.content.SharedPreferences.Editor;
|
2011-12-21 08:44:08 -08:00
|
|
|
import android.content.SyncResult;
|
2012-01-14 09:20:31 -08:00
|
|
|
import android.database.sqlite.SQLiteConstraintException;
|
|
|
|
import android.database.sqlite.SQLiteException;
|
2011-12-21 08:44:08 -08:00
|
|
|
import android.os.Bundle;
|
|
|
|
import android.os.Handler;
|
|
|
|
import android.util.Log;
|
|
|
|
|
|
|
|
public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSessionCallback {
|
|
|
|
private static final String LOG_TAG = "SyncAdapter";
|
2012-01-14 09:20:31 -08:00
|
|
|
|
|
|
|
private static final String PREFS_EARLIEST_NEXT_SYNC = "earliestnextsync";
|
|
|
|
private static final String PREFS_INVALIDATE_AUTH_TOKEN = "invalidateauthtoken";
|
|
|
|
|
|
|
|
private static final int SHARED_PREFERENCES_MODE = 0;
|
|
|
|
private static final int BACKOFF_PAD_SECONDS = 5;
|
|
|
|
|
2011-12-21 08:44:08 -08:00
|
|
|
private final AccountManager mAccountManager;
|
|
|
|
private final Context mContext;
|
|
|
|
|
|
|
|
public SyncAdapter(Context context, boolean autoInitialize) {
|
|
|
|
super(context, autoInitialize);
|
|
|
|
mContext = context;
|
2012-01-14 09:20:31 -08:00
|
|
|
Log.d(LOG_TAG, "AccountManager.get(" + mContext + ")");
|
2011-12-21 08:44:08 -08:00
|
|
|
mAccountManager = AccountManager.get(context);
|
|
|
|
}
|
|
|
|
|
2012-01-14 09:20:31 -08:00
|
|
|
/**
|
|
|
|
* Backoff.
|
|
|
|
*/
|
|
|
|
public synchronized long getEarliestNextSync() {
|
|
|
|
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
|
|
|
|
return sharedPreferences.getLong(PREFS_EARLIEST_NEXT_SYNC, 0);
|
|
|
|
}
|
|
|
|
public synchronized void setEarliestNextSync(long next) {
|
|
|
|
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
|
|
|
|
Editor edit = sharedPreferences.edit();
|
|
|
|
edit.putLong(PREFS_EARLIEST_NEXT_SYNC, next);
|
|
|
|
edit.commit();
|
|
|
|
}
|
|
|
|
public synchronized void extendEarliestNextSync(long next) {
|
|
|
|
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
|
|
|
|
if (sharedPreferences.getLong(PREFS_EARLIEST_NEXT_SYNC, 0) >= next) {
|
2011-12-21 08:44:08 -08:00
|
|
|
return;
|
|
|
|
}
|
2012-01-14 09:20:31 -08:00
|
|
|
Editor edit = sharedPreferences.edit();
|
|
|
|
edit.putLong(PREFS_EARLIEST_NEXT_SYNC, next);
|
|
|
|
edit.commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
public synchronized boolean getShouldInvalidateAuthToken() {
|
|
|
|
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
|
|
|
|
return sharedPreferences.getBoolean(PREFS_INVALIDATE_AUTH_TOKEN, false);
|
|
|
|
}
|
|
|
|
public synchronized void clearShouldInvalidateAuthToken() {
|
|
|
|
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
|
|
|
|
Editor edit = sharedPreferences.edit();
|
|
|
|
edit.remove(PREFS_INVALIDATE_AUTH_TOKEN);
|
|
|
|
edit.commit();
|
|
|
|
}
|
|
|
|
public synchronized void setShouldInvalidateAuthToken() {
|
|
|
|
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
|
|
|
|
Editor edit = sharedPreferences.edit();
|
|
|
|
edit.putBoolean(PREFS_INVALIDATE_AUTH_TOKEN, true);
|
|
|
|
edit.commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void handleException(Exception e, SyncResult syncResult) {
|
|
|
|
setShouldInvalidateAuthToken();
|
|
|
|
try {
|
|
|
|
if (e instanceof SQLiteConstraintException) {
|
|
|
|
Log.e(LOG_TAG, "Constraint exception. Aborting sync.", e);
|
|
|
|
syncResult.stats.numParseExceptions++; // This is as good as we can do.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (e instanceof SQLiteException) {
|
|
|
|
Log.e(LOG_TAG, "Couldn't open database (locked?). Aborting sync.", e);
|
|
|
|
syncResult.stats.numIoExceptions++;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (e instanceof OperationCanceledException) {
|
|
|
|
Log.e(LOG_TAG, "Operation canceled. Aborting sync.", e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (e instanceof AuthenticatorException) {
|
|
|
|
syncResult.stats.numParseExceptions++;
|
|
|
|
Log.e(LOG_TAG, "AuthenticatorException. Aborting sync.", e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (e instanceof IOException) {
|
|
|
|
syncResult.stats.numIoExceptions++;
|
|
|
|
Log.e(LOG_TAG, "IOException. Aborting sync.", e);
|
|
|
|
e.printStackTrace();
|
|
|
|
return;
|
|
|
|
}
|
2011-12-21 08:44:08 -08:00
|
|
|
syncResult.stats.numIoExceptions++;
|
2012-01-14 09:20:31 -08:00
|
|
|
Log.e(LOG_TAG, "Unknown exception. Aborting sync.", e);
|
|
|
|
} finally {
|
|
|
|
notifyMonitor();
|
2011-12-21 08:44:08 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private AccountManagerFuture<Bundle> getAuthToken(final Account account,
|
|
|
|
AccountManagerCallback<Bundle> callback,
|
|
|
|
Handler handler) {
|
|
|
|
return mAccountManager.getAuthToken(account, Constants.AUTHTOKEN_TYPE_PLAIN, true, callback, handler);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void invalidateAuthToken(Account account) {
|
|
|
|
AccountManagerFuture<Bundle> future = getAuthToken(account, null, null);
|
|
|
|
String token;
|
|
|
|
try {
|
|
|
|
token = future.getResult().getString(AccountManager.KEY_AUTHTOKEN);
|
|
|
|
mAccountManager.invalidateAuthToken(Constants.ACCOUNTTYPE_SYNC, token);
|
|
|
|
} catch (Exception e) {
|
|
|
|
Log.e(LOG_TAG, "Couldn't invalidate auth token: " + e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onSyncCanceled() {
|
|
|
|
super.onSyncCanceled();
|
|
|
|
// TODO: cancel the sync!
|
|
|
|
// From the docs: "This will be invoked on a separate thread than the sync
|
|
|
|
// thread and so you must consider the multi-threaded implications of the
|
|
|
|
// work that you do in this method."
|
|
|
|
}
|
|
|
|
|
|
|
|
public Object syncMonitor = new Object();
|
|
|
|
private SyncResult syncResult;
|
|
|
|
|
2012-01-14 09:20:31 -08:00
|
|
|
/**
|
|
|
|
* Return the number of milliseconds until we're allowed to sync again,
|
|
|
|
* or 0 if now is fine.
|
|
|
|
*/
|
|
|
|
public long delayMilliseconds() {
|
|
|
|
long earliestNextSync = getEarliestNextSync();
|
|
|
|
if (earliestNextSync <= 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
long now = System.currentTimeMillis();
|
|
|
|
return Math.max(0, earliestNextSync - now);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean shouldBackOff() {
|
|
|
|
return delayMilliseconds() > 0;
|
|
|
|
}
|
2011-12-21 08:44:08 -08:00
|
|
|
|
2011-12-21 08:44:08 -08:00
|
|
|
@Override
|
|
|
|
public void onPerformSync(final Account account,
|
|
|
|
final Bundle extras,
|
|
|
|
final String authority,
|
|
|
|
final ContentProviderClient provider,
|
|
|
|
final SyncResult syncResult) {
|
|
|
|
|
2012-01-14 09:20:31 -08:00
|
|
|
long delay = delayMilliseconds();
|
|
|
|
if (delay > 0) {
|
|
|
|
Log.i(LOG_TAG, "Not syncing: must wait another " + delay + "ms.");
|
|
|
|
long remainingSeconds = delay / 1000;
|
|
|
|
syncResult.delayUntil = remainingSeconds + BACKOFF_PAD_SECONDS;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2011-12-21 08:44:08 -08:00
|
|
|
// TODO: don't clear the auth token unless we have a sync error.
|
|
|
|
Log.i(LOG_TAG, "Got onPerformSync. Extras bundle is " + extras);
|
|
|
|
Log.d(LOG_TAG, "Extras clusterURL: " + extras.getString("clusterURL"));
|
|
|
|
Log.i(LOG_TAG, "Account name: " + account.name);
|
2012-01-14 09:20:31 -08:00
|
|
|
|
|
|
|
// TODO: don't always invalidate; use getShouldInvalidateAuthToken.
|
|
|
|
// However, this fixes Bug 716815, so it'll do for now.
|
|
|
|
Log.d(LOG_TAG, "Invalidating auth token.");
|
2011-12-21 08:44:08 -08:00
|
|
|
invalidateAuthToken(account);
|
|
|
|
|
|
|
|
final SyncAdapter self = this;
|
2012-01-14 09:20:31 -08:00
|
|
|
final AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
|
2011-12-21 08:44:08 -08:00
|
|
|
@Override
|
|
|
|
public void run(AccountManagerFuture<Bundle> future) {
|
|
|
|
Log.i(LOG_TAG, "AccountManagerCallback invoked.");
|
|
|
|
// TODO: N.B.: Future must not be used on the main thread.
|
|
|
|
try {
|
2012-01-14 09:20:31 -08:00
|
|
|
Bundle bundle = future.getResult(60L, TimeUnit.SECONDS);
|
|
|
|
if (bundle.containsKey("KEY_INTENT")) {
|
|
|
|
Log.w(LOG_TAG, "KEY_INTENT included in AccountManagerFuture bundle. Problem?");
|
|
|
|
}
|
|
|
|
String username = bundle.getString(Constants.OPTION_USERNAME);
|
|
|
|
String syncKey = bundle.getString(Constants.OPTION_SYNCKEY);
|
|
|
|
String serverURL = bundle.getString(Constants.OPTION_SERVER);
|
|
|
|
String password = bundle.getString(AccountManager.KEY_AUTHTOKEN);
|
2011-12-21 08:44:08 -08:00
|
|
|
Log.d(LOG_TAG, "Username: " + username);
|
|
|
|
Log.d(LOG_TAG, "Server: " + serverURL);
|
2012-01-14 09:20:31 -08:00
|
|
|
Log.d(LOG_TAG, "Password? " + (password != null));
|
|
|
|
Log.d(LOG_TAG, "Key? " + (syncKey != null));
|
2011-12-21 08:44:08 -08:00
|
|
|
if (password == null) {
|
|
|
|
Log.e(LOG_TAG, "No password: aborting sync.");
|
2012-01-14 09:20:31 -08:00
|
|
|
syncResult.stats.numAuthExceptions++;
|
|
|
|
notifyMonitor();
|
2011-12-21 08:44:08 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (syncKey == null) {
|
|
|
|
Log.e(LOG_TAG, "No Sync Key: aborting sync.");
|
2012-01-14 09:20:31 -08:00
|
|
|
syncResult.stats.numAuthExceptions++;
|
|
|
|
notifyMonitor();
|
2011-12-21 08:44:08 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
KeyBundle keyBundle = new KeyBundle(username, syncKey);
|
2012-01-14 09:20:31 -08:00
|
|
|
|
|
|
|
// Support multiple accounts by mapping each server/account pair to a branch of the
|
|
|
|
// shared preferences space.
|
|
|
|
String prefsPath = Utils.getPrefsPath(username, serverURL);
|
2011-12-21 08:44:08 -08:00
|
|
|
self.performSync(account, extras, authority, provider, syncResult,
|
2012-01-14 09:20:31 -08:00
|
|
|
username, password, prefsPath, serverURL, keyBundle);
|
2011-12-21 08:44:08 -08:00
|
|
|
} catch (Exception e) {
|
|
|
|
self.handleException(e, syncResult);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2012-01-14 09:20:31 -08:00
|
|
|
|
|
|
|
final Handler handler = null;
|
|
|
|
final Runnable fetchAuthToken = new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
getAuthToken(account, callback, handler);
|
|
|
|
}
|
|
|
|
};
|
2011-12-21 08:44:08 -08:00
|
|
|
synchronized (syncMonitor) {
|
2012-01-14 09:20:31 -08:00
|
|
|
// Perform the work in a new thread from within this synchronized block,
|
|
|
|
// which allows us to be waiting on the monitor before the callback can
|
|
|
|
// notify us in a failure case. Oh, concurrent programming.
|
|
|
|
new Thread(fetchAuthToken).start();
|
|
|
|
|
|
|
|
Log.i(LOG_TAG, "Waiting on sync monitor.");
|
2011-12-21 08:44:08 -08:00
|
|
|
try {
|
|
|
|
syncMonitor.wait();
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
Log.i(LOG_TAG, "Waiting on sync monitor interrupted.", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Now that we have a sync key and password, go ahead and do the work.
|
2012-01-14 09:20:31 -08:00
|
|
|
* @param prefsPath TODO
|
2011-12-21 08:44:08 -08:00
|
|
|
* @throws NoSuchAlgorithmException
|
|
|
|
* @throws IllegalArgumentException
|
|
|
|
* @throws SyncConfigurationException
|
|
|
|
* @throws AlreadySyncingException
|
|
|
|
* @throws NonObjectJSONException
|
|
|
|
* @throws ParseException
|
|
|
|
* @throws IOException
|
|
|
|
*/
|
|
|
|
protected void performSync(Account account, Bundle extras, String authority,
|
|
|
|
ContentProviderClient provider,
|
|
|
|
SyncResult syncResult,
|
|
|
|
String username, String password,
|
2012-01-14 09:20:31 -08:00
|
|
|
String prefsPath,
|
|
|
|
String serverURL, KeyBundle keyBundle)
|
2011-12-21 08:44:08 -08:00
|
|
|
throws NoSuchAlgorithmException,
|
|
|
|
SyncConfigurationException,
|
|
|
|
IllegalArgumentException,
|
|
|
|
AlreadySyncingException,
|
|
|
|
IOException, ParseException,
|
|
|
|
NonObjectJSONException {
|
|
|
|
Log.i(LOG_TAG, "Performing sync.");
|
|
|
|
this.syncResult = syncResult;
|
|
|
|
// TODO: default serverURL.
|
|
|
|
GlobalSession globalSession = new GlobalSession(SyncConfiguration.DEFAULT_USER_API,
|
2012-01-14 09:20:31 -08:00
|
|
|
serverURL, username, password, prefsPath,
|
|
|
|
keyBundle, this, this.mContext, extras);
|
2011-12-21 08:44:08 -08:00
|
|
|
|
|
|
|
globalSession.start();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private void notifyMonitor() {
|
|
|
|
synchronized (syncMonitor) {
|
|
|
|
Log.i(LOG_TAG, "Notifying sync monitor.");
|
2012-01-14 09:20:31 -08:00
|
|
|
syncMonitor.notifyAll();
|
2011-12-21 08:44:08 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Implementing GlobalSession callbacks.
|
|
|
|
@Override
|
|
|
|
public void handleError(GlobalSession globalSession, Exception ex) {
|
2012-01-14 09:20:31 -08:00
|
|
|
Log.i(LOG_TAG, "GlobalSession indicated error. Flagging auth token as invalid, just in case.");
|
|
|
|
setShouldInvalidateAuthToken();
|
2011-12-21 08:44:08 -08:00
|
|
|
this.updateStats(globalSession, ex);
|
|
|
|
notifyMonitor();
|
|
|
|
}
|
|
|
|
|
2012-01-14 09:20:31 -08:00
|
|
|
@Override
|
|
|
|
public void handleAborted(GlobalSession globalSession, String reason) {
|
|
|
|
Log.w(LOG_TAG, "Sync aborted: " + reason);
|
|
|
|
notifyMonitor();
|
|
|
|
}
|
|
|
|
|
2011-12-21 08:44:08 -08:00
|
|
|
/**
|
|
|
|
* Introspect the exception, incrementing the appropriate stat counters.
|
|
|
|
* TODO: increment number of inserts, deletes, conflicts.
|
|
|
|
*
|
|
|
|
* @param globalSession
|
|
|
|
* @param ex
|
|
|
|
*/
|
|
|
|
private void updateStats(GlobalSession globalSession,
|
|
|
|
Exception ex) {
|
|
|
|
if (ex instanceof SyncException) {
|
|
|
|
((SyncException) ex).updateStats(globalSession, syncResult);
|
|
|
|
}
|
|
|
|
// TODO: non-SyncExceptions.
|
|
|
|
// TODO: wouldn't it be nice to update stats for *every* exception we get?
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void handleSuccess(GlobalSession globalSession) {
|
|
|
|
Log.i(LOG_TAG, "GlobalSession indicated success.");
|
2012-01-14 09:20:31 -08:00
|
|
|
Log.i(LOG_TAG, "Prefs target: " + globalSession.config.prefsPath);
|
|
|
|
globalSession.config.persistToPrefs();
|
2011-12-21 08:44:08 -08:00
|
|
|
notifyMonitor();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void handleStageCompleted(Stage currentState,
|
|
|
|
GlobalSession globalSession) {
|
|
|
|
Log.i(LOG_TAG, "Stage completed: " + currentState);
|
|
|
|
}
|
2012-01-14 09:20:31 -08:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public void requestBackoff(long backoff) {
|
|
|
|
if (backoff > 0) {
|
|
|
|
this.extendEarliestNextSync(System.currentTimeMillis() + backoff);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|