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