1 /*
2  * Copyright (C) 2018 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.systemui.appops;
18 
19 import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
20 import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
21 import static android.media.AudioManager.ACTION_MICROPHONE_MUTE_CHANGED;
22 
23 import android.app.AppOpsManager;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.media.AudioManager;
29 import android.media.AudioRecordingConfiguration;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.os.UserHandle;
33 import android.permission.PermissionManager;
34 import android.util.ArraySet;
35 import android.util.Log;
36 import android.util.SparseArray;
37 
38 import androidx.annotation.WorkerThread;
39 
40 import com.android.internal.annotations.GuardedBy;
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.systemui.Dumpable;
43 import com.android.systemui.broadcast.BroadcastDispatcher;
44 import com.android.systemui.dagger.SysUISingleton;
45 import com.android.systemui.dagger.qualifiers.Background;
46 import com.android.systemui.dump.DumpManager;
47 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
48 import com.android.systemui.util.Assert;
49 import com.android.systemui.util.time.SystemClock;
50 
51 import java.io.PrintWriter;
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56 
57 import javax.inject.Inject;
58 
59 /**
60  * Controller to keep track of applications that have requested access to given App Ops
61  *
62  * It can be subscribed to with callbacks. Additionally, it passes on the information to
63  * NotificationPresenter to be displayed to the user.
64  */
65 @SysUISingleton
66 public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsController,
67         AppOpsManager.OnOpActiveChangedListener,
68         AppOpsManager.OnOpNotedInternalListener, IndividualSensorPrivacyController.Callback,
69         Dumpable {
70 
71     // This is the minimum time that we will keep AppOps that are noted on record. If multiple
72     // occurrences of the same (op, package, uid) happen in a shorter interval, they will not be
73     // notified to listeners.
74     private static final long NOTED_OP_TIME_DELAY_MS = 5000;
75     private static final String TAG = "AppOpsControllerImpl";
76     private static final boolean DEBUG = false;
77 
78     private final BroadcastDispatcher mDispatcher;
79     private final Context mContext;
80     private final AppOpsManager mAppOps;
81     private final AudioManager mAudioManager;
82     private final IndividualSensorPrivacyController mSensorPrivacyController;
83     private final SystemClock mClock;
84 
85     private H mBGHandler;
86     private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>();
87     private final SparseArray<Set<Callback>> mCallbacksByCode = new SparseArray<>();
88     private boolean mListening;
89     private boolean mMicMuted;
90     private boolean mCameraDisabled;
91 
92     @GuardedBy("mActiveItems")
93     private final List<AppOpItem> mActiveItems = new ArrayList<>();
94     @GuardedBy("mNotedItems")
95     private final List<AppOpItem> mNotedItems = new ArrayList<>();
96     @GuardedBy("mActiveItems")
97     private final SparseArray<ArrayList<AudioRecordingConfiguration>> mRecordingsByUid =
98             new SparseArray<>();
99 
100     protected static final int[] OPS = new int[] {
101             AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION,
102             AppOpsManager.OP_CAMERA,
103             AppOpsManager.OP_PHONE_CALL_CAMERA,
104             AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
105             AppOpsManager.OP_RECORD_AUDIO,
106             AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
107             AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
108             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
109             AppOpsManager.OP_COARSE_LOCATION,
110             AppOpsManager.OP_FINE_LOCATION
111     };
112 
113     @Inject
AppOpsControllerImpl( Context context, @Background Looper bgLooper, DumpManager dumpManager, AudioManager audioManager, IndividualSensorPrivacyController sensorPrivacyController, BroadcastDispatcher dispatcher, SystemClock clock )114     public AppOpsControllerImpl(
115             Context context,
116             @Background Looper bgLooper,
117             DumpManager dumpManager,
118             AudioManager audioManager,
119             IndividualSensorPrivacyController sensorPrivacyController,
120             BroadcastDispatcher dispatcher,
121             SystemClock clock
122     ) {
123         mDispatcher = dispatcher;
124         mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
125         mBGHandler = new H(bgLooper);
126         final int numOps = OPS.length;
127         for (int i = 0; i < numOps; i++) {
128             mCallbacksByCode.put(OPS[i], new ArraySet<>());
129         }
130         mAudioManager = audioManager;
131         mSensorPrivacyController = sensorPrivacyController;
132         mMicMuted = audioManager.isMicrophoneMute()
133                 || mSensorPrivacyController.isSensorBlocked(MICROPHONE);
134         mCameraDisabled = mSensorPrivacyController.isSensorBlocked(CAMERA);
135         mContext = context;
136         mClock = clock;
137         dumpManager.registerDumpable(TAG, this);
138     }
139 
140     @VisibleForTesting
setBGHandler(H handler)141     protected void setBGHandler(H handler) {
142         mBGHandler = handler;
143     }
144 
145     @VisibleForTesting
setListening(boolean listening)146     protected void setListening(boolean listening) {
147         mListening = listening;
148         if (listening) {
149             // System UI could be restarted while ops are active, so fetch the currently active ops
150             // once System UI starts listening again.
151             fetchCurrentActiveOps();
152 
153             mAppOps.startWatchingActive(OPS, this);
154             mAppOps.startWatchingNoted(OPS, this);
155             mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback, mBGHandler);
156             mSensorPrivacyController.addCallback(this);
157 
158             mMicMuted = mAudioManager.isMicrophoneMute()
159                     || mSensorPrivacyController.isSensorBlocked(MICROPHONE);
160             mCameraDisabled = mSensorPrivacyController.isSensorBlocked(CAMERA);
161 
162             mBGHandler.post(() -> mAudioRecordingCallback.onRecordingConfigChanged(
163                     mAudioManager.getActiveRecordingConfigurations()));
164             mDispatcher.registerReceiverWithHandler(this,
165                     new IntentFilter(ACTION_MICROPHONE_MUTE_CHANGED), mBGHandler);
166 
167         } else {
168             mAppOps.stopWatchingActive(this);
169             mAppOps.stopWatchingNoted(this);
170             mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback);
171             mSensorPrivacyController.removeCallback(this);
172 
173             mBGHandler.removeCallbacksAndMessages(null); // null removes all
174             mDispatcher.unregisterReceiver(this);
175             synchronized (mActiveItems) {
176                 mActiveItems.clear();
177                 mRecordingsByUid.clear();
178             }
179             synchronized (mNotedItems) {
180                 mNotedItems.clear();
181             }
182         }
183     }
184 
fetchCurrentActiveOps()185     private void fetchCurrentActiveOps() {
186         List<AppOpsManager.PackageOps> packageOps = mAppOps.getPackagesForOps(OPS);
187         for (AppOpsManager.PackageOps op : packageOps) {
188             for (AppOpsManager.OpEntry entry : op.getOps()) {
189                 for (Map.Entry<String, AppOpsManager.AttributedOpEntry> attributedOpEntry :
190                         entry.getAttributedOpEntries().entrySet()) {
191                     if (attributedOpEntry.getValue().isRunning()) {
192                         onOpActiveChanged(
193                                 entry.getOpStr(),
194                                 op.getUid(),
195                                 op.getPackageName(),
196                                 /* attributionTag= */ attributedOpEntry.getKey(),
197                                 /* active= */ true,
198                                 // AppOpsManager doesn't have a way to fetch attribution flags or
199                                 // chain ID given an op entry, so default them to none.
200                                 AppOpsManager.ATTRIBUTION_FLAGS_NONE,
201                                 AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
202                     }
203                 }
204             }
205         }
206     }
207 
208     /**
209      * Adds a callback that will get notifified when an AppOp of the type the controller tracks
210      * changes
211      *
212      * @param callback Callback to report changes
213      * @param opsCodes App Ops the callback is interested in checking
214      *
215      * @see #removeCallback(int[], Callback)
216      */
217     @Override
addCallback(int[] opsCodes, AppOpsController.Callback callback)218     public void addCallback(int[] opsCodes, AppOpsController.Callback callback) {
219         boolean added = false;
220         final int numCodes = opsCodes.length;
221         for (int i = 0; i < numCodes; i++) {
222             if (mCallbacksByCode.contains(opsCodes[i])) {
223                 mCallbacksByCode.get(opsCodes[i]).add(callback);
224                 added = true;
225             } else {
226                 if (DEBUG) Log.wtf(TAG, "APP_OP " + opsCodes[i] + " not supported");
227             }
228         }
229         if (added) mCallbacks.add(callback);
230         if (!mCallbacks.isEmpty()) setListening(true);
231     }
232 
233     /**
234      * Removes a callback from those notified when an AppOp of the type the controller tracks
235      * changes
236      *
237      * @param callback Callback to stop reporting changes
238      * @param opsCodes App Ops the callback was interested in checking
239      *
240      * @see #addCallback(int[], Callback)
241      */
242     @Override
removeCallback(int[] opsCodes, AppOpsController.Callback callback)243     public void removeCallback(int[] opsCodes, AppOpsController.Callback callback) {
244         final int numCodes = opsCodes.length;
245         for (int i = 0; i < numCodes; i++) {
246             if (mCallbacksByCode.contains(opsCodes[i])) {
247                 mCallbacksByCode.get(opsCodes[i]).remove(callback);
248             }
249         }
250         mCallbacks.remove(callback);
251         if (mCallbacks.isEmpty()) setListening(false);
252     }
253 
254     // Find item number in list, only call if the list passed is locked
getAppOpItemLocked(List<AppOpItem> appOpList, int code, int uid, String packageName)255     private AppOpItem getAppOpItemLocked(List<AppOpItem> appOpList, int code, int uid,
256             String packageName) {
257         final int itemsQ = appOpList.size();
258         for (int i = 0; i < itemsQ; i++) {
259             AppOpItem item = appOpList.get(i);
260             if (item.getCode() == code && item.getUid() == uid
261                     && item.getPackageName().equals(packageName)) {
262                 return item;
263             }
264         }
265         return null;
266     }
267 
updateActives(int code, int uid, String packageName, boolean active)268     private boolean updateActives(int code, int uid, String packageName, boolean active) {
269         synchronized (mActiveItems) {
270             AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName);
271             if (item == null && active) {
272                 item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime());
273                 if (isOpMicrophone(code)) {
274                     item.setDisabled(isAnyRecordingPausedLocked(uid));
275                 } else if (isOpCamera(code)) {
276                     item.setDisabled(mCameraDisabled);
277                 }
278                 mActiveItems.add(item);
279                 if (DEBUG) Log.w(TAG, "Added item: " + item.toString());
280                 return !item.isDisabled();
281             } else if (item != null && !active) {
282                 mActiveItems.remove(item);
283                 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString());
284                 return true;
285             }
286             return false;
287         }
288     }
289 
removeNoted(int code, int uid, String packageName)290     private void removeNoted(int code, int uid, String packageName) {
291         AppOpItem item;
292         synchronized (mNotedItems) {
293             item = getAppOpItemLocked(mNotedItems, code, uid, packageName);
294             if (item == null) return;
295             mNotedItems.remove(item);
296             if (DEBUG) Log.w(TAG, "Removed item: " + item.toString());
297         }
298         boolean active;
299         // Check if the item is also active
300         synchronized (mActiveItems) {
301             active = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null;
302         }
303         if (!active) {
304             notifySuscribersWorker(code, uid, packageName, false);
305         }
306     }
307 
addNoted(int code, int uid, String packageName)308     private boolean addNoted(int code, int uid, String packageName) {
309         AppOpItem item;
310         boolean createdNew = false;
311         synchronized (mNotedItems) {
312             item = getAppOpItemLocked(mNotedItems, code, uid, packageName);
313             if (item == null) {
314                 item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime());
315                 mNotedItems.add(item);
316                 if (DEBUG) Log.w(TAG, "Added item: " + item.toString());
317                 createdNew = true;
318             }
319         }
320         // We should keep this so we make sure it cannot time out.
321         mBGHandler.removeCallbacksAndMessages(item);
322         mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS);
323         return createdNew;
324     }
325 
isUserVisible(String packageName)326     private boolean isUserVisible(String packageName) {
327         return PermissionManager.shouldShowPackageForIndicatorCached(mContext, packageName);
328     }
329 
330     @WorkerThread
getActiveAppOps()331     public List<AppOpItem> getActiveAppOps() {
332         return getActiveAppOps(false);
333     }
334 
335     /**
336      * Returns a copy of the list containing all the active AppOps that the controller tracks.
337      *
338      * Call from a worker thread as it may perform long operations.
339      *
340      * @return List of active AppOps information
341      */
342     @WorkerThread
getActiveAppOps(boolean showPaused)343     public List<AppOpItem> getActiveAppOps(boolean showPaused) {
344         return getActiveAppOpsForUser(UserHandle.USER_ALL, showPaused);
345     }
346 
347     /**
348      * Returns a copy of the list containing all the active AppOps that the controller tracks, for
349      * a given user id.
350      *
351      * Call from a worker thread as it may perform long operations.
352      *
353      * @param userId User id to track, can be {@link UserHandle#USER_ALL}
354      *
355      * @return List of active AppOps information for that user id
356      */
357     @WorkerThread
getActiveAppOpsForUser(int userId, boolean showPaused)358     public List<AppOpItem> getActiveAppOpsForUser(int userId, boolean showPaused) {
359         Assert.isNotMainThread();
360         List<AppOpItem> list = new ArrayList<>();
361         synchronized (mActiveItems) {
362             final int numActiveItems = mActiveItems.size();
363             for (int i = 0; i < numActiveItems; i++) {
364                 AppOpItem item = mActiveItems.get(i);
365                 if ((userId == UserHandle.USER_ALL
366                         || UserHandle.getUserId(item.getUid()) == userId)
367                         && isUserVisible(item.getPackageName())
368                         && (showPaused || !item.isDisabled())) {
369                     list.add(item);
370                 }
371             }
372         }
373         synchronized (mNotedItems) {
374             final int numNotedItems = mNotedItems.size();
375             for (int i = 0; i < numNotedItems; i++) {
376                 AppOpItem item = mNotedItems.get(i);
377                 if ((userId == UserHandle.USER_ALL
378                         || UserHandle.getUserId(item.getUid()) == userId)
379                         && isUserVisible(item.getPackageName())) {
380                     list.add(item);
381                 }
382             }
383         }
384         return list;
385     }
386 
notifySuscribers(int code, int uid, String packageName, boolean active)387     private void notifySuscribers(int code, int uid, String packageName, boolean active) {
388         mBGHandler.post(() -> notifySuscribersWorker(code, uid, packageName, active));
389     }
390 
391     /**
392      * Required to override, delegate to other. Should not be called.
393      */
onOpActiveChanged(String op, int uid, String packageName, boolean active)394     public void onOpActiveChanged(String op, int uid, String packageName, boolean active) {
395         onOpActiveChanged(op, uid, packageName, null, active,
396                 AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
397     }
398 
399     // Get active app ops, and check if their attributions are trusted
400     @Override
onOpActiveChanged(String op, int uid, String packageName, String attributionTag, boolean active, int attributionFlags, int attributionChainId)401     public void onOpActiveChanged(String op, int uid, String packageName, String attributionTag,
402             boolean active, int attributionFlags, int attributionChainId) {
403         int code = AppOpsManager.strOpToOp(op);
404         if (DEBUG) {
405             Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s,%d,%d)", code, uid, packageName,
406                     Boolean.toString(active), attributionChainId, attributionFlags));
407         }
408         if (active && attributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE
409                 && attributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE
410                 && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR) == 0
411                 && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_TRUSTED) == 0) {
412             // if this attribution chain isn't trusted, and this isn't the accessor, do not show it.
413             return;
414         }
415         boolean activeChanged = updateActives(code, uid, packageName, active);
416         if (!activeChanged) return; // early return
417         // Check if the item is also noted, in that case, there's no update.
418         boolean alsoNoted;
419         synchronized (mNotedItems) {
420             alsoNoted = getAppOpItemLocked(mNotedItems, code, uid, packageName) != null;
421         }
422         // If active is true, we only send the update if the op is not actively noted (already true)
423         // If active is false, we only send the update if the op is not actively noted (prevent
424         // early removal)
425         if (!alsoNoted) {
426             notifySuscribers(code, uid, packageName, active);
427         }
428     }
429 
430     @Override
onOpNoted(int code, int uid, String packageName, String attributionTag, @AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result)431     public void onOpNoted(int code, int uid, String packageName,
432             String attributionTag, @AppOpsManager.OpFlags int flags,
433             @AppOpsManager.Mode int result) {
434         if (DEBUG) {
435             Log.w(TAG, "Noted op: " + code + " with result "
436                     + AppOpsManager.MODE_NAMES[result] + " for package " + packageName);
437         }
438         if (result != AppOpsManager.MODE_ALLOWED) return;
439         boolean notedAdded = addNoted(code, uid, packageName);
440         if (!notedAdded) return; // early return
441         boolean alsoActive;
442         synchronized (mActiveItems) {
443             alsoActive = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null;
444         }
445         if (!alsoActive) {
446             notifySuscribers(code, uid, packageName, true);
447         }
448     }
449 
notifySuscribersWorker(int code, int uid, String packageName, boolean active)450     private void notifySuscribersWorker(int code, int uid, String packageName, boolean active) {
451         if (mCallbacksByCode.contains(code) && isUserVisible(packageName)) {
452             if (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName);
453             for (Callback cb: mCallbacksByCode.get(code)) {
454                 cb.onActiveStateChanged(code, uid, packageName, active);
455             }
456         }
457     }
458 
459     @Override
dump(PrintWriter pw, String[] args)460     public void dump(PrintWriter pw, String[] args) {
461         pw.println("AppOpsController state:");
462         pw.println("  Listening: " + mListening);
463         pw.println("  Active Items:");
464         for (int i = 0; i < mActiveItems.size(); i++) {
465             final AppOpItem item = mActiveItems.get(i);
466             pw.print("    "); pw.println(item.toString());
467         }
468         pw.println("  Noted Items:");
469         for (int i = 0; i < mNotedItems.size(); i++) {
470             final AppOpItem item = mNotedItems.get(i);
471             pw.print("    "); pw.println(item.toString());
472         }
473 
474     }
475 
isAnyRecordingPausedLocked(int uid)476     private boolean isAnyRecordingPausedLocked(int uid) {
477         if (mMicMuted) {
478             return true;
479         }
480         List<AudioRecordingConfiguration> configs = mRecordingsByUid.get(uid);
481         if (configs == null) return false;
482         int configsNum = configs.size();
483         for (int i = 0; i < configsNum; i++) {
484             AudioRecordingConfiguration config = configs.get(i);
485             if (config.isClientSilenced()) return true;
486         }
487         return false;
488     }
489 
updateSensorDisabledStatus()490     private void updateSensorDisabledStatus() {
491         synchronized (mActiveItems) {
492             int size = mActiveItems.size();
493             for (int i = 0; i < size; i++) {
494                 AppOpItem item = mActiveItems.get(i);
495 
496                 boolean paused = false;
497                 if (isOpMicrophone(item.getCode())) {
498                     paused = isAnyRecordingPausedLocked(item.getUid());
499                 } else if (isOpCamera(item.getCode())) {
500                     paused = mCameraDisabled;
501                 }
502 
503                 if (item.isDisabled() != paused) {
504                     item.setDisabled(paused);
505                     notifySuscribers(
506                             item.getCode(),
507                             item.getUid(),
508                             item.getPackageName(),
509                             !item.isDisabled()
510                     );
511                 }
512             }
513         }
514     }
515 
516     private AudioManager.AudioRecordingCallback mAudioRecordingCallback =
517             new AudioManager.AudioRecordingCallback() {
518         @Override
519         public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
520             synchronized (mActiveItems) {
521                 mRecordingsByUid.clear();
522                 final int recordingsCount = configs.size();
523                 for (int i = 0; i < recordingsCount; i++) {
524                     AudioRecordingConfiguration recording = configs.get(i);
525 
526                     ArrayList<AudioRecordingConfiguration> recordings = mRecordingsByUid.get(
527                             recording.getClientUid());
528                     if (recordings == null) {
529                         recordings = new ArrayList<>();
530                         mRecordingsByUid.put(recording.getClientUid(), recordings);
531                     }
532                     recordings.add(recording);
533                 }
534             }
535             updateSensorDisabledStatus();
536         }
537     };
538 
539     @Override
onReceive(Context context, Intent intent)540     public void onReceive(Context context, Intent intent) {
541         mMicMuted = mAudioManager.isMicrophoneMute()
542                 || mSensorPrivacyController.isSensorBlocked(MICROPHONE);
543         updateSensorDisabledStatus();
544     }
545 
546     @Override
onSensorBlockedChanged(int sensor, boolean blocked)547     public void onSensorBlockedChanged(int sensor, boolean blocked) {
548         mBGHandler.post(() -> {
549             if (sensor == CAMERA) {
550                 mCameraDisabled = blocked;
551             } else if (sensor == MICROPHONE) {
552                 mMicMuted = mAudioManager.isMicrophoneMute() || blocked;
553             }
554             updateSensorDisabledStatus();
555         });
556     }
557 
558     @Override
isMicMuted()559     public boolean isMicMuted() {
560         return mMicMuted;
561     }
562 
isOpCamera(int op)563     private boolean isOpCamera(int op) {
564         return op == AppOpsManager.OP_CAMERA || op == AppOpsManager.OP_PHONE_CALL_CAMERA;
565     }
566 
isOpMicrophone(int op)567     private boolean isOpMicrophone(int op) {
568         return op == AppOpsManager.OP_RECORD_AUDIO || op == AppOpsManager.OP_PHONE_CALL_MICROPHONE
569                 || op == AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
570     }
571 
572     protected class H extends Handler {
H(Looper looper)573         H(Looper looper) {
574             super(looper);
575         }
576 
scheduleRemoval(AppOpItem item, long timeToRemoval)577         public void scheduleRemoval(AppOpItem item, long timeToRemoval) {
578             removeCallbacksAndMessages(item);
579             postDelayed(new Runnable() {
580                 @Override
581                 public void run() {
582                     removeNoted(item.getCode(), item.getUid(), item.getPackageName());
583                 }
584             }, item, timeToRemoval);
585         }
586     }
587 }
588