1 /* <lambda>null2 * Copyright (C) 2022 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.privacy 18 19 import android.app.AppOpsManager 20 import android.content.Context 21 import android.content.pm.UserInfo 22 import android.os.UserHandle 23 import com.android.internal.annotations.GuardedBy 24 import com.android.internal.annotations.VisibleForTesting 25 import com.android.systemui.appops.AppOpItem 26 import com.android.systemui.appops.AppOpsController 27 import com.android.systemui.dagger.SysUISingleton 28 import com.android.systemui.dagger.qualifiers.Background 29 import com.android.systemui.privacy.logging.PrivacyLogger 30 import com.android.systemui.settings.UserTracker 31 import com.android.systemui.util.asIndenting 32 import com.android.systemui.util.concurrency.DelayableExecutor 33 import com.android.systemui.util.withIncreasedIndent 34 import java.io.PrintWriter 35 import javax.inject.Inject 36 37 /** 38 * Monitors privacy items backed by app ops: 39 * - Mic & Camera 40 * - Location 41 * 42 * If [PrivacyConfig.micCameraAvailable] / [PrivacyConfig.locationAvailable] are disabled, 43 * the corresponding PrivacyItems will not be reported. 44 */ 45 @SysUISingleton 46 class AppOpsPrivacyItemMonitor @Inject constructor( 47 private val appOpsController: AppOpsController, 48 private val userTracker: UserTracker, 49 private val privacyConfig: PrivacyConfig, 50 @Background private val bgExecutor: DelayableExecutor, 51 private val logger: PrivacyLogger 52 ) : PrivacyItemMonitor { 53 54 @VisibleForTesting 55 companion object { 56 val OPS_MIC_CAMERA = intArrayOf( 57 AppOpsManager.OP_CAMERA, 58 AppOpsManager.OP_PHONE_CALL_CAMERA, 59 AppOpsManager.OP_RECORD_AUDIO, 60 AppOpsManager.OP_PHONE_CALL_MICROPHONE, 61 AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, 62 AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, 63 AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO) 64 val OPS_LOCATION = intArrayOf( 65 AppOpsManager.OP_COARSE_LOCATION, 66 AppOpsManager.OP_FINE_LOCATION) 67 val OPS = OPS_MIC_CAMERA + OPS_LOCATION 68 val USER_INDEPENDENT_OPS = intArrayOf(AppOpsManager.OP_PHONE_CALL_CAMERA, 69 AppOpsManager.OP_PHONE_CALL_MICROPHONE) 70 } 71 72 private val lock = Any() 73 74 @GuardedBy("lock") 75 private var callback: PrivacyItemMonitor.Callback? = null 76 @GuardedBy("lock") 77 private var micCameraAvailable = privacyConfig.micCameraAvailable 78 @GuardedBy("lock") 79 private var locationAvailable = privacyConfig.locationAvailable 80 @GuardedBy("lock") 81 private var listening = false 82 83 private val appOpsCallback = object : AppOpsController.Callback { 84 override fun onActiveStateChanged( 85 code: Int, 86 uid: Int, 87 packageName: String, 88 active: Boolean 89 ) { 90 synchronized(lock) { 91 // Check if we care about this code right now 92 if (code in OPS_MIC_CAMERA && !micCameraAvailable) { 93 return 94 } 95 if (code in OPS_LOCATION && !locationAvailable) { 96 return 97 } 98 if (userTracker.userProfiles.any { it.id == UserHandle.getUserId(uid) } || 99 code in USER_INDEPENDENT_OPS) { 100 logger.logUpdatedItemFromAppOps(code, uid, packageName, active) 101 dispatchOnPrivacyItemsChanged() 102 } 103 } 104 } 105 } 106 107 @VisibleForTesting 108 internal val userTrackerCallback = object : UserTracker.Callback { 109 override fun onUserChanged(newUser: Int, userContext: Context) { 110 onCurrentProfilesChanged() 111 } 112 113 override fun onProfilesChanged(profiles: List<UserInfo>) { 114 onCurrentProfilesChanged() 115 } 116 } 117 118 private val configCallback = object : PrivacyConfig.Callback { 119 override fun onFlagLocationChanged(flag: Boolean) { 120 onFlagChanged() 121 } 122 123 override fun onFlagMicCameraChanged(flag: Boolean) { 124 onFlagChanged() 125 } 126 127 private fun onFlagChanged() { 128 synchronized(lock) { 129 micCameraAvailable = privacyConfig.micCameraAvailable 130 locationAvailable = privacyConfig.locationAvailable 131 setListeningStateLocked() 132 } 133 dispatchOnPrivacyItemsChanged() 134 } 135 } 136 137 init { 138 privacyConfig.addCallback(configCallback) 139 } 140 141 override fun startListening(callback: PrivacyItemMonitor.Callback) { 142 synchronized(lock) { 143 this.callback = callback 144 setListeningStateLocked() 145 } 146 } 147 148 override fun stopListening() { 149 synchronized(lock) { 150 this.callback = null 151 setListeningStateLocked() 152 } 153 } 154 155 /** 156 * Updates listening status based on whether there are callbacks and the indicators are enabled. 157 * 158 * Always listen to all OPS so we don't have to figure out what we should be listening to. We 159 * still have to filter anyway. Updates are filtered in the callback. 160 * 161 * This is only called from private (add/remove)Callback and from the config listener, all in 162 * main thread. 163 */ 164 @GuardedBy("lock") 165 private fun setListeningStateLocked() { 166 val shouldListen = callback != null && (micCameraAvailable || locationAvailable) 167 if (listening == shouldListen) { 168 return 169 } 170 171 listening = shouldListen 172 if (shouldListen) { 173 appOpsController.addCallback(OPS, appOpsCallback) 174 userTracker.addCallback(userTrackerCallback, bgExecutor) 175 onCurrentProfilesChanged() 176 } else { 177 appOpsController.removeCallback(OPS, appOpsCallback) 178 userTracker.removeCallback(userTrackerCallback) 179 } 180 } 181 182 override fun getActivePrivacyItems(): List<PrivacyItem> { 183 val activeAppOps = appOpsController.getActiveAppOps(true) 184 val currentUserProfiles = userTracker.userProfiles 185 186 return synchronized(lock) { 187 activeAppOps.filter { 188 currentUserProfiles.any { user -> user.id == UserHandle.getUserId(it.uid) } || 189 it.code in USER_INDEPENDENT_OPS 190 }.mapNotNull { toPrivacyItemLocked(it) } 191 }.distinct() 192 } 193 194 @GuardedBy("lock") 195 private fun privacyItemForAppOpEnabledLocked(code: Int): Boolean { 196 if (code in OPS_LOCATION) { 197 return locationAvailable 198 } else if (code in OPS_MIC_CAMERA) { 199 return micCameraAvailable 200 } else { 201 return false 202 } 203 } 204 205 @GuardedBy("lock") 206 private fun toPrivacyItemLocked(appOpItem: AppOpItem): PrivacyItem? { 207 if (!privacyItemForAppOpEnabledLocked(appOpItem.code)) { 208 return null 209 } 210 val type: PrivacyType = when (appOpItem.code) { 211 AppOpsManager.OP_PHONE_CALL_CAMERA, 212 AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA 213 AppOpsManager.OP_COARSE_LOCATION, 214 AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION 215 AppOpsManager.OP_PHONE_CALL_MICROPHONE, 216 AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, 217 AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, 218 AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, 219 AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE 220 else -> return null 221 } 222 val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid) 223 return PrivacyItem(type, app, appOpItem.timeStartedElapsed, appOpItem.isDisabled) 224 } 225 226 private fun onCurrentProfilesChanged() { 227 val currentUserIds = userTracker.userProfiles.map { it.id } 228 logger.logCurrentProfilesChanged(currentUserIds) 229 dispatchOnPrivacyItemsChanged() 230 } 231 232 private fun dispatchOnPrivacyItemsChanged() { 233 val cb = synchronized(lock) { callback } 234 if (cb != null) { 235 bgExecutor.execute { 236 cb.onPrivacyItemsChanged() 237 } 238 } 239 } 240 241 override fun dump(pw: PrintWriter, args: Array<out String>) { 242 val ipw = pw.asIndenting() 243 ipw.println("AppOpsPrivacyItemMonitor:") 244 ipw.withIncreasedIndent { 245 synchronized(lock) { 246 ipw.println("Listening: $listening") 247 ipw.println("micCameraAvailable: $micCameraAvailable") 248 ipw.println("locationAvailable: $locationAvailable") 249 ipw.println("Callback: $callback") 250 } 251 ipw.println("Current user ids: ${userTracker.userProfiles.map { it.id }}") 252 } 253 ipw.flush() 254 } 255 } 256