Batch alarms to reduce device wakeups

The default Alarm Manager behavior for KLP+ apps will be to aggressively
coalesce alarms, trading exact timeliness of delivery for minimizing the
number of alarm-delivery points, especially wakeup points.

There is new API in AlarmManager, setExact() and setExactRepeating(),
for use by apps that absolutely *must* get their alarms at a specific
point in time.

Bug 9532215

Change-Id: I40b4eea90220211cc958172d2629664b921ff051
diff --git a/services/java/com/android/server/AlarmManagerService.java b/services/java/com/android/server/AlarmManagerService.java
index 0be9ca5..b1489ad 100644
--- a/services/java/com/android/server/AlarmManagerService.java
+++ b/services/java/com/android/server/AlarmManagerService.java
@@ -18,7 +18,6 @@
 
 import android.app.Activity;
 import android.app.ActivityManagerNative;
-import android.app.AlarmManager;
 import android.app.IAlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -38,7 +37,6 @@
 import android.os.UserHandle;
 import android.os.WorkSource;
 import android.text.TextUtils;
-import android.text.format.Time;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.TimeUtils;
@@ -57,31 +55,38 @@
 import java.util.Map;
 import java.util.TimeZone;
 
+import static android.app.AlarmManager.RTC_WAKEUP;
+import static android.app.AlarmManager.RTC;
+import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
+import static android.app.AlarmManager.ELAPSED_REALTIME;
+
 import com.android.internal.util.LocalLog;
 
 class AlarmManagerService extends IAlarmManager.Stub {
     // The threshold for how long an alarm can be late before we print a
     // warning message.  The time duration is in milliseconds.
     private static final long LATE_ALARM_THRESHOLD = 10 * 1000;
-    
-    private static final int RTC_WAKEUP_MASK = 1 << AlarmManager.RTC_WAKEUP;
-    private static final int RTC_MASK = 1 << AlarmManager.RTC;
-    private static final int ELAPSED_REALTIME_WAKEUP_MASK = 1 << AlarmManager.ELAPSED_REALTIME_WAKEUP; 
-    private static final int ELAPSED_REALTIME_MASK = 1 << AlarmManager.ELAPSED_REALTIME;
+
+    private static final int RTC_WAKEUP_MASK = 1 << RTC_WAKEUP;
+    private static final int RTC_MASK = 1 << RTC;
+    private static final int ELAPSED_REALTIME_WAKEUP_MASK = 1 << ELAPSED_REALTIME_WAKEUP; 
+    private static final int ELAPSED_REALTIME_MASK = 1 << ELAPSED_REALTIME;
     private static final int TIME_CHANGED_MASK = 1 << 16;
     private static final int IS_WAKEUP_MASK = RTC_WAKEUP_MASK|ELAPSED_REALTIME_WAKEUP_MASK;
 
-    // Alignment quantum for inexact repeating alarms
-    private static final long QUANTUM = AlarmManager.INTERVAL_FIFTEEN_MINUTES;
+    // Mask for testing whether a given alarm type is wakeup vs non-wakeup
+    private static final int TYPE_NONWAKEUP_MASK = 0x1; // low bit => non-wakeup
 
     private static final String TAG = "AlarmManager";
     private static final String ClockReceiver_TAG = "ClockReceiver";
     private static final boolean localLOGV = false;
+    private static final boolean DEBUG_BATCH = localLOGV || false;
     private static final int ALARM_EVENT = 1;
     private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
     
     private static final Intent mBackgroundIntent
             = new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND);
+    private static final IncreasingTimeOrder sIncreasingTimeOrder = new IncreasingTimeOrder();
     
     private static final boolean WAKEUP_STATS = true;
 
@@ -90,14 +95,10 @@
     private final LocalLog mLog = new LocalLog(TAG);
 
     private Object mLock = new Object();
-    
-    private final ArrayList<Alarm> mRtcWakeupAlarms = new ArrayList<Alarm>();
-    private final ArrayList<Alarm> mRtcAlarms = new ArrayList<Alarm>();
-    private final ArrayList<Alarm> mElapsedRealtimeWakeupAlarms = new ArrayList<Alarm>();
-    private final ArrayList<Alarm> mElapsedRealtimeAlarms = new ArrayList<Alarm>();
-    private final IncreasingTimeOrder mIncreasingTimeOrder = new IncreasingTimeOrder();
-    
+
     private int mDescriptor;
+    private long mNextWakeup;
+    private long mNextNonWakeup;
     private int mBroadcastRefCount = 0;
     private PowerManager.WakeLock mWakeLock;
     private ArrayList<InFlight> mInFlight = new ArrayList<InFlight>();
@@ -124,6 +125,277 @@
     private final LinkedList<WakeupEvent> mRecentWakeups = new LinkedList<WakeupEvent>();
     private final long RECENT_WAKEUP_PERIOD = 1000L * 60 * 60 * 24; // one day
 
+    static final class Batch {
+        long start;     // These endpoints are always in ELAPSED
+        long end;
+        boolean standalone; // certain "batches" don't participate in coalescing
+
+        final ArrayList<Alarm> alarms = new ArrayList<Alarm>();
+
+        Batch() {
+            start = 0;
+            end = Long.MAX_VALUE;
+        }
+
+        Batch(Alarm seed) {
+            start = seed.whenElapsed;
+            end = seed.maxWhen;
+            alarms.add(seed);
+        }
+
+        int size() {
+            return alarms.size();
+        }
+
+        Alarm get(int index) {
+            return alarms.get(index);
+        }
+
+        boolean canHold(long whenElapsed, long maxWhen) {
+            return (end >= whenElapsed) && (start <= maxWhen);
+        }
+
+        boolean add(Alarm alarm) {
+            boolean newStart = false;
+            // narrows the batch if necessary; presumes that canHold(alarm) is true
+            int index = Collections.binarySearch(alarms, alarm, sIncreasingTimeOrder);
+            if (index < 0) {
+                index = 0 - index - 1;
+            }
+            alarms.add(index, alarm);
+            if (DEBUG_BATCH) {
+                Slog.v(TAG, "Adding " + alarm + " to " + this);
+            }
+            if (alarm.whenElapsed > start) {
+                start = alarm.whenElapsed;
+                newStart = true;
+            }
+            if (alarm.maxWhen < end) {
+                end = alarm.maxWhen;
+            }
+
+            if (DEBUG_BATCH) {
+                Slog.v(TAG, "    => now " + this);
+            }
+            return newStart;
+        }
+
+        boolean remove(final PendingIntent operation) {
+            boolean didRemove = false;
+            long newStart = 0;  // recalculate endpoints as we go
+            long newEnd = Long.MAX_VALUE;
+            for (int i = 0; i < alarms.size(); i++) {
+                Alarm alarm = alarms.get(i);
+                if (alarm.operation.equals(operation)) {
+                    alarms.remove(i);
+                    didRemove = true;
+                } else {
+                    if (alarm.whenElapsed > newStart) {
+                        newStart = alarm.whenElapsed;
+                    }
+                    if (alarm.maxWhen < newEnd) {
+                        newEnd = alarm.maxWhen;
+                    }
+                    i++;
+                }
+            }
+            if (didRemove) {
+                // commit the new batch bounds
+                start = newStart;
+                end = newEnd;
+            }
+            return didRemove;
+        }
+
+        boolean remove(final String packageName) {
+            boolean didRemove = false;
+            long newStart = 0;  // recalculate endpoints as we go
+            long newEnd = Long.MAX_VALUE;
+            for (int i = 0; i < alarms.size(); i++) {
+                Alarm alarm = alarms.get(i);
+                if (alarm.operation.getTargetPackage().equals(packageName)) {
+                    alarms.remove(i);
+                    didRemove = true;
+                } else {
+                    if (alarm.whenElapsed > newStart) {
+                        newStart = alarm.whenElapsed;
+                    }
+                    if (alarm.maxWhen < newEnd) {
+                        newEnd = alarm.maxWhen;
+                    }
+                    i++;
+                }
+            }
+            if (didRemove) {
+                // commit the new batch bounds
+                start = newStart;
+                end = newEnd;
+            }
+            return didRemove;
+        }
+
+        boolean remove(final int userHandle) {
+            boolean didRemove = false;
+            long newStart = 0;  // recalculate endpoints as we go
+            long newEnd = Long.MAX_VALUE;
+            for (int i = 0; i < alarms.size(); i++) {
+                Alarm alarm = alarms.get(i);
+                if (UserHandle.getUserId(alarm.operation.getCreatorUid()) == userHandle) {
+                    alarms.remove(i);
+                    didRemove = true;
+                } else {
+                    if (alarm.whenElapsed > newStart) {
+                        newStart = alarm.whenElapsed;
+                    }
+                    if (alarm.maxWhen < newEnd) {
+                        newEnd = alarm.maxWhen;
+                    }
+                    i++;
+                }
+            }
+            if (didRemove) {
+                // commit the new batch bounds
+                start = newStart;
+                end = newEnd;
+            }
+            return didRemove;
+        }
+
+        boolean hasPackage(final String packageName) {
+            final int N = alarms.size();
+            for (int i = 0; i < N; i++) {
+                Alarm a = alarms.get(i);
+                if (a.operation.getTargetPackage().equals(packageName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        boolean hasWakeups() {
+            final int N = alarms.size();
+            for (int i = 0; i < N; i++) {
+                Alarm a = alarms.get(i);
+                // non-wakeup alarms are types 1 and 3, i.e. have the low bit set
+                if ((a.type & TYPE_NONWAKEUP_MASK) == 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder b = new StringBuilder(40);
+            b.append("Batch{"); b.append(Integer.toHexString(this.hashCode()));
+            b.append(" num="); b.append(size());
+            b.append(" start="); b.append(start);
+            b.append(" end="); b.append(end);
+            if (standalone) {
+                b.append(" STANDALONE");
+            }
+            b.append('}');
+            return b.toString();
+        }
+    }
+
+    static class BatchTimeOrder implements Comparator<Batch> {
+        public int compare(Batch b1, Batch b2) {
+            long when1 = b1.start;
+            long when2 = b2.start;
+            if (when1 - when2 > 0) {
+                return 1;
+            }
+            if (when1 - when2 < 0) {
+                return -1;
+            }
+            return 0;
+        }
+    }
+    
+    // minimum recurrence period or alarm futurity for us to be able to fuzz it
+    private static final long MAX_FUZZABLE_INTERVAL = 10000;
+    private static final BatchTimeOrder sBatchOrder = new BatchTimeOrder();
+    private final ArrayList<Batch> mAlarmBatches = new ArrayList<Batch>();
+
+    static long convertToElapsed(long when, int type) {
+        final boolean isRtc = (type == RTC || type == RTC_WAKEUP);
+        if (isRtc) {
+            when -= System.currentTimeMillis() - SystemClock.elapsedRealtime();
+        }
+        return when;
+    }
+
+    // Apply a heuristic to { recurrence interval, futurity of the trigger time } to
+    // calculate the end of our nominal delivery window for the alarm.
+    static long maxTriggerTime(long now, long triggerAtTime, long interval) {
+        // Current heuristic: batchable window is 75% of either the recurrence interval
+        // [for a periodic alarm] or of the time from now to the desired delivery time,
+        // with a minimum delay/interval of 10 seconds, under which we will simply not
+        // defer the alarm.
+        long futurity = (interval == 0)
+                ? (triggerAtTime - now)
+                : interval;
+        if (futurity < MAX_FUZZABLE_INTERVAL) {
+            futurity = 0;
+        }
+        return triggerAtTime + (long)(.75 * futurity);
+    }
+
+    // returns true if the batch was added at the head
+    static boolean addBatchLocked(ArrayList<Batch> list, Batch newBatch) {
+        int index = Collections.binarySearch(list, newBatch, sBatchOrder);
+        if (index < 0) {
+            index = 0 - index - 1;
+        }
+        list.add(index, newBatch);
+        return (index == 0);
+    }
+
+    Batch attemptCoalesceLocked(long whenElapsed, long maxWhen) {
+        final int N = mAlarmBatches.size();
+        for (int i = 0; i < N; i++) {
+            Batch b = mAlarmBatches.get(i);
+            if (!b.standalone && b.canHold(whenElapsed, maxWhen)) {
+                return b;
+            }
+        }
+        return null;
+    }
+
+    // The RTC clock has moved arbitrarily, so we need to recalculate all the batching
+    void rebatchAllAlarms() {
+        if (DEBUG_BATCH) {
+            Slog.v(TAG, "RTC changed; rebatching");
+        }
+        synchronized (mLock) {
+            ArrayList<Batch> oldSet = (ArrayList<Batch>) mAlarmBatches.clone();
+            mAlarmBatches.clear();
+            final long nowElapsed = SystemClock.elapsedRealtime();
+            for (Batch batch : oldSet) {
+                final int N = batch.size();
+                for (int i = 0; i < N; i++) {
+                    Alarm a = batch.get(i);
+                    long whenElapsed = convertToElapsed(a.when, a.type);
+                    long maxElapsed = (a.whenElapsed == a.maxWhen)
+                            ? whenElapsed
+                            : maxTriggerTime(nowElapsed, whenElapsed, a.repeatInterval);
+                    if (batch.standalone) {
+                        // this will also be the only alarm in the batch
+                        a = new Alarm(a.type, a.when, whenElapsed, maxElapsed,
+                                a.repeatInterval, a.operation);
+                        Batch newBatch = new Batch(a);
+                        newBatch.standalone = true;
+                        addBatchLocked(mAlarmBatches, newBatch);
+                    } else {
+                        setImplLocked(a.type, a.when, whenElapsed, maxElapsed,
+                                a.repeatInterval, a.operation, false);
+                    }
+                }
+            }
+        }
+    }
+
     private static final class InFlight extends Intent {
         final PendingIntent mPendingIntent;
         final Pair<String, ComponentName> mTarget;
@@ -184,6 +456,7 @@
     public AlarmManagerService(Context context) {
         mContext = context;
         mDescriptor = init();
+        mNextWakeup = mNextNonWakeup = 0;
 
         // We have to set current TimeZone info to kernel
         // because kernel doesn't keep this after reboot
@@ -224,77 +497,89 @@
             super.finalize();
         }
     }
-    
-    public void set(int type, long triggerAtTime, PendingIntent operation) {
-        setRepeating(type, triggerAtTime, 0, operation);
+
+    @Override
+    public void set(int type, long triggerAtTime, long interval,
+            PendingIntent operation, boolean isExact) {
+        set(type, triggerAtTime, interval, operation, isExact, false);
     }
-    
-    public void setRepeating(int type, long triggerAtTime, long interval, 
-            PendingIntent operation) {
+
+    public void set(int type, long triggerAtTime, long interval,
+            PendingIntent operation, boolean isExact, boolean isStandalone) {
         if (operation == null) {
             Slog.w(TAG, "set/setRepeating ignored because there is no intent");
             return;
         }
+
+        if (type < RTC_WAKEUP || type > ELAPSED_REALTIME) {
+            throw new IllegalArgumentException("Invalid alarm type " + type);
+        }
+
+        long nowElapsed = SystemClock.elapsedRealtime();
+        long triggerElapsed = convertToElapsed(triggerAtTime, type);
+        long maxElapsed = (isExact)
+                ? triggerElapsed
+                : maxTriggerTime(nowElapsed, triggerElapsed, interval);
+
         synchronized (mLock) {
-            Alarm alarm = new Alarm();
-            alarm.type = type;
-            alarm.when = triggerAtTime;
-            alarm.repeatInterval = interval;
-            alarm.operation = operation;
-
-            // Remove this alarm if already scheduled.
-            removeLocked(operation);
-
-            if (localLOGV) Slog.v(TAG, "set: " + alarm);
-
-            int index = addAlarmLocked(alarm);
-            if (index == 0) {
-                setLocked(alarm);
+            if (DEBUG_BATCH) {
+                Slog.v(TAG, "set(" + operation + ") : type=" + type
+                        + " triggerAtTime=" + triggerAtTime
+                        + " tElapsed=" + triggerElapsed + " maxElapsed=" + maxElapsed
+                        + " interval=" + interval + " standalone=" + isStandalone);
             }
+            setImplLocked(type, triggerAtTime, triggerElapsed, maxElapsed,
+                    interval, operation, isStandalone);
         }
     }
-    
-    public void setInexactRepeating(int type, long triggerAtTime, long interval, 
-            PendingIntent operation) {
-        if (operation == null) {
-            Slog.w(TAG, "setInexactRepeating ignored because there is no intent");
-            return;
-        }
 
-        if (interval <= 0) {
-            Slog.w(TAG, "setInexactRepeating ignored because interval " + interval
-                    + " is invalid");
-            return;
-        }
+    private void setImplLocked(int type, long when, long whenElapsed, long maxWhen, long interval,
+            PendingIntent operation, boolean isStandalone) {
+        Alarm a = new Alarm(type, when, whenElapsed, maxWhen, interval, operation);
+        removeLocked(operation);
 
-        // If the requested interval isn't a multiple of 15 minutes, just treat it as exact
-        if (interval % QUANTUM != 0) {
-            if (localLOGV) Slog.v(TAG, "Interval " + interval + " not a quantum multiple");
-            setRepeating(type, triggerAtTime, interval, operation);
-            return;
-        }
-
-        // Translate times into the ELAPSED timebase for alignment purposes so that
-        // alignment never tries to match against wall clock times.
-        final boolean isRtc = (type == AlarmManager.RTC || type == AlarmManager.RTC_WAKEUP);
-        final long skew = (isRtc)
-                ? System.currentTimeMillis() - SystemClock.elapsedRealtime()
-                : 0;
-
-        // Slip forward to the next ELAPSED-timebase quantum after the stated time.  If
-        // we're *at* a quantum point, leave it alone.
-        final long adjustedTriggerTime;
-        long offset = (triggerAtTime - skew) % QUANTUM;
-        if (offset != 0) {
-            adjustedTriggerTime = triggerAtTime - offset + QUANTUM;
+        final boolean reschedule;
+        Batch batch = (isStandalone) ? null : attemptCoalesceLocked(whenElapsed, maxWhen);
+        if (batch == null) {
+            batch = new Batch(a);
+            batch.standalone = isStandalone;
+            if (DEBUG_BATCH) {
+                Slog.v(TAG, "Starting new alarm batch " + batch);
+            }
+            reschedule = addBatchLocked(mAlarmBatches, batch);
         } else {
-            adjustedTriggerTime = triggerAtTime;
+            reschedule = batch.add(a);
         }
 
-        // Set the alarm based on the quantum-aligned start time
-        if (localLOGV) Slog.v(TAG, "setInexactRepeating: type=" + type + " interval=" + interval
-                + " trigger=" + adjustedTriggerTime + " orig=" + triggerAtTime);
-        setRepeating(type, adjustedTriggerTime, interval, operation);
+        if (reschedule) {
+            rescheduleKernelAlarmsLocked();
+        }
+    }
+
+    private Batch findFirstWakeupBatchLocked() {
+        final int N = mAlarmBatches.size();
+        for (int i = 0; i < N; i++) {
+            Batch b = mAlarmBatches.get(i);
+            if (b.hasWakeups()) {
+                return b;
+            }
+        }
+        return null;
+    }
+
+    private void rescheduleKernelAlarmsLocked() {
+        // Schedule the next upcoming wakeup alarm.  If there is a deliverable batch
+        // prior to that which contains no wakeups, we schedule that as well.
+        final Batch firstWakeup = findFirstWakeupBatchLocked();
+        final Batch firstBatch = mAlarmBatches.get(0);
+        if (firstWakeup != null && mNextWakeup != firstWakeup.start) {
+            mNextWakeup = firstWakeup.start;
+            setLocked(ELAPSED_REALTIME_WAKEUP, firstWakeup.start);
+        }
+        if (firstBatch != firstWakeup && mNextNonWakeup != firstBatch.start) {
+            mNextNonWakeup = firstBatch.start;
+            setLocked(ELAPSED_REALTIME, firstBatch.start);
+        }
     }
 
     public void setTime(long millis) {
@@ -356,160 +641,61 @@
     }
     
     public void removeLocked(PendingIntent operation) {
-        removeLocked(mRtcWakeupAlarms, operation);
-        removeLocked(mRtcAlarms, operation);
-        removeLocked(mElapsedRealtimeWakeupAlarms, operation);
-        removeLocked(mElapsedRealtimeAlarms, operation);
-    }
-
-    private void removeLocked(ArrayList<Alarm> alarmList,
-            PendingIntent operation) {
-        if (alarmList.size() <= 0) {
-            return;
-        }
-
-        // iterator over the list removing any it where the intent match
-        for (int i=0; i<alarmList.size(); i++) {
-            Alarm alarm = alarmList.get(i);
-            if (alarm.operation.equals(operation)) {
-                alarmList.remove(i);
-                i--;
+        for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
+            Batch b = mAlarmBatches.get(i);
+            b.remove(operation);
+            if (b.size() == 0) {
+                mAlarmBatches.remove(i);
             }
         }
     }
 
     public void removeLocked(String packageName) {
-        removeLocked(mRtcWakeupAlarms, packageName);
-        removeLocked(mRtcAlarms, packageName);
-        removeLocked(mElapsedRealtimeWakeupAlarms, packageName);
-        removeLocked(mElapsedRealtimeAlarms, packageName);
-    }
-
-    private void removeLocked(ArrayList<Alarm> alarmList,
-            String packageName) {
-        if (alarmList.size() <= 0) {
-            return;
-        }
-
-        // iterator over the list removing any it where the intent match
-        for (int i=0; i<alarmList.size(); i++) {
-            Alarm alarm = alarmList.get(i);
-            if (alarm.operation.getTargetPackage().equals(packageName)) {
-                alarmList.remove(i);
-                i--;
+        for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
+            Batch b = mAlarmBatches.get(i);
+            b.remove(packageName);
+            if (b.size() == 0) {
+                mAlarmBatches.remove(i);
             }
         }
     }
 
     public void removeUserLocked(int userHandle) {
-        removeUserLocked(mRtcWakeupAlarms, userHandle);
-        removeUserLocked(mRtcAlarms, userHandle);
-        removeUserLocked(mElapsedRealtimeWakeupAlarms, userHandle);
-        removeUserLocked(mElapsedRealtimeAlarms, userHandle);
-    }
-
-    private void removeUserLocked(ArrayList<Alarm> alarmList, int userHandle) {
-        if (alarmList.size() <= 0) {
-            return;
-        }
-
-        // iterator over the list removing any it where the intent match
-        for (int i=0; i<alarmList.size(); i++) {
-            Alarm alarm = alarmList.get(i);
-            if (UserHandle.getUserId(alarm.operation.getCreatorUid()) == userHandle) {
-                alarmList.remove(i);
-                i--;
+        for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
+            Batch b = mAlarmBatches.get(i);
+            b.remove(userHandle);
+            if (b.size() == 0) {
+                mAlarmBatches.remove(i);
             }
         }
     }
-    
-    public boolean lookForPackageLocked(String packageName) {
-        return lookForPackageLocked(mRtcWakeupAlarms, packageName)
-                || lookForPackageLocked(mRtcAlarms, packageName)
-                || lookForPackageLocked(mElapsedRealtimeWakeupAlarms, packageName)
-                || lookForPackageLocked(mElapsedRealtimeAlarms, packageName);
-    }
 
-    private boolean lookForPackageLocked(ArrayList<Alarm> alarmList, String packageName) {
-        for (int i=alarmList.size()-1; i>=0; i--) {
-            if (alarmList.get(i).operation.getTargetPackage().equals(packageName)) {
+    public boolean lookForPackageLocked(String packageName) {
+        for (int i = 0; i < mAlarmBatches.size(); i++) {
+            Batch b = mAlarmBatches.get(i);
+            if (b.hasPackage(packageName)) {
                 return true;
             }
         }
         return false;
     }
-    
-    private ArrayList<Alarm> getAlarmList(int type) {
-        switch (type) {
-            case AlarmManager.RTC_WAKEUP:              return mRtcWakeupAlarms;
-            case AlarmManager.RTC:                     return mRtcAlarms;
-            case AlarmManager.ELAPSED_REALTIME_WAKEUP: return mElapsedRealtimeWakeupAlarms;
-            case AlarmManager.ELAPSED_REALTIME:        return mElapsedRealtimeAlarms;
-        }
-        
-        return null;
-    }
-    
-    private int addAlarmLocked(Alarm alarm) {
-        ArrayList<Alarm> alarmList = getAlarmList(alarm.type);
-        
-        int index = Collections.binarySearch(alarmList, alarm, mIncreasingTimeOrder);
-        if (index < 0) {
-            index = 0 - index - 1;
-        }
-        if (localLOGV) Slog.v(TAG, "Adding alarm " + alarm + " at " + index);
-        alarmList.add(index, alarm);
 
-        if (localLOGV) {
-            // Display the list of alarms for this alarm type
-            Slog.v(TAG, "alarms: " + alarmList.size() + " type: " + alarm.type);
-            int position = 0;
-            for (Alarm a : alarmList) {
-                Time time = new Time();
-                time.set(a.when);
-                String timeStr = time.format("%b %d %I:%M:%S %p");
-                Slog.v(TAG, position + ": " + timeStr
-                        + " " + a.operation.getTargetPackage());
-                position += 1;
-            }
-        }
-        
-        return index;
-    }
-    
-    public long timeToNextAlarm() {
-        long nextAlarm = Long.MAX_VALUE;
-        synchronized (mLock) {
-            for (int i=AlarmManager.RTC_WAKEUP;
-                    i<=AlarmManager.ELAPSED_REALTIME; i++) {
-                ArrayList<Alarm> alarmList = getAlarmList(i);
-                if (alarmList.size() > 0) {
-                    Alarm a = alarmList.get(0);
-                    if (a.when < nextAlarm) {
-                        nextAlarm = a.when;
-                    }
-                }
-            }
-        }
-        return nextAlarm;
-    }
-    
-    private void setLocked(Alarm alarm)
+    private void setLocked(int type, long when)
     {
         if (mDescriptor != -1)
         {
             // The kernel never triggers alarms with negative wakeup times
             // so we ensure they are positive.
             long alarmSeconds, alarmNanoseconds;
-            if (alarm.when < 0) {
+            if (when < 0) {
                 alarmSeconds = 0;
                 alarmNanoseconds = 0;
             } else {
-                alarmSeconds = alarm.when / 1000;
-                alarmNanoseconds = (alarm.when % 1000) * 1000 * 1000;
+                alarmSeconds = when / 1000;
+                alarmNanoseconds = (when % 1000) * 1000 * 1000;
             }
             
-            set(mDescriptor, alarm.type, alarmSeconds, alarmNanoseconds);
+            set(mDescriptor, type, alarmSeconds, alarmNanoseconds);
         }
         else
         {
@@ -517,7 +703,7 @@
             msg.what = ALARM_EVENT;
             
             mHandler.removeMessages(ALARM_EVENT);
-            mHandler.sendMessageAtTime(msg, alarm.when);
+            mHandler.sendMessageAtTime(msg, when);
         }
     }
     
@@ -533,29 +719,28 @@
         
         synchronized (mLock) {
             pw.println("Current Alarm Manager state:");
-            if (mRtcWakeupAlarms.size() > 0 || mRtcAlarms.size() > 0) {
-                final long now = System.currentTimeMillis();
-                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-                pw.println(" ");
-                pw.print("  Realtime wakeup (now=");
-                        pw.print(sdf.format(new Date(now))); pw.println("):");
-                if (mRtcWakeupAlarms.size() > 0) {
-                    dumpAlarmList(pw, mRtcWakeupAlarms, "  ", "RTC_WAKEUP", now);
-                }
-                if (mRtcAlarms.size() > 0) {
-                    dumpAlarmList(pw, mRtcAlarms, "  ", "RTC", now);
-                }
-            }
-            if (mElapsedRealtimeWakeupAlarms.size() > 0 || mElapsedRealtimeAlarms.size() > 0) {
-                final long now = SystemClock.elapsedRealtime();
-                pw.println(" ");
-                pw.print("  Elapsed realtime wakeup (now=");
-                        TimeUtils.formatDuration(now, pw); pw.println("):");
-                if (mElapsedRealtimeWakeupAlarms.size() > 0) {
-                    dumpAlarmList(pw, mElapsedRealtimeWakeupAlarms, "  ", "ELAPSED_WAKEUP", now);
-                }
-                if (mElapsedRealtimeAlarms.size() > 0) {
-                    dumpAlarmList(pw, mElapsedRealtimeAlarms, "  ", "ELAPSED", now);
+            final long nowRTC = System.currentTimeMillis();
+            final long nowELAPSED = SystemClock.elapsedRealtime();
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+            pw.print("nowRTC="); pw.print(nowRTC);
+            pw.print("="); pw.print(sdf.format(new Date(nowRTC)));
+            pw.print(" nowELAPSED="); pw.println(nowELAPSED);
+
+            long nextWakeupRTC = mNextWakeup + (nowRTC - nowELAPSED);
+            long nextNonWakeupRTC = mNextNonWakeup + (nowRTC - nowELAPSED);
+            pw.print("Next alarm: "); pw.print(mNextNonWakeup);
+                    pw.print(" = "); pw.println(sdf.format(new Date(nextNonWakeupRTC)));
+            pw.print("Next wakeup: "); pw.print(mNextWakeup);
+                    pw.print(" = "); pw.println(sdf.format(new Date(nextWakeupRTC)));
+
+            if (mAlarmBatches.size() > 0) {
+                pw.println();
+                pw.print("Pending alarm batches: ");
+                pw.println(mAlarmBatches.size());
+                for (Batch b : mAlarmBatches) {
+                    pw.print(b); pw.println(':');
+                    dumpAlarmList(pw, b.alarms, "  ", nowELAPSED, nowRTC);
                 }
             }
 
@@ -662,7 +847,6 @@
             if (WAKEUP_STATS) {
                 pw.println();
                 pw.println("  Recent Wakeup History:");
-                final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss.SSS");
                 long last = -1;
                 for (WakeupEvent event : mRecentWakeups) {
                     pw.print("    "); pw.print(sdf.format(new Date(event.when)));
@@ -691,77 +875,78 @@
             a.dump(pw, prefix + "  ", now);
         }
     }
-    
+
+    private static final String labelForType(int type) {
+        switch (type) {
+        case RTC: return "RTC";
+        case RTC_WAKEUP : return "RTC_WAKEUP";
+        case ELAPSED_REALTIME : return "ELAPSED";
+        case ELAPSED_REALTIME_WAKEUP: return "ELAPSED_WAKEUP";
+        default:
+            break;
+        }
+        return "--unknown--";
+    }
+
+    private static final void dumpAlarmList(PrintWriter pw, ArrayList<Alarm> list,
+            String prefix, long nowELAPSED, long nowRTC) {
+        for (int i=list.size()-1; i>=0; i--) {
+            Alarm a = list.get(i);
+            final String label = labelForType(a.type);
+            long now = (a.type <= RTC) ? nowRTC : nowELAPSED;
+            pw.print(prefix); pw.print(label); pw.print(" #"); pw.print(i);
+                    pw.print(": "); pw.println(a);
+            a.dump(pw, prefix + "  ", now);
+        }
+    }
+
     private native int init();
     private native void close(int fd);
     private native void set(int fd, int type, long seconds, long nanoseconds);
     private native int waitForAlarm(int fd);
     private native int setKernelTimezone(int fd, int minuteswest);
 
-    private void triggerAlarmsLocked(ArrayList<Alarm> alarmList,
-                                     ArrayList<Alarm> triggerList,
-                                     long now)
-    {
-        ArrayList<Alarm> repeats = null;
+    private void triggerAlarmsLocked(ArrayList<Alarm> triggerList, long nowELAPSED, long nowRTC) {
+        Batch batch;
 
-        for (int i=0; i<alarmList.size(); i++) {
-            Alarm alarm = alarmList.get(i);
-
-            if (localLOGV) Slog.v(TAG, "Checking active alarm when=" + alarm.when + " " + alarm);
-
-            if (alarm.when > now) {
-                // don't fire alarms in the future
+        // batches are temporally sorted, so we need only pull from the
+        // start of the list until we either empty it or hit a batch
+        // that is not yet deliverable
+        while ((batch = mAlarmBatches.get(0)) != null) {
+            if (batch.start > nowELAPSED) {
+                // Everything else is scheduled for the future
                 break;
             }
-            
-            // If the alarm is late, then print a warning message.
-            // Note that this can happen if the user creates a new event on
-            // the Calendar app with a reminder that is in the past. In that
-            // case, the reminder alarm will fire immediately.
-            if (localLOGV && now - alarm.when > LATE_ALARM_THRESHOLD) {
-                Slog.v(TAG, "alarm is late! alarm time: " + alarm.when
-                        + " now: " + now + " delay (in seconds): "
-                        + (now - alarm.when) / 1000);
-            }
 
-            // Recurring alarms may have passed several alarm intervals while the
-            // phone was asleep or off, so pass a trigger count when sending them.
-            if (localLOGV) Slog.v(TAG, "Alarm triggering: " + alarm);
-            alarm.count = 1;
-            if (alarm.repeatInterval > 0) {
-                // this adjustment will be zero if we're late by
-                // less than one full repeat interval
-                alarm.count += (now - alarm.when) / alarm.repeatInterval;
-            }
-            triggerList.add(alarm);
-            
-            // remove the alarm from the list
-            alarmList.remove(i);
-            i--;
+            // We will (re)schedule some alarms now; don't let that interfere
+            // with delivery of this current batch
+            mAlarmBatches.remove(0);
 
-            // if it repeats queue it up to be read-added to the list
-            if (alarm.repeatInterval > 0) {
-                if (repeats == null) {
-                    repeats = new ArrayList<Alarm>();
+            final int N = batch.size();
+            for (int i = 0; i < N; i++) {
+                Alarm alarm = batch.get(i);
+                alarm.count = 1;
+                triggerList.add(alarm);
+
+                // Recurring alarms may have passed several alarm intervals while the
+                // phone was asleep or off, so pass a trigger count when sending them.
+                if (alarm.repeatInterval > 0) {
+                    // this adjustment will be zero if we're late by
+                    // less than one full repeat interval
+                    alarm.count += (nowELAPSED - alarm.whenElapsed) / alarm.repeatInterval;
+
+                    // Also schedule its next recurrence
+                    final long delta = alarm.count * alarm.repeatInterval;
+                    final long nextElapsed = alarm.whenElapsed + delta;
+                    setImplLocked(alarm.type, alarm.when + delta, nextElapsed,
+                            maxTriggerTime(nowELAPSED, nextElapsed, alarm.repeatInterval),
+                            alarm.repeatInterval, alarm.operation, batch.standalone);
                 }
-                repeats.add(alarm);
-            }
-        }
 
-        // reset any repeating alarms.
-        if (repeats != null) {
-            for (int i=0; i<repeats.size(); i++) {
-                Alarm alarm = repeats.get(i);
-                alarm.when += alarm.count * alarm.repeatInterval;
-                addAlarmLocked(alarm);
             }
         }
-        
-        if (alarmList.size() > 0) {
-            setLocked(alarmList.get(0));
-        }
     }
-    
+
     /**
      * This Comparator sorts Alarms into increasing time order.
      */
@@ -783,15 +968,21 @@
         public int type;
         public int count;
         public long when;
+        public long whenElapsed;    // 'when' in the elapsed time base
+        public long maxWhen;        // also in the elapsed time base
         public long repeatInterval;
         public PendingIntent operation;
         
-        public Alarm() {
-            when = 0;
-            repeatInterval = 0;
-            operation = null;
+        public Alarm(int _type, long _when, long _whenElapsed, long _maxWhen,
+                long _interval, PendingIntent _op) {
+            type = _type;
+            when = _when;
+            whenElapsed = _whenElapsed;
+            maxWhen = _maxWhen;
+            repeatInterval = _interval;
+            operation = _op;
         }
-        
+
         @Override
         public String toString()
         {
@@ -808,6 +999,7 @@
 
         public void dump(PrintWriter pw, String prefix, long now) {
             pw.print(prefix); pw.print("type="); pw.print(type);
+                    pw.print(" whenElapsed="); pw.print(whenElapsed);
                     pw.print(" when="); TimeUtils.formatDuration(when, now, pw);
                     pw.print(" repeatInterval="); pw.print(repeatInterval);
                     pw.print(" count="); pw.println(count);
@@ -815,16 +1007,22 @@
         }
     }
 
-    void recordWakeupAlarms(ArrayList<Alarm> alarms, long now, long skewToRTC) {
-        for (Alarm a : alarms) {
-            if (a.when > now) {
+    void recordWakeupAlarms(ArrayList<Batch> batches, long nowELAPSED, long nowRTC) {
+        final int numBatches = batches.size();
+        for (int i = 0; i < numBatches; i++) {
+            Batch b = batches.get(i);
+            if (b.start > nowELAPSED) {
                 break;
             }
 
-            WakeupEvent e = new WakeupEvent(now + skewToRTC,
-                    a.operation.getCreatorUid(),
-                    a.operation.getIntent().getAction());
-            mRecentWakeups.add(e);
+            final int numAlarms = b.alarms.size();
+            for (int j = 0; j < numAlarms; j++) {
+                Alarm a = b.alarms.get(i);
+                WakeupEvent e = new WakeupEvent(nowRTC,
+                        a.operation.getCreatorUid(),
+                        a.operation.getIntent().getAction());
+                mRecentWakeups.add(e);
+            }
         }
     }
 
@@ -847,6 +1045,7 @@
 
                 if ((result & TIME_CHANGED_MASK) != 0) {
                     remove(mTimeTickSender);
+                    rebatchAllAlarms();
                     mClockReceiver.scheduleTimeTickEvent();
                     Intent intent = new Intent(Intent.ACTION_TIME_CHANGED);
                     intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
@@ -873,28 +1072,14 @@
                                 mRecentWakeups.remove();
                             }
 
-                            recordWakeupAlarms(mRtcWakeupAlarms,
-                                    nowRTC,
-                                    0);
-                            recordWakeupAlarms(mElapsedRealtimeWakeupAlarms,
-                                    nowELAPSED,
-                                    nowRTC - nowELAPSED);
+                            recordWakeupAlarms(mAlarmBatches, nowELAPSED, nowRTC);
                         }
                     }
 
-                    if ((result & RTC_WAKEUP_MASK) != 0)
-                        triggerAlarmsLocked(mRtcWakeupAlarms, triggerList, nowRTC);
-                    
-                    if ((result & RTC_MASK) != 0)
-                        triggerAlarmsLocked(mRtcAlarms, triggerList, nowRTC);
-                    
-                    if ((result & ELAPSED_REALTIME_WAKEUP_MASK) != 0)
-                        triggerAlarmsLocked(mElapsedRealtimeWakeupAlarms, triggerList, nowELAPSED);
-                    
-                    if ((result & ELAPSED_REALTIME_MASK) != 0)
-                        triggerAlarmsLocked(mElapsedRealtimeAlarms, triggerList, nowELAPSED);
-                    
-                    // now trigger the alarms
+                    triggerAlarmsLocked(triggerList, nowELAPSED, nowRTC);
+                    rescheduleKernelAlarmsLocked();
+
+                    // now deliver the alarm intents
                     for (int i=0; i<triggerList.size(); i++) {
                         Alarm alarm = triggerList.get(i);
                         try {
@@ -930,8 +1115,8 @@
                             } else {
                                 fs.nesting++;
                             }
-                            if (alarm.type == AlarmManager.ELAPSED_REALTIME_WAKEUP
-                                    || alarm.type == AlarmManager.RTC_WAKEUP) {
+                            if (alarm.type == ELAPSED_REALTIME_WAKEUP
+                                    || alarm.type == RTC_WAKEUP) {
                                 bs.numWakeup++;
                                 fs.numWakeup++;
                                 ActivityManagerNative.noteWakeupAlarm(
@@ -980,10 +1165,8 @@
                 ArrayList<Alarm> triggerList = new ArrayList<Alarm>();
                 synchronized (mLock) {
                     final long nowRTC = System.currentTimeMillis();
-                    triggerAlarmsLocked(mRtcWakeupAlarms, triggerList, nowRTC);
-                    triggerAlarmsLocked(mRtcAlarms, triggerList, nowRTC);
-                    triggerAlarmsLocked(mElapsedRealtimeWakeupAlarms, triggerList, nowRTC);
-                    triggerAlarmsLocked(mElapsedRealtimeAlarms, triggerList, nowRTC);
+                    final long nowELAPSED = SystemClock.elapsedRealtime();
+                    triggerAlarmsLocked(triggerList, nowELAPSED, nowRTC);
                 }
                 
                 // now trigger the alarms without the lock held
@@ -1035,8 +1218,8 @@
             // the top of the next minute.
             final long tickEventDelay = nextTime - currentTime;
 
-            set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + tickEventDelay,
-                    mTimeTickSender);
+            set(ELAPSED_REALTIME, SystemClock.elapsedRealtime() + tickEventDelay,
+                    0, mTimeTickSender, true, true);
         }
 	
         public void scheduleDateChangedEvent() {
@@ -1048,7 +1231,7 @@
             calendar.set(Calendar.MILLISECOND, 0);
             calendar.add(Calendar.DAY_OF_MONTH, 1);
       
-            set(AlarmManager.RTC, calendar.getTimeInMillis(), mDateChangeSender);
+            set(RTC, calendar.getTimeInMillis(), 0, mDateChangeSender, true, true);
         }
     }