enhance the sync manager backoff logic and add support for retry-after
moved SyncQueue and SyncOperation into their own top-level classes
to ease maintainability and testing6
removed some dead code
diff --git a/api/current.xml b/api/current.xml
index 5246bf0..99bf173 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -40253,6 +40253,16 @@
visibility="public"
>
</field>
+<field name="delayUntil"
+ type="long"
+ transient="false"
+ volatile="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
<field name="fullSyncRequested"
type="boolean"
transient="false"
@@ -71769,7 +71779,7 @@
type="float"
transient="false"
volatile="false"
- value="0.001f"
+ value="0.0010f"
static="true"
final="true"
deprecated="not deprecated"
@@ -208805,7 +208815,7 @@
deprecated="not deprecated"
visibility="public"
>
-<parameter name="t" type="T">
+<parameter name="arg0" type="T">
</parameter>
</method>
</interface>
diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java
index ee26d3c..e3ccd00 100644
--- a/core/java/android/accounts/AccountManagerService.java
+++ b/core/java/android/accounts/AccountManagerService.java
@@ -506,7 +506,7 @@
+ ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null);
}
}
-
+
public void removeAccount(IAccountManagerResponse response, Account account) {
checkManageAccountsPermission();
long identityToken = clearCallingIdentity();
@@ -1660,9 +1660,16 @@
}
}
} else {
+ Account[] accounts = getAccounts(null /* type */);
+ fout.println("Accounts: " + accounts.length);
+ for (Account account : accounts) {
+ fout.println(" " + account);
+ }
+
+ fout.println();
synchronized (mSessions) {
final long now = SystemClock.elapsedRealtime();
- fout.println("AccountManagerService: " + mSessions.size() + " sessions");
+ fout.println("Active Sessions: " + mSessions.size());
for (Session session : mSessions.values()) {
fout.println(" " + session.toDebugString(now));
}
diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java
index a9c61dc..c9077bc 100644
--- a/core/java/android/content/SyncManager.java
+++ b/core/java/android/content/SyncManager.java
@@ -16,8 +16,6 @@
package android.content;
-import com.google.android.collect.Maps;
-
import com.android.internal.R;
import com.android.internal.util.ArrayUtils;
@@ -29,7 +27,6 @@
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.RegisteredServicesCache;
@@ -45,15 +42,14 @@
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.text.format.Time;
-import android.util.Config;
import android.util.EventLog;
import android.util.Log;
+import android.util.Pair;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@@ -65,12 +61,8 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
-import java.util.Map;
-import java.util.PriorityQueue;
import java.util.Random;
import java.util.Collection;
import java.util.concurrent.CountDownLatch;
@@ -78,7 +70,7 @@
/**
* @hide
*/
-class SyncManager implements OnAccountsUpdateListener {
+public class SyncManager implements OnAccountsUpdateListener {
private static final String TAG = "SyncManager";
// used during dumping of the Sync history
@@ -144,9 +136,6 @@
private Context mContext;
- private String mStatusText = "";
- private long mHeartbeatTime = 0;
-
private volatile Account[] mAccounts = null;
volatile private PowerManager.WakeLock mSyncWakeLock;
@@ -156,12 +145,9 @@
private final NotificationManager mNotificationMgr;
private AlarmManager mAlarmService = null;
- private HandlerThread mSyncThread;
-
- private volatile IPackageManager mPackageManager;
private final SyncStorageEngine mSyncStorageEngine;
- private final SyncQueue mSyncQueue;
+ public final SyncQueue mSyncQueue;
private ActiveSyncContext mActiveSyncContext = null;
@@ -312,8 +298,6 @@
private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs";
- private final boolean mFactoryTest;
-
private volatile boolean mBootCompleted = false;
private ConnectivityManager getConnectivityManager() {
@@ -327,8 +311,6 @@
}
public SyncManager(Context context, boolean factoryTest) {
- mFactoryTest = factoryTest;
-
// Initialize the SyncStorageEngine first, before registering observers
// and creating threads and so on; it may fail if the disk is full.
SyncStorageEngine.init(context);
@@ -337,11 +319,10 @@
mContext = context;
- mSyncThread = new HandlerThread("SyncHandlerThread", Process.THREAD_PRIORITY_BACKGROUND);
- mSyncThread.start();
- mSyncHandler = new SyncHandler(mSyncThread.getLooper());
-
- mPackageManager = null;
+ HandlerThread syncThread = new HandlerThread("SyncHandlerThread",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ syncThread.start();
+ mSyncHandler = new SyncHandler(syncThread.getLooper());
mSyncAdapters = new SyncAdaptersCache(mContext);
@@ -635,16 +616,12 @@
if (isLoggable) {
Log.v(TAG, "not syncing because sync is disabled");
}
- setStatusText("Sync is disabled.");
return;
}
final boolean backgroundDataUsageAllowed = !mBootCompleted ||
getConnectivityManager().getBackgroundDataSetting();
- if (!mDataConnectionIsConnected) setStatusText("No data connection");
- if (mStorageIsLow) setStatusText("Memory low");
-
if (extras == null) extras = new Bundle();
Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
@@ -670,7 +647,6 @@
if (isLoggable) {
Log.v(TAG, "scheduleSync: no accounts configured, dropping");
}
- setStatusText("No accounts are configured.");
return;
}
}
@@ -759,10 +735,6 @@
}
}
- private void setStatusText(String message) {
- mStatusText = message;
- }
-
public void scheduleLocalSync(Account account, String authority) {
final Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
@@ -770,17 +742,6 @@
false /* onlyThoseWithUnkownSyncableState */);
}
- private IPackageManager getPackageManager() {
- // Don't bother synchronizing on this. The worst that can happen is that two threads
- // can try to get the package manager at the same time but only one result gets
- // used. Since there is only one package manager in the system this doesn't matter.
- if (mPackageManager == null) {
- IBinder b = ServiceManager.getService("package");
- mPackageManager = IPackageManager.Stub.asInterface(b);
- }
- return mPackageManager;
- }
-
public SyncAdapterType[] getSyncAdapterTypes() {
final Collection<RegisteredServicesCache.ServiceInfo<SyncAdapterType>> serviceInfos =
mSyncAdapters.getAllServices();
@@ -793,11 +754,6 @@
return types;
}
- public void updateHeartbeatTime() {
- mHeartbeatTime = SystemClock.elapsedRealtime();
- mSyncStorageEngine.reportActiveChange();
- }
-
private void sendSyncAlarmMessage() {
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_ALARM");
mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_SYNC_ALARM);
@@ -840,22 +796,24 @@
}
}
- private void rescheduleImmediately(SyncOperation syncOperation) {
- SyncOperation rescheduledSyncOperation = new SyncOperation(syncOperation);
- rescheduledSyncOperation.setDelay(0);
- scheduleSyncOperation(rescheduledSyncOperation);
+ private void clearBackoffSetting(SyncOperation op) {
+ mSyncStorageEngine.setBackoff(op.account, op.authority,
+ SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE);
}
- private long rescheduleWithDelay(SyncOperation syncOperation) {
- long newDelayInMs;
+ private void increaseBackoffSetting(SyncOperation op) {
+ final long now = SystemClock.elapsedRealtime();
- if (syncOperation.delay <= 0) {
+ final Pair<Long, Long> previousSettings =
+ mSyncStorageEngine.getBackoff(op.account, op.authority);
+ long newDelayInMs;
+ if (previousSettings == null || previousSettings.second <= 0) {
// The initial delay is the jitterized INITIAL_SYNC_RETRY_TIME_IN_MS
newDelayInMs = jitterize(INITIAL_SYNC_RETRY_TIME_IN_MS,
(long)(INITIAL_SYNC_RETRY_TIME_IN_MS * 1.1));
} else {
// Subsequent delays are the double of the previous delay
- newDelayInMs = syncOperation.delay * 2;
+ newDelayInMs = previousSettings.second * 2;
}
// Cap the delay
@@ -866,10 +824,20 @@
newDelayInMs = maxSyncRetryTimeInSeconds * 1000;
}
- SyncOperation rescheduledSyncOperation = new SyncOperation(syncOperation);
- rescheduledSyncOperation.setDelay(newDelayInMs);
- scheduleSyncOperation(rescheduledSyncOperation);
- return newDelayInMs;
+ mSyncStorageEngine.setBackoff(op.account, op.authority,
+ now + newDelayInMs, newDelayInMs);
+ }
+
+ private void setDelayUntilTime(SyncOperation op, long delayUntilSeconds) {
+ final long delayUntil = delayUntilSeconds * 1000;
+ final long absoluteNow = System.currentTimeMillis();
+ long newDelayUntilTime;
+ if (delayUntil > absoluteNow) {
+ newDelayUntilTime = SystemClock.elapsedRealtime() + (delayUntil - absoluteNow);
+ } else {
+ newDelayUntilTime = 0;
+ }
+ mSyncStorageEngine.setDelayUntilTime(op.account, op.authority, newDelayUntilTime);
}
/**
@@ -905,28 +873,26 @@
public void scheduleSyncOperation(SyncOperation syncOperation) {
// If this operation is expedited and there is a sync in progress then
// reschedule the current operation and send a cancel for it.
- final boolean expedited = syncOperation.delay < 0;
final ActiveSyncContext activeSyncContext = mActiveSyncContext;
- if (expedited && activeSyncContext != null) {
- final boolean activeIsExpedited = activeSyncContext.mSyncOperation.delay < 0;
+ if (syncOperation.expedited && activeSyncContext != null) {
final boolean hasSameKey =
activeSyncContext.mSyncOperation.key.equals(syncOperation.key);
// This request is expedited and there is a sync in progress.
// Interrupt the current sync only if it is not expedited and if it has a different
// key than the one we are scheduling.
- if (!activeIsExpedited && !hasSameKey) {
- rescheduleImmediately(activeSyncContext.mSyncOperation);
+ if (!activeSyncContext.mSyncOperation.expedited && !hasSameKey) {
+ scheduleSyncOperation(new SyncOperation(activeSyncContext.mSyncOperation));
sendSyncFinishedOrCanceledMessage(activeSyncContext,
null /* no result since this is a cancel */);
}
}
- boolean operationEnqueued;
+ boolean queueChanged;
synchronized (mSyncQueue) {
- operationEnqueued = mSyncQueue.add(syncOperation);
+ queueChanged = mSyncQueue.add(syncOperation);
}
- if (operationEnqueued) {
+ if (queueChanged) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "scheduleSyncOperation: enqueued " + syncOperation);
}
@@ -945,16 +911,17 @@
* @param authority limit the removals to operations with this authority, if non-null
*/
public void clearScheduledSyncOperations(Account account, String authority) {
+ mSyncStorageEngine.setBackoff(account, authority,
+ SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE);
synchronized (mSyncQueue) {
mSyncQueue.clear(account, authority);
}
}
- void maybeRescheduleSync(SyncResult syncResult, SyncOperation previousSyncOperation) {
+ void maybeRescheduleSync(SyncResult syncResult, SyncOperation operation) {
boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG);
if (isLoggable) {
- Log.d(TAG, "encountered error(s) during the sync: " + syncResult + ", "
- + previousSyncOperation);
+ Log.d(TAG, "encountered error(s) during the sync: " + syncResult + ", " + operation);
}
// If this sync aborted because the internal sync loop retried too many times then
@@ -963,119 +930,33 @@
// If this was a two-way sync then retry soft errors with an exponential backoff.
// If this was an upward sync then schedule a two-way sync immediately.
// Otherwise do not reschedule.
- if (syncResult.tooManyRetries) {
+ if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)) {
+ Log.d(TAG, "not retrying sync operation because it is a manual sync: "
+ + operation);
+ } else if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false)) {
+ final SyncOperation newSyncOperation = new SyncOperation(operation);
+ newSyncOperation.extras.remove(ContentResolver.SYNC_EXTRAS_UPLOAD);
+ Log.d(TAG, "retrying sync operation as a two-way sync because an upload-only sync "
+ + "encountered an error: " + operation);
+ scheduleSyncOperation(newSyncOperation);
+ } else if (syncResult.tooManyRetries) {
Log.d(TAG, "not retrying sync operation because it retried too many times: "
- + previousSyncOperation);
+ + operation);
} else if (syncResult.madeSomeProgress()) {
if (isLoggable) {
- Log.d(TAG, "retrying sync operation immediately because "
- + "even though it had an error it achieved some success");
+ Log.d(TAG, "retrying sync operation because even though it had an error "
+ + "it achieved some success");
}
- rescheduleImmediately(previousSyncOperation);
- } else if (previousSyncOperation.extras.getBoolean(
- ContentResolver.SYNC_EXTRAS_UPLOAD, false)) {
- final SyncOperation newSyncOperation = new SyncOperation(previousSyncOperation);
- newSyncOperation.extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
- newSyncOperation.setDelay(0);
- if (Config.LOGD) {
- Log.d(TAG, "retrying sync operation as a two-way sync because an upload-only sync "
- + "encountered an error: " + previousSyncOperation);
- }
- scheduleSyncOperation(newSyncOperation);
+ scheduleSyncOperation(new SyncOperation(operation));
} else if (syncResult.hasSoftError()) {
- long delay = rescheduleWithDelay(previousSyncOperation);
- if (delay >= 0) {
- if (isLoggable) {
- Log.d(TAG, "retrying sync operation in " + delay + " ms because "
- + "it encountered a soft error: " + previousSyncOperation);
- }
+ if (isLoggable) {
+ Log.d(TAG, "retrying sync operation because it encountered a soft error: "
+ + operation);
}
+ scheduleSyncOperation(new SyncOperation(operation));
} else {
- if (Config.LOGD) {
- Log.d(TAG, "not retrying sync operation because the error is a hard error: "
- + previousSyncOperation);
- }
- }
- }
-
- /**
- * Value type that represents a sync operation.
- */
- static class SyncOperation implements Comparable {
- final Account account;
- int syncSource;
- String authority;
- Bundle extras;
- final String key;
- long earliestRunTime;
- long delay;
- SyncStorageEngine.PendingOperation pendingOperation;
-
- SyncOperation(Account account, int source, String authority, Bundle extras, long delay) {
- this.account = account;
- this.syncSource = source;
- this.authority = authority;
- this.extras = new Bundle(extras);
- this.setDelay(delay);
- this.key = toKey();
- }
-
- SyncOperation(SyncOperation other) {
- this.account = other.account;
- this.syncSource = other.syncSource;
- this.authority = other.authority;
- this.extras = new Bundle(other.extras);
- this.delay = other.delay;
- this.earliestRunTime = other.earliestRunTime;
- this.key = toKey();
- }
-
- public void setDelay(long delay) {
- this.delay = delay;
- if (delay >= 0) {
- this.earliestRunTime = SystemClock.elapsedRealtime() + delay;
- } else {
- this.earliestRunTime = 0;
- }
- }
-
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("authority: ").append(authority);
- sb.append(" account: ").append(account);
- sb.append(" extras: ");
- extrasToStringBuilder(extras, sb);
- sb.append(" syncSource: ").append(syncSource);
- sb.append(" when: ").append(earliestRunTime);
- sb.append(" delay: ").append(delay);
- sb.append(" key: {").append(key).append("}");
- if (pendingOperation != null) sb.append(" pendingOperation: ").append(pendingOperation);
- return sb.toString();
- }
-
- private String toKey() {
- StringBuilder sb = new StringBuilder();
- sb.append("authority: ").append(authority);
- sb.append(" account: ").append(account);
- sb.append(" extras: ");
- extrasToStringBuilder(extras, sb);
- return sb.toString();
- }
-
- private static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
- sb.append("[");
- for (String key : bundle.keySet()) {
- sb.append(key).append("=").append(bundle.get(key)).append(" ");
- }
- sb.append("]");
- }
-
- public int compareTo(Object o) {
- SyncOperation other = (SyncOperation)o;
- if (earliestRunTime == other.earliestRunTime) {
- return 0;
- }
- return (earliestRunTime < other.earliestRunTime) ? -1 : 1;
+ Log.d(TAG, "not retrying sync operation because the error is a hard error: "
+ + operation);
}
}
@@ -1100,10 +981,7 @@
}
public void sendHeartbeat() {
- // ignore this call if it corresponds to an old sync session
- if (mActiveSyncContext == this) {
- SyncManager.this.updateHeartbeatTime();
- }
+ // heartbeats are no longer used
}
public void onFinished(SyncResult result) {
@@ -1255,7 +1133,8 @@
pw.print(" #"); pw.print(i); pw.print(": account=");
pw.print(op.account.name); pw.print(":");
pw.print(op.account.type); pw.print(" authority=");
- pw.println(op.authority);
+ pw.print(op.authority); pw.print(" expedited=");
+ pw.println(op.expedited);
if (op.extras != null && op.extras.size() > 0) {
sb.setLength(0);
SyncOperation.extrasToStringBuilder(op.extras, sb);
@@ -1295,6 +1174,24 @@
}
pw.print(" "); pw.print(authority.authority);
pw.println(":");
+ final String syncable = authority.syncable > 0
+ ? "syncable"
+ : (authority.syncable == 0 ? "not syncable" : "not initialized");
+ final String enabled = authority.enabled ? "enabled" : "disabled";
+ final String delayUntil = authority.delayUntil > now
+ ? "delay for " + ((authority.delayUntil - now) / 1000) + " sec"
+ : "no delay required";
+ final String backoff = authority.backoffTime > now
+ ? "backoff for " + ((authority.backoffTime - now) / 1000)
+ + " sec"
+ : "no backoff required";
+ final String backoffDelay = authority.backoffDelay > 0
+ ? ("the backoff increment is " + authority.backoffDelay / 1000
+ + " sec")
+ : "no backoff increment";
+ pw.println(String.format(
+ " settings: %s, %s, %s, %s, %s",
+ enabled, syncable, backoff, backoffDelay, delayUntil));
pw.print(" count: local="); pw.print(status.numSourceLocal);
pw.print(" poll="); pw.print(status.numSourcePoll);
pw.print(" server="); pw.print(status.numSourceServer);
@@ -1568,11 +1465,9 @@
}
SyncHandlerMessagePayload payload = (SyncHandlerMessagePayload)msg.obj;
if (mActiveSyncContext != payload.activeSyncContext) {
- if (Config.LOGD) {
- Log.d(TAG, "handleSyncHandlerMessage: sync context doesn't match, "
- + "dropping: mActiveSyncContext " + mActiveSyncContext
- + " != " + payload.activeSyncContext);
- }
+ Log.d(TAG, "handleSyncHandlerMessage: sync context doesn't match, "
+ + "dropping: mActiveSyncContext " + mActiveSyncContext
+ + " != " + payload.activeSyncContext);
return;
}
runSyncFinishedOrCanceled(payload.syncResult);
@@ -1685,14 +1580,12 @@
if (now > activeSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC) {
SyncOperation nextSyncOperation;
synchronized (mSyncQueue) {
- nextSyncOperation = mSyncQueue.head();
+ nextSyncOperation = mSyncQueue.nextReadyToRun(now);
}
- if (nextSyncOperation != null && nextSyncOperation.earliestRunTime <= now) {
- if (Config.LOGD) {
- Log.d(TAG, "canceling and rescheduling sync because it ran too long: "
- + activeSyncContext.mSyncOperation);
- }
- rescheduleImmediately(activeSyncContext.mSyncOperation);
+ if (nextSyncOperation != null) {
+ Log.d(TAG, "canceling and rescheduling sync because it ran too long: "
+ + activeSyncContext.mSyncOperation);
+ scheduleSyncOperation(new SyncOperation(activeSyncContext.mSyncOperation));
sendSyncFinishedOrCanceledMessage(activeSyncContext,
null /* no result since this is a cancel */);
} else {
@@ -1712,7 +1605,6 @@
if (isLoggable) {
Log.v(TAG, "runStateIdle: no data connection, skipping");
}
- setStatusText("No data connection");
return;
}
@@ -1720,7 +1612,6 @@
if (isLoggable) {
Log.v(TAG, "runStateIdle: memory low, skipping");
}
- setStatusText("Memory low");
return;
}
@@ -1731,7 +1622,6 @@
if (isLoggable) {
Log.v(TAG, "runStateIdle: accounts not known, skipping");
}
- setStatusText("Accounts not known yet");
return;
}
@@ -1742,8 +1632,9 @@
final boolean backgroundDataUsageAllowed =
getConnectivityManager().getBackgroundDataSetting();
synchronized (mSyncQueue) {
+ final long now = SystemClock.elapsedRealtime();
while (true) {
- op = mSyncQueue.head();
+ op = mSyncQueue.nextReadyToRun(now);
if (op == null) {
if (isLoggable) {
Log.v(TAG, "runStateIdle: no more sync operations, returning");
@@ -1751,73 +1642,57 @@
return;
}
+ // we are either going to run this sync or drop it so go ahead and remove it
+ // from the queue now
+ mSyncQueue.remove(op);
+
// Sync is disabled, drop this operation.
if (!isSyncEnabled()) {
if (isLoggable) {
Log.v(TAG, "runStateIdle: sync disabled, dropping " + op);
}
- mSyncQueue.popHead();
- continue;
- }
-
- // skip the sync if it isn't manual and auto sync is disabled
- final boolean manualSync = op.extras.getBoolean(
- ContentResolver.SYNC_EXTRAS_MANUAL, false);
- final boolean syncAutomatically =
- mSyncStorageEngine.getSyncAutomatically(op.account, op.authority)
- && mSyncStorageEngine.getMasterSyncAutomatically();
- boolean syncAllowed =
- manualSync || (backgroundDataUsageAllowed && syncAutomatically);
- int isSyncable = mSyncStorageEngine.getIsSyncable(op.account, op.authority);
- if (isSyncable == 0) {
- // if not syncable, don't allow
- syncAllowed = false;
- } else if (isSyncable < 0) {
- // if the syncable state is unknown, only allow initialization syncs
- syncAllowed =
- op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
- }
- if (!syncAllowed) {
- if (isLoggable) {
- Log.v(TAG, "runStateIdle: sync off, dropping " + op);
- }
- mSyncQueue.popHead();
continue;
}
// skip the sync if the account of this operation no longer exists
if (!ArrayUtils.contains(accounts, op.account)) {
- mSyncQueue.popHead();
if (isLoggable) {
Log.v(TAG, "runStateIdle: account not present, dropping " + op);
}
continue;
}
- // go ahead and try to sync this syncOperation
- if (isLoggable) {
- Log.v(TAG, "runStateIdle: found sync candidate: " + op);
+ // skip the sync if it isn't manual and auto sync is disabled
+ final boolean manualSync =
+ op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
+ final boolean syncAutomatically =
+ mSyncStorageEngine.getSyncAutomatically(op.account, op.authority)
+ && mSyncStorageEngine.getMasterSyncAutomatically();
+ if (!(manualSync || (backgroundDataUsageAllowed && syncAutomatically))) {
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: sync of this operation is not allowed, "
+ + "dropping " + op);
+ }
+ continue;
}
+
+ if (mSyncStorageEngine.getIsSyncable(op.account, op.authority) <= 0) {
+ // if not syncable or if the syncable is unknown (< 0), don't allow
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: sync of this operation is not allowed, "
+ + "dropping " + op);
+ }
+ continue;
+ }
+
+ // go ahead and try to sync this syncOperation
break;
}
- // If the first SyncOperation isn't ready to run schedule a wakeup and
- // get out.
- final long now = SystemClock.elapsedRealtime();
- if (op.earliestRunTime > now) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "runStateIdle: the time is " + now + " yet the next "
- + "sync operation is for " + op.earliestRunTime + ": " + op);
- }
- return;
- }
-
- // We will do this sync. Remove it from the queue and run it outside of the
- // synchronized block.
+ // We will do this sync. Run it outside of the synchronized block.
if (isLoggable) {
Log.v(TAG, "runStateIdle: we are going to sync " + op);
}
- mSyncQueue.popHead();
}
// connect to the sync adapter
@@ -1825,9 +1700,7 @@
RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo =
mSyncAdapters.getServiceInfo(syncAdapterType);
if (syncAdapterInfo == null) {
- if (Config.LOGD) {
- Log.d(TAG, "can't find a sync adapter for " + syncAdapterType);
- }
+ Log.d(TAG, "can't find a sync adapter for " + syncAdapterType);
runStateIdle();
return;
}
@@ -1861,25 +1734,22 @@
syncAdapter.startSync(mActiveSyncContext, syncOperation.authority,
syncOperation.account, syncOperation.extras);
} catch (RemoteException remoteExc) {
- if (Config.LOGD) {
- Log.d(TAG, "runStateIdle: caught a RemoteException, rescheduling", remoteExc);
- }
+ Log.d(TAG, "runStateIdle: caught a RemoteException, rescheduling", remoteExc);
mActiveSyncContext.unBindFromSyncAdapter();
mActiveSyncContext = null;
mSyncStorageEngine.setActiveSync(mActiveSyncContext);
- rescheduleWithDelay(syncOperation);
+ increaseBackoffSetting(syncOperation);
+ scheduleSyncOperation(new SyncOperation(syncOperation));
} catch (RuntimeException exc) {
mActiveSyncContext.unBindFromSyncAdapter();
mActiveSyncContext = null;
mSyncStorageEngine.setActiveSync(mActiveSyncContext);
- Log.e(TAG, "Caught a RuntimeException while starting the sync " + syncOperation,
- exc);
+ Log.e(TAG, "Caught RuntimeException while starting the sync " + syncOperation, exc);
}
}
private void runSyncFinishedOrCanceled(SyncResult syncResult) {
boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
- if (isLoggable) Log.v(TAG, "runSyncFinishedOrCanceled");
final ActiveSyncContext activeSyncContext = mActiveSyncContext;
mActiveSyncContext = null;
mSyncStorageEngine.setActiveSync(mActiveSyncContext);
@@ -1893,32 +1763,34 @@
int upstreamActivity;
if (syncResult != null) {
if (isLoggable) {
- Log.v(TAG, "runSyncFinishedOrCanceled: is a finished: operation "
+ Log.v(TAG, "runSyncFinishedOrCanceled [finished]: "
+ syncOperation + ", result " + syncResult);
}
if (!syncResult.hasError()) {
- if (isLoggable) {
- Log.v(TAG, "finished sync operation " + syncOperation);
- }
historyMessage = SyncStorageEngine.MESG_SUCCESS;
// TODO: set these correctly when the SyncResult is extended to include it
downstreamActivity = 0;
upstreamActivity = 0;
+ clearBackoffSetting(syncOperation);
} else {
- maybeRescheduleSync(syncResult, syncOperation);
- if (Config.LOGD) {
- Log.d(TAG, "failed sync operation " + syncOperation);
+ Log.d(TAG, "failed sync operation " + syncOperation + ", " + syncResult);
+ // the operation failed so increase the backoff time
+ if (!syncResult.syncAlreadyInProgress) {
+ increaseBackoffSetting(syncOperation);
}
+ // reschedule the sync if so indicated by the syncResult
+ maybeRescheduleSync(syncResult, syncOperation);
historyMessage = Integer.toString(syncResultToErrorNumber(syncResult));
// TODO: set these correctly when the SyncResult is extended to include it
downstreamActivity = 0;
upstreamActivity = 0;
}
+
+ setDelayUntilTime(syncOperation, syncResult.delayUntil);
} else {
if (isLoggable) {
- Log.v(TAG, "runSyncFinishedOrCanceled: is a cancel: operation "
- + syncOperation);
+ Log.v(TAG, "runSyncFinishedOrCanceled [canceled]: " + syncOperation);
}
if (activeSyncContext.mSyncAdapter != null) {
try {
@@ -2070,19 +1942,17 @@
if (mAccounts == null) return;
if (mStorageIsLow) return;
+ final long now = SystemClock.elapsedRealtime();
+
// Compute the alarm fire time:
// - not syncing: time of the next sync operation
// - syncing, no notification: time from sync start to notification create time
// - syncing, with notification: time till timeout of the active sync operation
- Long alarmTime = null;
+ Long alarmTime;
ActiveSyncContext activeSyncContext = mActiveSyncContext;
if (activeSyncContext == null) {
- SyncOperation syncOperation;
synchronized (mSyncQueue) {
- syncOperation = mSyncQueue.head();
- }
- if (syncOperation != null) {
- alarmTime = syncOperation.earliestRunTime;
+ alarmTime = mSyncQueue.nextRunTime(now);
}
} else {
final long notificationTime =
@@ -2104,7 +1974,7 @@
when += ERROR_NOTIFICATION_DELAY_MS;
// convert when fron absolute time to elapsed run time
long delay = when - System.currentTimeMillis();
- when = SystemClock.elapsedRealtime() + delay;
+ when = now + delay;
alarmTime = alarmTime != null ? Math.min(alarmTime, when) : when;
}
}
@@ -2228,221 +2098,4 @@
}
}
- static class SyncQueue {
- private SyncStorageEngine mSyncStorageEngine;
-
- private static final boolean DEBUG_CHECK_DATA_CONSISTENCY = false;
-
- // A priority queue of scheduled SyncOperations that is designed to make it quick
- // to find the next SyncOperation that should be considered for running.
- private final PriorityQueue<SyncOperation> mOpsByWhen = new PriorityQueue<SyncOperation>();
-
- // A Map of SyncOperations operationKey -> SyncOperation that is designed for
- // quick lookup of an enqueued SyncOperation.
- private final HashMap<String, SyncOperation> mOpsByKey = Maps.newHashMap();
-
- public SyncQueue(SyncStorageEngine syncStorageEngine) {
- mSyncStorageEngine = syncStorageEngine;
- ArrayList<SyncStorageEngine.PendingOperation> ops
- = mSyncStorageEngine.getPendingOperations();
- final int N = ops.size();
- for (int i=0; i<N; i++) {
- SyncStorageEngine.PendingOperation op = ops.get(i);
- SyncOperation syncOperation = new SyncOperation(
- op.account, op.syncSource, op.authority, op.extras, 0);
- syncOperation.pendingOperation = op;
- add(syncOperation, op);
- }
-
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
- }
-
- public boolean add(SyncOperation operation) {
- return add(new SyncOperation(operation),
- null /* this is not coming from the database */);
- }
-
- private boolean add(SyncOperation operation,
- SyncStorageEngine.PendingOperation pop) {
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(pop == null);
-
- // If this operation is expedited then set its earliestRunTime to be immediately
- // before the head of the list, or not if none are in the list.
- if (operation.delay < 0) {
- SyncOperation headOperation = head();
- if (headOperation != null) {
- operation.earliestRunTime = Math.min(SystemClock.elapsedRealtime(),
- headOperation.earliestRunTime - 1);
- } else {
- operation.earliestRunTime = SystemClock.elapsedRealtime();
- }
- }
-
- // - if an operation with the same key exists and this one should run earlier,
- // delete the old one and add the new one
- // - if an operation with the same key exists and if this one should run
- // later, ignore it
- // - if no operation exists then add the new one
- final String operationKey = operation.key;
- SyncOperation existingOperation = mOpsByKey.get(operationKey);
-
- // if this operation matches an existing operation that is being retried (delay > 0)
- // and this isn't a manual sync operation, ignore this operation
- if (existingOperation != null && existingOperation.delay > 0) {
- if (!operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)) {
- return false;
- }
- }
-
- if (existingOperation != null
- && operation.earliestRunTime >= existingOperation.earliestRunTime) {
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(pop == null);
- return false;
- }
-
- if (existingOperation != null) {
- removeByKey(operationKey);
- }
-
- operation.pendingOperation = pop;
- if (operation.pendingOperation == null) {
- pop = new SyncStorageEngine.PendingOperation(
- operation.account, operation.syncSource,
- operation.authority, operation.extras);
- pop = mSyncStorageEngine.insertIntoPending(pop);
- if (pop == null) {
- throw new IllegalStateException("error adding pending sync operation "
- + operation);
- }
- operation.pendingOperation = pop;
- }
-
- if (DEBUG_CHECK_DATA_CONSISTENCY) {
- debugCheckDataStructures(
- false /* don't compare with the DB, since we know
- it is inconsistent right now */ );
- }
- mOpsByKey.put(operationKey, operation);
- mOpsByWhen.add(operation);
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(pop == null);
- return true;
- }
-
- public void removeByKey(String operationKey) {
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
- SyncOperation operationToRemove = mOpsByKey.remove(operationKey);
- if (!mOpsByWhen.remove(operationToRemove)) {
- throw new IllegalStateException(
- "unable to find " + operationToRemove + " in mOpsByWhen");
- }
-
- if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) {
- final String errorMessage = "unable to find pending row for " + operationToRemove;
- Log.e(TAG, errorMessage, new IllegalStateException(errorMessage));
- }
-
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
- }
-
- public SyncOperation head() {
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
- return mOpsByWhen.peek();
- }
-
- public void popHead() {
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
- SyncOperation operation = mOpsByWhen.remove();
- if (mOpsByKey.remove(operation.key) == null) {
- throw new IllegalStateException("unable to find " + operation + " in mOpsByKey");
- }
-
- if (!mSyncStorageEngine.deleteFromPending(operation.pendingOperation)) {
- final String errorMessage = "unable to find pending row for " + operation;
- Log.e(TAG, errorMessage, new IllegalStateException(errorMessage));
- }
-
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
- }
-
- public void clear(Account account, String authority) {
- Iterator<Map.Entry<String, SyncOperation>> entries = mOpsByKey.entrySet().iterator();
- while (entries.hasNext()) {
- Map.Entry<String, SyncOperation> entry = entries.next();
- SyncOperation syncOperation = entry.getValue();
- if (account != null && !syncOperation.account.equals(account)) continue;
- if (authority != null && !syncOperation.authority.equals(authority)) continue;
-
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
- entries.remove();
- if (!mOpsByWhen.remove(syncOperation)) {
- throw new IllegalStateException(
- "unable to find " + syncOperation + " in mOpsByWhen");
- }
-
- if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) {
- final String errorMessage = "unable to find pending row for " + syncOperation;
- Log.e(TAG, errorMessage, new IllegalStateException(errorMessage));
- }
-
- if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
- }
- }
-
- public void dump(StringBuilder sb) {
- sb.append("SyncQueue: ").append(mOpsByWhen.size()).append(" operation(s)\n");
- for (SyncOperation operation : mOpsByWhen) {
- sb.append(operation).append("\n");
- }
- }
-
- private void debugCheckDataStructures(boolean checkDatabase) {
- if (mOpsByKey.size() != mOpsByWhen.size()) {
- throw new IllegalStateException("size mismatch: "
- + mOpsByKey .size() + " != " + mOpsByWhen.size());
- }
- for (SyncOperation operation : mOpsByWhen) {
- if (!mOpsByKey.containsKey(operation.key)) {
- throw new IllegalStateException(
- "operation " + operation + " is in mOpsByWhen but not mOpsByKey");
- }
- }
- for (Map.Entry<String, SyncOperation> entry : mOpsByKey.entrySet()) {
- final SyncOperation operation = entry.getValue();
- final String key = entry.getKey();
- if (!key.equals(operation.key)) {
- throw new IllegalStateException(
- "operation " + operation + " in mOpsByKey doesn't match key " + key);
- }
- if (!mOpsByWhen.contains(operation)) {
- throw new IllegalStateException(
- "operation " + operation + " is in mOpsByKey but not mOpsByWhen");
- }
- }
-
- if (checkDatabase) {
- final int N = mSyncStorageEngine.getPendingOperationCount();
- if (mOpsByKey.size() != N) {
- ArrayList<SyncStorageEngine.PendingOperation> ops
- = mSyncStorageEngine.getPendingOperations();
- StringBuilder sb = new StringBuilder();
- for (int i=0; i<N; i++) {
- SyncStorageEngine.PendingOperation op = ops.get(i);
- sb.append("#");
- sb.append(i);
- sb.append(": account=");
- sb.append(op.account);
- sb.append(" syncSource=");
- sb.append(op.syncSource);
- sb.append(" authority=");
- sb.append(op.authority);
- sb.append("\n");
- }
- dump(sb);
- throw new IllegalStateException("DB size mismatch: "
- + mOpsByKey.size() + " != " + N + "\n"
- + sb.toString());
- }
- }
- }
- }
}
diff --git a/core/java/android/content/SyncOperation.java b/core/java/android/content/SyncOperation.java
new file mode 100644
index 0000000..2d6e833
--- /dev/null
+++ b/core/java/android/content/SyncOperation.java
@@ -0,0 +1,95 @@
+package android.content;
+
+import android.accounts.Account;
+import android.os.Bundle;
+import android.os.SystemClock;
+
+/**
+ * Value type that represents a sync operation.
+ * @hide
+ */
+public class SyncOperation implements Comparable {
+ public final Account account;
+ public int syncSource;
+ public String authority;
+ public Bundle extras;
+ public final String key;
+ public long earliestRunTime;
+ public boolean expedited;
+ public SyncStorageEngine.PendingOperation pendingOperation;
+
+ public SyncOperation(Account account, int source, String authority, Bundle extras,
+ long delay) {
+ this.account = account;
+ this.syncSource = source;
+ this.authority = authority;
+ this.extras = new Bundle(extras);
+ removeFalseExtra(ContentResolver.SYNC_EXTRAS_UPLOAD);
+ removeFalseExtra(ContentResolver.SYNC_EXTRAS_MANUAL);
+ removeFalseExtra(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS);
+ removeFalseExtra(ContentResolver.SYNC_EXTRAS_EXPEDITED);
+ removeFalseExtra(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS);
+ final long now = SystemClock.elapsedRealtime();
+ if (delay < 0) {
+ this.expedited = true;
+ this.earliestRunTime = now;
+ } else {
+ this.expedited = false;
+ this.earliestRunTime = now + delay;
+ }
+ this.key = toKey();
+ }
+
+ private void removeFalseExtra(String extraName) {
+ if (!extras.getBoolean(extraName, false)) {
+ extras.remove(extraName);
+ }
+ }
+
+ SyncOperation(SyncOperation other) {
+ this.account = other.account;
+ this.syncSource = other.syncSource;
+ this.authority = other.authority;
+ this.extras = new Bundle(other.extras);
+ this.expedited = other.expedited;
+ this.earliestRunTime = SystemClock.elapsedRealtime();
+ this.key = toKey();
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("authority: ").append(authority);
+ sb.append(" account: ").append(account);
+ sb.append(" extras: ");
+ extrasToStringBuilder(extras, sb);
+ sb.append(" syncSource: ").append(syncSource);
+ sb.append(" when: ").append(earliestRunTime);
+ sb.append(" expedited: ").append(expedited);
+ return sb.toString();
+ }
+
+ private String toKey() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("authority: ").append(authority);
+ sb.append(" account: ").append(account);
+ sb.append(" extras: ");
+ extrasToStringBuilder(extras, sb);
+ return sb.toString();
+ }
+
+ public static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
+ sb.append("[");
+ for (String key : bundle.keySet()) {
+ sb.append(key).append("=").append(bundle.get(key)).append(" ");
+ }
+ sb.append("]");
+ }
+
+ public int compareTo(Object o) {
+ SyncOperation other = (SyncOperation)o;
+ if (earliestRunTime == other.earliestRunTime) {
+ return 0;
+ }
+ return (earliestRunTime < other.earliestRunTime) ? -1 : 1;
+ }
+}
diff --git a/core/java/android/content/SyncQueue.java b/core/java/android/content/SyncQueue.java
new file mode 100644
index 0000000..a9f15d9
--- /dev/null
+++ b/core/java/android/content/SyncQueue.java
@@ -0,0 +1,190 @@
+package android.content;
+
+import com.google.android.collect.Maps;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Pair;
+import android.util.Log;
+import android.accounts.Account;
+
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Iterator;
+
+/**
+ *
+ * @hide
+ */
+public class SyncQueue {
+ private static final String TAG = "SyncManager";
+ private SyncStorageEngine mSyncStorageEngine;
+
+ // A Map of SyncOperations operationKey -> SyncOperation that is designed for
+ // quick lookup of an enqueued SyncOperation.
+ private final HashMap<String, SyncOperation> mOperationsMap = Maps.newHashMap();
+
+ public SyncQueue(SyncStorageEngine syncStorageEngine) {
+ mSyncStorageEngine = syncStorageEngine;
+ ArrayList<SyncStorageEngine.PendingOperation> ops
+ = mSyncStorageEngine.getPendingOperations();
+ final int N = ops.size();
+ for (int i=0; i<N; i++) {
+ SyncStorageEngine.PendingOperation op = ops.get(i);
+ // -1 is a special value that means expedited
+ final int delay = op.expedited ? -1 : 0;
+ SyncOperation syncOperation = new SyncOperation(
+ op.account, op.syncSource, op.authority, op.extras, delay);
+ syncOperation.pendingOperation = op;
+ add(syncOperation, op);
+ }
+ }
+
+ public boolean add(SyncOperation operation) {
+ return add(operation, null /* this is not coming from the database */);
+ }
+
+ private boolean add(SyncOperation operation,
+ SyncStorageEngine.PendingOperation pop) {
+ // - if an operation with the same key exists and this one should run earlier,
+ // update the earliestRunTime of the existing to the new time
+ // - if an operation with the same key exists and if this one should run
+ // later, ignore it
+ // - if no operation exists then add the new one
+ final String operationKey = operation.key;
+ final SyncOperation existingOperation = mOperationsMap.get(operationKey);
+
+ if (existingOperation != null) {
+ boolean changed = false;
+ if (existingOperation.expedited == operation.expedited) {
+ final long newRunTime =
+ Math.min(existingOperation.earliestRunTime, operation.earliestRunTime);
+ if (existingOperation.earliestRunTime != newRunTime) {
+ existingOperation.earliestRunTime = newRunTime;
+ changed = true;
+ }
+ } else {
+ if (operation.expedited) {
+ existingOperation.expedited = true;
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ operation.pendingOperation = pop;
+ if (operation.pendingOperation == null) {
+ pop = new SyncStorageEngine.PendingOperation(
+ operation.account, operation.syncSource,
+ operation.authority, operation.extras, operation.expedited);
+ pop = mSyncStorageEngine.insertIntoPending(pop);
+ if (pop == null) {
+ throw new IllegalStateException("error adding pending sync operation "
+ + operation);
+ }
+ operation.pendingOperation = pop;
+ }
+
+ mOperationsMap.put(operationKey, operation);
+ return true;
+ }
+
+ public void remove(SyncOperation operation) {
+ SyncOperation operationToRemove = mOperationsMap.remove(operation.key);
+ if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) {
+ final String errorMessage = "unable to find pending row for " + operationToRemove;
+ Log.e(TAG, errorMessage, new IllegalStateException(errorMessage));
+ }
+ }
+
+ /**
+ * Find the operation that should run next. Operations are sorted by their earliestRunTime,
+ * prioritizing expedited operations. The earliestRunTime is adjusted by the sync adapter's
+ * backoff and delayUntil times, if any.
+ * @param now the current {@link android.os.SystemClock#elapsedRealtime()}
+ * @return the operation that should run next and when it should run. The time may be in
+ * the future. It is expressed in milliseconds since boot.
+ */
+ private Pair<SyncOperation, Long> nextOperation(long now) {
+ SyncOperation lowestOp = null;
+ long lowestOpRunTime = 0;
+ for (SyncOperation op : mOperationsMap.values()) {
+ // effectiveRunTime:
+ // - backoffTime > currentTime : backoffTime
+ // - backoffTime <= currentTime : op.runTime
+ Pair<Long, Long> backoff = null;
+ long delayUntilTime = 0;
+ final boolean isManualSync =
+ op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
+ if (!isManualSync) {
+ backoff = mSyncStorageEngine.getBackoff(op.account, op.authority);
+ delayUntilTime = mSyncStorageEngine.getDelayUntilTime(op.account, op.authority);
+ }
+ long backoffTime = Math.max(backoff != null ? backoff.first : 0, delayUntilTime);
+ long opRunTime = backoffTime > now ? backoffTime : op.earliestRunTime;
+ if (lowestOp == null
+ || (lowestOp.expedited == op.expedited
+ ? opRunTime < lowestOpRunTime
+ : op.expedited)) {
+ lowestOp = op;
+ lowestOpRunTime = opRunTime;
+ }
+ }
+ if (lowestOp == null) {
+ return null;
+ }
+ return Pair.create(lowestOp, lowestOpRunTime);
+ }
+
+ /**
+ * Return when the next SyncOperation will be ready to run or null if there are
+ * none.
+ * @param now the current {@link android.os.SystemClock#elapsedRealtime()}, used to
+ * decide if the sync operation is ready to run
+ * @return when the next SyncOperation will be ready to run, expressed in elapsedRealtime()
+ */
+ public Long nextRunTime(long now) {
+ Pair<SyncOperation, Long> nextOpAndRunTime = nextOperation(now);
+ if (nextOpAndRunTime == null) {
+ return null;
+ }
+ return nextOpAndRunTime.second;
+ }
+
+ /**
+ * Find and return the SyncOperation that should be run next and is ready to run.
+ * @param now the current {@link android.os.SystemClock#elapsedRealtime()}, used to
+ * decide if the sync operation is ready to run
+ * @return the SyncOperation that should be run next and is ready to run.
+ */
+ public SyncOperation nextReadyToRun(long now) {
+ Pair<SyncOperation, Long> nextOpAndRunTime = nextOperation(now);
+ if (nextOpAndRunTime == null || nextOpAndRunTime.second > now) {
+ return null;
+ }
+ return nextOpAndRunTime.first;
+ }
+
+ public void clear(Account account, String authority) {
+ Iterator<Map.Entry<String, SyncOperation>> entries = mOperationsMap.entrySet().iterator();
+ while (entries.hasNext()) {
+ Map.Entry<String, SyncOperation> entry = entries.next();
+ SyncOperation syncOperation = entry.getValue();
+ if (account != null && !syncOperation.account.equals(account)) continue;
+ if (authority != null && !syncOperation.authority.equals(authority)) continue;
+ entries.remove();
+ if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) {
+ final String errorMessage = "unable to find pending row for " + syncOperation;
+ Log.e(TAG, errorMessage, new IllegalStateException(errorMessage));
+ }
+ }
+ }
+
+ public void dump(StringBuilder sb) {
+ sb.append("SyncQueue: ").append(mOperationsMap.size()).append(" operation(s)\n");
+ for (SyncOperation operation : mOperationsMap.values()) {
+ sb.append(operation).append("\n");
+ }
+ }
+}
diff --git a/core/java/android/content/SyncResult.java b/core/java/android/content/SyncResult.java
index 57161b6..3fbe847 100644
--- a/core/java/android/content/SyncResult.java
+++ b/core/java/android/content/SyncResult.java
@@ -14,6 +14,9 @@
public boolean fullSyncRequested;
public boolean partialSyncUnavailable;
public boolean moreRecordsToGet;
+
+ // in seconds since epoch
+ public long delayUntil;
public final SyncStats stats;
public static final SyncResult ALREADY_IN_PROGRESS;
@@ -32,6 +35,7 @@
this.fullSyncRequested = false;
this.partialSyncUnavailable = false;
this.moreRecordsToGet = false;
+ this.delayUntil = 0;
this.stats = new SyncStats();
}
@@ -43,6 +47,7 @@
fullSyncRequested = parcel.readInt() != 0;
partialSyncUnavailable = parcel.readInt() != 0;
moreRecordsToGet = parcel.readInt() != 0;
+ delayUntil = parcel.readLong();
stats = new SyncStats(parcel);
}
@@ -80,6 +85,7 @@
fullSyncRequested = false;
partialSyncUnavailable = false;
moreRecordsToGet = false;
+ delayUntil = 0;
stats.clear();
}
@@ -105,6 +111,7 @@
parcel.writeInt(fullSyncRequested ? 1 : 0);
parcel.writeInt(partialSyncUnavailable ? 1 : 0);
parcel.writeInt(moreRecordsToGet ? 1 : 0);
+ parcel.writeLong(delayUntil);
stats.writeToParcel(parcel, flags);
}
@@ -123,6 +130,7 @@
sb.append(" partialSyncUnavailable: ").append(partialSyncUnavailable);
}
if (moreRecordsToGet) sb.append(" moreRecordsToGet: ").append(moreRecordsToGet);
+ if (delayUntil > 0) sb.append(" delayUntil: ").append(delayUntil);
sb.append(stats);
return sb.toString();
}
diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java
index 4c53201..db70096 100644
--- a/core/java/android/content/SyncStorageEngine.java
+++ b/core/java/android/content/SyncStorageEngine.java
@@ -36,10 +36,11 @@
import android.os.Parcel;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
-import android.os.SystemProperties;
+import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;
import android.util.Xml;
+import android.util.Pair;
import java.io.File;
import java.io.FileInputStream;
@@ -88,6 +89,8 @@
/** Enum value for a user-initiated sync. */
public static final int SOURCE_USER = 3;
+ public static final long NOT_IN_BACKOFF_MODE = -1;
+
private static final Intent SYNC_CONNECTION_SETTING_CHANGED_INTENT =
new Intent("com.android.sync.SYNC_CONN_STATUS_CHANGED");
@@ -117,16 +120,18 @@
final int syncSource;
final String authority;
final Bundle extras; // note: read-only.
+ final boolean expedited;
int authorityId;
byte[] flatExtras;
PendingOperation(Account account, int source,
- String authority, Bundle extras) {
+ String authority, Bundle extras, boolean expedited) {
this.account = account;
this.syncSource = source;
this.authority = authority;
this.extras = extras != null ? new Bundle(extras) : extras;
+ this.expedited = expedited;
this.authorityId = -1;
}
@@ -136,6 +141,7 @@
this.authority = other.authority;
this.extras = other.extras;
this.authorityId = other.authorityId;
+ this.expedited = other.expedited;
}
}
@@ -155,6 +161,9 @@
final int ident;
boolean enabled;
int syncable;
+ long backoffTime;
+ long backoffDelay;
+ long delayUntil;
AuthorityInfo(Account account, String authority, int ident) {
this.account = account;
@@ -162,6 +171,8 @@
this.ident = ident;
enabled = SYNC_ENABLED_DEFAULT;
syncable = -1; // default to "unknown"
+ backoffTime = -1; // if < 0 then we aren't in backoff mode
+ backoffDelay = -1; // if < 0 then we aren't in backoff mode
}
}
@@ -280,7 +291,7 @@
public static void init(Context context) {
if (sSyncStorageEngine != null) {
- throw new IllegalStateException("already initialized");
+ return;
}
sSyncStorageEngine = new SyncStorageEngine(context);
}
@@ -380,7 +391,7 @@
}
if (!wasEnabled && sync) {
- mContext.getContentResolver().requestSync(account, providerName, new Bundle());
+ ContentResolver.requestSync(account, providerName, new Bundle());
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
}
@@ -424,11 +435,91 @@
}
if (oldState <= 0 && syncable > 0) {
- mContext.getContentResolver().requestSync(account, providerName, new Bundle());
+ ContentResolver.requestSync(account, providerName, new Bundle());
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
}
+ public Pair<Long, Long> getBackoff(Account account, String providerName) {
+ synchronized (mAuthorities) {
+ AuthorityInfo authority = getAuthorityLocked(account, providerName, "getBackoff");
+ if (authority == null || authority.backoffTime < 0) {
+ return null;
+ }
+ return Pair.create(authority.backoffTime, authority.backoffDelay);
+ }
+ }
+
+ public void setBackoff(Account account, String providerName,
+ long nextSyncTime, long nextDelay) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "setBackoff: " + account + ", provider " + providerName
+ + " -> nextSyncTime " + nextSyncTime + ", nextDelay " + nextDelay);
+ }
+ boolean changed = false;
+ synchronized (mAuthorities) {
+ if (account == null || providerName == null) {
+ for (AccountInfo accountInfo : mAccounts.values()) {
+ if (account != null && !account.equals(accountInfo.account)) continue;
+ for (AuthorityInfo authorityInfo : accountInfo.authorities.values()) {
+ if (providerName != null && !providerName.equals(authorityInfo.authority)) {
+ continue;
+ }
+ if (authorityInfo.backoffTime != nextSyncTime
+ || authorityInfo.backoffDelay != nextDelay) {
+ authorityInfo.backoffTime = nextSyncTime;
+ authorityInfo.backoffDelay = nextDelay;
+ changed = true;
+ }
+ }
+ }
+ } else {
+ AuthorityInfo authority =
+ getOrCreateAuthorityLocked(account, providerName, -1, false);
+ if (authority.backoffTime == nextSyncTime && authority.backoffDelay == nextDelay) {
+ return;
+ }
+ authority.backoffTime = nextSyncTime;
+ authority.backoffDelay = nextDelay;
+ changed = true;
+ }
+ if (changed) {
+ writeAccountInfoLocked();
+ }
+ }
+
+ if (changed) {
+ reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+ }
+ }
+
+ public void setDelayUntilTime(Account account, String providerName, long delayUntil) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "setDelayUntil: " + account + ", provider " + providerName
+ + " -> delayUntil " + delayUntil);
+ }
+ synchronized (mAuthorities) {
+ AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
+ if (authority.delayUntil == delayUntil) {
+ return;
+ }
+ authority.delayUntil = delayUntil;
+ writeAccountInfoLocked();
+ }
+
+ reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+ }
+
+ public long getDelayUntilTime(Account account, String providerName) {
+ synchronized (mAuthorities) {
+ AuthorityInfo authority = getAuthorityLocked(account, providerName, "getDelayUntil");
+ if (authority == null) {
+ return 0;
+ }
+ return authority.delayUntil;
+ }
+ }
+
public void setMasterSyncAutomatically(boolean flag) {
boolean old;
synchronized (mAuthorities) {
@@ -437,7 +528,7 @@
writeAccountInfoLocked();
}
if (!old && flag) {
- mContext.getContentResolver().requestSync(null, null, new Bundle());
+ ContentResolver.requestSync(null, null, new Bundle());
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
mContext.sendBroadcast(SYNC_CONNECTION_SETTING_CHANGED_INTENT);
@@ -512,9 +603,6 @@
SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
status.pending = true;
- status.initialize = op.extras != null &&
- op.extras.containsKey(ContentResolver.SYNC_EXTRAS_INITIALIZE) &&
- op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE);
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING);
@@ -1414,7 +1502,7 @@
}
}
- public static final int PENDING_OPERATION_VERSION = 1;
+ public static final int PENDING_OPERATION_VERSION = 2;
/**
* Read all pending operations back in to the initial engine state.
@@ -1429,7 +1517,7 @@
final int SIZE = in.dataSize();
while (in.dataPosition() < SIZE) {
int version = in.readInt();
- if (version != PENDING_OPERATION_VERSION) {
+ if (version != PENDING_OPERATION_VERSION && version != 1) {
Log.w(TAG, "Unknown pending operation version "
+ version + "; dropping all ops");
break;
@@ -1437,6 +1525,12 @@
int authorityId = in.readInt();
int syncSource = in.readInt();
byte[] flatExtras = in.createByteArray();
+ boolean expedited;
+ if (version == PENDING_OPERATION_VERSION) {
+ expedited = in.readInt() != 0;
+ } else {
+ expedited = false;
+ }
AuthorityInfo authority = mAuthorities.get(authorityId);
if (authority != null) {
Bundle extras = null;
@@ -1445,12 +1539,13 @@
}
PendingOperation op = new PendingOperation(
authority.account, syncSource,
- authority.authority, extras);
+ authority.authority, extras, expedited);
op.authorityId = authorityId;
op.flatExtras = flatExtras;
if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account
+ " auth=" + op.authority
+ " src=" + op.syncSource
+ + " expedited=" + op.expedited
+ " extras=" + op.extras);
mPendingOperations.add(op);
}
@@ -1468,6 +1563,7 @@
op.flatExtras = flattenBundle(op.extras);
}
out.writeByteArray(op.flatExtras);
+ out.writeInt(op.expedited ? 1 : 0);
}
/**
diff --git a/tests/CoreTests/android/content/SyncQueueTest.java b/tests/CoreTests/android/content/SyncQueueTest.java
new file mode 100644
index 0000000..5f4ab78
--- /dev/null
+++ b/tests/CoreTests/android/content/SyncQueueTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.test.AndroidTestCase;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContext;
+import android.test.mock.MockContentResolver;
+import android.accounts.Account;
+import android.os.Bundle;
+import android.os.SystemClock;
+
+public class SyncQueueTest extends AndroidTestCase {
+ private static final Account ACCOUNT1 = new Account("test.account1", "test.type1");
+ private static final Account ACCOUNT2 = new Account("test.account2", "test.type2");
+ private static final String AUTHORITY1 = "test.authority1";
+ private static final String AUTHORITY2 = "test.authority2";
+ private static final String AUTHORITY3 = "test.authority3";
+
+ private SyncStorageEngine mSettings;
+ private SyncQueue mSyncQueue;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ MockContentResolver mockResolver = new MockContentResolver();
+ mSettings = SyncStorageEngine.newTestInstance(new TestContext(mockResolver, getContext()));
+ mSyncQueue = new SyncQueue(mSettings);
+ }
+
+ public void testSyncQueueOrder() throws Exception {
+ final SyncOperation op1 = new SyncOperation(
+ ACCOUNT1, SyncStorageEngine.SOURCE_USER, AUTHORITY1, newTestBundle("1"), 0);
+ final SyncOperation op2 = new SyncOperation(
+ ACCOUNT2, SyncStorageEngine.SOURCE_USER, AUTHORITY2, newTestBundle("2"), 100);
+ final SyncOperation op3 = new SyncOperation(
+ ACCOUNT1, SyncStorageEngine.SOURCE_USER, AUTHORITY1, newTestBundle("3"), 150);
+ final SyncOperation op4 = new SyncOperation(
+ ACCOUNT2, SyncStorageEngine.SOURCE_USER, AUTHORITY2, newTestBundle("4"), 60);
+ final SyncOperation op5 = new SyncOperation(
+ ACCOUNT1, SyncStorageEngine.SOURCE_USER, AUTHORITY1, newTestBundle("5"), 80);
+ final SyncOperation op6 = new SyncOperation(
+ ACCOUNT2, SyncStorageEngine.SOURCE_USER, AUTHORITY2, newTestBundle("6"), 0);
+ op6.expedited = true;
+
+ mSyncQueue.add(op1);
+ mSyncQueue.add(op2);
+ mSyncQueue.add(op3);
+ mSyncQueue.add(op4);
+ mSyncQueue.add(op5);
+ mSyncQueue.add(op6);
+
+ long now = SystemClock.elapsedRealtime() + 200;
+
+ assertEquals(op6, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op6);
+
+ assertEquals(op1, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op1);
+
+ assertEquals(op4, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op4);
+
+ assertEquals(op5, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op5);
+
+ assertEquals(op2, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op2);
+
+ assertEquals(op3, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op3);
+ }
+
+ public void testOrderWithBackoff() throws Exception {
+ final SyncOperation op1 = new SyncOperation(
+ ACCOUNT1, SyncStorageEngine.SOURCE_USER, AUTHORITY1, newTestBundle("1"), 0);
+ final SyncOperation op2 = new SyncOperation(
+ ACCOUNT2, SyncStorageEngine.SOURCE_USER, AUTHORITY2, newTestBundle("2"), 100);
+ final SyncOperation op3 = new SyncOperation(
+ ACCOUNT1, SyncStorageEngine.SOURCE_USER, AUTHORITY1, newTestBundle("3"), 150);
+ final SyncOperation op4 = new SyncOperation(
+ ACCOUNT2, SyncStorageEngine.SOURCE_USER, AUTHORITY3, newTestBundle("4"), 60);
+ final SyncOperation op5 = new SyncOperation(
+ ACCOUNT1, SyncStorageEngine.SOURCE_USER, AUTHORITY1, newTestBundle("5"), 80);
+ final SyncOperation op6 = new SyncOperation(
+ ACCOUNT2, SyncStorageEngine.SOURCE_USER, AUTHORITY2, newTestBundle("6"), 0);
+ op6.expedited = true;
+
+ mSyncQueue.add(op1);
+ mSyncQueue.add(op2);
+ mSyncQueue.add(op3);
+ mSyncQueue.add(op4);
+ mSyncQueue.add(op5);
+ mSyncQueue.add(op6);
+
+ long now = SystemClock.elapsedRealtime() + 200;
+
+ assertEquals(op6, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op6);
+
+ assertEquals(op1, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op1);
+
+ mSettings.setBackoff(ACCOUNT2, AUTHORITY3, now + 200, 5);
+ assertEquals(op5, mSyncQueue.nextReadyToRun(now));
+
+ mSettings.setBackoff(ACCOUNT2, AUTHORITY3, SyncStorageEngine.NOT_IN_BACKOFF_MODE, 0);
+ assertEquals(op4, mSyncQueue.nextReadyToRun(now));
+
+ mSettings.setDelayUntilTime(ACCOUNT2, AUTHORITY3, now + 200);
+ assertEquals(op5, mSyncQueue.nextReadyToRun(now));
+
+ mSettings.setDelayUntilTime(ACCOUNT2, AUTHORITY3, 0);
+ assertEquals(op4, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op4);
+
+ assertEquals(op5, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op5);
+
+ assertEquals(op2, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op2);
+
+ assertEquals(op3, mSyncQueue.nextReadyToRun(now));
+ mSyncQueue.remove(op3);
+ }
+
+ Bundle newTestBundle(String val) {
+ Bundle bundle = new Bundle();
+ bundle.putString("test", val);
+ return bundle;
+ }
+
+ static class TestContext extends ContextWrapper {
+ ContentResolver mResolver;
+
+ public TestContext(ContentResolver resolver, Context realContext) {
+ super(new RenamingDelegatingContext(new MockContext(), realContext, "test."));
+ mResolver = resolver;
+ }
+
+ @Override
+ public void enforceCallingOrSelfPermission(String permission, String message) {
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mResolver;
+ }
+ }
+}