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.FileDescriptor;
52 import java.io.PrintWriter;
53 import java.util.ArrayList;
54 import java.util.List;
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.OnOpNotedListener, 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_PHONE_CALL_MICROPHONE,
107             AppOpsManager.OP_COARSE_LOCATION,
108             AppOpsManager.OP_FINE_LOCATION
109     };
110 
111     @Inject
AppOpsControllerImpl( Context context, @Background Looper bgLooper, DumpManager dumpManager, AudioManager audioManager, IndividualSensorPrivacyController sensorPrivacyController, BroadcastDispatcher dispatcher, SystemClock clock )112     public AppOpsControllerImpl(
113             Context context,
114             @Background Looper bgLooper,
115             DumpManager dumpManager,
116             AudioManager audioManager,
117             IndividualSensorPrivacyController sensorPrivacyController,
118             BroadcastDispatcher dispatcher,
119             SystemClock clock
120     ) {
121         mDispatcher = dispatcher;
122         mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
123         mBGHandler = new H(bgLooper);
124         final int numOps = OPS.length;
125         for (int i = 0; i < numOps; i++) {
126             mCallbacksByCode.put(OPS[i], new ArraySet<>());
127         }
128         mAudioManager = audioManager;
129         mSensorPrivacyController = sensorPrivacyController;
130         mMicMuted = audioManager.isMicrophoneMute()
131                 || mSensorPrivacyController.isSensorBlocked(MICROPHONE);
132         mCameraDisabled = mSensorPrivacyController.isSensorBlocked(CAMERA);
133         mContext = context;
134         mClock = clock;
135         dumpManager.registerDumpable(TAG, this);
136     }
137 
138     @VisibleForTesting
setBGHandler(H handler)139     protected void setBGHandler(H handler) {
140         mBGHandler = handler;
141     }
142 
143     @VisibleForTesting
setListening(boolean listening)144     protected void setListening(boolean listening) {
145         mListening = listening;
146         if (listening) {
147             mAppOps.startWatchingActive(OPS, this);
148             mAppOps.startWatchingNoted(OPS, this);
149             mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback, mBGHandler);
150             mSensorPrivacyController.addCallback(this);
151 
152             mMicMuted = mAudioManager.isMicrophoneMute()
153                     || mSensorPrivacyController.isSensorBlocked(MICROPHONE);
154             mCameraDisabled = mSensorPrivacyController.isSensorBlocked(CAMERA);
155 
156             mBGHandler.post(() -> mAudioRecordingCallback.onRecordingConfigChanged(
157                     mAudioManager.getActiveRecordingConfigurations()));
158             mDispatcher.registerReceiverWithHandler(this,
159                     new IntentFilter(ACTION_MICROPHONE_MUTE_CHANGED), mBGHandler);
160 
161         } else {
162             mAppOps.stopWatchingActive(this);
163             mAppOps.stopWatchingNoted(this);
164             mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback);
165             mSensorPrivacyController.removeCallback(this);
166 
167             mBGHandler.removeCallbacksAndMessages(null); // null removes all
168             mDispatcher.unregisterReceiver(this);
169             synchronized (mActiveItems) {
170                 mActiveItems.clear();
171                 mRecordingsByUid.clear();
172             }
173             synchronized (mNotedItems) {
174                 mNotedItems.clear();
175             }
176         }
177     }
178 
179     /**
180      * Adds a callback that will get notifified when an AppOp of the type the controller tracks
181      * changes
182      *
183      * @param callback Callback to report changes
184      * @param opsCodes App Ops the callback is interested in checking
185      *
186      * @see #removeCallback(int[], Callback)
187      */
188     @Override
addCallback(int[] opsCodes, AppOpsController.Callback callback)189     public void addCallback(int[] opsCodes, AppOpsController.Callback callback) {
190         boolean added = false;
191         final int numCodes = opsCodes.length;
192         for (int i = 0; i < numCodes; i++) {
193             if (mCallbacksByCode.contains(opsCodes[i])) {
194                 mCallbacksByCode.get(opsCodes[i]).add(callback);
195                 added = true;
196             } else {
197                 if (DEBUG) Log.wtf(TAG, "APP_OP " + opsCodes[i] + " not supported");
198             }
199         }
200         if (added) mCallbacks.add(callback);
201         if (!mCallbacks.isEmpty()) setListening(true);
202     }
203 
204     /**
205      * Removes a callback from those notified when an AppOp of the type the controller tracks
206      * changes
207      *
208      * @param callback Callback to stop reporting changes
209      * @param opsCodes App Ops the callback was interested in checking
210      *
211      * @see #addCallback(int[], Callback)
212      */
213     @Override
removeCallback(int[] opsCodes, AppOpsController.Callback callback)214     public void removeCallback(int[] opsCodes, AppOpsController.Callback callback) {
215         final int numCodes = opsCodes.length;
216         for (int i = 0; i < numCodes; i++) {
217             if (mCallbacksByCode.contains(opsCodes[i])) {
218                 mCallbacksByCode.get(opsCodes[i]).remove(callback);
219             }
220         }
221         mCallbacks.remove(callback);
222         if (mCallbacks.isEmpty()) setListening(false);
223     }
224 
225     // Find item number in list, only call if the list passed is locked
getAppOpItemLocked(List<AppOpItem> appOpList, int code, int uid, String packageName)226     private AppOpItem getAppOpItemLocked(List<AppOpItem> appOpList, int code, int uid,
227             String packageName) {
228         final int itemsQ = appOpList.size();
229         for (int i = 0; i < itemsQ; i++) {
230             AppOpItem item = appOpList.get(i);
231             if (item.getCode() == code && item.getUid() == uid
232                     && item.getPackageName().equals(packageName)) {
233                 return item;
234             }
235         }
236         return null;
237     }
238 
updateActives(int code, int uid, String packageName, boolean active)239     private boolean updateActives(int code, int uid, String packageName, boolean active) {
240         synchronized (mActiveItems) {
241             AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName);
242             if (item == null && active) {
243                 item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime());
244                 if (isOpMicrophone(code)) {
245                     item.setDisabled(isAnyRecordingPausedLocked(uid));
246                 } else if (isOpCamera(code)) {
247                     item.setDisabled(mCameraDisabled);
248                 }
249                 mActiveItems.add(item);
250                 if (DEBUG) Log.w(TAG, "Added item: " + item.toString());
251                 return !item.isDisabled();
252             } else if (item != null && !active) {
253                 mActiveItems.remove(item);
254                 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString());
255                 return true;
256             }
257             return false;
258         }
259     }
260 
removeNoted(int code, int uid, String packageName)261     private void removeNoted(int code, int uid, String packageName) {
262         AppOpItem item;
263         synchronized (mNotedItems) {
264             item = getAppOpItemLocked(mNotedItems, code, uid, packageName);
265             if (item == null) return;
266             mNotedItems.remove(item);
267             if (DEBUG) Log.w(TAG, "Removed item: " + item.toString());
268         }
269         boolean active;
270         // Check if the item is also active
271         synchronized (mActiveItems) {
272             active = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null;
273         }
274         if (!active) {
275             notifySuscribersWorker(code, uid, packageName, false);
276         }
277     }
278 
addNoted(int code, int uid, String packageName)279     private boolean addNoted(int code, int uid, String packageName) {
280         AppOpItem item;
281         boolean createdNew = false;
282         synchronized (mNotedItems) {
283             item = getAppOpItemLocked(mNotedItems, code, uid, packageName);
284             if (item == null) {
285                 item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime());
286                 mNotedItems.add(item);
287                 if (DEBUG) Log.w(TAG, "Added item: " + item.toString());
288                 createdNew = true;
289             }
290         }
291         // We should keep this so we make sure it cannot time out.
292         mBGHandler.removeCallbacksAndMessages(item);
293         mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS);
294         return createdNew;
295     }
296 
isUserVisible(String packageName)297     private boolean isUserVisible(String packageName) {
298         return PermissionManager.shouldShowPackageForIndicatorCached(mContext, packageName);
299     }
300 
301     @WorkerThread
getActiveAppOps()302     public List<AppOpItem> getActiveAppOps() {
303         return getActiveAppOps(false);
304     }
305 
306     /**
307      * Returns a copy of the list containing all the active AppOps that the controller tracks.
308      *
309      * Call from a worker thread as it may perform long operations.
310      *
311      * @return List of active AppOps information
312      */
313     @WorkerThread
getActiveAppOps(boolean showPaused)314     public List<AppOpItem> getActiveAppOps(boolean showPaused) {
315         return getActiveAppOpsForUser(UserHandle.USER_ALL, showPaused);
316     }
317 
318     /**
319      * Returns a copy of the list containing all the active AppOps that the controller tracks, for
320      * a given user id.
321      *
322      * Call from a worker thread as it may perform long operations.
323      *
324      * @param userId User id to track, can be {@link UserHandle#USER_ALL}
325      *
326      * @return List of active AppOps information for that user id
327      */
328     @WorkerThread
getActiveAppOpsForUser(int userId, boolean showPaused)329     public List<AppOpItem> getActiveAppOpsForUser(int userId, boolean showPaused) {
330         Assert.isNotMainThread();
331         List<AppOpItem> list = new ArrayList<>();
332         synchronized (mActiveItems) {
333             final int numActiveItems = mActiveItems.size();
334             for (int i = 0; i < numActiveItems; i++) {
335                 AppOpItem item = mActiveItems.get(i);
336                 if ((userId == UserHandle.USER_ALL
337                         || UserHandle.getUserId(item.getUid()) == userId)
338                         && isUserVisible(item.getPackageName())
339                         && (showPaused || !item.isDisabled())) {
340                     list.add(item);
341                 }
342             }
343         }
344         synchronized (mNotedItems) {
345             final int numNotedItems = mNotedItems.size();
346             for (int i = 0; i < numNotedItems; i++) {
347                 AppOpItem item = mNotedItems.get(i);
348                 if ((userId == UserHandle.USER_ALL
349                         || UserHandle.getUserId(item.getUid()) == userId)
350                         && isUserVisible(item.getPackageName())) {
351                     list.add(item);
352                 }
353             }
354         }
355         return list;
356     }
357 
notifySuscribers(int code, int uid, String packageName, boolean active)358     private void notifySuscribers(int code, int uid, String packageName, boolean active) {
359         mBGHandler.post(() -> notifySuscribersWorker(code, uid, packageName, active));
360     }
361 
362     /**
363      * Required to override, delegate to other. Should not be called.
364      */
onOpActiveChanged(String op, int uid, String packageName, boolean active)365     public void onOpActiveChanged(String op, int uid, String packageName, boolean active) {
366         onOpActiveChanged(op, uid, packageName, null, active,
367                 AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
368     }
369 
370     // Get active app ops, and check if their attributions are trusted
371     @Override
onOpActiveChanged(String op, int uid, String packageName, String attributionTag, boolean active, int attributionFlags, int attributionChainId)372     public void onOpActiveChanged(String op, int uid, String packageName, String attributionTag,
373             boolean active, int attributionFlags, int attributionChainId) {
374         int code = AppOpsManager.strOpToOp(op);
375         if (DEBUG) {
376             Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s,%d,%d)", code, uid, packageName,
377                     Boolean.toString(active), attributionChainId, attributionFlags));
378         }
379         if (attributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE
380                 && attributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE
381                 && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR) == 0
382                 && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_TRUSTED) == 0) {
383             // if this attribution chain isn't trusted, and this isn't the accessor, do not show it.
384             return;
385         }
386         boolean activeChanged = updateActives(code, uid, packageName, active);
387         if (!activeChanged) return; // early return
388         // Check if the item is also noted, in that case, there's no update.
389         boolean alsoNoted;
390         synchronized (mNotedItems) {
391             alsoNoted = getAppOpItemLocked(mNotedItems, code, uid, packageName) != null;
392         }
393         // If active is true, we only send the update if the op is not actively noted (already true)
394         // If active is false, we only send the update if the op is not actively noted (prevent
395         // early removal)
396         if (!alsoNoted) {
397             notifySuscribers(code, uid, packageName, active);
398         }
399     }
400 
401     @Override
onOpNoted(int code, int uid, String packageName, String attributionTag, @AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result)402     public void onOpNoted(int code, int uid, String packageName,
403             String attributionTag, @AppOpsManager.OpFlags int flags,
404             @AppOpsManager.Mode int result) {
405         if (DEBUG) {
406             Log.w(TAG, "Noted op: " + code + " with result "
407                     + AppOpsManager.MODE_NAMES[result] + " for package " + packageName);
408         }
409         if (result != AppOpsManager.MODE_ALLOWED) return;
410         boolean notedAdded = addNoted(code, uid, packageName);
411         if (!notedAdded) return; // early return
412         boolean alsoActive;
413         synchronized (mActiveItems) {
414             alsoActive = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null;
415         }
416         if (!alsoActive) {
417             notifySuscribers(code, uid, packageName, true);
418         }
419     }
420 
notifySuscribersWorker(int code, int uid, String packageName, boolean active)421     private void notifySuscribersWorker(int code, int uid, String packageName, boolean active) {
422         if (mCallbacksByCode.contains(code) && isUserVisible(packageName)) {
423             if (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName);
424             for (Callback cb: mCallbacksByCode.get(code)) {
425                 cb.onActiveStateChanged(code, uid, packageName, active);
426             }
427         }
428     }
429 
430     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)431     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
432         pw.println("AppOpsController state:");
433         pw.println("  Listening: " + mListening);
434         pw.println("  Active Items:");
435         for (int i = 0; i < mActiveItems.size(); i++) {
436             final AppOpItem item = mActiveItems.get(i);
437             pw.print("    "); pw.println(item.toString());
438         }
439         pw.println("  Noted Items:");
440         for (int i = 0; i < mNotedItems.size(); i++) {
441             final AppOpItem item = mNotedItems.get(i);
442             pw.print("    "); pw.println(item.toString());
443         }
444 
445     }
446 
isAnyRecordingPausedLocked(int uid)447     private boolean isAnyRecordingPausedLocked(int uid) {
448         if (mMicMuted) {
449             return true;
450         }
451         List<AudioRecordingConfiguration> configs = mRecordingsByUid.get(uid);
452         if (configs == null) return false;
453         int configsNum = configs.size();
454         for (int i = 0; i < configsNum; i++) {
455             AudioRecordingConfiguration config = configs.get(i);
456             if (config.isClientSilenced()) return true;
457         }
458         return false;
459     }
460 
updateSensorDisabledStatus()461     private void updateSensorDisabledStatus() {
462         synchronized (mActiveItems) {
463             int size = mActiveItems.size();
464             for (int i = 0; i < size; i++) {
465                 AppOpItem item = mActiveItems.get(i);
466 
467                 boolean paused = false;
468                 if (isOpMicrophone(item.getCode())) {
469                     paused = isAnyRecordingPausedLocked(item.getUid());
470                 } else if (isOpCamera(item.getCode())) {
471                     paused = mCameraDisabled;
472                 }
473 
474                 if (item.isDisabled() != paused) {
475                     item.setDisabled(paused);
476                     notifySuscribers(
477                             item.getCode(),
478                             item.getUid(),
479                             item.getPackageName(),
480                             !item.isDisabled()
481                     );
482                 }
483             }
484         }
485     }
486 
487     private AudioManager.AudioRecordingCallback mAudioRecordingCallback =
488             new AudioManager.AudioRecordingCallback() {
489         @Override
490         public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
491             synchronized (mActiveItems) {
492                 mRecordingsByUid.clear();
493                 final int recordingsCount = configs.size();
494                 for (int i = 0; i < recordingsCount; i++) {
495                     AudioRecordingConfiguration recording = configs.get(i);
496 
497                     ArrayList<AudioRecordingConfiguration> recordings = mRecordingsByUid.get(
498                             recording.getClientUid());
499                     if (recordings == null) {
500                         recordings = new ArrayList<>();
501                         mRecordingsByUid.put(recording.getClientUid(), recordings);
502                     }
503                     recordings.add(recording);
504                 }
505             }
506             updateSensorDisabledStatus();
507         }
508     };
509 
510     @Override
onReceive(Context context, Intent intent)511     public void onReceive(Context context, Intent intent) {
512         mMicMuted = mAudioManager.isMicrophoneMute()
513                 || mSensorPrivacyController.isSensorBlocked(MICROPHONE);
514         updateSensorDisabledStatus();
515     }
516 
517     @Override
onSensorBlockedChanged(int sensor, boolean blocked)518     public void onSensorBlockedChanged(int sensor, boolean blocked) {
519         mBGHandler.post(() -> {
520             if (sensor == CAMERA) {
521                 mCameraDisabled = blocked;
522             } else if (sensor == MICROPHONE) {
523                 mMicMuted = mAudioManager.isMicrophoneMute() || blocked;
524             }
525             updateSensorDisabledStatus();
526         });
527     }
528 
529     @Override
isMicMuted()530     public boolean isMicMuted() {
531         return mMicMuted;
532     }
533 
isOpCamera(int op)534     private boolean isOpCamera(int op) {
535         return op == AppOpsManager.OP_CAMERA || op == AppOpsManager.OP_PHONE_CALL_CAMERA;
536     }
537 
isOpMicrophone(int op)538     private boolean isOpMicrophone(int op) {
539         return op == AppOpsManager.OP_RECORD_AUDIO || op == AppOpsManager.OP_PHONE_CALL_MICROPHONE;
540     }
541 
542     protected class H extends Handler {
H(Looper looper)543         H(Looper looper) {
544             super(looper);
545         }
546 
scheduleRemoval(AppOpItem item, long timeToRemoval)547         public void scheduleRemoval(AppOpItem item, long timeToRemoval) {
548             removeCallbacksAndMessages(item);
549             postDelayed(new Runnable() {
550                 @Override
551                 public void run() {
552                     removeNoted(item.getCode(), item.getUid(), item.getPackageName());
553                 }
554             }, item, timeToRemoval);
555         }
556     }
557 }
558