Fix alarm delivery-order sorting

We also refine the order of delivery within any given package.  Now,
we identify which apps have wakeup alarms being delivered in the
current pass, and deliver all of that app's alarms before moving
on to alarm delivery to apps who are only receiving non-wakeup alarms
in the current delivery pass.  The TIME_TICK alarm is also hoisted to
the start of the current delivery pass if present.

Bug 17778686

Change-Id: I6306a00fe657787a77d0254c0807ac51e810fdcf
diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java
index 47396bd..8b524dd 100644
--- a/services/core/java/com/android/server/AlarmManagerService.java
+++ b/services/core/java/com/android/server/AlarmManagerService.java
@@ -58,6 +58,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.Locale;
 import java.util.TimeZone;
@@ -141,6 +142,25 @@
     private final SparseArray<AlarmManager.AlarmClockInfo> mHandlerSparseAlarmClockArray =
             new SparseArray<>();
 
+    // Alarm delivery ordering bookkeeping
+    static final int PRIO_TICK = 0;
+    static final int PRIO_WAKEUP = 1;
+    static final int PRIO_NORMAL = 2;
+
+    class PriorityClass {
+        int seq;
+        int priority;
+
+        PriorityClass() {
+            seq = mCurrentSeq - 1;
+            priority = PRIO_NORMAL;
+        }
+    }
+
+    final HashMap<String, PriorityClass> mPriorities =
+            new HashMap<String, PriorityClass>();
+    int mCurrentSeq = 0;
+
     class WakeupEvent {
         public long when;
         public int uid;
@@ -356,22 +376,62 @@
     final Comparator<Alarm> mAlarmDispatchComparator = new Comparator<Alarm>() {
         @Override
         public int compare(Alarm lhs, Alarm rhs) {
-            if ((!lhs.operation.getCreatorPackage().equals(rhs.operation.getCreatorPackage()))
-                    && lhs.wakeup != rhs.wakeup) {
-                // We want to put wakeup alarms before non-wakeup alarms, since they are
-                // the things that drive most activity in the alarm manager.  However,
-                // alarms from the same package should always be ordered strictly by time.
-                return lhs.wakeup ? -1 : 1;
+            // priority class trumps everything.  TICK < WAKEUP < NORMAL
+            if (lhs.priorityClass.priority < rhs.priorityClass.priority) {
+                return -1;
+            } else if (lhs.priorityClass.priority > rhs.priorityClass.priority) {
+                return 1;
             }
+
+            // within each class, sort by nominal delivery time
             if (lhs.whenElapsed < rhs.whenElapsed) {
                 return -1;
             } else if (lhs.whenElapsed > rhs.whenElapsed) {
                 return 1;
             }
+
+            // same priority class + same target delivery time
             return 0;
         }
     };
 
+    void calculateDeliveryPriorities(ArrayList<Alarm> alarms) {
+        final int N = alarms.size();
+        for (int i = 0; i < N; i++) {
+            Alarm a = alarms.get(i);
+
+            final int alarmPrio;
+            if (Intent.ACTION_TIME_TICK.equals(a.operation.getIntent().getAction())) {
+                alarmPrio = PRIO_TICK;
+            } else if (a.wakeup) {
+                alarmPrio = PRIO_WAKEUP;
+            } else {
+                alarmPrio = PRIO_NORMAL;
+            }
+
+            PriorityClass packagePrio = a.priorityClass;
+            if (packagePrio == null) packagePrio = mPriorities.get(a.operation.getCreatorPackage());
+            if (packagePrio == null) {
+                packagePrio = a.priorityClass = new PriorityClass(); // lowest prio & stale sequence
+                mPriorities.put(a.operation.getCreatorPackage(), packagePrio);
+            }
+            a.priorityClass = packagePrio;
+
+            if (packagePrio.seq != mCurrentSeq) {
+                // first alarm we've seen in the current delivery generation from this package
+                packagePrio.priority = alarmPrio;
+                packagePrio.seq = mCurrentSeq;
+            } else {
+                // Multiple alarms from this package being delivered in this generation;
+                // bump the package's delivery class if it's warranted.
+                // TICK < WAKEUP < NORMAL
+                if (alarmPrio < packagePrio.priority) {
+                    packagePrio.priority = alarmPrio;
+                }
+            }
+        }
+    }
+
     // minimum recurrence period or alarm futurity for us to be able to fuzz it
     static final long MIN_FUZZABLE_INTERVAL = 10000;
     static final BatchTimeOrder sBatchOrder = new BatchTimeOrder();
@@ -1381,6 +1441,10 @@
             }
         }
 
+        // This is a new alarm delivery set; bump the sequence number to indicate that
+        // all apps' alarm delivery classes should be recalculated.
+        mCurrentSeq++;
+        calculateDeliveryPriorities(triggerList);
         Collections.sort(triggerList, mAlarmDispatchComparator);
 
         if (localLOGV) {
@@ -1423,6 +1487,7 @@
         public long repeatInterval;
         public final AlarmManager.AlarmClockInfo alarmClock;
         public final int userId;
+        public PriorityClass priorityClass;
 
         public Alarm(int _type, long _when, long _whenElapsed, long _windowLength, long _maxWhen,
                 long _interval, PendingIntent _op, WorkSource _ws,
@@ -1676,6 +1741,7 @@
                         rescheduleKernelAlarmsLocked();
                         updateNextAlarmClockLocked();
                         if (mPendingNonWakeupAlarms.size() > 0) {
+                            calculateDeliveryPriorities(mPendingNonWakeupAlarms);
                             triggerList.addAll(mPendingNonWakeupAlarms);
                             Collections.sort(triggerList, mAlarmDispatchComparator);
                             final long thisDelayTime = nowELAPSED - mStartCurrentDelayTime;
@@ -1889,6 +1955,7 @@
                 if (pkgList != null && (pkgList.length > 0)) {
                     for (String pkg : pkgList) {
                         removeLocked(pkg);
+                        mPriorities.remove(pkg);
                         for (int i=mBroadcastStats.size()-1; i>=0; i--) {
                             ArrayMap<String, BroadcastStats> uidStats = mBroadcastStats.valueAt(i);
                             if (uidStats.remove(pkg) != null) {