Make a separate active sync queue for initialization and regular syncs.

This entails allowing multiple syncs to happen in parallel, with
different limits for regular and initialization syncs.

Change-Id: I0e47c6515af5c98faf899f91855b342b0d0c708c
diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java
index 950d339..c9115c5 100644
--- a/core/java/android/content/SyncManager.java
+++ b/core/java/android/content/SyncManager.java
@@ -16,6 +16,18 @@
 
 package android.content;
 
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
 import com.android.internal.R;
@@ -36,17 +48,6 @@
 import android.content.pm.RegisteredServicesCacheListener;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.Process;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.os.WorkSource;
 import android.provider.Settings;
 import android.text.format.DateUtils;
@@ -58,11 +59,13 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Random;
-import java.util.Collection;
 import java.util.concurrent.CountDownLatch;
 
 /**
@@ -81,30 +84,17 @@
     private static final long MAX_TIME_PER_SYNC;
 
     static {
-        String localSyncDelayString = SystemProperties.get("sync.local_sync_delay");
-        long localSyncDelay = 30 * 1000; // 30 seconds
-        if (localSyncDelayString != null) {
-            try {
-                localSyncDelay = Long.parseLong(localSyncDelayString);
-            } catch (NumberFormatException nfe) {
-                // ignore, use default
-            }
-        }
-        LOCAL_SYNC_DELAY = localSyncDelay;
-
-        String maxTimePerSyncString = SystemProperties.get("sync.max_time_per_sync");
-        long maxTimePerSync = 5 * 60 * 1000; // 5 minutes
-        if (maxTimePerSyncString != null) {
-            try {
-                maxTimePerSync = Long.parseLong(maxTimePerSyncString);
-            } catch (NumberFormatException nfe) {
-                // ignore, use default
-            }
-        }
-        MAX_TIME_PER_SYNC = maxTimePerSync;
+        MAX_SIMULTANEOUS_INITIALIZATION_SYNCS = SystemProperties.getInt("sync.max_init_syncs", 5);
+        MAX_SIMULTANEOUS_REGULAR_SYNCS = SystemProperties.getInt("sync.max_regular_syncs", 2);
+        LOCAL_SYNC_DELAY =
+                SystemProperties.getLong("sync.local_sync_delay", 30 * 1000 /* 30 seconds */);
+        MAX_TIME_PER_SYNC =
+                SystemProperties.getLong("sync.max_time_per_sync", 5 * 60 * 1000 /* 5 minutes */);
+        SYNC_NOTIFICATION_DELAY =
+                SystemProperties.getLong("sync.notification_delay", 30 * 1000 /* 30 seconds */);
     }
 
-    private static final long SYNC_NOTIFICATION_DELAY = 30 * 1000; // 30 seconds
+    private static final long SYNC_NOTIFICATION_DELAY;
 
     /**
      * When retrying a sync for the first time use this delay. After that
@@ -123,21 +113,21 @@
      */
     private static final int DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS = 10;
 
-    /**
-     * An error notification is sent if sync of any of the providers has been failing for this long.
-     */
-    private static final long ERROR_NOTIFICATION_DELAY_MS = 1000 * 60 * 10; // 10 minutes
-
     private static final int INITIALIZATION_UNBIND_DELAY_MS = 5000;
 
     private static final String SYNC_WAKE_LOCK_PREFIX = "*sync*";
     private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarm";
+    private static final String SYNC_LOOP_WAKE_LOCK = "SyncLoopWakeLock";
+
+    private static final int MAX_SIMULTANEOUS_REGULAR_SYNCS;
+    private static final int MAX_SIMULTANEOUS_INITIALIZATION_SYNCS;
 
     private Context mContext;
 
     private volatile Account[] mAccounts = INITIAL_ACCOUNTS_ARRAY;
 
     volatile private PowerManager.WakeLock mHandleAlarmWakeLock;
+    volatile private PowerManager.WakeLock mSyncManagerWakeLock;
     volatile private boolean mDataConnectionIsConnected = false;
     volatile private boolean mStorageIsLow = false;
 
@@ -147,10 +137,8 @@
     private final SyncStorageEngine mSyncStorageEngine;
     public final SyncQueue mSyncQueue;
 
-    private ActiveSyncContext mActiveSyncContext = null;
+    private final ArrayList<ActiveSyncContext> mActiveSyncContexts = Lists.newArrayList();
 
-    // set if the sync error indicator should be reported.
-    private boolean mNeedSyncErrorNotification = false;
     // set if the sync active indicator should be reported
     private boolean mNeedSyncActiveNotification = false;
 
@@ -200,6 +188,9 @@
 
     private final PowerManager mPowerManager;
 
+    private static final long SYNC_ALARM_TIMEOUT_MIN = 30 * 1000; // 30 seconds
+    private static final long SYNC_ALARM_TIMEOUT_MAX = 2 * 60 * 60 * 1000; // two hours
+
     public void onAccountsUpdated(Account[] accounts) {
         // remember if this was the first time this was called after an update
         final boolean justBootedUp = mAccounts == INITIAL_ACCOUNTS_ARRAY;
@@ -207,11 +198,10 @@
 
         // if a sync is in progress yet it is no longer in the accounts list,
         // cancel it
-        ActiveSyncContext activeSyncContext = mActiveSyncContext;
-        if (activeSyncContext != null) {
-            if (!ArrayUtils.contains(accounts, activeSyncContext.mSyncOperation.account)) {
+        for (ActiveSyncContext currentSyncContext : mActiveSyncContexts) {
+            if (!ArrayUtils.contains(accounts, currentSyncContext.mSyncOperation.account)) {
                 Log.d(TAG, "canceling sync since the account has been removed");
-                sendSyncFinishedOrCanceledMessage(activeSyncContext,
+                sendSyncFinishedOrCanceledMessage(currentSyncContext,
                         null /* no result since this is a cancel */);
             }
         }
@@ -238,6 +228,7 @@
             // If this was the bootup case then don't sync everything, instead only
             // sync those that have an unknown syncable state, which will give them
             // a chance to set their syncable state.
+
             boolean onlyThoseWithUnkownSyncableState = justBootedUp;
             scheduleSync(null, null, null, 0 /* no delay */, onlyThoseWithUnkownSyncableState);
         }
@@ -371,6 +362,15 @@
                 HANDLE_SYNC_ALARM_WAKE_LOCK);
         mHandleAlarmWakeLock.setReferenceCounted(false);
 
+        // This WakeLock is used to ensure that we stay awake while running the sync loop
+        // message handler. Normally we will hold a sync adapter wake lock while it is being
+        // synced but during the execution of the sync loop it might finish a sync for
+        // one sync adapter before starting the sync for the other sync adapter and we
+        // don't want the device to go to sleep during that window.
+        mSyncManagerWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                SYNC_LOOP_WAKE_LOCK);
+        mSyncManagerWakeLock.setReferenceCounted(false);
+
         mSyncStorageEngine.addStatusChangeListener(
                 ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, new ISyncStatusObserver.Stub() {
             public void onStatusChanged(int which) {
@@ -427,8 +427,8 @@
         Intent intent = new Intent();
         intent.setAction("android.content.SyncAdapter");
         intent.setComponent(syncAdapterInfo.componentName);
-        if (!mContext.bindService(intent, new InitializerServiceConnection(account, authority, mContext,
-                mMainHandler),
+        if (!mContext.bindService(intent,
+                new InitializerServiceConnection(account, authority, mContext, mMainHandler),
                 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND)) {
             Log.w(TAG, "initializeSyncAdapter: failed to bind to " + intent);
         }
@@ -508,8 +508,8 @@
      * @param requestedAccount the account to sync, may be null to signify all accounts
      * @param requestedAuthority the authority to sync, may be null to indicate all authorities
      * @param extras a Map of SyncAdapter-specific information to control
-*          syncs of a specific provider. Can be null. Is ignored
-*          if the url is null.
+     *          syncs of a specific provider. Can be null. Is ignored
+     *          if the url is null.
      * @param delay how many milliseconds in the future to wait before performing this
      * @param onlyThoseWithUnkownSyncableState
      */
@@ -613,16 +613,29 @@
                         continue;
                     }
 
-                    if (isLoggable) {
-                        Log.v(TAG, "scheduleSync:"
-                                + " delay " + delay
-                                + ", source " + source
-                                + ", account " + account
-                                + ", authority " + authority
-                                + ", extras " + extras);
+                    Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff(account, authority);
+                    long delayUntil = mSyncStorageEngine.getDelayUntilTime(account, authority);
+                    final long backoffTime = backoff != null ? backoff.first : 0;
+                    if (isSyncable < 0) {
+                        Bundle newExtras = new Bundle();
+                        newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
+                        scheduleSyncOperation(
+                                new SyncOperation(account, source, authority, newExtras, 0,
+                                        backoffTime, delayUntil));
                     }
-                    scheduleSyncOperation(
-                            new SyncOperation(account, source, authority, extras, delay));
+                    if (!onlyThoseWithUnkownSyncableState) {
+                        if (isLoggable) {
+                            Log.v(TAG, "scheduleSync:"
+                                    + " delay " + delay
+                                    + ", source " + source
+                                    + ", account " + account
+                                    + ", authority " + authority
+                                    + ", extras " + extras);
+                        }
+                        scheduleSyncOperation(
+                                new SyncOperation(account, source, authority, extras, delay,
+                                        backoffTime, delayUntil));
+                    }
                 }
             }
         }
@@ -636,7 +649,8 @@
     }
 
     public SyncAdapterType[] getSyncAdapterTypes() {
-        final Collection<RegisteredServicesCache.ServiceInfo<SyncAdapterType>> serviceInfos =
+        final Collection<RegisteredServicesCache.ServiceInfo<SyncAdapterType>>
+                serviceInfos =
                 mSyncAdapters.getAllServices();
         SyncAdapterType[] types = new SyncAdapterType[serviceInfos.size()];
         int i = 0;
@@ -666,6 +680,14 @@
         mSyncHandler.sendMessage(msg);
     }
 
+    private void sendCancelSyncsMessage(final Account account, final String authority) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_CANCEL");
+        Message msg = mSyncHandler.obtainMessage();
+        msg.what = SyncHandler.MESSAGE_CANCEL;
+        msg.obj = Pair.create(account, authority);
+        mSyncHandler.sendMessage(msg);
+    }
+
     class SyncHandlerMessagePayload {
         public final ActiveSyncContext activeSyncContext;
         public final SyncResult syncResult;
@@ -683,11 +705,6 @@
         }
     }
 
-    private void clearBackoffSetting(SyncOperation op) {
-        mSyncStorageEngine.setBackoff(op.account, op.authority,
-                SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE);
-    }
-
     private void increaseBackoffSetting(SyncOperation op) {
         final long now = SystemClock.elapsedRealtime();
 
@@ -713,6 +730,9 @@
 
         mSyncStorageEngine.setBackoff(op.account, op.authority,
                 now + newDelayInMs, newDelayInMs);
+        synchronized (mSyncQueue) {
+            mSyncQueue.onBackoffChanged(op.account, op.authority, now + newDelayInMs);
+        }
     }
 
     private void setDelayUntilTime(SyncOperation op, long delayUntilSeconds) {
@@ -725,6 +745,9 @@
             newDelayUntilTime = 0;
         }
         mSyncStorageEngine.setDelayUntilTime(op.account, op.authority, newDelayUntilTime);
+        synchronized (mSyncQueue) {
+            mSyncQueue.onDelayUntilTimeChanged(op.account, op.authority, newDelayUntilTime);
+        }
     }
 
     /**
@@ -733,23 +756,7 @@
      * @param authority limit the cancelations to syncs with this authority, if non-null
      */
     public void cancelActiveSync(Account account, String authority) {
-        ActiveSyncContext activeSyncContext = mActiveSyncContext;
-        if (activeSyncContext != null) {
-            // if an authority was specified then only cancel the sync if it matches
-            if (account != null) {
-                if (!account.equals(activeSyncContext.mSyncOperation.account)) {
-                    return;
-                }
-            }
-            // if an account was specified then only cancel the sync if it matches
-            if (authority != null) {
-                if (!authority.equals(activeSyncContext.mSyncOperation.authority)) {
-                    return;
-                }
-            }
-            sendSyncFinishedOrCanceledMessage(activeSyncContext,
-                    null /* no result since this is a cancel */);
-        }
+        sendCancelSyncsMessage(account, authority);
     }
 
     /**
@@ -758,22 +765,6 @@
      * @param syncOperation the SyncOperation to schedule
      */
     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 ActiveSyncContext activeSyncContext = mActiveSyncContext;
-        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 (!activeSyncContext.mSyncOperation.expedited && !hasSameKey) {
-                scheduleSyncOperation(new SyncOperation(activeSyncContext.mSyncOperation));
-                sendSyncFinishedOrCanceledMessage(activeSyncContext,
-                        null /* no result since this is a cancel */);
-            }
-        }
-
         boolean queueChanged;
         synchronized (mSyncQueue) {
             queueChanged = mSyncQueue.add(syncOperation);
@@ -798,11 +789,11 @@
      * @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.remove(account, authority);
         }
+        mSyncStorageEngine.setBackoff(account, authority,
+                SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE);
     }
 
     void maybeRescheduleSync(SyncResult syncResult, SyncOperation operation) {
@@ -829,7 +820,8 @@
         if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false)) {
             Log.d(TAG, "not retrying sync operation because SYNC_EXTRAS_DO_NOT_RETRY was specified "
                     + operation);
-        } else if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false)) {
+        } else if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false)
+                && !syncResult.syncAlreadyInProgress) {
             operation.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);
@@ -850,7 +842,8 @@
             }
             scheduleSyncOperation(new SyncOperation(operation.account, operation.syncSource,
                     operation.authority, operation.extras,
-                    DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS * 1000));
+                    DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS * 1000,
+                    operation.backoff, operation.delayUntil));
         } else if (syncResult.hasSoftError()) {
             if (isLoggable) {
                 Log.d(TAG, "retrying sync operation because it encountered a soft error: "
@@ -873,15 +866,33 @@
         final long mStartTime;
         long mTimeoutStartTime;
         boolean mBound;
+        final PowerManager.WakeLock mSyncWakeLock;
+        final int mSyncAdapterUid;
+        SyncInfo mSyncInfo;
 
-        public ActiveSyncContext(SyncOperation syncOperation,
-                long historyRowId) {
+        /**
+         * Create an ActiveSyncContext for an impending sync and grab the wakelock for that
+         * sync adapter. Since this grabs the wakelock you need to be sure to call
+         * close() when you are done with this ActiveSyncContext, whether the sync succeeded
+         * or not.
+         * @param syncOperation the SyncOperation we are about to sync
+         * @param historyRowId the row in which to record the history info for this sync
+         * @param syncAdapterUid the UID of the application that contains the sync adapter
+         * for this sync. This is used to attribute the wakelock hold to that application.
+         */
+        public ActiveSyncContext(SyncOperation syncOperation, long historyRowId,
+                int syncAdapterUid) {
             super();
+            mSyncAdapterUid = syncAdapterUid;
             mSyncOperation = syncOperation;
             mHistoryRowId = historyRowId;
             mSyncAdapter = null;
             mStartTime = SystemClock.elapsedRealtime();
             mTimeoutStartTime = mStartTime;
+            mSyncWakeLock = mSyncHandler.getSyncWakeLock(
+                    mSyncOperation.account.type, mSyncOperation.authority);
+            mSyncWakeLock.setWorkSource(new WorkSource(syncAdapterUid));
+            mSyncWakeLock.acquire();
         }
 
         public void sendHeartbeat() {
@@ -889,6 +900,7 @@
         }
 
         public void onFinished(SyncResult result) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "onFinished: " + this);
             // include "this" in the message so that the handler can ignore it if this
             // ActiveSyncContext is no longer the mActiveSyncContext at message handling
             // time
@@ -936,6 +948,10 @@
             return bindResult;
         }
 
+        /**
+         * Performs the required cleanup, which is the releasing of the wakelock and
+         * unbinding from the sync adapter (if actually bound).
+         */
         protected void close() {
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.d(TAG, "unBindFromSyncAdapter: connection " + this);
@@ -944,6 +960,8 @@
                 mBound = false;
                 mContext.unbindService(this);
             }
+            mSyncWakeLock.setWorkSource(null);
+            mSyncWakeLock.release();
         }
 
         @Override
@@ -1003,62 +1021,28 @@
             pw.println("no alarm is scheduled (there had better not be any pending syncs)");
         }
 
-        final SyncManager.ActiveSyncContext activeSyncContext = mActiveSyncContext;
-
-        pw.print("active sync: "); pw.println(activeSyncContext);
-
         pw.print("notification info: ");
         sb.setLength(0);
         mSyncHandler.mSyncNotificationInfo.toString(sb);
         pw.println(sb.toString());
 
+        pw.println();
+        pw.println("Active Syncs: " + mActiveSyncContexts.size());
+        for (SyncManager.ActiveSyncContext activeSyncContext : mActiveSyncContexts) {
+            final long durationInSeconds = (now - activeSyncContext.mStartTime) / 1000;
+            pw.print("  ");
+            pw.print(DateUtils.formatElapsedTime(durationInSeconds));
+            pw.print(" - ");
+            pw.print(activeSyncContext.mSyncOperation.dump(false));
+            pw.println();
+        }
+
         synchronized (mSyncQueue) {
-            pw.print("sync queue: ");
             sb.setLength(0);
             mSyncQueue.dump(sb);
-            pw.println(sb.toString());
         }
-
-        SyncInfo active = mSyncStorageEngine.getCurrentSync();
-        if (active != null) {
-            SyncStorageEngine.AuthorityInfo authority
-                    = mSyncStorageEngine.getAuthority(active.authorityId);
-            final long durationInSeconds = (now - active.startTime) / 1000;
-            pw.print("Active sync: ");
-                    pw.print(authority != null ? authority.account : "<no account>");
-                    pw.print(" ");
-                    pw.print(authority != null ? authority.authority : "<no account>");
-                    if (activeSyncContext != null) {
-                        pw.print(" ");
-                        pw.print(SyncStorageEngine.SOURCES[
-                                activeSyncContext.mSyncOperation.syncSource]);
-                    }
-                    pw.print(", duration is ");
-                    pw.println(DateUtils.formatElapsedTime(durationInSeconds));
-        } else {
-            pw.println("No sync is in progress.");
-        }
-
-        ArrayList<SyncStorageEngine.PendingOperation> ops
-                = mSyncStorageEngine.getPendingOperations();
-        if (ops != null && ops.size() > 0) {
-            pw.println();
-            pw.println("Pending Syncs");
-            final int N = ops.size();
-            for (int i=0; i<N; i++) {
-                SyncStorageEngine.PendingOperation op = ops.get(i);
-                pw.print("  #"); pw.print(i); pw.print(": account=");
-                pw.print(op.account.name); pw.print(":");
-                pw.print(op.account.type); pw.print(" 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, false /* asKey */);
-                    pw.print("    extras: "); pw.println(sb.toString());
-                }
-            }
-        }
+        pw.println();
+        pw.print(sb.toString());
 
         // join the installed sync adapter with the accounts list and emit for everything
         pw.println();
@@ -1261,7 +1245,7 @@
 
         /** Call to let the tracker know that the sync state may have changed */
         public synchronized void update() {
-            final boolean isSyncInProgress = mActiveSyncContext != null;
+            final boolean isSyncInProgress = !mActiveSyncContexts.isEmpty();
             if (isSyncInProgress == mLastWasSyncing) return;
             final long now = SystemClock.elapsedRealtime();
             if (isSyncInProgress) {
@@ -1301,17 +1285,14 @@
         private static final int MESSAGE_CHECK_ALARMS = 3;
         private static final int MESSAGE_SERVICE_CONNECTED = 4;
         private static final int MESSAGE_SERVICE_DISCONNECTED = 5;
+        private static final int MESSAGE_CANCEL = 6;
 
         public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo();
         private Long mAlarmScheduleTime = null;
         public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker();
-        private PowerManager.WakeLock mSyncWakeLock;
-        private final HashMap<Pair<String, String>,  PowerManager.WakeLock> mWakeLocks =
+        private final HashMap<Pair<String, String>, PowerManager.WakeLock> mWakeLocks =
                 Maps.newHashMap();
 
-        // used to track if we have installed the error notification so that we don't reinstall
-        // it if sync is still failing
-        private boolean mErrorNotificationInstalled = false;
         private volatile CountDownLatch mReadyToRunLatch = new CountDownLatch(1);
         public void onBootCompleted() {
             mBootCompleted = true;
@@ -1351,12 +1332,6 @@
          * Used to keep track of whether a sync notification is active and who it is for.
          */
         class SyncNotificationInfo {
-            // only valid if isActive is true
-            public Account account;
-
-            // only valid if isActive is true
-            public String authority;
-
             // true iff the notification manager has been asked to send the notification
             public boolean isActive = false;
 
@@ -1365,10 +1340,7 @@
             public Long startTime = null;
 
             public void toString(StringBuilder sb) {
-                sb.append("account ").append(account)
-                        .append(", authority ").append(authority)
-                        .append(", isActive ").append(isActive)
-                        .append(", startTime ").append(startTime);
+                sb.append("isActive ").append(isActive).append(", startTime ").append(startTime);
             }
 
             @Override
@@ -1384,60 +1356,72 @@
         }
 
         public void handleMessage(Message msg) {
-            Long earliestFuturePollTime = null;
+            long earliestFuturePollTime = Long.MAX_VALUE;
+            long nextPendingSyncTime = Long.MAX_VALUE;
             try {
                 waitUntilReadyToRun();
+                mSyncManagerWakeLock.acquire();
                 // Always do this first so that we be sure that any periodic syncs that
                 // are ready to run have been converted into pending syncs. This allows the
                 // logic that considers the next steps to take based on the set of pending syncs
                 // to also take into account the periodic syncs.
                 earliestFuturePollTime = scheduleReadyPeriodicSyncs();
                 switch (msg.what) {
+                    case SyncHandler.MESSAGE_CANCEL: {
+                        Pair<Account, String> payload = (Pair<Account, String>)msg.obj;
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CANCEL: "
+                                    + payload.first + ", " + payload.second);
+                        }
+                        cancelActiveSyncLocked(payload.first, payload.second);
+                        nextPendingSyncTime = maybeStartNextSyncLocked();
+                        break;
+                    }
+
                     case SyncHandler.MESSAGE_SYNC_FINISHED:
                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
                             Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_SYNC_FINISHED");
                         }
                         SyncHandlerMessagePayload payload = (SyncHandlerMessagePayload)msg.obj;
-                        if (mActiveSyncContext != payload.activeSyncContext) {
-                            Log.d(TAG, "handleSyncHandlerMessage: sync context doesn't match, "
-                                    + "dropping: mActiveSyncContext " + mActiveSyncContext
-                                    + " != " + payload.activeSyncContext);
-                            return;
+                        if (!isSyncStillActive(payload.activeSyncContext)) {
+                            Log.d(TAG, "handleSyncHandlerMessage: dropping since the "
+                                    + "sync is no longer active: "
+                                    + payload.activeSyncContext);
+                            break;
                         }
-                        runSyncFinishedOrCanceled(payload.syncResult);
+                        runSyncFinishedOrCanceledLocked(payload.syncResult, payload.activeSyncContext);
 
-                        // since we are no longer syncing, check if it is time to start a new sync
-                        runStateIdle();
+                        // since a sync just finished check if it is time to start a new sync
+                        nextPendingSyncTime = maybeStartNextSyncLocked();
                         break;
 
                     case SyncHandler.MESSAGE_SERVICE_CONNECTED: {
                         ServiceConnectionData msgData = (ServiceConnectionData)msg.obj;
                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
                             Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CONNECTED: "
-                                    + msgData.activeSyncContext
-                                    + " active is " + mActiveSyncContext);
+                                    + msgData.activeSyncContext);
                         }
                         // check that this isn't an old message
-                        if (mActiveSyncContext == msgData.activeSyncContext) {
-                            runBoundToSyncAdapter(msgData.syncAdapter);
+                        if (isSyncStillActive(msgData.activeSyncContext)) {
+                            runBoundToSyncAdapter(msgData.activeSyncContext, msgData.syncAdapter);
                         }
                         break;
                     }
 
                     case SyncHandler.MESSAGE_SERVICE_DISCONNECTED: {
-                        ServiceConnectionData msgData = (ServiceConnectionData)msg.obj;
+                        final ActiveSyncContext currentSyncContext =
+                                ((ServiceConnectionData)msg.obj).activeSyncContext;
                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
                             Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_DISCONNECTED: "
-                                    + msgData.activeSyncContext
-                                    + " active is " + mActiveSyncContext);
+                                    + currentSyncContext);
                         }
                         // check that this isn't an old message
-                        if (mActiveSyncContext == msgData.activeSyncContext) {
+                        if (isSyncStillActive(currentSyncContext)) {
                             // cancel the sync if we have a syncadapter, which means one is
                             // outstanding
-                            if (mActiveSyncContext.mSyncAdapter != null) {
+                            if (currentSyncContext.mSyncAdapter != null) {
                                 try {
-                                    mActiveSyncContext.mSyncAdapter.cancelSync(mActiveSyncContext);
+                                    currentSyncContext.mSyncAdapter.cancelSync(currentSyncContext);
                                 } catch (RemoteException e) {
                                     // we don't need to retry this in this case
                                 }
@@ -1447,11 +1431,10 @@
                             // which is a soft error
                             SyncResult syncResult = new SyncResult();
                             syncResult.stats.numIoExceptions++;
-                            runSyncFinishedOrCanceled(syncResult);
+                            runSyncFinishedOrCanceledLocked(syncResult, currentSyncContext);
 
-                            // since we are no longer syncing, check if it is time to start a new
-                            // sync
-                            runStateIdle();
+                            // since a sync just finished check if it is time to start a new sync
+                            nextPendingSyncTime = maybeStartNextSyncLocked();
                         }
 
                         break;
@@ -1464,22 +1447,7 @@
                         }
                         mAlarmScheduleTime = null;
                         try {
-                            if (mActiveSyncContext != null) {
-                                if (isLoggable) {
-                                    Log.v(TAG, "handleSyncHandlerMessage: sync context is active");
-                                }
-                                runStateSyncing();
-                            }
-
-                            // if the above call to runStateSyncing() resulted in the end of a sync,
-                            // check if it is time to start a new sync
-                            if (mActiveSyncContext == null) {
-                                if (isLoggable) {
-                                    Log.v(TAG, "handleSyncHandlerMessage: "
-                                            + "sync context is not active");
-                                }
-                                runStateIdle();
-                            }
+                            nextPendingSyncTime = maybeStartNextSyncLocked();
                         } finally {
                             mHandleAlarmWakeLock.release();
                         }
@@ -1490,19 +1458,14 @@
                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
                             Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_CHECK_ALARMS");
                         }
-                        // we do all the work for this case in the finally block
+                        nextPendingSyncTime = maybeStartNextSyncLocked();
                         break;
                 }
             } finally {
-                final boolean isSyncInProgress = mActiveSyncContext != null;
-                if (!isSyncInProgress && mSyncWakeLock != null) {
-                    mSyncWakeLock.release();
-                    mSyncWakeLock = null;
-                }
-                manageSyncNotification();
-                manageErrorNotification();
-                manageSyncAlarm(earliestFuturePollTime);
+                manageSyncNotificationLocked();
+                manageSyncAlarmLocked(earliestFuturePollTime, nextPendingSyncTime);
                 mSyncTimeTracker.update();
+                mSyncManagerWakeLock.release();
             }
         }
 
@@ -1511,10 +1474,10 @@
          * @return the desired start time of the earliest future  periodic sync operation,
          * in milliseconds since boot
          */
-        private Long scheduleReadyPeriodicSyncs() {
+        private long scheduleReadyPeriodicSyncs() {
             final boolean backgroundDataUsageAllowed =
                     getConnectivityManager().getBackgroundDataSetting();
-            Long earliestFuturePollTime = null;
+            long earliestFuturePollTime = Long.MAX_VALUE;
             if (!backgroundDataUsageAllowed || !mSyncStorageEngine.getMasterSyncAutomatically()) {
                 return earliestFuturePollTime;
             }
@@ -1544,23 +1507,27 @@
                     long nextPollTimeAbsolute = lastPollTimeAbsolute + periodInSeconds * 1000;
                     // if it is ready to run then schedule it and mark it as having been scheduled
                     if (nextPollTimeAbsolute <= nowAbsolute) {
+                        final Pair<Long, Long> backoff =
+                                mSyncStorageEngine.getBackoff(info.account, info.authority);
                         scheduleSyncOperation(
                                 new SyncOperation(info.account, SyncStorageEngine.SOURCE_PERIODIC,
-                                        info.authority, extras, 0 /* delay */));
+                                        info.authority, extras, 0 /* delay */,
+                                        backoff != null ? backoff.first : 0,
+                                        mSyncStorageEngine.getDelayUntilTime(
+                                                info.account, info.authority)));
                         status.setPeriodicSyncTime(i, nowAbsolute);
                     } else {
                         // it isn't ready to run, remember this time if it is earlier than
                         // earliestFuturePollTime
-                        if (earliestFuturePollTime == null
-                                || nextPollTimeAbsolute < earliestFuturePollTime) {
+                        if (nextPollTimeAbsolute < earliestFuturePollTime) {
                             earliestFuturePollTime = nextPollTimeAbsolute;
                         }
                     }
                 }
             }
 
-            if (earliestFuturePollTime == null) {
-                return null;
+            if (earliestFuturePollTime == Long.MAX_VALUE) {
+                return Long.MAX_VALUE;
             }
 
             // convert absolute time to elapsed time
@@ -1570,47 +1537,23 @@
                       : (earliestFuturePollTime - nowAbsolute));
         }
 
-        private void runStateSyncing() {
-            // if the sync timeout has been reached then cancel it
-            ActiveSyncContext activeSyncContext = mActiveSyncContext;
-
-            final long now = SystemClock.elapsedRealtime();
-            if (now > activeSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC) {
-                Pair<SyncOperation, Long> nextOpAndRunTime;
-                synchronized (mSyncQueue) {
-                    nextOpAndRunTime = mSyncQueue.nextOperation();
-                }
-                if (nextOpAndRunTime != null && nextOpAndRunTime.second <= now) {
-                    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 {
-                    activeSyncContext.mTimeoutStartTime = now + MAX_TIME_PER_SYNC;
-                }
-            }
-
-            // no need to schedule an alarm, as that will be done by our caller.
-        }
-
-        private void runStateIdle() {
-            boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
-            if (isLoggable) Log.v(TAG, "runStateIdle");
+        private long maybeStartNextSyncLocked() {
+            final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+            if (isLoggable) Log.v(TAG, "maybeStartNextSync");
 
             // If we aren't ready to run (e.g. the data connection is down), get out.
             if (!mDataConnectionIsConnected) {
                 if (isLoggable) {
-                    Log.v(TAG, "runStateIdle: no data connection, skipping");
+                    Log.v(TAG, "maybeStartNextSync: no data connection, skipping");
                 }
-                return;
+                return Long.MAX_VALUE;
             }
 
             if (mStorageIsLow) {
                 if (isLoggable) {
-                    Log.v(TAG, "runStateIdle: memory low, skipping");
+                    Log.v(TAG, "maybeStartNextSync: memory low, skipping");
                 }
-                return;
+                return Long.MAX_VALUE;
             }
 
             // If the accounts aren't known yet then we aren't ready to run. We will be kicked
@@ -1618,46 +1561,56 @@
             Account[] accounts = mAccounts;
             if (accounts == INITIAL_ACCOUNTS_ARRAY) {
                 if (isLoggable) {
-                    Log.v(TAG, "runStateIdle: accounts not known, skipping");
+                    Log.v(TAG, "maybeStartNextSync: accounts not known, skipping");
                 }
-                return;
+                return Long.MAX_VALUE;
             }
 
             // Otherwise consume SyncOperations from the head of the SyncQueue until one is
             // found that is runnable (not disabled, etc). If that one is ready to run then
             // start it, otherwise just get out.
-            SyncOperation op;
-            int syncableState;
             final boolean backgroundDataUsageAllowed =
                     getConnectivityManager().getBackgroundDataSetting();
             final boolean masterSyncAutomatically = mSyncStorageEngine.getMasterSyncAutomatically();
 
-            synchronized (mSyncQueue) {
-                final long now = SystemClock.elapsedRealtime();
-                while (true) {
-                    Pair<SyncOperation, Long> nextOpAndRunTime = mSyncQueue.nextOperation();
-                    if (nextOpAndRunTime == null || nextOpAndRunTime.second > now) {
-                        if (isLoggable) {
-                            Log.v(TAG, "runStateIdle: no more ready sync operations, returning");
-                        }
-                        return;
-                    }
-                    op = nextOpAndRunTime.first;
+            final long now = SystemClock.elapsedRealtime();
 
-                    // we are either going to run this sync or drop it so go ahead and
-                    // remove it from the queue now
-                    mSyncQueue.remove(op);
+            // will be set to the next time that a sync should be considered for running
+            long nextReadyToRunTime = Long.MAX_VALUE;
+
+            // order the sync queue, dropping syncs that are not allowed
+            ArrayList<SyncOperation> operations = new ArrayList<SyncOperation>();
+            synchronized (mSyncQueue) {
+                if (isLoggable) {
+                    Log.v(TAG, "build the operation array, syncQueue size is "
+                        + mSyncQueue.mOperationsMap.size());
+                }
+                Iterator<SyncOperation> operationIterator =
+                        mSyncQueue.mOperationsMap.values().iterator();
+                while (operationIterator.hasNext()) {
+                    final SyncOperation op = operationIterator.next();
 
                     // drop the sync if the account of this operation no longer exists
                     if (!ArrayUtils.contains(mAccounts, op.account)) {
+                        operationIterator.remove();
+                        mSyncStorageEngine.deleteFromPending(op.pendingOperation);
                         continue;
                     }
 
-
-                    // drop this sync request if it isn't syncable, intializing the sync adapter
-                    // if the syncable state is set to "unknown"
-                    syncableState = mSyncStorageEngine.getIsSyncable(op.account, op.authority);
+                    // drop this sync request if it isn't syncable
+                    int syncableState = mSyncStorageEngine.getIsSyncable(op.account, op.authority);
                     if (syncableState == 0) {
+                        operationIterator.remove();
+                        mSyncStorageEngine.deleteFromPending(op.pendingOperation);
+                        continue;
+                    }
+
+                    // if the next run time is in the future, meaning there are no syncs ready
+                    // to run, return the time
+                    if (op.effectiveRunTime > now) {
+                        if (nextReadyToRunTime > op.effectiveRunTime) {
+                            nextReadyToRunTime = op.effectiveRunTime;
+                        }
                         continue;
                     }
 
@@ -1669,30 +1622,139 @@
                                 || !backgroundDataUsageAllowed
                                 || !mSyncStorageEngine.getSyncAutomatically(
                                        op.account, op.authority))) {
+                        operationIterator.remove();
+                        mSyncStorageEngine.deleteFromPending(op.pendingOperation);
                         continue;
                     }
 
-                    // go ahead and try to sync this syncOperation
-                    break;
-                }
-
-                // We will do this sync. Run it outside of the synchronized block.
-                if (isLoggable) {
-                    Log.v(TAG, "runStateIdle: we are going to sync " + op);
+                    operations.add(op);
                 }
             }
 
-            // convert the op into an initialization sync if the syncable state is "unknown" and
-            // op isn't already an initialization sync. If it is marked syncable then convert
-            // this into a regular sync
-            final boolean initializeIsSet =
-                    op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
-            if (syncableState < 0 && !initializeIsSet) {
-                op.extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
-                op = new SyncOperation(op);
-            } else if (syncableState > 0 && initializeIsSet) {
-                op.extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
-                op = new SyncOperation(op);
+            // find the next operation to dispatch, if one is ready
+            // iterate from the top, keep issuing (while potentially cancelling existing syncs)
+            // until the quotas are filled.
+            // once the quotas are filled iterate once more to find when the next one would be
+            // (also considering pre-emption reasons).
+            if (isLoggable) Log.v(TAG, "sort the candidate operations, size " + operations.size());
+            Collections.sort(operations);
+            if (isLoggable) Log.v(TAG, "dispatch all ready sync operations");
+            for (int i = 0, N = operations.size(); i < N; i++) {
+                final SyncOperation candidate = operations.get(i);
+                final boolean candidateIsInitialization = candidate.isInitialization();
+
+                int numInit = 0;
+                int numRegular = 0;
+                ActiveSyncContext conflict = null;
+                ActiveSyncContext longRunning = null;
+                ActiveSyncContext toReschedule = null;
+
+                for (ActiveSyncContext activeSyncContext : mActiveSyncContexts) {
+                    final SyncOperation activeOp = activeSyncContext.mSyncOperation;
+                    if (activeOp.isInitialization()) {
+                        numInit++;
+                    } else {
+                        numRegular++;
+                    }
+                    if (activeOp.account.type.equals(candidate.account.type)
+                            && activeOp.authority.equals(candidate.authority)) {
+                        conflict = activeSyncContext;
+                        // don't break out since we want to do a full count of the varieties
+                    } else {
+                        if (candidateIsInitialization == activeOp.isInitialization()
+                                && activeSyncContext.mStartTime + MAX_TIME_PER_SYNC < now) {
+                            longRunning = activeSyncContext;
+                            // don't break out since we want to do a full count of the varieties
+                        }
+                    }
+                }
+
+                if (isLoggable) {
+                    Log.v(TAG, "candidate " + (i + 1) + " of " + N + ": " + candidate);
+                    Log.v(TAG, "  numActiveInit=" + numInit + ", numActiveRegular=" + numRegular);
+                    Log.v(TAG, "  longRunning: " + longRunning);
+                    Log.v(TAG, "  conflict: " + conflict);
+                }
+
+                if (conflict != null) {
+                    if (candidateIsInitialization && !conflict.mSyncOperation.isInitialization()
+                            && numInit < MAX_SIMULTANEOUS_INITIALIZATION_SYNCS) {
+                        toReschedule = conflict;
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "canceling and rescheduling sync since an initialization "
+                                    + "takes higher priority, " + conflict);
+                        }
+                    } else if (candidate.expedited && !conflict.mSyncOperation.expedited
+                            && (candidateIsInitialization
+                                == conflict.mSyncOperation.isInitialization())) {
+                        toReschedule = conflict;
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "canceling and rescheduling sync since an expedited "
+                                    + "takes higher priority, " + conflict);
+                        }
+                    } else {
+                        continue;
+                    }
+                } else {
+                    final boolean roomAvailable = candidateIsInitialization 
+                            ? numInit < MAX_SIMULTANEOUS_INITIALIZATION_SYNCS 
+                            : numRegular < MAX_SIMULTANEOUS_REGULAR_SYNCS;
+                    if (roomAvailable) {
+                        // dispatch candidate
+                    } else if (longRunning != null
+                            && (candidateIsInitialization
+                                == longRunning.mSyncOperation.isInitialization())) {
+                        toReschedule = longRunning;
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "canceling and rescheduling sync since it ran roo long, "
+                                    + longRunning);
+                        }
+                    } else {
+                        continue;
+                    }
+                }
+
+                if (toReschedule != null) {
+                    runSyncFinishedOrCanceledLocked(null, toReschedule);
+                    scheduleSyncOperation(toReschedule.mSyncOperation);
+                }
+    
+                synchronized (mSyncQueue){
+                    mSyncQueue.remove(candidate);
+                }
+                dispatchSyncOperation(candidate);
+            }
+
+            return nextReadyToRunTime;
+     }
+
+        private boolean dispatchSyncOperation(SyncOperation op) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "maybeStartNextSync: we are going to sync " + op);
+                Log.v(TAG, "num active syncs: " + mActiveSyncContexts.size());
+                for (ActiveSyncContext syncContext : mActiveSyncContexts) {
+                    Log.v(TAG, syncContext.toString());
+                }
+            }
+
+            // if this is an initialization sync and there is already a sync running with
+            // the same account type and authority cancel that sync before starting this one
+            // since otherwise the syncadapter will likely reject this request
+            if (op.isInitialization()) {
+                Iterator<ActiveSyncContext> iterator = mActiveSyncContexts.iterator();
+                while (iterator.hasNext()) {
+                    ActiveSyncContext syncContext = iterator.next();
+                    if (!syncContext.mSyncOperation.isInitialization()
+                            && syncContext.mSyncOperation.account.type.equals(op.account.type)
+                            && syncContext.mSyncOperation.authority.equals(op.authority)) {
+                        Log.d(TAG, "canceling and rescheduling " + syncContext.mSyncOperation
+                                + " since we are about to start a sync that used the "
+                                + "same sync adapter, " + op);
+                        iterator.remove();
+                        runSyncFinishedOrCanceledLocked(null, syncContext);
+                        scheduleSyncOperation(syncContext.mSyncOperation);
+                    }
+                }
             }
 
             // connect to the sync adapter
@@ -1703,79 +1765,70 @@
                 Log.d(TAG, "can't find a sync adapter for " + syncAdapterType
                         + ", removing settings for it");
                 mSyncStorageEngine.removeAuthority(op.account, op.authority);
-                runStateIdle();
-                return;
+                return false;
             }
 
             ActiveSyncContext activeSyncContext =
-                    new ActiveSyncContext(op, insertStartSyncEvent(op));
-            mActiveSyncContext = activeSyncContext;
+                    new ActiveSyncContext(op, insertStartSyncEvent(op), syncAdapterInfo.uid);
+            activeSyncContext.mSyncInfo = mSyncStorageEngine.addActiveSync(activeSyncContext);
+            mActiveSyncContexts.add(activeSyncContext);
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                Log.v(TAG, "runStateIdle: setting mActiveSyncContext to " + mActiveSyncContext);
+                Log.v(TAG, "dispatchSyncOperation: starting " + activeSyncContext);
             }
-            mSyncStorageEngine.setActiveSync(mActiveSyncContext);
             if (!activeSyncContext.bindToSyncAdapter(syncAdapterInfo)) {
                 Log.e(TAG, "Bind attempt failed to " + syncAdapterInfo);
-                mActiveSyncContext.close();
-                mActiveSyncContext = null;
-                mSyncStorageEngine.setActiveSync(mActiveSyncContext);
-                runStateIdle();
-                return;
+                closeActiveSyncContext(activeSyncContext);
+                return false;
             }
 
-            // Find the wakelock for this account and authority and store it in mSyncWakeLock.
-            // Be sure to release the previous wakelock so that we don't end up with it being
-            // held until it is used again.
-            // There are a couple tricky things about this code:
-            // - make sure that we acquire the new wakelock before releasing the old one,
-            //   otherwise the device might go to sleep as soon as we release it.
-            // - since we use non-reference counted wakelocks we have to be sure not to do
-            //   the release if the wakelock didn't change. Othewise we would do an
-            //   acquire followed by a release on the same lock, resulting in no lock
-            //   being held.
-            PowerManager.WakeLock oldWakeLock = mSyncWakeLock;
-            try {
-                mSyncWakeLock = getSyncWakeLock(op.account.type, op.authority);
-				mSyncWakeLock.setWorkSource(new WorkSource(syncAdapterInfo.uid));
-                mSyncWakeLock.acquire();
-            } finally {
-                if (oldWakeLock != null && oldWakeLock != mSyncWakeLock) {
-                    oldWakeLock.release();
-                }
-            }
-
-            // no need to schedule an alarm, as that will be done by our caller.
-
-            // the next step will occur when we get either a timeout or a
-            // MESSAGE_SERVICE_CONNECTED or MESSAGE_SERVICE_DISCONNECTED message
+            return true;
         }
 
-        private void runBoundToSyncAdapter(ISyncAdapter syncAdapter) {
-            mActiveSyncContext.mSyncAdapter = syncAdapter;
-            final SyncOperation syncOperation = mActiveSyncContext.mSyncOperation;
+        private void runBoundToSyncAdapter(ActiveSyncContext activeSyncContext,
+              ISyncAdapter syncAdapter) {
+            activeSyncContext.mSyncAdapter = syncAdapter;
+            final SyncOperation syncOperation = activeSyncContext.mSyncOperation;
             try {
-                syncAdapter.startSync(mActiveSyncContext, syncOperation.authority,
+                syncAdapter.startSync(activeSyncContext, syncOperation.authority,
                         syncOperation.account, syncOperation.extras);
             } catch (RemoteException remoteExc) {
-                Log.d(TAG, "runStateIdle: caught a RemoteException, rescheduling", remoteExc);
-                mActiveSyncContext.close();
-                mActiveSyncContext = null;
-                mSyncStorageEngine.setActiveSync(mActiveSyncContext);
+                Log.d(TAG, "maybeStartNextSync: caught a RemoteException, rescheduling", remoteExc);
+                closeActiveSyncContext(activeSyncContext);
                 increaseBackoffSetting(syncOperation);
                 scheduleSyncOperation(new SyncOperation(syncOperation));
             } catch (RuntimeException exc) {
-                mActiveSyncContext.close();
-                mActiveSyncContext = null;
-                mSyncStorageEngine.setActiveSync(mActiveSyncContext);
+                closeActiveSyncContext(activeSyncContext);
                 Log.e(TAG, "Caught RuntimeException while starting the sync " + syncOperation, exc);
             }
         }
 
-        private void runSyncFinishedOrCanceled(SyncResult syncResult) {
+        private void cancelActiveSyncLocked(Account account, String authority) {
+            ArrayList<ActiveSyncContext> activeSyncs =
+                    new ArrayList<ActiveSyncContext>(mActiveSyncContexts);
+            for (ActiveSyncContext activeSyncContext : activeSyncs) {
+                if (activeSyncContext != null) {
+                    // if an authority was specified then only cancel the sync if it matches
+                    if (account != null) {
+                        if (!account.equals(activeSyncContext.mSyncOperation.account)) {
+                            return;
+                        }
+                    }
+                    // if an account was specified then only cancel the sync if it matches
+                    if (authority != null) {
+                        if (!authority.equals(activeSyncContext.mSyncOperation.authority)) {
+                            return;
+                        }
+                    }
+                    runSyncFinishedOrCanceledLocked(null /* no result since this is a cancel */,
+                            activeSyncContext);
+                }
+            }
+        }
+
+        private void runSyncFinishedOrCanceledLocked(SyncResult syncResult,
+                ActiveSyncContext activeSyncContext) {
             boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
-            final ActiveSyncContext activeSyncContext = mActiveSyncContext;
-            mActiveSyncContext = null;
-            mSyncStorageEngine.setActiveSync(mActiveSyncContext);
+            closeActiveSyncContext(activeSyncContext);
 
             final SyncOperation syncOperation = activeSyncContext.mSyncOperation;
 
@@ -1795,16 +1848,6 @@
                     // TODO: set these correctly when the SyncResult is extended to include it
                     downstreamActivity = 0;
                     upstreamActivity = 0;
-                    clearBackoffSetting(syncOperation);
-                    // if this was an initialization sync and the sync adapter is now
-                    // marked syncable then reschedule the sync. The next time it runs it
-                    // will be made into a regular sync.
-                    if (syncOperation.extras.getBoolean(
-                                ContentResolver.SYNC_EXTRAS_INITIALIZE, false)
-                            && mSyncStorageEngine.getIsSyncable(
-                                syncOperation.account, syncOperation.authority) > 0) {
-                        scheduleSyncOperation(new SyncOperation(syncOperation));
-                    }
                 } else {
                     Log.d(TAG, "failed sync operation " + syncOperation + ", " + syncResult);
                     // the operation failed so increase the backoff time
@@ -1839,8 +1882,6 @@
             stopSyncEvent(activeSyncContext.mHistoryRowId, syncOperation, historyMessage,
                     upstreamActivity, downstreamActivity, elapsedTime);
 
-            activeSyncContext.close();
-
             if (syncResult != null && syncResult.tooManyDeletions) {
                 installHandleTooManyDeletesNotification(syncOperation.account,
                         syncOperation.authority, syncResult.stats.numDeletes);
@@ -1851,11 +1892,18 @@
 
             if (syncResult != null && syncResult.fullSyncRequested) {
                 scheduleSyncOperation(new SyncOperation(syncOperation.account,
-                        syncOperation.syncSource, syncOperation.authority, new Bundle(), 0));
+                        syncOperation.syncSource, syncOperation.authority, new Bundle(), 0,
+                        syncOperation.backoff, syncOperation.delayUntil));
             }
             // no need to schedule an alarm, as that will be done by our caller.
         }
 
+        private void closeActiveSyncContext(ActiveSyncContext activeSyncContext) {
+            activeSyncContext.close();
+            mActiveSyncContexts.remove(activeSyncContext);
+            mSyncStorageEngine.removeActiveSync(activeSyncContext.mSyncInfo);
+        }
+
         /**
          * Convert the error-containing SyncResult into the Sync.History error number. Since
          * the SyncResult may indicate multiple errors at once, this method just returns the
@@ -1885,11 +1933,11 @@
             throw new IllegalStateException("we are not in an error state, " + syncResult);
         }
 
-        private void manageSyncNotification() {
+        private void manageSyncNotificationLocked() {
             boolean shouldCancel;
             boolean shouldInstall;
 
-            if (mActiveSyncContext == null) {
+            if (mActiveSyncContexts.isEmpty()) {
                 mSyncNotificationInfo.startTime = null;
 
                 // we aren't syncing. if the notification is active then remember that we need
@@ -1898,34 +1946,38 @@
                 shouldInstall = false;
             } else {
                 // we are syncing
-                final SyncOperation syncOperation = mActiveSyncContext.mSyncOperation;
-
                 final long now = SystemClock.elapsedRealtime();
                 if (mSyncNotificationInfo.startTime == null) {
                     mSyncNotificationInfo.startTime = now;
                 }
 
-                // cancel the notification if it is up and the authority or account is wrong
-                shouldCancel = mSyncNotificationInfo.isActive &&
-                        (!syncOperation.authority.equals(mSyncNotificationInfo.authority)
-                        || !syncOperation.account.equals(mSyncNotificationInfo.account));
-
-                // there are four cases:
-                // - the notification is up and there is no change: do nothing
-                // - the notification is up but we should cancel since it is stale:
-                //   need to install
+                // there are three cases:
+                // - the notification is up: do nothing
                 // - the notification is not up but it isn't time yet: don't install
                 // - the notification is not up and it is time: need to install
 
                 if (mSyncNotificationInfo.isActive) {
-                    shouldInstall = shouldCancel;
+                    shouldInstall = shouldCancel = false;
                 } else {
+                    // it isn't currently up, so there is nothing to cancel
+                    shouldCancel = false;
+
                     final boolean timeToShowNotification =
                             now > mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY;
-                    // show the notification immediately if this is a manual sync
-                    final boolean manualSync = syncOperation.extras
-                            .getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
-                    shouldInstall = timeToShowNotification || manualSync;
+                    if (timeToShowNotification) {
+                        shouldInstall = true;
+                    } else {
+                        // show the notification immediately if this is a manual sync
+                        shouldInstall = false;
+                        for (ActiveSyncContext activeSyncContext : mActiveSyncContexts) {
+                            final boolean manualSync = activeSyncContext.mSyncOperation.extras
+                                    .getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
+                            if (manualSync) {
+                                shouldInstall = true;
+                                break;
+                            }
+                        }
+                    }
                 }
             }
 
@@ -1936,94 +1988,82 @@
             }
 
             if (shouldInstall) {
-                SyncOperation syncOperation = mActiveSyncContext.mSyncOperation;
                 mNeedSyncActiveNotification = true;
                 sendSyncStateIntent();
                 mSyncNotificationInfo.isActive = true;
-                mSyncNotificationInfo.account = syncOperation.account;
-                mSyncNotificationInfo.authority = syncOperation.authority;
             }
         }
 
-        /**
-         * Check if there were any long-lasting errors, if so install the error notification,
-         * otherwise cancel the error notification.
-         */
-        private void manageErrorNotification() {
-            //
-            long when = mSyncStorageEngine.getInitialSyncFailureTime();
-            if ((when > 0) && (when + ERROR_NOTIFICATION_DELAY_MS < System.currentTimeMillis())) {
-                if (!mErrorNotificationInstalled) {
-                    mNeedSyncErrorNotification = true;
-                    sendSyncStateIntent();
-                }
-                mErrorNotificationInstalled = true;
-            } else {
-                if (mErrorNotificationInstalled) {
-                    mNeedSyncErrorNotification = false;
-                    sendSyncStateIntent();
-                }
-                mErrorNotificationInstalled = false;
-            }
-        }
-
-        private void manageSyncAlarm(Long earliestFuturePollElapsedTime) {
+        private void manageSyncAlarmLocked(long nextPeriodicEventElapsedTime,
+                long nextPendingEventElapsedTime) {
             // in each of these cases the sync loop will be kicked, which will cause this
             // method to be called again
             if (!mDataConnectionIsConnected) return;
             if (mStorageIsLow) return;
 
-            final long now = SystemClock.elapsedRealtime();
+            // When the status bar notification should be raised
+            final long notificationTime =
+                    (!mSyncHandler.mSyncNotificationInfo.isActive
+                            && mSyncHandler.mSyncNotificationInfo.startTime != null)
+                            ? mSyncHandler.mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY
+                            : Long.MAX_VALUE;
 
-            // 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;
-            ActiveSyncContext activeSyncContext = mActiveSyncContext;
-            if (activeSyncContext == null) {
-                synchronized (mSyncQueue) {
-                    final Pair<SyncOperation, Long> candidate = mSyncQueue.nextOperation();
-                    if (earliestFuturePollElapsedTime == null && candidate == null) {
-                        alarmTime = null;
-                    } else if (earliestFuturePollElapsedTime == null) {
-                        alarmTime = candidate.second;
-                    } else if (candidate == null) {
-                        alarmTime = earliestFuturePollElapsedTime;
-                    } else {
-                        alarmTime = Math.min(earliestFuturePollElapsedTime, candidate.second);
-                    }
+            // When we should consider canceling an active sync
+            long earliestTimeoutTime = Long.MAX_VALUE;
+            for (ActiveSyncContext currentSyncContext : mActiveSyncContexts) {
+                final long currentSyncTimeoutTime =
+                        currentSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC;
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "manageSyncAlarm: active sync, mTimeoutStartTime + MAX is "
+                            + currentSyncTimeoutTime);
                 }
-            } else {
-                final long notificationTime =
-                        mSyncHandler.mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY;
-                final long timeoutTime =
-                        mActiveSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC;
-                if (mSyncHandler.mSyncNotificationInfo.isActive) {
-                    alarmTime = timeoutTime;
-                } else {
-                    alarmTime = Math.min(notificationTime, timeoutTime);
+                if (earliestTimeoutTime > currentSyncTimeoutTime) {
+                    earliestTimeoutTime = currentSyncTimeoutTime;
                 }
             }
 
-            // adjust the alarmTime so that we will wake up when it is time to
-            // install the error notification
-            if (!mErrorNotificationInstalled) {
-                long when = mSyncStorageEngine.getInitialSyncFailureTime();
-                if (when > 0) {
-                    when += ERROR_NOTIFICATION_DELAY_MS;
-                    // convert when fron absolute time to elapsed run time
-                    long delay = when - System.currentTimeMillis();
-                    when = now + delay;
-                    alarmTime = alarmTime != null ? Math.min(alarmTime, when) : when;
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "manageSyncAlarm: notificationTime is " + notificationTime);
+            }
+
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "manageSyncAlarm: earliestTimeoutTime is " + earliestTimeoutTime);
+            }
+
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "manageSyncAlarm: nextPeriodicEventElapsedTime is "
+                        + nextPeriodicEventElapsedTime);
+            }
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "manageSyncAlarm: nextPendingEventElapsedTime is "
+                        + nextPendingEventElapsedTime);
+            }
+
+            long alarmTime = Math.min(notificationTime, earliestTimeoutTime);
+            alarmTime = Math.min(alarmTime, nextPeriodicEventElapsedTime);
+            alarmTime = Math.min(alarmTime, nextPendingEventElapsedTime);
+
+            // Bound the alarm time.
+            final long now = SystemClock.elapsedRealtime();
+            if (alarmTime < now + SYNC_ALARM_TIMEOUT_MIN) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "manageSyncAlarm: the alarmTime is too small, "
+                            + alarmTime + ", setting to " + (now + SYNC_ALARM_TIMEOUT_MIN));
                 }
+                alarmTime = now + SYNC_ALARM_TIMEOUT_MIN;
+            } else if (alarmTime > now + SYNC_ALARM_TIMEOUT_MAX) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "manageSyncAlarm: the alarmTime is too large, "
+                            + alarmTime + ", setting to " + (now + SYNC_ALARM_TIMEOUT_MIN));
+                }
+                alarmTime = now + SYNC_ALARM_TIMEOUT_MAX;
             }
 
             // determine if we need to set or cancel the alarm
             boolean shouldSet = false;
             boolean shouldCancel = false;
             final boolean alarmIsActive = mAlarmScheduleTime != null;
-            final boolean needAlarm = alarmTime != null;
+            final boolean needAlarm = alarmTime != Long.MAX_VALUE;
             if (needAlarm) {
                 if (!alarmIsActive || alarmTime < mAlarmScheduleTime) {
                     shouldSet = true;
@@ -2035,6 +2075,11 @@
             // set or cancel the alarm as directed
             ensureAlarmService();
             if (shouldSet) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "requesting that the alarm manager wake us up at elapsed time "
+                            + alarmTime + ", now is " + now + ", " + ((alarmTime - now) / 1000)
+                            + " secs from now");
+                }
                 mAlarmScheduleTime = alarmTime;
                 mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTime,
                         mSyncAlarmIntent);
@@ -2048,7 +2093,7 @@
             Intent syncStateIntent = new Intent(Intent.ACTION_SYNC_STATE_CHANGED);
             syncStateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
             syncStateIntent.putExtra("active", mNeedSyncActiveNotification);
-            syncStateIntent.putExtra("failing", mNeedSyncErrorNotification);
+            syncStateIntent.putExtra("failing", false);
             mContext.sendBroadcast(syncStateIntent);
         }
 
@@ -2137,4 +2182,13 @@
                     resultMessage, downstreamActivity, upstreamActivity);
         }
     }
+
+    private boolean isSyncStillActive(ActiveSyncContext activeSyncContext) {
+        for (ActiveSyncContext sync : mActiveSyncContexts) {
+            if (sync == activeSyncContext) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/core/java/android/content/SyncOperation.java b/core/java/android/content/SyncOperation.java
index b0160885..3d7f3fbf 100644
--- a/core/java/android/content/SyncOperation.java
+++ b/core/java/android/content/SyncOperation.java
@@ -33,9 +33,12 @@
     public long earliestRunTime;
     public boolean expedited;
     public SyncStorageEngine.PendingOperation pendingOperation;
+    public Long backoff;
+    public long delayUntil;
+    public long effectiveRunTime;
 
     public SyncOperation(Account account, int source, String authority, Bundle extras,
-            long delayInMs) {
+            long delayInMs, long backoff, long delayUntil) {
         this.account = account;
         this.syncSource = source;
         this.authority = authority;
@@ -48,6 +51,8 @@
         removeFalseExtra(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS);
         removeFalseExtra(ContentResolver.SYNC_EXTRAS_EXPEDITED);
         removeFalseExtra(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS);
+        this.delayUntil = delayUntil;
+        this.backoff = backoff;
         final long now = SystemClock.elapsedRealtime();
         if (delayInMs < 0) {
             this.expedited = true;
@@ -56,6 +61,7 @@
             this.expedited = false;
             this.earliestRunTime = now + delayInMs;
         }
+        updateEffectiveRunTime();
         this.key = toKey();
     }
 
@@ -72,49 +78,78 @@
         this.extras = new Bundle(other.extras);
         this.expedited = other.expedited;
         this.earliestRunTime = SystemClock.elapsedRealtime();
+        this.backoff = other.backoff;
+        this.delayUntil = other.delayUntil;
+        this.updateEffectiveRunTime();
         this.key = toKey();
     }
 
     public String toString() {
+        return dump(true);
+    }
+
+    public String dump(boolean useOneLine) {
         StringBuilder sb = new StringBuilder();
-        sb.append("authority: ").append(authority);
-        sb.append(" account: ").append(account);
-        sb.append(" extras: ");
-        extrasToStringBuilder(extras, sb, false /* asKey */);
-        sb.append(" syncSource: ").append(syncSource);
-        sb.append(" when: ").append(earliestRunTime);
-        sb.append(" expedited: ").append(expedited);
+        sb.append(account.name);
+        sb.append(" (" + account.type + ")");
+        sb.append(", " + authority);
+        sb.append(", ");
+        sb.append(SyncStorageEngine.SOURCES[syncSource]);
+        sb.append(", earliestRunTime " + earliestRunTime);
+        if (expedited) {
+            sb.append(", EXPEDITED");
+        }
+        if (!useOneLine && !extras.keySet().isEmpty()) {
+            sb.append("\n    ");
+            extrasToStringBuilder(extras, sb);
+        }
         return sb.toString();
     }
 
+    public boolean isInitialization() {
+        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
+    }
+
+    public boolean ignoreBackoff() {
+        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
+    }
+
     private String toKey() {
         StringBuilder sb = new StringBuilder();
         sb.append("authority: ").append(authority);
-	sb.append(" account {name=" + account.name + ", type=" + account.type + "}");
+    	sb.append(" account {name=" + account.name + ", type=" + account.type + "}");
         sb.append(" extras: ");
-        extrasToStringBuilder(extras, sb, true /* asKey */);
+        extrasToStringBuilder(extras, sb);
         return sb.toString();
     }
 
-    public static void extrasToStringBuilder(Bundle bundle, StringBuilder sb, boolean asKey) {
+    public static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
         sb.append("[");
         for (String key : bundle.keySet()) {
-            // if we are writing this as a key don't consider whether this
-            // is an initialization sync or not when computing the key since
-            // we set this flag appropriately when dispatching the sync request.
-            if (asKey && ContentResolver.SYNC_EXTRAS_INITIALIZE.equals(key)) {
-                continue;
-            }
             sb.append(key).append("=").append(bundle.get(key)).append(" ");
         }
         sb.append("]");
     }
 
+    public void updateEffectiveRunTime() {
+        effectiveRunTime = ignoreBackoff()
+                ? earliestRunTime
+                : Math.max(
+                    Math.max(earliestRunTime, delayUntil),
+                    backoff);
+    }
+
     public int compareTo(Object o) {
         SyncOperation other = (SyncOperation)o;
-        if (earliestRunTime == other.earliestRunTime) {
+
+        if (expedited != other.expedited) {
+            return expedited ? -1 : 1;
+        }
+
+        if (effectiveRunTime == other.effectiveRunTime) {
             return 0;
         }
-        return (earliestRunTime < other.earliestRunTime) ? -1 : 1;
+
+        return effectiveRunTime < other.effectiveRunTime ? -1 : 1;
     }
 }
diff --git a/core/java/android/content/SyncQueue.java b/core/java/android/content/SyncQueue.java
index 28baa0d..f826147 100644
--- a/core/java/android/content/SyncQueue.java
+++ b/core/java/android/content/SyncQueue.java
@@ -16,17 +16,18 @@
 
 package android.content;
 
-import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
+import android.os.SystemClock;
+import android.text.format.DateUtils;
 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.HashMap;
 import java.util.Iterator;
+import java.util.Map;
 
 /**
  *
@@ -38,7 +39,7 @@
 
     // 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 final HashMap<String, SyncOperation> mOperationsMap = Maps.newHashMap();
 
     public SyncQueue(SyncStorageEngine syncStorageEngine) {
         mSyncStorageEngine = syncStorageEngine;
@@ -47,8 +48,11 @@
         final int N = ops.size();
         for (int i=0; i<N; i++) {
             SyncStorageEngine.PendingOperation op = ops.get(i);
+            final Pair<Long, Long> backoff = syncStorageEngine.getBackoff(op.account, op.authority);
             SyncOperation syncOperation = new SyncOperation(
-                    op.account, op.syncSource, op.authority, op.extras, 0 /* delay */);
+                    op.account, op.syncSource, op.authority, op.extras, 0 /* delay */,
+                    backoff != null ? backoff.first : 0,
+                    syncStorageEngine.getDelayUntilTime(op.account, op.authority));
             syncOperation.expedited = op.expedited;
             syncOperation.pendingOperation = op;
             add(syncOperation, op);
@@ -119,65 +123,26 @@
         }
     }
 
-    /**
-     * Find the operation that should run next. Operations are sorted by their earliestRunTime,
-     * prioritizing first those with a syncable state of "unknown" that aren't retries then
-     * expedited operations.
-     * The earliestRunTime is adjusted by the sync adapter's backoff and delayUntil times, if any.
-     * @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.
-     */
-    public Pair<SyncOperation, Long> nextOperation() {
-        SyncOperation best = null;
-        long bestRunTime = 0;
-        boolean bestSyncableIsUnknownAndNotARetry = false;
+    public void onBackoffChanged(Account account, String providerName, long backoff) {
+        // for each op that matches the account and provider update its
+        // backoff and effectiveStartTime
         for (SyncOperation op : mOperationsMap.values()) {
-            long opRunTime = op.earliestRunTime;
-            if (!op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) {
-                Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff(op.account, op.authority);
-                long delayUntil = mSyncStorageEngine.getDelayUntilTime(op.account, op.authority);
-                opRunTime = Math.max(
-                        Math.max(opRunTime, delayUntil),
-                        backoff != null ? backoff.first : 0);
-            }
-            // we know a sync is a retry if the intialization flag is set, since that will only
-            // be set by the sync dispatching code, thus if it is set it must have already been
-            // dispatched
-            final boolean syncableIsUnknownAndNotARetry =
-                    !op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)
-                    && mSyncStorageEngine.getIsSyncable(op.account, op.authority) < 0;
-            // if the unsyncable state differs, make the current the best if it is unsyncable
-            // else, if the expedited state differs, make the current the best if it is expedited
-            // else, make the current the best if it is earlier than the best
-            if (best == null
-                    || ((bestSyncableIsUnknownAndNotARetry == syncableIsUnknownAndNotARetry)
-                        ? (best.expedited == op.expedited
-                           ? opRunTime < bestRunTime
-                           : op.expedited)
-                        : syncableIsUnknownAndNotARetry)) {
-                best = op;
-                bestSyncableIsUnknownAndNotARetry = syncableIsUnknownAndNotARetry;
-                bestRunTime = opRunTime;
+            if (op.account.equals(account) && op.authority.equals(providerName)) {
+                op.backoff = backoff;
+                op.updateEffectiveRunTime();
             }
         }
-        if (best == null) {
-            return null;
-        }
-        return Pair.create(best, bestRunTime);
     }
 
-    /**
-     * 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 Pair<SyncOperation, Long> nextReadyToRun(long now) {
-        Pair<SyncOperation, Long> nextOpAndRunTime = nextOperation();
-        if (nextOpAndRunTime == null || nextOpAndRunTime.second > now) {
-            return null;
+    public void onDelayUntilTimeChanged(Account account, String providerName, long delayUntil) {
+        // for each op that matches the account and provider update its
+        // delayUntilTime and effectiveStartTime
+        for (SyncOperation op : mOperationsMap.values()) {
+            if (op.account.equals(account) && op.authority.equals(providerName)) {
+                op.delayUntil = delayUntil;
+                op.updateEffectiveRunTime();
+            }
         }
-        return nextOpAndRunTime;
     }
 
     public void remove(Account account, String authority) {
@@ -200,9 +165,17 @@
     }
 
     public void dump(StringBuilder sb) {
+        final long now = SystemClock.elapsedRealtime();
         sb.append("SyncQueue: ").append(mOperationsMap.size()).append(" operation(s)\n");
         for (SyncOperation operation : mOperationsMap.values()) {
-            sb.append(operation).append("\n");
+            sb.append("  ");
+            if (operation.effectiveRunTime <= now) {
+                sb.append("READY");
+            } else {
+                sb.append(DateUtils.formatElapsedTime((operation.effectiveRunTime - now) / 1000));
+            }
+            sb.append(" - ");
+            sb.append(operation.dump(false)).append("\n");
         }
     }
 }
diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java
index 6413cec..487f6ce 100644
--- a/core/java/android/content/SyncStorageEngine.java
+++ b/core/java/android/content/SyncStorageEngine.java
@@ -229,7 +229,7 @@
     private final ArrayList<PendingOperation> mPendingOperations =
             new ArrayList<PendingOperation>();
 
-    private SyncInfo mCurrentSync;
+    private final ArrayList<SyncInfo> mCurrentSyncs = new ArrayList<SyncInfo>();
 
     private final SparseArray<SyncStatusInfo> mSyncStatus =
             new SparseArray<SyncStatusInfo>();
@@ -690,23 +690,12 @@
 
     /**
      * Returns true if there is currently a sync operation for the given
-     * account or authority in the pending list, or actively being processed.
+     * account or authority actively being processed.
      */
     public boolean isSyncActive(Account account, String authority) {
         synchronized (mAuthorities) {
-            int i = mPendingOperations.size();
-            while (i > 0) {
-                i--;
-                // TODO(fredq): this probably shouldn't be considering
-                // pending operations.
-                PendingOperation op = mPendingOperations.get(i);
-                if (op.account.equals(account) && op.authority.equals(authority)) {
-                    return true;
-                }
-            }
-
-            if (mCurrentSync != null) {
-                AuthorityInfo ainfo = getAuthority(mCurrentSync.authorityId);
+            for (SyncInfo syncInfo : mCurrentSyncs) {
+                AuthorityInfo ainfo = getAuthority(syncInfo.authorityId);
                 if (ainfo != null && ainfo.account.equals(account)
                         && ainfo.authority.equals(authority)) {
                     return true;
@@ -887,40 +876,47 @@
     }
 
     /**
-     * Called when the currently active sync is changing (there can only be
-     * one at a time).  Either supply a valid ActiveSyncContext with information
-     * about the sync, or null to stop the currently active sync.
+     * Called when a sync is starting. Supply a valid ActiveSyncContext with information
+     * about the sync.
      */
-    public void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
+    public SyncInfo addActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
+        final SyncInfo syncInfo;
         synchronized (mAuthorities) {
-            if (activeSyncContext != null) {
-                if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                    Log.v(TAG, "setActiveSync: account="
-                        + activeSyncContext.mSyncOperation.account
-                        + " auth=" + activeSyncContext.mSyncOperation.authority
-                        + " src=" + activeSyncContext.mSyncOperation.syncSource
-                        + " extras=" + activeSyncContext.mSyncOperation.extras);
-                }
-                if (mCurrentSync != null) {
-                    Log.w(TAG, "setActiveSync called with existing active sync!");
-                }
-                AuthorityInfo authority = getAuthorityLocked(
-                        activeSyncContext.mSyncOperation.account,
-                        activeSyncContext.mSyncOperation.authority,
-                        "setActiveSync");
-                if (authority == null) {
-                    return;
-                }
-                mCurrentSync = new SyncInfo(authority.ident,
-                        authority.account, authority.authority,
-                        activeSyncContext.mStartTime);
-            } else {
-                if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "setActiveSync: null");
-                mCurrentSync = null;
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "setActiveSync: account="
+                    + activeSyncContext.mSyncOperation.account
+                    + " auth=" + activeSyncContext.mSyncOperation.authority
+                    + " src=" + activeSyncContext.mSyncOperation.syncSource
+                    + " extras=" + activeSyncContext.mSyncOperation.extras);
             }
+            AuthorityInfo authority = getOrCreateAuthorityLocked(
+                    activeSyncContext.mSyncOperation.account,
+                    activeSyncContext.mSyncOperation.authority,
+                    -1 /* assign a new identifier if creating a new authority */,
+                    true /* write to storage if this results in a change */);
+            syncInfo = new SyncInfo(authority.ident,
+                    authority.account, authority.authority,
+                    activeSyncContext.mStartTime);
+            mCurrentSyncs.add(syncInfo);
         }
 
-        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE);
+        reportActiveChange();
+        return syncInfo;
+    }
+
+    /**
+     * Called to indicate that a previously active sync is no longer active.
+     */
+    public void removeActiveSync(SyncInfo syncInfo) {
+        synchronized (mAuthorities) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "removeActiveSync: account="
+                        + syncInfo.account + " auth=" + syncInfo.authority);
+            }
+            mCurrentSyncs.remove(syncInfo);
+        }
+
+        reportActiveChange();
     }
 
     /**
@@ -1095,10 +1091,26 @@
      * Return the currently active sync information, or null if there is no
      * active sync.  Note that the returned object is the real, live active
      * sync object, so be careful what you do with it.
+     * <p>
+     * Since multiple concurrent syncs are now supported you should use
+     * {@link #getCurrentSyncs()} to get the accurate list of current syncs.
+     * This method returns the first item from the list of current syncs
+     * or null if there are none.
+     * @deprecated use {@link #getCurrentSyncs()}
      */
     public SyncInfo getCurrentSync() {
         synchronized (mAuthorities) {
-            return mCurrentSync;
+            return !mCurrentSyncs.isEmpty() ? mCurrentSyncs.get(0) : null;
+        }
+    }
+
+    /**
+     * Return a list of the currently active syncs. Note that the returned items are the
+     * real, live active sync objects, so be careful what you do with it.
+     */
+    public List<SyncInfo> getCurrentSyncs() {
+        synchronized (mAuthorities) {
+            return new ArrayList<SyncInfo>(mCurrentSyncs);
         }
     }
 
diff --git a/core/tests/coretests/src/android/content/SyncQueueTest.java b/core/tests/coretests/src/android/content/SyncQueueTest.java
deleted file mode 100644
index 1da59d1..0000000
--- a/core/tests/coretests/src/android/content/SyncQueueTest.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * 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).first);
-        mSyncQueue.remove(op6);
-
-        assertEquals(op1, mSyncQueue.nextReadyToRun(now).first);
-        mSyncQueue.remove(op1);
-
-        assertEquals(op4, mSyncQueue.nextReadyToRun(now).first);
-        mSyncQueue.remove(op4);
-
-        assertEquals(op5, mSyncQueue.nextReadyToRun(now).first);
-        mSyncQueue.remove(op5);
-
-        assertEquals(op2, mSyncQueue.nextReadyToRun(now).first);
-        mSyncQueue.remove(op2);
-
-        assertEquals(op3, mSyncQueue.nextReadyToRun(now).first);
-        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).first);
-        mSyncQueue.remove(op6);
-
-        assertEquals(op1, mSyncQueue.nextReadyToRun(now).first);
-        mSyncQueue.remove(op1);
-
-        mSettings.setBackoff(ACCOUNT2,  AUTHORITY3, now + 200, 5);
-        assertEquals(op5, mSyncQueue.nextReadyToRun(now).first);
-
-        mSettings.setBackoff(ACCOUNT2,  AUTHORITY3, SyncStorageEngine.NOT_IN_BACKOFF_MODE, 0);
-        assertEquals(op4, mSyncQueue.nextReadyToRun(now).first);
-
-        mSettings.setDelayUntilTime(ACCOUNT2,  AUTHORITY3, now + 200);
-        assertEquals(op5, mSyncQueue.nextReadyToRun(now).first);
-
-        mSettings.setDelayUntilTime(ACCOUNT2,  AUTHORITY3, 0);
-        assertEquals(op4, mSyncQueue.nextReadyToRun(now).first);
-        mSyncQueue.remove(op4);
-
-        assertEquals(op5, mSyncQueue.nextReadyToRun(now).first);
-        mSyncQueue.remove(op5);
-
-        assertEquals(op2, mSyncQueue.nextReadyToRun(now).first);
-        mSyncQueue.remove(op2);
-
-        assertEquals(op3, mSyncQueue.nextReadyToRun(now).first);
-        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;
-        }
-    }
-}