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