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.BroadcastReceiver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.net.Uri;
31 import android.os.Handler;
32 import android.os.SystemClock;
33 import android.os.UserHandle;
34 import android.util.ArraySet;
35 import android.util.IndentingPrintWriter;
36 import android.util.Pair;
37 import android.util.Slog;
38 import android.util.SparseArrayMap;
39 import android.util.proto.ProtoOutputStream;
40 import android.util.quota.QuotaTrackerProto;
41 
42 import com.android.internal.annotations.GuardedBy;
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.internal.os.BackgroundThread;
45 import com.android.server.FgThread;
46 import com.android.server.LocalServices;
47 import com.android.server.SystemServiceManager;
48 
49 import java.util.PriorityQueue;
50 
51 /**
52  * Base class for trackers that track whether an app has exceeded a count quota.
53  *
54  * Quotas are applied per userId-package-tag combination (UPTC). Tags can be null.
55  *
56  * Count and duration limits can be applied at the same time. Each limit is evaluated and
57  * controlled independently. If a UPTC reaches one of the limits, it will be considered out
58  * of quota until it is below that limit again. Limits are applied according to the category
59  * the UPTC is placed in. Categories are basic constructs to apply different limits to
60  * different groups of UPTCs. For example, standby buckets can be a set of categories, or
61  * foreground & background could be two categories. If every UPTC should have the same limits
62  * applied, then only one category is needed.
63  *
64  * Note: all limits are enforced per category unless explicitly stated otherwise.
65  *
66  * @hide
67  */
68 abstract class QuotaTracker {
69     private static final String TAG = QuotaTracker.class.getSimpleName();
70     private static final boolean DEBUG = false;
71 
72     private static final String ALARM_TAG_QUOTA_CHECK = "*" + TAG + ".quota_check*";
73 
74     @VisibleForTesting
75     static class Injector {
getElapsedRealtime()76         long getElapsedRealtime() {
77             return SystemClock.elapsedRealtime();
78         }
79 
isAlarmManagerReady()80         boolean isAlarmManagerReady() {
81             return LocalServices.getService(SystemServiceManager.class).isBootCompleted();
82         }
83     }
84 
85     final Object mLock = new Object();
86     final Categorizer mCategorizer;
87     @GuardedBy("mLock")
88     private final ArraySet<QuotaChangeListener> mQuotaChangeListeners = new ArraySet<>();
89 
90     /**
91      * Listener to track and manage when each package comes back within quota.
92      */
93     @GuardedBy("mLock")
94     private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener();
95 
96     /** "Free quota status" for apps. */
97     @GuardedBy("mLock")
98     private final SparseArrayMap<String, Boolean> mFreeQuota = new SparseArrayMap<>();
99 
100     private final AlarmManager mAlarmManager;
101     protected final Context mContext;
102     protected final Injector mInjector;
103 
104     @GuardedBy("mLock")
105     private boolean mIsQuotaFree;
106 
107     /**
108      * If QuotaTracker should actively track events and check quota. If false, quota will be free
109      * and events will not be tracked.
110      */
111     @GuardedBy("mLock")
112     private boolean mIsEnabled = true;
113 
114     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
115         private String getPackageName(Intent intent) {
116             final Uri uri = intent.getData();
117             return uri != null ? uri.getSchemeSpecificPart() : null;
118         }
119 
120         @Override
121         public void onReceive(Context context, Intent intent) {
122             if (intent == null
123                     || intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
124                 return;
125             }
126             final String action = intent.getAction();
127             if (action == null) {
128                 Slog.e(TAG, "Received intent with null action");
129                 return;
130             }
131             switch (action) {
132                 case Intent.ACTION_PACKAGE_FULLY_REMOVED:
133                     final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
134                     synchronized (mLock) {
135                         onAppRemovedLocked(UserHandle.getUserId(uid), getPackageName(intent));
136                     }
137                     break;
138                 case Intent.ACTION_USER_REMOVED:
139                     final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
140                     synchronized (mLock) {
141                         onUserRemovedLocked(userId);
142                     }
143                     break;
144             }
145         }
146     };
147 
148     /** The maximum period any Category can have. */
149     @VisibleForTesting
150     static final long MAX_WINDOW_SIZE_MS = 30 * 24 * 60 * MINUTE_IN_MILLIS; // 1 month
151 
152     /**
153      * The minimum time any window size can be. A minimum window size helps to avoid CPU
154      * churn/looping in cases where there are registered listeners for when UPTCs go in and out of
155      * quota.
156      */
157     @VisibleForTesting
158     static final long MIN_WINDOW_SIZE_MS = 20_000;
159 
QuotaTracker(@onNull Context context, @NonNull Categorizer categorizer, @NonNull Injector injector)160     QuotaTracker(@NonNull Context context, @NonNull Categorizer categorizer,
161             @NonNull Injector injector) {
162         mCategorizer = categorizer;
163         mContext = context;
164         mInjector = injector;
165         mAlarmManager = mContext.getSystemService(AlarmManager.class);
166 
167         final IntentFilter filter = new IntentFilter();
168         filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
169         filter.addDataScheme("package");
170         context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null,
171                 BackgroundThread.getHandler());
172         final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
173         context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, userFilter, null,
174                 BackgroundThread.getHandler());
175     }
176 
177     // Exposed API to users.
178 
179     /** Remove all saved events from the tracker. */
clear()180     public void clear() {
181         synchronized (mLock) {
182             mInQuotaAlarmListener.clearLocked();
183             mFreeQuota.clear();
184 
185             dropEverythingLocked();
186         }
187     }
188 
189     /**
190      * @return true if the UPTC is within quota, false otherwise.
191      * @throws IllegalStateException if given categorizer returns a Category that's not recognized.
192      */
isWithinQuota(int userId, @NonNull String packageName, @Nullable String tag)193     public boolean isWithinQuota(int userId, @NonNull String packageName, @Nullable String tag) {
194         synchronized (mLock) {
195             return isWithinQuotaLocked(userId, packageName, tag);
196         }
197     }
198 
199     /**
200      * Indicates whether quota is currently free or not for a specific app. If quota is free, any
201      * currently ongoing events or instantaneous events won't be counted until quota is no longer
202      * free.
203      */
setQuotaFree(int userId, @NonNull String packageName, boolean isFree)204     public void setQuotaFree(int userId, @NonNull String packageName, boolean isFree) {
205         synchronized (mLock) {
206             final boolean wasFree = mFreeQuota.getOrDefault(userId, packageName, Boolean.FALSE);
207             if (wasFree != isFree) {
208                 mFreeQuota.add(userId, packageName, isFree);
209                 onQuotaFreeChangedLocked(userId, packageName, isFree);
210             }
211         }
212     }
213 
214     /** Indicates whether quota is currently free or not for all apps. */
setQuotaFree(boolean isFree)215     public void setQuotaFree(boolean isFree) {
216         synchronized (mLock) {
217             if (mIsQuotaFree == isFree) {
218                 return;
219             }
220             mIsQuotaFree = isFree;
221 
222             if (!mIsEnabled) {
223                 return;
224             }
225             onQuotaFreeChangedLocked(mIsQuotaFree);
226         }
227         scheduleQuotaCheck();
228     }
229 
230     /**
231      * Register a {@link QuotaChangeListener} to be notified of when apps go in and out of quota.
232      */
registerQuotaChangeListener(QuotaChangeListener listener)233     public void registerQuotaChangeListener(QuotaChangeListener listener) {
234         synchronized (mLock) {
235             if (mQuotaChangeListeners.add(listener) && mQuotaChangeListeners.size() == 1) {
236                 scheduleQuotaCheck();
237             }
238         }
239     }
240 
241     /** Unregister the listener from future quota change notifications. */
unregisterQuotaChangeListener(QuotaChangeListener listener)242     public void unregisterQuotaChangeListener(QuotaChangeListener listener) {
243         synchronized (mLock) {
244             mQuotaChangeListeners.remove(listener);
245         }
246     }
247 
248     // Configuration APIs
249 
250     /**
251      * Completely enables or disables the quota tracker. If the tracker is disabled, all events and
252      * internal tracking data will be dropped.
253      */
setEnabled(boolean enable)254     public void setEnabled(boolean enable) {
255         synchronized (mLock) {
256             if (mIsEnabled == enable) {
257                 return;
258             }
259             mIsEnabled = enable;
260 
261             if (!mIsEnabled) {
262                 clear();
263             }
264         }
265     }
266 
267     // Internal implementation.
268 
269     @GuardedBy("mLock")
isEnabledLocked()270     boolean isEnabledLocked() {
271         return mIsEnabled;
272     }
273 
274     /** Returns true if global quota is free. */
275     @GuardedBy("mLock")
isQuotaFreeLocked()276     boolean isQuotaFreeLocked() {
277         return mIsQuotaFree;
278     }
279 
280     /** Returns true if global quota is free or if quota is free for the given userId-package. */
281     @GuardedBy("mLock")
isQuotaFreeLocked(int userId, @NonNull String packageName)282     boolean isQuotaFreeLocked(int userId, @NonNull String packageName) {
283         return mIsQuotaFree || mFreeQuota.getOrDefault(userId, packageName, Boolean.FALSE);
284     }
285 
286     /**
287      * Returns true only if quota is free for the given userId-package. Global quota is not taken
288      * into account.
289      */
290     @GuardedBy("mLock")
isIndividualQuotaFreeLocked(int userId, @NonNull String packageName)291     boolean isIndividualQuotaFreeLocked(int userId, @NonNull String packageName) {
292         return mFreeQuota.getOrDefault(userId, packageName, Boolean.FALSE);
293     }
294 
295     /** The tracker has been disabled. Drop all events and internal tracking data. */
296     @GuardedBy("mLock")
dropEverythingLocked()297     abstract void dropEverythingLocked();
298 
299     /** The global free quota status changed. */
300     @GuardedBy("mLock")
onQuotaFreeChangedLocked(boolean isFree)301     abstract void onQuotaFreeChangedLocked(boolean isFree);
302 
303     /** The individual free quota status for the userId-package changed. */
304     @GuardedBy("mLock")
onQuotaFreeChangedLocked(int userId, @NonNull String packageName, boolean isFree)305     abstract void onQuotaFreeChangedLocked(int userId, @NonNull String packageName, boolean isFree);
306 
307     /** Get the Handler used by the tracker. This Handler's thread will receive alarm callbacks. */
308     @NonNull
getHandler()309     abstract Handler getHandler();
310 
311     /** Makes sure to call out to AlarmManager on a separate thread. */
scheduleAlarm(@larmManager.AlarmType int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener)312     void scheduleAlarm(@AlarmManager.AlarmType int type, long triggerAtMillis, String tag,
313             AlarmManager.OnAlarmListener listener) {
314         // We don't know at what level in the lock hierarchy this tracker will be, so make sure to
315         // call out to AlarmManager without the lock held. The operation should be fast enough so
316         // put it on the FgThread.
317         FgThread.getHandler().post(() -> {
318             if (mInjector.isAlarmManagerReady()) {
319                 mAlarmManager.set(type, triggerAtMillis, tag, listener, getHandler());
320             } else {
321                 Slog.w(TAG, "Alarm not scheduled because boot isn't completed");
322             }
323         });
324     }
325 
326     /** Makes sure to call out to AlarmManager on a separate thread. */
cancelAlarm(AlarmManager.OnAlarmListener listener)327     void cancelAlarm(AlarmManager.OnAlarmListener listener) {
328         // We don't know at what level in the lock hierarchy this tracker will be, so make sure to
329         // call out to AlarmManager without the lock held. The operation should be fast enough so
330         // put it on the FgThread.
331         FgThread.getHandler().post(() -> {
332             if (mInjector.isAlarmManagerReady()) {
333                 mAlarmManager.cancel(listener);
334             } else {
335                 Slog.w(TAG, "Alarm not cancelled because boot isn't completed");
336             }
337         });
338     }
339 
340     /** Check the quota status of the specific UPTC. */
maybeUpdateQuotaStatus(int userId, @NonNull String packageName, @Nullable String tag)341     abstract void maybeUpdateQuotaStatus(int userId, @NonNull String packageName,
342             @Nullable String tag);
343 
344     /** Check the quota status of all UPTCs in case a listener needs to be notified. */
345     @GuardedBy("mLock")
maybeUpdateAllQuotaStatusLocked()346     abstract void maybeUpdateAllQuotaStatusLocked();
347 
348     /** Schedule a quota check for all apps. */
scheduleQuotaCheck()349     void scheduleQuotaCheck() {
350         // Using BackgroundThread because of the risk of lock contention.
351         BackgroundThread.getHandler().post(() -> {
352             synchronized (mLock) {
353                 if (mQuotaChangeListeners.size() > 0) {
354                     maybeUpdateAllQuotaStatusLocked();
355                 }
356             }
357         });
358     }
359 
360     @GuardedBy("mLock")
handleRemovedAppLocked(int userId, @NonNull String packageName)361     abstract void handleRemovedAppLocked(int userId, @NonNull String packageName);
362 
363     @GuardedBy("mLock")
onAppRemovedLocked(final int userId, @NonNull String packageName)364     void onAppRemovedLocked(final int userId, @NonNull String packageName) {
365         if (packageName == null) {
366             Slog.wtf(TAG, "Told app removed but given null package name.");
367             return;
368         }
369 
370         mInQuotaAlarmListener.removeAlarmsLocked(userId, packageName);
371 
372         mFreeQuota.delete(userId, packageName);
373 
374         handleRemovedAppLocked(userId, packageName);
375     }
376 
377     @GuardedBy("mLock")
handleRemovedUserLocked(int userId)378     abstract void handleRemovedUserLocked(int userId);
379 
380     @GuardedBy("mLock")
onUserRemovedLocked(int userId)381     private void onUserRemovedLocked(int userId) {
382         mInQuotaAlarmListener.removeAlarmsLocked(userId);
383         mFreeQuota.delete(userId);
384 
385         handleRemovedUserLocked(userId);
386     }
387 
388     @GuardedBy("mLock")
isWithinQuotaLocked(int userId, @NonNull String packageName, @Nullable String tag)389     abstract boolean isWithinQuotaLocked(int userId, @NonNull String packageName,
390             @Nullable String tag);
391 
postQuotaStatusChanged(final int userId, @NonNull final String packageName, @Nullable final String tag)392     void postQuotaStatusChanged(final int userId, @NonNull final String packageName,
393             @Nullable final String tag) {
394         BackgroundThread.getHandler().post(() -> {
395             final QuotaChangeListener[] listeners;
396             synchronized (mLock) {
397                 // Only notify all listeners if we aren't directing to one listener.
398                 listeners = mQuotaChangeListeners.toArray(
399                         new QuotaChangeListener[mQuotaChangeListeners.size()]);
400             }
401             for (QuotaChangeListener listener : listeners) {
402                 listener.onQuotaStateChanged(userId, packageName, tag);
403             }
404         });
405     }
406 
407     /**
408      * Return the time (in the elapsed realtime timebase) when the UPTC will have quota again. This
409      * value is only valid if the UPTC is currently out of quota.
410      */
411     @GuardedBy("mLock")
getInQuotaTimeElapsedLocked(int userId, @NonNull String packageName, @Nullable String tag)412     abstract long getInQuotaTimeElapsedLocked(int userId, @NonNull String packageName,
413             @Nullable String tag);
414 
415     /**
416      * Maybe schedule a non-wakeup alarm for the next time this package will have quota to run
417      * again. This should only be called if the package is already out of quota.
418      */
419     @GuardedBy("mLock")
420     @VisibleForTesting
maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)421     void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName,
422             @Nullable final String tag) {
423         if (mQuotaChangeListeners.size() == 0) {
424             // No need to schedule the alarm since we won't do anything when the app gets quota
425             // again.
426             return;
427         }
428 
429         final String pkgString = string(userId, packageName, tag);
430 
431         if (isWithinQuota(userId, packageName, tag)) {
432             // Already in quota. Why was this method called?
433             if (DEBUG) {
434                 Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
435                         + " even though it's within quota");
436             }
437             mInQuotaAlarmListener.removeAlarmLocked(new Uptc(userId, packageName, tag));
438             maybeUpdateQuotaStatus(userId, packageName, tag);
439             return;
440         }
441 
442         mInQuotaAlarmListener.addAlarmLocked(new Uptc(userId, packageName, tag),
443                 getInQuotaTimeElapsedLocked(userId, packageName, tag));
444     }
445 
446     @GuardedBy("mLock")
cancelScheduledStartAlarmLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)447     void cancelScheduledStartAlarmLocked(final int userId,
448             @NonNull final String packageName, @Nullable final String tag) {
449         mInQuotaAlarmListener.removeAlarmLocked(new Uptc(userId, packageName, tag));
450     }
451 
452     static class AlarmQueue extends PriorityQueue<Pair<Uptc, Long>> {
AlarmQueue()453         AlarmQueue() {
454             super(1, (o1, o2) -> (int) (o1.second - o2.second));
455         }
456 
457         /**
458          * Remove any instances of the Uptc from the queue.
459          *
460          * @return true if an instance was removed, false otherwise.
461          */
remove(@onNull Uptc uptc)462         boolean remove(@NonNull Uptc uptc) {
463             boolean removed = false;
464             Pair[] alarms = toArray(new Pair[size()]);
465             for (int i = alarms.length - 1; i >= 0; --i) {
466                 if (uptc.equals(alarms[i].first)) {
467                     remove(alarms[i]);
468                     removed = true;
469                 }
470             }
471             return removed;
472         }
473     }
474 
475     /** Track when UPTCs are expected to come back into quota. */
476     private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener {
477         @GuardedBy("mLock")
478         private final AlarmQueue mAlarmQueue = new AlarmQueue();
479         /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
480         @GuardedBy("mLock")
481         private long mTriggerTimeElapsed = 0;
482 
483         @GuardedBy("mLock")
addAlarmLocked(@onNull Uptc uptc, long inQuotaTimeElapsed)484         void addAlarmLocked(@NonNull Uptc uptc, long inQuotaTimeElapsed) {
485             mAlarmQueue.remove(uptc);
486             mAlarmQueue.offer(new Pair<>(uptc, inQuotaTimeElapsed));
487             setNextAlarmLocked();
488         }
489 
490         @GuardedBy("mLock")
clearLocked()491         void clearLocked() {
492             cancelAlarm(this);
493             mAlarmQueue.clear();
494             mTriggerTimeElapsed = 0;
495         }
496 
497         @GuardedBy("mLock")
removeAlarmLocked(@onNull Uptc uptc)498         void removeAlarmLocked(@NonNull Uptc uptc) {
499             if (mAlarmQueue.remove(uptc)) {
500                 if (mAlarmQueue.size() == 0) {
501                     cancelAlarm(this);
502                 } else {
503                     setNextAlarmLocked();
504                 }
505             }
506         }
507 
508         @GuardedBy("mLock")
removeAlarmsLocked(int userId)509         void removeAlarmsLocked(int userId) {
510             boolean removed = false;
511             Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
512             for (int i = alarms.length - 1; i >= 0; --i) {
513                 final Uptc uptc = (Uptc) alarms[i].first;
514                 if (userId == uptc.userId) {
515                     mAlarmQueue.remove(alarms[i]);
516                     removed = true;
517                 }
518             }
519             if (removed) {
520                 setNextAlarmLocked();
521             }
522         }
523 
524         @GuardedBy("mLock")
removeAlarmsLocked(int userId, @NonNull String packageName)525         void removeAlarmsLocked(int userId, @NonNull String packageName) {
526             boolean removed = false;
527             Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
528             for (int i = alarms.length - 1; i >= 0; --i) {
529                 final Uptc uptc = (Uptc) alarms[i].first;
530                 if (userId == uptc.userId && packageName.equals(uptc.packageName)) {
531                     mAlarmQueue.remove(alarms[i]);
532                     removed = true;
533                 }
534             }
535             if (removed) {
536                 setNextAlarmLocked();
537             }
538         }
539 
540         @GuardedBy("mLock")
setNextAlarmLocked()541         private void setNextAlarmLocked() {
542             if (mAlarmQueue.size() > 0) {
543                 final long nextTriggerTimeElapsed = mAlarmQueue.peek().second;
544                 // Only schedule the alarm if one of the following is true:
545                 // 1. There isn't one currently scheduled
546                 // 2. The new alarm is significantly earlier than the previous alarm. If it's
547                 // earlier but not significantly so, then we essentially delay the notification a
548                 // few extra minutes.
549                 if (mTriggerTimeElapsed == 0
550                         || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS
551                         || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
552                     // Use a non-wakeup alarm for this
553                     scheduleAlarm(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed,
554                             ALARM_TAG_QUOTA_CHECK, this);
555                     mTriggerTimeElapsed = nextTriggerTimeElapsed;
556                 }
557             } else {
558                 cancelAlarm(this);
559                 mTriggerTimeElapsed = 0;
560             }
561         }
562 
563         @Override
onAlarm()564         public void onAlarm() {
565             synchronized (mLock) {
566                 while (mAlarmQueue.size() > 0) {
567                     final Pair<Uptc, Long> alarm = mAlarmQueue.peek();
568                     if (alarm.second <= mInjector.getElapsedRealtime()) {
569                         getHandler().post(() -> maybeUpdateQuotaStatus(
570                                 alarm.first.userId, alarm.first.packageName, alarm.first.tag));
571                         mAlarmQueue.remove(alarm);
572                     } else {
573                         break;
574                     }
575                 }
576                 setNextAlarmLocked();
577             }
578         }
579 
580         @GuardedBy("mLock")
dumpLocked(IndentingPrintWriter pw)581         void dumpLocked(IndentingPrintWriter pw) {
582             pw.println("In quota alarms:");
583             pw.increaseIndent();
584 
585             if (mAlarmQueue.size() == 0) {
586                 pw.println("NOT WAITING");
587             } else {
588                 Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
589                 for (int i = 0; i < alarms.length; ++i) {
590                     final Uptc uptc = (Uptc) alarms[i].first;
591                     pw.print(uptc);
592                     pw.print(": ");
593                     pw.print(alarms[i].second);
594                     pw.println();
595                 }
596             }
597 
598             pw.decreaseIndent();
599         }
600 
601         @GuardedBy("mLock")
dumpLocked(ProtoOutputStream proto, long fieldId)602         void dumpLocked(ProtoOutputStream proto, long fieldId) {
603             final long token = proto.start(fieldId);
604 
605             proto.write(QuotaTrackerProto.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED,
606                     mTriggerTimeElapsed);
607 
608             Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
609             for (int i = 0; i < alarms.length; ++i) {
610                 final long aToken = proto.start(QuotaTrackerProto.InQuotaAlarmListener.ALARMS);
611 
612                 final Uptc uptc = (Uptc) alarms[i].first;
613                 uptc.dumpDebug(proto, QuotaTrackerProto.InQuotaAlarmListener.Alarm.UPTC);
614                 proto.write(QuotaTrackerProto.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED,
615                         (Long) alarms[i].second);
616 
617                 proto.end(aToken);
618             }
619 
620             proto.end(token);
621         }
622     }
623 
624     //////////////////////////// DATA DUMP //////////////////////////////
625 
626     /** Dump state in text format. */
dump(final IndentingPrintWriter pw)627     public void dump(final IndentingPrintWriter pw) {
628         pw.println("QuotaTracker:");
629         pw.increaseIndent();
630 
631         synchronized (mLock) {
632             pw.println("Is enabled: " + mIsEnabled);
633             pw.println("Is global quota free: " + mIsQuotaFree);
634             pw.println("Current elapsed time: " + mInjector.getElapsedRealtime());
635             pw.println();
636 
637             pw.println();
638             mInQuotaAlarmListener.dumpLocked(pw);
639 
640             pw.println();
641             pw.println("Per-app free quota:");
642             pw.increaseIndent();
643             for (int u = 0; u < mFreeQuota.numMaps(); ++u) {
644                 final int userId = mFreeQuota.keyAt(u);
645                 for (int p = 0; p < mFreeQuota.numElementsForKey(userId); ++p) {
646                     final String pkgName = mFreeQuota.keyAt(u, p);
647 
648                     pw.print(string(userId, pkgName, null));
649                     pw.print(": ");
650                     pw.println(mFreeQuota.get(userId, pkgName));
651                 }
652             }
653             pw.decreaseIndent();
654         }
655 
656         pw.decreaseIndent();
657     }
658 
659     /**
660      * Dump state to proto.
661      *
662      * @param proto   The ProtoOutputStream to write to.
663      * @param fieldId The field ID of the {@link QuotaTrackerProto}.
664      */
dump(ProtoOutputStream proto, long fieldId)665     public void dump(ProtoOutputStream proto, long fieldId) {
666         final long token = proto.start(fieldId);
667 
668         synchronized (mLock) {
669             proto.write(QuotaTrackerProto.IS_ENABLED, mIsEnabled);
670             proto.write(QuotaTrackerProto.IS_GLOBAL_QUOTA_FREE, mIsQuotaFree);
671             proto.write(QuotaTrackerProto.ELAPSED_REALTIME, mInjector.getElapsedRealtime());
672             mInQuotaAlarmListener.dumpLocked(proto, QuotaTrackerProto.IN_QUOTA_ALARM_LISTENER);
673         }
674 
675         proto.end(token);
676     }
677 }
678