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