1 /*
2  * Copyright (C) 2019 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.utils.quota;
18 
19 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
20 
21 import static com.android.server.utils.quota.Uptc.string;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.app.AlarmManager;
26 import android.content.Context;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.os.Message;
30 import android.util.ArrayMap;
31 import android.util.IndentingPrintWriter;
32 import android.util.LongArrayQueue;
33 import android.util.Slog;
34 import android.util.TimeUtils;
35 import android.util.proto.ProtoOutputStream;
36 import android.util.quota.CountQuotaTrackerProto;
37 
38 import com.android.internal.annotations.GuardedBy;
39 import com.android.internal.annotations.VisibleForTesting;
40 
41 import java.util.function.Consumer;
42 import java.util.function.Function;
43 
44 /**
45  * Class that tracks whether an app has exceeded its defined count quota.
46  *
47  * Quotas are applied per userId-package-tag combination (UPTC). Tags can be null.
48  *
49  * This tracker tracks the count of instantaneous events.
50  *
51  * Limits are applied according to the category the UPTC is placed in. If a UPTC reaches its limit,
52  * it will be considered out of quota until it is below that limit again. A {@link Category} is a
53  * basic construct to apply different limits to different groups of UPTCs. For example, standby
54  * buckets can be a set of categories, or foreground & background could be two categories. If every
55  * UPTC should have the same limits applied, then only one category is needed
56  * ({@see Category.SINGLE_CATEGORY}).
57  *
58  * Note: all limits are enforced per category unless explicitly stated otherwise.
59  *
60  * Test: atest com.android.server.utils.quota.CountQuotaTrackerTest
61  *
62  * @hide
63  */
64 public class CountQuotaTracker extends QuotaTracker {
65     private static final String TAG = CountQuotaTracker.class.getSimpleName();
66     private static final boolean DEBUG = false;
67 
68     private static final String ALARM_TAG_CLEANUP = "*" + TAG + ".cleanup*";
69 
70     @VisibleForTesting
71     static class ExecutionStats {
72         /**
73          * The time after which this record should be considered invalid (out of date), in the
74          * elapsed realtime timebase.
75          */
76         public long expirationTimeElapsed;
77 
78         /** The window size that's used when counting the number of events. */
79         public long windowSizeMs;
80         /** The maximum number of events allowed within the window size. */
81         public int countLimit;
82 
83         /** The total number of events that occurred in the window. */
84         public int countInWindow;
85 
86         /**
87          * The time after which the app will be under the category quota again. This is only valid
88          * if {@link #countInWindow} >= {@link #countLimit}.
89          */
90         public long inQuotaTimeElapsed;
91 
92         @Override
toString()93         public String toString() {
94             return "expirationTime=" + expirationTimeElapsed + ", "
95                     + "windowSizeMs=" + windowSizeMs + ", "
96                     + "countLimit=" + countLimit + ", "
97                     + "countInWindow=" + countInWindow + ", "
98                     + "inQuotaTime=" + inQuotaTimeElapsed;
99         }
100 
101         @Override
equals(Object obj)102         public boolean equals(Object obj) {
103             if (obj instanceof ExecutionStats) {
104                 ExecutionStats other = (ExecutionStats) obj;
105                 return this.expirationTimeElapsed == other.expirationTimeElapsed
106                         && this.windowSizeMs == other.windowSizeMs
107                         && this.countLimit == other.countLimit
108                         && this.countInWindow == other.countInWindow
109                         && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed;
110             }
111             return false;
112         }
113 
114         @Override
hashCode()115         public int hashCode() {
116             int result = 0;
117             result = 31 * result + Long.hashCode(expirationTimeElapsed);
118             result = 31 * result + Long.hashCode(windowSizeMs);
119             result = 31 * result + countLimit;
120             result = 31 * result + countInWindow;
121             result = 31 * result + Long.hashCode(inQuotaTimeElapsed);
122             return result;
123         }
124     }
125 
126     /** List of times of all instantaneous events for a UPTC, in chronological order. */
127     // TODO(146148168): introduce a bucketized mode that's more efficient but less accurate
128     @GuardedBy("mLock")
129     private final UptcMap<LongArrayQueue> mEventTimes = new UptcMap<>();
130 
131     /** Cached calculation results for each app. */
132     @GuardedBy("mLock")
133     private final UptcMap<ExecutionStats> mExecutionStatsCache = new UptcMap<>();
134 
135     private final Handler mHandler;
136 
137     @GuardedBy("mLock")
138     private long mNextCleanupTimeElapsed = 0;
139     @GuardedBy("mLock")
140     private final AlarmManager.OnAlarmListener mEventCleanupAlarmListener = () ->
141             CountQuotaTracker.this.mHandler.obtainMessage(MSG_CLEAN_UP_EVENTS).sendToTarget();
142 
143     /** The rolling window size for each Category's count limit. */
144     @GuardedBy("mLock")
145     private final ArrayMap<Category, Long> mCategoryCountWindowSizesMs = new ArrayMap<>();
146 
147     /**
148      * The maximum count for each Category. For each max value count in the map, the app will
149      * not be allowed any more events within the latest time interval of its rolling window size.
150      *
151      * @see #mCategoryCountWindowSizesMs
152      */
153     @GuardedBy("mLock")
154     private final ArrayMap<Category, Integer> mMaxCategoryCounts = new ArrayMap<>();
155 
156     /** The longest period a registered category applies to. */
157     @GuardedBy("mLock")
158     private long mMaxPeriodMs = 0;
159 
160     /** Drop any old events. */
161     private static final int MSG_CLEAN_UP_EVENTS = 1;
162 
CountQuotaTracker(@onNull Context context, @NonNull Categorizer categorizer)163     public CountQuotaTracker(@NonNull Context context, @NonNull Categorizer categorizer) {
164         this(context, categorizer, new Injector());
165     }
166 
167     @VisibleForTesting
CountQuotaTracker(@onNull Context context, @NonNull Categorizer categorizer, Injector injector)168     CountQuotaTracker(@NonNull Context context, @NonNull Categorizer categorizer,
169             Injector injector) {
170         super(context, categorizer, injector);
171 
172         mHandler = new CqtHandler(context.getMainLooper());
173     }
174 
175     // Exposed API to users.
176 
177     /**
178      * Record that an instantaneous event happened.
179      *
180      * @return true if the UPTC is within quota, false otherwise.
181      */
noteEvent(int userId, @NonNull String packageName, @Nullable String tag)182     public boolean noteEvent(int userId, @NonNull String packageName, @Nullable String tag) {
183         synchronized (mLock) {
184             if (!isEnabledLocked() || isQuotaFreeLocked(userId, packageName)) {
185                 return true;
186             }
187             final long nowElapsed = mInjector.getElapsedRealtime();
188 
189             final LongArrayQueue times = mEventTimes
190                     .getOrCreate(userId, packageName, tag, mCreateLongArrayQueue);
191             times.addLast(nowElapsed);
192             final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, tag);
193             stats.countInWindow++;
194             stats.expirationTimeElapsed = Math.min(stats.expirationTimeElapsed,
195                     nowElapsed + stats.windowSizeMs);
196             if (stats.countInWindow == stats.countLimit) {
197                 final long windowEdgeElapsed = nowElapsed - stats.windowSizeMs;
198                 while (times.size() > 0 && times.peekFirst() < windowEdgeElapsed) {
199                     times.removeFirst();
200                 }
201                 stats.inQuotaTimeElapsed = times.peekFirst() + stats.windowSizeMs;
202                 postQuotaStatusChanged(userId, packageName, tag);
203             } else if (stats.countLimit > 9
204                     && stats.countInWindow == stats.countLimit * 4 / 5) {
205                 // TODO: log high watermark to statsd
206                 Slog.w(TAG, string(userId, packageName, tag)
207                         + " has reached 80% of it's count limit of " + stats.countLimit);
208             }
209             maybeScheduleCleanupAlarmLocked();
210             return isWithinQuotaLocked(stats);
211         }
212     }
213 
214     /**
215      * Set count limit over a rolling time window for the specified category.
216      *
217      * @param category     The category these limits apply to.
218      * @param limit        The maximum event count an app can have in the rolling window. Must be
219      *                     nonnegative.
220      * @param timeWindowMs The rolling time window (in milliseconds) to use when checking quota
221      *                     usage. Must be at least {@value #MIN_WINDOW_SIZE_MS} and no longer than
222      *                     {@value #MAX_WINDOW_SIZE_MS}
223      */
setCountLimit(@onNull Category category, int limit, long timeWindowMs)224     public void setCountLimit(@NonNull Category category, int limit, long timeWindowMs) {
225         if (limit < 0 || timeWindowMs < 0) {
226             throw new IllegalArgumentException("Limit and window size must be nonnegative.");
227         }
228         synchronized (mLock) {
229             final Integer oldLimit = mMaxCategoryCounts.put(category, limit);
230             final long newWindowSizeMs = Math.max(MIN_WINDOW_SIZE_MS,
231                     Math.min(timeWindowMs, MAX_WINDOW_SIZE_MS));
232             final Long oldWindowSizeMs = mCategoryCountWindowSizesMs.put(category, newWindowSizeMs);
233             if (oldLimit != null && oldWindowSizeMs != null
234                     && oldLimit == limit && oldWindowSizeMs == newWindowSizeMs) {
235                 // No change.
236                 return;
237             }
238             mDeleteOldEventTimesFunctor.updateMaxPeriod();
239             mMaxPeriodMs = mDeleteOldEventTimesFunctor.mMaxPeriodMs;
240             invalidateAllExecutionStatsLocked();
241         }
242         scheduleQuotaCheck();
243     }
244 
245     /**
246      * Gets the count limit for the specified category.
247      */
getLimit(@onNull Category category)248     public int getLimit(@NonNull Category category) {
249         synchronized (mLock) {
250             final Integer limit = mMaxCategoryCounts.get(category);
251             if (limit == null) {
252                 throw new IllegalArgumentException("Limit for " + category + " not defined");
253             }
254             return limit;
255         }
256     }
257 
258     /**
259      * Gets the count time window for the specified category.
260      */
getWindowSizeMs(@onNull Category category)261     public long getWindowSizeMs(@NonNull Category category) {
262         synchronized (mLock) {
263             final Long limitMs = mCategoryCountWindowSizesMs.get(category);
264             if (limitMs == null) {
265                 throw new IllegalArgumentException("Limit for " + category + " not defined");
266             }
267             return limitMs;
268         }
269     }
270 
271     // Internal implementation.
272 
273     @Override
274     @GuardedBy("mLock")
dropEverythingLocked()275     void dropEverythingLocked() {
276         mExecutionStatsCache.clear();
277         mEventTimes.clear();
278     }
279 
280     @Override
281     @GuardedBy("mLock")
282     @NonNull
getHandler()283     Handler getHandler() {
284         return mHandler;
285     }
286 
287     @Override
288     @GuardedBy("mLock")
getInQuotaTimeElapsedLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)289     long getInQuotaTimeElapsedLocked(final int userId, @NonNull final String packageName,
290             @Nullable final String tag) {
291         return getExecutionStatsLocked(userId, packageName, tag).inQuotaTimeElapsed;
292     }
293 
294     @Override
295     @GuardedBy("mLock")
handleRemovedAppLocked(final int userId, @NonNull String packageName)296     void handleRemovedAppLocked(final int userId, @NonNull String packageName) {
297         if (packageName == null) {
298             Slog.wtf(TAG, "Told app removed but given null package name.");
299             return;
300         }
301 
302         mEventTimes.delete(userId, packageName);
303         mExecutionStatsCache.delete(userId, packageName);
304     }
305 
306     @Override
307     @GuardedBy("mLock")
handleRemovedUserLocked(int userId)308     void handleRemovedUserLocked(int userId) {
309         mEventTimes.delete(userId);
310         mExecutionStatsCache.delete(userId);
311     }
312 
313     @Override
314     @GuardedBy("mLock")
isWithinQuotaLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)315     boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
316             @Nullable final String tag) {
317         if (!isEnabledLocked()) return true;
318 
319         // Quota constraint is not enforced when quota is free.
320         if (isQuotaFreeLocked(userId, packageName)) {
321             return true;
322         }
323 
324         return isWithinQuotaLocked(getExecutionStatsLocked(userId, packageName, tag));
325     }
326 
327     @Override
328     @GuardedBy("mLock")
maybeUpdateAllQuotaStatusLocked()329     void maybeUpdateAllQuotaStatusLocked() {
330         final UptcMap<Boolean> doneMap = new UptcMap<>();
331         mEventTimes.forEach((userId, packageName, tag, events) -> {
332             if (!doneMap.contains(userId, packageName, tag)) {
333                 maybeUpdateStatusForUptcLocked(userId, packageName, tag);
334                 doneMap.add(userId, packageName, tag, Boolean.TRUE);
335             }
336         });
337 
338     }
339 
340     @Override
maybeUpdateQuotaStatus(final int userId, @NonNull final String packageName, @Nullable final String tag)341     void maybeUpdateQuotaStatus(final int userId, @NonNull final String packageName,
342             @Nullable final String tag) {
343         synchronized (mLock) {
344             maybeUpdateStatusForUptcLocked(userId, packageName, tag);
345         }
346     }
347 
348     @Override
349     @GuardedBy("mLock")
onQuotaFreeChangedLocked(boolean isFree)350     void onQuotaFreeChangedLocked(boolean isFree) {
351         // Nothing to do here.
352     }
353 
354     @Override
355     @GuardedBy("mLock")
onQuotaFreeChangedLocked(int userId, @NonNull String packageName, boolean isFree)356     void onQuotaFreeChangedLocked(int userId, @NonNull String packageName, boolean isFree) {
357         maybeUpdateStatusForPkgLocked(userId, packageName);
358     }
359 
360     @GuardedBy("mLock")
isWithinQuotaLocked(@onNull final ExecutionStats stats)361     private boolean isWithinQuotaLocked(@NonNull final ExecutionStats stats) {
362         return isUnderCountQuotaLocked(stats);
363     }
364 
365     @GuardedBy("mLock")
isUnderCountQuotaLocked(@onNull ExecutionStats stats)366     private boolean isUnderCountQuotaLocked(@NonNull ExecutionStats stats) {
367         return stats.countInWindow < stats.countLimit;
368     }
369 
370     /** Returns the execution stats of the app in the most recent window. */
371     @GuardedBy("mLock")
372     @VisibleForTesting
373     @NonNull
getExecutionStatsLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)374     ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName,
375             @Nullable final String tag) {
376         return getExecutionStatsLocked(userId, packageName, tag, true);
377     }
378 
379     @GuardedBy("mLock")
380     @NonNull
getExecutionStatsLocked(final int userId, @NonNull final String packageName, @Nullable String tag, final boolean refreshStatsIfOld)381     private ExecutionStats getExecutionStatsLocked(final int userId,
382             @NonNull final String packageName, @Nullable String tag,
383             final boolean refreshStatsIfOld) {
384         final ExecutionStats stats =
385                 mExecutionStatsCache.getOrCreate(userId, packageName, tag, mCreateExecutionStats);
386         if (refreshStatsIfOld) {
387             final Category category = mCategorizer.getCategory(userId, packageName, tag);
388             final long countWindowSizeMs = mCategoryCountWindowSizesMs.getOrDefault(category,
389                     Long.MAX_VALUE);
390             final int countLimit = mMaxCategoryCounts.getOrDefault(category, Integer.MAX_VALUE);
391             if (stats.expirationTimeElapsed <= mInjector.getElapsedRealtime()
392                     || stats.windowSizeMs != countWindowSizeMs
393                     || stats.countLimit != countLimit) {
394                 // The stats are no longer valid.
395                 stats.windowSizeMs = countWindowSizeMs;
396                 stats.countLimit = countLimit;
397                 updateExecutionStatsLocked(userId, packageName, tag, stats);
398             }
399         }
400 
401         return stats;
402     }
403 
404     @GuardedBy("mLock")
405     @VisibleForTesting
updateExecutionStatsLocked(final int userId, @NonNull final String packageName, @Nullable final String tag, @NonNull ExecutionStats stats)406     void updateExecutionStatsLocked(final int userId, @NonNull final String packageName,
407             @Nullable final String tag, @NonNull ExecutionStats stats) {
408         stats.countInWindow = 0;
409         if (stats.countLimit == 0) {
410             // UPTC won't be in quota until configuration changes.
411             stats.inQuotaTimeElapsed = Long.MAX_VALUE;
412         } else {
413             stats.inQuotaTimeElapsed = 0;
414         }
415 
416         // This can be used to determine when an app will have enough quota to transition from
417         // out-of-quota to in-quota.
418         final long nowElapsed = mInjector.getElapsedRealtime();
419         stats.expirationTimeElapsed = nowElapsed + mMaxPeriodMs;
420 
421         final LongArrayQueue events = mEventTimes.get(userId, packageName, tag);
422         if (events == null) {
423             return;
424         }
425 
426         // The minimum time between the start time and the beginning of the events that were
427         // looked at --> how much time the stats will be valid for.
428         long emptyTimeMs = Long.MAX_VALUE - nowElapsed;
429 
430         final long eventStartWindowElapsed = nowElapsed - stats.windowSizeMs;
431         for (int i = events.size() - 1; i >= 0; --i) {
432             final long eventTimeElapsed = events.get(i);
433             if (eventTimeElapsed < eventStartWindowElapsed) {
434                 // This event happened before the window. No point in going any further.
435                 break;
436             }
437             stats.countInWindow++;
438             emptyTimeMs = Math.min(emptyTimeMs, eventTimeElapsed - eventStartWindowElapsed);
439 
440             if (stats.countInWindow >= stats.countLimit) {
441                 stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
442                         eventTimeElapsed + stats.windowSizeMs);
443             }
444         }
445 
446         stats.expirationTimeElapsed = nowElapsed + emptyTimeMs;
447     }
448 
449     /** Invalidate ExecutionStats for all apps. */
450     @GuardedBy("mLock")
invalidateAllExecutionStatsLocked()451     private void invalidateAllExecutionStatsLocked() {
452         final long nowElapsed = mInjector.getElapsedRealtime();
453         mExecutionStatsCache.forEach((appStats) -> {
454             if (appStats != null) {
455                 appStats.expirationTimeElapsed = nowElapsed;
456             }
457         });
458     }
459 
460     @GuardedBy("mLock")
invalidateAllExecutionStatsLocked(final int userId, @NonNull final String packageName)461     private void invalidateAllExecutionStatsLocked(final int userId,
462             @NonNull final String packageName) {
463         final ArrayMap<String, ExecutionStats> appStats =
464                 mExecutionStatsCache.get(userId, packageName);
465         if (appStats != null) {
466             final long nowElapsed = mInjector.getElapsedRealtime();
467             final int numStats = appStats.size();
468             for (int i = 0; i < numStats; ++i) {
469                 final ExecutionStats stats = appStats.valueAt(i);
470                 if (stats != null) {
471                     stats.expirationTimeElapsed = nowElapsed;
472                 }
473             }
474         }
475     }
476 
477     @GuardedBy("mLock")
invalidateExecutionStatsLocked(final int userId, @NonNull final String packageName, @Nullable String tag)478     private void invalidateExecutionStatsLocked(final int userId, @NonNull final String packageName,
479             @Nullable String tag) {
480         final ExecutionStats stats = mExecutionStatsCache.get(userId, packageName, tag);
481         if (stats != null) {
482             stats.expirationTimeElapsed = mInjector.getElapsedRealtime();
483         }
484     }
485 
486     private static final class EarliestEventTimeFunctor implements Consumer<LongArrayQueue> {
487         long earliestTimeElapsed = Long.MAX_VALUE;
488 
489         @Override
accept(LongArrayQueue events)490         public void accept(LongArrayQueue events) {
491             if (events != null && events.size() > 0) {
492                 earliestTimeElapsed = Math.min(earliestTimeElapsed, events.get(0));
493             }
494         }
495 
reset()496         void reset() {
497             earliestTimeElapsed = Long.MAX_VALUE;
498         }
499     }
500 
501     private final EarliestEventTimeFunctor mEarliestEventTimeFunctor =
502             new EarliestEventTimeFunctor();
503 
504     /** Schedule a cleanup alarm if necessary and there isn't already one scheduled. */
505     @GuardedBy("mLock")
506     @VisibleForTesting
maybeScheduleCleanupAlarmLocked()507     void maybeScheduleCleanupAlarmLocked() {
508         if (mNextCleanupTimeElapsed > mInjector.getElapsedRealtime()) {
509             // There's already an alarm scheduled. Just stick with that one. There's no way we'll
510             // end up scheduling an earlier alarm.
511             if (DEBUG) {
512                 Slog.v(TAG, "Not scheduling cleanup since there's already one at "
513                         + mNextCleanupTimeElapsed + " (in " + (mNextCleanupTimeElapsed
514                         - mInjector.getElapsedRealtime()) + "ms)");
515             }
516             return;
517         }
518 
519         mEarliestEventTimeFunctor.reset();
520         mEventTimes.forEach(mEarliestEventTimeFunctor);
521         final long earliestEndElapsed = mEarliestEventTimeFunctor.earliestTimeElapsed;
522         if (earliestEndElapsed == Long.MAX_VALUE) {
523             // Couldn't find a good time to clean up. Maybe this was called after we deleted all
524             // events.
525             if (DEBUG) {
526                 Slog.d(TAG, "Didn't find a time to schedule cleanup");
527             }
528             return;
529         }
530 
531         // Need to keep events for all apps up to the max period, regardless of their current
532         // category.
533         long nextCleanupElapsed = earliestEndElapsed + mMaxPeriodMs;
534         if (nextCleanupElapsed - mNextCleanupTimeElapsed <= 10 * MINUTE_IN_MILLIS) {
535             // No need to clean up too often. Delay the alarm if the next cleanup would be too soon
536             // after it.
537             nextCleanupElapsed += 10 * MINUTE_IN_MILLIS;
538         }
539         mNextCleanupTimeElapsed = nextCleanupElapsed;
540         scheduleAlarm(AlarmManager.ELAPSED_REALTIME, nextCleanupElapsed, ALARM_TAG_CLEANUP,
541                 mEventCleanupAlarmListener);
542         if (DEBUG) {
543             Slog.d(TAG, "Scheduled next cleanup for " + mNextCleanupTimeElapsed);
544         }
545     }
546 
547     @GuardedBy("mLock")
maybeUpdateStatusForPkgLocked(final int userId, @NonNull final String packageName)548     private boolean maybeUpdateStatusForPkgLocked(final int userId,
549             @NonNull final String packageName) {
550         final UptcMap<Boolean> done = new UptcMap<>();
551 
552         if (!mEventTimes.contains(userId, packageName)) {
553             return false;
554         }
555         final ArrayMap<String, LongArrayQueue> events = mEventTimes.get(userId, packageName);
556         if (events == null) {
557             Slog.wtf(TAG,
558                     "Events map was null even though mEventTimes said it contained "
559                             + string(userId, packageName, null));
560             return false;
561         }
562 
563         // Lambdas can't interact with non-final outer variables.
564         final boolean[] changed = {false};
565         events.forEach((tag, eventList) -> {
566             if (!done.contains(userId, packageName, tag)) {
567                 changed[0] |= maybeUpdateStatusForUptcLocked(userId, packageName, tag);
568                 done.add(userId, packageName, tag, Boolean.TRUE);
569             }
570         });
571 
572         return changed[0];
573     }
574 
575     /**
576      * Posts that the quota status for the UPTC has changed if it has changed. Avoid calling if
577      * there are no {@link QuotaChangeListener}s registered as the work done will be useless.
578      *
579      * @return true if the in/out quota status changed
580      */
581     @GuardedBy("mLock")
maybeUpdateStatusForUptcLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)582     private boolean maybeUpdateStatusForUptcLocked(final int userId,
583             @NonNull final String packageName, @Nullable final String tag) {
584         final boolean oldInQuota = isWithinQuotaLocked(
585                 getExecutionStatsLocked(userId, packageName, tag, false));
586 
587         final boolean newInQuota;
588         if (!isEnabledLocked() || isQuotaFreeLocked(userId, packageName)) {
589             newInQuota = true;
590         } else {
591             newInQuota = isWithinQuotaLocked(
592                     getExecutionStatsLocked(userId, packageName, tag, true));
593         }
594 
595         if (!newInQuota) {
596             maybeScheduleStartAlarmLocked(userId, packageName, tag);
597         } else {
598             cancelScheduledStartAlarmLocked(userId, packageName, tag);
599         }
600 
601         if (oldInQuota != newInQuota) {
602             if (DEBUG) {
603                 Slog.d(TAG,
604                         "Quota status changed from " + oldInQuota + " to " + newInQuota + " for "
605                                 + string(userId, packageName, tag));
606             }
607             postQuotaStatusChanged(userId, packageName, tag);
608             return true;
609         }
610 
611         return false;
612     }
613 
614     private final class DeleteEventTimesFunctor implements Consumer<LongArrayQueue> {
615         private long mMaxPeriodMs;
616 
617         @Override
accept(LongArrayQueue times)618         public void accept(LongArrayQueue times) {
619             if (times != null) {
620                 // Remove everything older than mMaxPeriodMs time ago.
621                 while (times.size() > 0
622                         && times.peekFirst() <= mInjector.getElapsedRealtime() - mMaxPeriodMs) {
623                     times.removeFirst();
624                 }
625             }
626         }
627 
updateMaxPeriod()628         private void updateMaxPeriod() {
629             long maxPeriodMs = 0;
630             for (int i = mCategoryCountWindowSizesMs.size() - 1; i >= 0; --i) {
631                 maxPeriodMs = Long.max(maxPeriodMs, mCategoryCountWindowSizesMs.valueAt(i));
632             }
633             mMaxPeriodMs = maxPeriodMs;
634         }
635     }
636 
637     private final DeleteEventTimesFunctor mDeleteOldEventTimesFunctor =
638             new DeleteEventTimesFunctor();
639 
640     @GuardedBy("mLock")
641     @VisibleForTesting
deleteObsoleteEventsLocked()642     void deleteObsoleteEventsLocked() {
643         mEventTimes.forEach(mDeleteOldEventTimesFunctor);
644     }
645 
646     private class CqtHandler extends Handler {
CqtHandler(Looper looper)647         CqtHandler(Looper looper) {
648             super(looper);
649         }
650 
651         @Override
handleMessage(Message msg)652         public void handleMessage(Message msg) {
653             synchronized (mLock) {
654                 switch (msg.what) {
655                     case MSG_CLEAN_UP_EVENTS: {
656                         if (DEBUG) {
657                             Slog.d(TAG, "Cleaning up events.");
658                         }
659                         deleteObsoleteEventsLocked();
660                         maybeScheduleCleanupAlarmLocked();
661                         break;
662                     }
663                 }
664             }
665         }
666     }
667 
668     private Function<Void, LongArrayQueue> mCreateLongArrayQueue = aVoid -> new LongArrayQueue();
669     private Function<Void, ExecutionStats> mCreateExecutionStats = aVoid -> new ExecutionStats();
670 
671     //////////////////////// TESTING HELPERS /////////////////////////////
672 
673     @VisibleForTesting
674     @Nullable
getEvents(int userId, String packageName, String tag)675     LongArrayQueue getEvents(int userId, String packageName, String tag) {
676         return mEventTimes.get(userId, packageName, tag);
677     }
678 
679     //////////////////////////// DATA DUMP //////////////////////////////
680 
681     /** Dump state in text format. */
dump(final IndentingPrintWriter pw)682     public void dump(final IndentingPrintWriter pw) {
683         pw.print(TAG);
684         pw.println(":");
685         pw.increaseIndent();
686 
687         synchronized (mLock) {
688             super.dump(pw);
689             pw.println();
690 
691             pw.println("Instantaneous events:");
692             pw.increaseIndent();
693             mEventTimes.forEach((userId, pkgName, tag, events) -> {
694                 if (events.size() > 0) {
695                     pw.print(string(userId, pkgName, tag));
696                     pw.println(":");
697                     pw.increaseIndent();
698                     pw.print(events.get(0));
699                     for (int i = 1; i < events.size(); ++i) {
700                         pw.print(", ");
701                         pw.print(events.get(i));
702                     }
703                     pw.decreaseIndent();
704                     pw.println();
705                 }
706             });
707             pw.decreaseIndent();
708 
709             pw.println();
710             pw.println("Cached execution stats:");
711             pw.increaseIndent();
712             mExecutionStatsCache.forEach((userId, pkgName, tag, stats) -> {
713                 if (stats != null) {
714                     pw.print(string(userId, pkgName, tag));
715                     pw.println(":");
716                     pw.increaseIndent();
717                     pw.println(stats);
718                     pw.decreaseIndent();
719                 }
720             });
721             pw.decreaseIndent();
722 
723             pw.println();
724             pw.println("Limits:");
725             pw.increaseIndent();
726             final int numCategories = mCategoryCountWindowSizesMs.size();
727             for (int i = 0; i < numCategories; ++i) {
728                 final Category category = mCategoryCountWindowSizesMs.keyAt(i);
729                 pw.print(category);
730                 pw.print(": ");
731                 pw.print(mMaxCategoryCounts.get(category));
732                 pw.print(" events in ");
733                 pw.println(TimeUtils.formatDuration(mCategoryCountWindowSizesMs.get(category)));
734             }
735             pw.decreaseIndent();
736         }
737         pw.decreaseIndent();
738     }
739 
740     /**
741      * Dump state to proto.
742      *
743      * @param proto   The ProtoOutputStream to write to.
744      * @param fieldId The field ID of the {@link CountQuotaTrackerProto}.
745      */
dump(ProtoOutputStream proto, long fieldId)746     public void dump(ProtoOutputStream proto, long fieldId) {
747         final long token = proto.start(fieldId);
748 
749         synchronized (mLock) {
750             super.dump(proto, CountQuotaTrackerProto.BASE_QUOTA_DATA);
751 
752             for (int i = 0; i < mCategoryCountWindowSizesMs.size(); ++i) {
753                 final Category category = mCategoryCountWindowSizesMs.keyAt(i);
754                 final long clToken = proto.start(CountQuotaTrackerProto.COUNT_LIMIT);
755                 category.dumpDebug(proto, CountQuotaTrackerProto.CountLimit.CATEGORY);
756                 proto.write(CountQuotaTrackerProto.CountLimit.LIMIT,
757                         mMaxCategoryCounts.get(category));
758                 proto.write(CountQuotaTrackerProto.CountLimit.WINDOW_SIZE_MS,
759                         mCategoryCountWindowSizesMs.get(category));
760                 proto.end(clToken);
761             }
762 
763             mExecutionStatsCache.forEach((userId, pkgName, tag, stats) -> {
764                 final boolean isQuotaFree = isIndividualQuotaFreeLocked(userId, pkgName);
765 
766                 final long usToken = proto.start(CountQuotaTrackerProto.UPTC_STATS);
767 
768                 (new Uptc(userId, pkgName, tag))
769                         .dumpDebug(proto, CountQuotaTrackerProto.UptcStats.UPTC);
770 
771                 proto.write(CountQuotaTrackerProto.UptcStats.IS_QUOTA_FREE, isQuotaFree);
772 
773                 final LongArrayQueue events = mEventTimes.get(userId, pkgName, tag);
774                 if (events != null) {
775                     for (int j = events.size() - 1; j >= 0; --j) {
776                         final long eToken = proto.start(CountQuotaTrackerProto.UptcStats.EVENTS);
777                         proto.write(CountQuotaTrackerProto.Event.TIMESTAMP_ELAPSED, events.get(j));
778                         proto.end(eToken);
779                     }
780                 }
781 
782                 final long statsToken = proto.start(
783                         CountQuotaTrackerProto.UptcStats.EXECUTION_STATS);
784                 proto.write(
785                         CountQuotaTrackerProto.ExecutionStats.EXPIRATION_TIME_ELAPSED,
786                         stats.expirationTimeElapsed);
787                 proto.write(
788                         CountQuotaTrackerProto.ExecutionStats.WINDOW_SIZE_MS,
789                         stats.windowSizeMs);
790                 proto.write(CountQuotaTrackerProto.ExecutionStats.COUNT_LIMIT, stats.countLimit);
791                 proto.write(
792                         CountQuotaTrackerProto.ExecutionStats.COUNT_IN_WINDOW,
793                         stats.countInWindow);
794                 proto.write(
795                         CountQuotaTrackerProto.ExecutionStats.IN_QUOTA_TIME_ELAPSED,
796                         stats.inQuotaTimeElapsed);
797                 proto.end(statsToken);
798 
799                 proto.end(usToken);
800             });
801 
802             proto.end(token);
803         }
804     }
805 }
806