1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.alarm;
18 
19 import static com.android.server.alarm.AlarmManagerService.DEBUG_BATCH;
20 import static com.android.server.alarm.AlarmManagerService.clampPositive;
21 import static com.android.server.alarm.AlarmManagerService.dumpAlarmList;
22 import static com.android.server.alarm.AlarmManagerService.isTimeTickAlarm;
23 
24 import android.app.AlarmManager;
25 import android.util.IndentingPrintWriter;
26 import android.util.Slog;
27 import android.util.proto.ProtoOutputStream;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.util.StatLogger;
31 
32 import java.text.SimpleDateFormat;
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.Comparator;
36 import java.util.function.Predicate;
37 
38 /**
39  * Batching implementation of an Alarm Store.
40  * This keeps the alarms in batches, which are sorted on the start time of their delivery window.
41  */
42 public class BatchingAlarmStore implements AlarmStore {
43     @VisibleForTesting
44     static final String TAG = BatchingAlarmStore.class.getSimpleName();
45 
46     private final ArrayList<Batch> mAlarmBatches = new ArrayList<>();
47     private int mSize;
48     private Runnable mOnAlarmClockRemoved;
49 
50     interface Stats {
51         int REBATCH_ALL_ALARMS = 0;
52         int GET_COUNT = 1;
53     }
54 
55     final StatLogger mStatLogger = new StatLogger(TAG + " stats", new String[]{
56             "REBATCH_ALL_ALARMS",
57             "GET_COUNT",
58     });
59 
60     private static final Comparator<Batch> sBatchOrder = Comparator.comparingLong(b -> b.mStart);
61 
62     private static final Comparator<Alarm> sIncreasingTimeOrder = Comparator.comparingLong(
63             Alarm::getWhenElapsed);
64 
65     @Override
add(Alarm a)66     public void add(Alarm a) {
67         insertAndBatchAlarm(a);
68         mSize++;
69     }
70 
71     @Override
addAll(ArrayList<Alarm> alarms)72     public void addAll(ArrayList<Alarm> alarms) {
73         if (alarms == null) {
74             return;
75         }
76         for (final Alarm a : alarms) {
77             add(a);
78         }
79     }
80 
81     @Override
remove(Predicate<Alarm> whichAlarms)82     public ArrayList<Alarm> remove(Predicate<Alarm> whichAlarms) {
83         final ArrayList<Alarm> removed = new ArrayList<>();
84         for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
85             final Batch b = mAlarmBatches.get(i);
86             removed.addAll(b.remove(whichAlarms));
87             if (b.size() == 0) {
88                 mAlarmBatches.remove(i);
89             }
90         }
91         if (!removed.isEmpty()) {
92             mSize -= removed.size();
93             // Not needed if only whole batches were removed, but keeping existing behavior.
94             rebatchAllAlarms();
95         }
96         return removed;
97     }
98 
99     @Override
setAlarmClockRemovalListener(Runnable listener)100     public void setAlarmClockRemovalListener(Runnable listener) {
101         mOnAlarmClockRemoved = listener;
102     }
103 
104     @Override
getNextWakeFromIdleAlarm()105     public Alarm getNextWakeFromIdleAlarm() {
106         for (final Batch batch : mAlarmBatches) {
107             if ((batch.mFlags & AlarmManager.FLAG_WAKE_FROM_IDLE) == 0) {
108                 continue;
109             }
110             for (int i = 0; i < batch.size(); i++) {
111                 final Alarm a = batch.get(i);
112                 if ((a.flags & AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
113                     return a;
114                 }
115             }
116         }
117         return null;
118     }
119 
rebatchAllAlarms()120     private void rebatchAllAlarms() {
121         final long start = mStatLogger.getTime();
122         final ArrayList<Batch> oldBatches = (ArrayList<Batch>) mAlarmBatches.clone();
123         mAlarmBatches.clear();
124         for (final Batch batch : oldBatches) {
125             for (int i = 0; i < batch.size(); i++) {
126                 insertAndBatchAlarm(batch.get(i));
127             }
128         }
129         mStatLogger.logDurationStat(Stats.REBATCH_ALL_ALARMS, start);
130     }
131 
132     @Override
size()133     public int size() {
134         return mSize;
135     }
136 
137     @Override
getNextWakeupDeliveryTime()138     public long getNextWakeupDeliveryTime() {
139         for (Batch b : mAlarmBatches) {
140             if (b.hasWakeups()) {
141                 return b.mStart;
142             }
143         }
144         return 0;
145     }
146 
147     @Override
getNextDeliveryTime()148     public long getNextDeliveryTime() {
149         if (mAlarmBatches.size() > 0) {
150             return mAlarmBatches.get(0).mStart;
151         }
152         return 0;
153     }
154 
155     @Override
removePendingAlarms(long nowElapsed)156     public ArrayList<Alarm> removePendingAlarms(long nowElapsed) {
157         final ArrayList<Alarm> removedAlarms = new ArrayList<>();
158         while (mAlarmBatches.size() > 0) {
159             final Batch batch = mAlarmBatches.get(0);
160             if (batch.mStart > nowElapsed) {
161                 break;
162             }
163             mAlarmBatches.remove(0);
164             for (int i = 0; i < batch.size(); i++) {
165                 removedAlarms.add(batch.get(i));
166             }
167         }
168         mSize -= removedAlarms.size();
169         return removedAlarms;
170     }
171 
172     @Override
updateAlarmDeliveries(AlarmDeliveryCalculator deliveryCalculator)173     public boolean updateAlarmDeliveries(AlarmDeliveryCalculator deliveryCalculator) {
174         boolean changed = false;
175         for (final Batch b : mAlarmBatches) {
176             for (int i = 0; i < b.size(); i++) {
177                 changed |= deliveryCalculator.updateAlarmDelivery(b.get(i));
178             }
179         }
180         if (changed) {
181             rebatchAllAlarms();
182         }
183         return changed;
184     }
185 
186     @Override
asList()187     public ArrayList<Alarm> asList() {
188         final ArrayList<Alarm> allAlarms = new ArrayList<>();
189         for (final Batch batch : mAlarmBatches) {
190             for (int i = 0; i < batch.size(); i++) {
191                 allAlarms.add(batch.get(i));
192             }
193         }
194         return allAlarms;
195     }
196 
197     @Override
dump(IndentingPrintWriter ipw, long nowElapsed, SimpleDateFormat sdf)198     public void dump(IndentingPrintWriter ipw, long nowElapsed, SimpleDateFormat sdf) {
199         ipw.print("Pending alarm batches: ");
200         ipw.println(mAlarmBatches.size());
201         for (Batch b : mAlarmBatches) {
202             ipw.print(b);
203             ipw.println(':');
204             ipw.increaseIndent();
205             dumpAlarmList(ipw, b.mAlarms, nowElapsed, sdf);
206             ipw.decreaseIndent();
207         }
208         mStatLogger.dump(ipw);
209     }
210 
211     @Override
dumpProto(ProtoOutputStream pos, long nowElapsed)212     public void dumpProto(ProtoOutputStream pos, long nowElapsed) {
213         for (Batch b : mAlarmBatches) {
214             b.dumpDebug(pos, AlarmManagerServiceDumpProto.PENDING_ALARM_BATCHES, nowElapsed);
215         }
216     }
217 
218     @Override
getName()219     public String getName() {
220         return TAG;
221     }
222 
223     @Override
getCount(Predicate<Alarm> condition)224     public int getCount(Predicate<Alarm> condition) {
225         long start = mStatLogger.getTime();
226 
227         int count = 0;
228         for (Batch b : mAlarmBatches) {
229             for (int i = 0; i < b.size(); i++) {
230                 if (condition.test(b.get(i))) {
231                     count++;
232                 }
233             }
234         }
235         mStatLogger.logDurationStat(Stats.GET_COUNT, start);
236         return count;
237     }
238 
insertAndBatchAlarm(Alarm alarm)239     private void insertAndBatchAlarm(Alarm alarm) {
240         final int whichBatch = ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) ? -1
241                 : attemptCoalesce(alarm.getWhenElapsed(), alarm.getMaxWhenElapsed());
242 
243         if (whichBatch < 0) {
244             addBatch(mAlarmBatches, new Batch(alarm));
245         } else {
246             final Batch batch = mAlarmBatches.get(whichBatch);
247             if (batch.add(alarm)) {
248                 // The start time of this batch advanced, so batch ordering may
249                 // have just been broken.  Move it to where it now belongs.
250                 mAlarmBatches.remove(whichBatch);
251                 addBatch(mAlarmBatches, batch);
252             }
253         }
254     }
255 
addBatch(ArrayList<Batch> list, Batch newBatch)256     static void addBatch(ArrayList<Batch> list, Batch newBatch) {
257         int index = Collections.binarySearch(list, newBatch, sBatchOrder);
258         if (index < 0) {
259             index = 0 - index - 1;
260         }
261         list.add(index, newBatch);
262     }
263 
264     // Return the index of the matching batch, or -1 if none found.
attemptCoalesce(long whenElapsed, long maxWhen)265     private int attemptCoalesce(long whenElapsed, long maxWhen) {
266         final int n = mAlarmBatches.size();
267         for (int i = 0; i < n; i++) {
268             Batch b = mAlarmBatches.get(i);
269             if ((b.mFlags & AlarmManager.FLAG_STANDALONE) == 0 && b.canHold(whenElapsed, maxWhen)) {
270                 return i;
271             }
272         }
273         return -1;
274     }
275 
276     final class Batch {
277         long mStart;     // These endpoints are always in ELAPSED
278         long mEnd;
279         int mFlags;      // Flags for alarms, such as FLAG_STANDALONE.
280 
281         final ArrayList<Alarm> mAlarms = new ArrayList<>();
282 
Batch(Alarm seed)283         Batch(Alarm seed) {
284             mStart = seed.getWhenElapsed();
285             mEnd = clampPositive(seed.getMaxWhenElapsed());
286             mFlags = seed.flags;
287             mAlarms.add(seed);
288         }
289 
size()290         int size() {
291             return mAlarms.size();
292         }
293 
get(int index)294         Alarm get(int index) {
295             return mAlarms.get(index);
296         }
297 
canHold(long whenElapsed, long maxWhen)298         boolean canHold(long whenElapsed, long maxWhen) {
299             return (mEnd >= whenElapsed) && (mStart <= maxWhen);
300         }
301 
add(Alarm alarm)302         boolean add(Alarm alarm) {
303             boolean newStart = false;
304             // narrows the batch if necessary; presumes that canHold(alarm) is true
305             int index = Collections.binarySearch(mAlarms, alarm, sIncreasingTimeOrder);
306             if (index < 0) {
307                 index = 0 - index - 1;
308             }
309             mAlarms.add(index, alarm);
310             if (DEBUG_BATCH) {
311                 Slog.v(TAG, "Adding " + alarm + " to " + this);
312             }
313             if (alarm.getWhenElapsed() > mStart) {
314                 mStart = alarm.getWhenElapsed();
315                 newStart = true;
316             }
317             if (alarm.getMaxWhenElapsed() < mEnd) {
318                 mEnd = alarm.getMaxWhenElapsed();
319             }
320             mFlags |= alarm.flags;
321 
322             if (DEBUG_BATCH) {
323                 Slog.v(TAG, "    => now " + this);
324             }
325             return newStart;
326         }
327 
remove(Predicate<Alarm> predicate)328         ArrayList<Alarm> remove(Predicate<Alarm> predicate) {
329             final ArrayList<Alarm> removed = new ArrayList<>();
330             long newStart = 0;  // recalculate endpoints as we go
331             long newEnd = Long.MAX_VALUE;
332             int newFlags = 0;
333             for (int i = 0; i < mAlarms.size(); ) {
334                 Alarm alarm = mAlarms.get(i);
335                 if (predicate.test(alarm)) {
336                     removed.add(mAlarms.remove(i));
337                     if (alarm.alarmClock != null && mOnAlarmClockRemoved != null) {
338                         mOnAlarmClockRemoved.run();
339                     }
340                     if (isTimeTickAlarm(alarm)) {
341                         // This code path is not invoked when delivering alarms, only when removing
342                         // alarms due to the caller cancelling it or getting uninstalled, etc.
343                         Slog.wtf(TAG, "Removed TIME_TICK alarm");
344                     }
345                 } else {
346                     if (alarm.getWhenElapsed() > newStart) {
347                         newStart = alarm.getWhenElapsed();
348                     }
349                     if (alarm.getMaxWhenElapsed() < newEnd) {
350                         newEnd = alarm.getMaxWhenElapsed();
351                     }
352                     newFlags |= alarm.flags;
353                     i++;
354                 }
355             }
356             if (!removed.isEmpty()) {
357                 // commit the new batch bounds
358                 mStart = newStart;
359                 mEnd = newEnd;
360                 mFlags = newFlags;
361             }
362             return removed;
363         }
364 
hasWakeups()365         boolean hasWakeups() {
366             final int n = mAlarms.size();
367             for (int i = 0; i < n; i++) {
368                 Alarm a = mAlarms.get(i);
369                 if (a.wakeup) {
370                     return true;
371                 }
372             }
373             return false;
374         }
375 
376         @Override
toString()377         public String toString() {
378             StringBuilder b = new StringBuilder(40);
379             b.append("Batch{");
380             b.append(Integer.toHexString(this.hashCode()));
381             b.append(" num=");
382             b.append(size());
383             b.append(" start=");
384             b.append(mStart);
385             b.append(" end=");
386             b.append(mEnd);
387             if (mFlags != 0) {
388                 b.append(" flgs=0x");
389                 b.append(Integer.toHexString(mFlags));
390             }
391             b.append('}');
392             return b.toString();
393         }
394 
dumpDebug(ProtoOutputStream proto, long fieldId, long nowElapsed)395         public void dumpDebug(ProtoOutputStream proto, long fieldId, long nowElapsed) {
396             final long token = proto.start(fieldId);
397 
398             proto.write(BatchProto.START_REALTIME, mStart);
399             proto.write(BatchProto.END_REALTIME, mEnd);
400             proto.write(BatchProto.FLAGS, mFlags);
401             for (Alarm a : mAlarms) {
402                 a.dumpDebug(proto, BatchProto.ALARMS, nowElapsed);
403             }
404 
405             proto.end(token);
406         }
407     }
408 }
409