Synchronize/align periodic sync alarms based on a random per device seed value.

Removes duplicates SyncHandler.MESSAGE_CHECK_ALARM messages from the queue.

Change-Id: Ib29a06da904cebdd45ee05c9d9a6bf00b72100eb
diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java
index 06dfe90..cb3d9cf 100644
--- a/core/java/android/content/SyncManager.java
+++ b/core/java/android/content/SyncManager.java
@@ -204,6 +204,9 @@
 
     private final PowerManager mPowerManager;
 
+    // Use this as a random offset to seed all periodic syncs
+    private int mSyncRandomOffsetMillis;
+
     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
 
@@ -472,6 +475,9 @@
             // do this synchronously to ensure we have the accounts before this call returns
             onAccountsUpdated(null);
         }
+
+        // Pick a random second in a day to seed all periodic syncs
+        mSyncRandomOffsetMillis = mSyncStorageEngine.getSyncRandomOffset() * 1000;
     }
 
     /**
@@ -700,6 +706,7 @@
 
     private void sendCheckAlarmsMessage() {
         if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_CHECK_ALARMS");
+        mSyncHandler.removeMessages(SyncHandler.MESSAGE_CHECK_ALARMS);
         mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_CHECK_ALARMS);
     }
 
@@ -748,6 +755,8 @@
     }
 
     private void increaseBackoffSetting(SyncOperation op) {
+        // TODO: Use this function to align it to an already scheduled sync
+        //       operation in the specified window
         final long now = SystemClock.elapsedRealtime();
 
         final Pair<Long, Long> previousSettings =
@@ -1094,6 +1103,8 @@
         final long now = SystemClock.elapsedRealtime();
         pw.print("now: "); pw.print(now);
         pw.println(" (" + formatTime(System.currentTimeMillis()) + ")");
+        pw.print("offset: "); pw.print(DateUtils.formatElapsedTime(mSyncRandomOffsetMillis/1000));
+        pw.println(" (HH:MM:SS)");
         pw.print("uptime: "); pw.print(DateUtils.formatElapsedTime(now/1000));
                 pw.println(" (HH:MM:SS)");
         pw.print("time spent syncing: ");
@@ -1805,6 +1816,9 @@
             AccountAndUser[] accounts = mAccounts;
 
             final long nowAbsolute = System.currentTimeMillis();
+            final long shiftedNowAbsolute = (0 < nowAbsolute - mSyncRandomOffsetMillis)
+                                               ? (nowAbsolute  - mSyncRandomOffsetMillis) : 0;
+
             ArrayList<SyncStorageEngine.AuthorityInfo> infos = mSyncStorageEngine.getAuthorities();
             for (SyncStorageEngine.AuthorityInfo info : infos) {
                 // skip the sync if the account of this operation no longer exists
@@ -1826,16 +1840,32 @@
                 SyncStatusInfo status = mSyncStorageEngine.getOrCreateSyncStatus(info);
                 for (int i = 0, N = info.periodicSyncs.size(); i < N; i++) {
                     final Bundle extras = info.periodicSyncs.get(i).first;
-                    final Long periodInSeconds = info.periodicSyncs.get(i).second;
+                    final Long periodInMillis = info.periodicSyncs.get(i).second * 1000;
                     // find when this periodic sync was last scheduled to run
                     final long lastPollTimeAbsolute = status.getPeriodicSyncTime(i);
-                    // compute when this periodic sync should next run - this can be in the future
-                    // for example if the user changed the time, synced and changed back.
-                    final long nextPollTimeAbsolute = lastPollTimeAbsolute > nowAbsolute
-                            ? nowAbsolute
-                            : lastPollTimeAbsolute + periodInSeconds * 1000;
-                    // if it is ready to run then schedule it and mark it as having been scheduled
-                    if (nextPollTimeAbsolute <= nowAbsolute) {
+
+                    long remainingMillis
+                            = periodInMillis - (shiftedNowAbsolute % periodInMillis);
+
+                    /*
+                     * Sync scheduling strategy:
+                     *    Set the next periodic sync based on a random offset (in seconds).
+                     *
+                     *    Also sync right now if any of the following cases hold
+                     *    and mark it as having been scheduled
+                     *
+                     * Case 1:  This sync is ready to run now.
+                     * Case 2:  If the lastPollTimeAbsolute is in the future,
+                     *          sync now and reinitialize. This can happen for
+                     *          example if the user changed the time, synced and
+                     *          changed back.
+                     * Case 3:  If we failed to sync at the last scheduled time
+                     */
+                    if (remainingMillis == periodInMillis  // Case 1
+                            || lastPollTimeAbsolute > nowAbsolute // Case 2
+                            || (nowAbsolute - lastPollTimeAbsolute
+                                    >= periodInMillis)) { // Case 3
+                        // Sync now
                         final Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff(
                                 info.account, info.userId, info.authority);
                         final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo =
@@ -1853,12 +1883,13 @@
                                                 info.account, info.userId, info.authority),
                                         syncAdapterInfo.type.allowParallelSyncs()));
                         status.setPeriodicSyncTime(i, nowAbsolute);
-                    } else {
-                        // it isn't ready to run, remember this time if it is earlier than
-                        // earliestFuturePollTime
-                        if (nextPollTimeAbsolute < earliestFuturePollTime) {
-                            earliestFuturePollTime = nextPollTimeAbsolute;
-                        }
+                    }
+                    // Compute when this periodic sync should next run
+                    final long nextPollTimeAbsolute = nowAbsolute + remainingMillis;
+
+                    // remember this time if it is earlier than earliestFuturePollTime
+                    if (nextPollTimeAbsolute < earliestFuturePollTime) {
+                        earliestFuturePollTime = nextPollTimeAbsolute;
                     }
                 }
             }
diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java
index 9c81c9e..5ab9e5a 100644
--- a/core/java/android/content/SyncStorageEngine.java
+++ b/core/java/android/content/SyncStorageEngine.java
@@ -37,6 +37,7 @@
 import android.os.Parcel;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.Xml;
@@ -49,6 +50,7 @@
 import java.util.Calendar;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.Random;
 import java.util.TimeZone;
 import java.util.List;
 
@@ -65,6 +67,7 @@
 
     private static final String XML_ATTR_NEXT_AUTHORITY_ID = "nextAuthorityId";
     private static final String XML_ATTR_LISTEN_FOR_TICKLES = "listen-for-tickles";
+    private static final String XML_ATTR_SYNC_RANDOM_OFFSET = "offsetInSeconds";
     private static final String XML_ATTR_ENABLED = "enabled";
     private static final String XML_ATTR_USER = "user";
     private static final String XML_TAG_LISTEN_FOR_TICKLES = "listenForTickles";
@@ -277,6 +280,8 @@
 
     private static volatile SyncStorageEngine sSyncStorageEngine = null;
 
+    private int mSyncRandomOffset;
+
     /**
      * This file contains the core engine state: all accounts and the
      * settings for them.  It must never be lost, and should be changed
@@ -375,6 +380,10 @@
         }
     }
 
+    public int getSyncRandomOffset() {
+        return mSyncRandomOffset;
+    }
+
     public void addStatusChangeListener(int mask, ISyncStatusObserver callback) {
         synchronized (mAuthorities) {
             mChangeListeners.register(callback, mask);
@@ -1465,6 +1474,16 @@
                 } catch (NumberFormatException e) {
                     // don't care
                 }
+                String offsetString = parser.getAttributeValue(null, XML_ATTR_SYNC_RANDOM_OFFSET);
+                try {
+                    mSyncRandomOffset = (offsetString == null) ? 0 : Integer.parseInt(offsetString);
+                } catch (NumberFormatException e) {
+                    mSyncRandomOffset = 0;
+                }
+                if (mSyncRandomOffset == 0) {
+                    Random random = new Random(System.currentTimeMillis());
+                    mSyncRandomOffset = random.nextInt(86400);
+                }
                 mMasterSyncAutomatically.put(0, listen == null || Boolean.parseBoolean(listen));
                 eventType = parser.next();
                 AuthorityInfo authority = null;
@@ -1705,6 +1724,7 @@
             out.startTag(null, "accounts");
             out.attribute(null, "version", Integer.toString(ACCOUNTS_VERSION));
             out.attribute(null, XML_ATTR_NEXT_AUTHORITY_ID, Integer.toString(mNextAuthorityId));
+            out.attribute(null, XML_ATTR_SYNC_RANDOM_OFFSET, Integer.toString(mSyncRandomOffset));
 
             // Write the Sync Automatically flags for each user
             final int M = mMasterSyncAutomatically.size();