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