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