1 /* 2 * Copyright (C) 2020 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.Intent 22 import android.content.IntentFilter 23 import android.content.pm.UserInfo 24 import android.os.UserHandle 25 import android.provider.DeviceConfig 26 import com.android.internal.annotations.VisibleForTesting 27 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags 28 import com.android.systemui.Dumpable 29 import com.android.systemui.appops.AppOpItem 30 import com.android.systemui.appops.AppOpsController 31 import com.android.systemui.dagger.SysUISingleton 32 import com.android.systemui.dagger.qualifiers.Background 33 import com.android.systemui.dagger.qualifiers.Main 34 import com.android.systemui.dump.DumpManager 35 import com.android.systemui.privacy.logging.PrivacyLogger 36 import com.android.systemui.settings.UserTracker 37 import com.android.systemui.util.DeviceConfigProxy 38 import com.android.systemui.util.concurrency.DelayableExecutor 39 import com.android.systemui.util.time.SystemClock 40 import java.io.FileDescriptor 41 import java.io.PrintWriter 42 import java.lang.ref.WeakReference 43 import java.util.concurrent.Executor 44 import javax.inject.Inject 45 46 @SysUISingleton 47 class PrivacyItemController @Inject constructor( 48 private val appOpsController: AppOpsController, 49 @Main uiExecutor: DelayableExecutor, 50 @Background private val bgExecutor: DelayableExecutor, 51 private val deviceConfigProxy: DeviceConfigProxy, 52 private val userTracker: UserTracker, 53 private val logger: PrivacyLogger, 54 private val systemClock: SystemClock, 55 dumpManager: DumpManager 56 ) : Dumpable { 57 58 @VisibleForTesting 59 internal companion object { 60 val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA, 61 AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO, 62 AppOpsManager.OP_PHONE_CALL_MICROPHONE) 63 val OPS_LOCATION = intArrayOf( 64 AppOpsManager.OP_COARSE_LOCATION, 65 AppOpsManager.OP_FINE_LOCATION) 66 val OPS = OPS_MIC_CAMERA + OPS_LOCATION 67 val intentFilter = IntentFilter().apply { 68 addAction(Intent.ACTION_USER_SWITCHED) 69 addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) 70 addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) 71 } 72 const val TAG = "PrivacyItemController" 73 private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED 74 private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED 75 private const val DEFAULT_MIC_CAMERA = true 76 private const val DEFAULT_LOCATION = false 77 @VisibleForTesting const val TIME_TO_HOLD_INDICATORS = 5000L 78 } 79 80 @VisibleForTesting 81 internal var privacyList = emptyList<PrivacyItem>() 82 @Synchronized get() = field.toList() // Returns a shallow copy of the list 83 @Synchronized set 84 85 private fun isMicCameraEnabled(): Boolean { 86 return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, 87 MIC_CAMERA, DEFAULT_MIC_CAMERA) 88 } 89 90 private fun isLocationEnabled(): Boolean { 91 return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, 92 LOCATION, DEFAULT_LOCATION) 93 } 94 95 private var currentUserIds = emptyList<Int>() 96 private var listening = false 97 private val callbacks = mutableListOf<WeakReference<Callback>>() 98 private val internalUiExecutor = MyExecutor(uiExecutor) 99 100 private var holdingRunnableCanceler: Runnable? = null 101 102 private val notifyChanges = Runnable { 103 val list = privacyList 104 callbacks.forEach { it.get()?.onPrivacyItemsChanged(list) } 105 } 106 107 private val updateListAndNotifyChanges = Runnable { 108 updatePrivacyList() 109 uiExecutor.execute(notifyChanges) 110 } 111 112 var micCameraAvailable = isMicCameraEnabled() 113 private set 114 var locationAvailable = isLocationEnabled() 115 116 var allIndicatorsAvailable = micCameraAvailable && locationAvailable 117 118 private val devicePropertiesChangedListener = 119 object : DeviceConfig.OnPropertiesChangedListener { 120 override fun onPropertiesChanged(properties: DeviceConfig.Properties) { 121 if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) && 122 (properties.keyset.contains(MIC_CAMERA) || 123 properties.keyset.contains(LOCATION))) { 124 125 // Running on the ui executor so can iterate on callbacks 126 if (properties.keyset.contains(MIC_CAMERA)) { 127 micCameraAvailable = properties.getBoolean(MIC_CAMERA, DEFAULT_MIC_CAMERA) 128 allIndicatorsAvailable = micCameraAvailable && locationAvailable 129 callbacks.forEach { it.get()?.onFlagMicCameraChanged(micCameraAvailable) } 130 } 131 132 if (properties.keyset.contains(LOCATION)) { 133 locationAvailable = properties.getBoolean(LOCATION, DEFAULT_LOCATION) 134 allIndicatorsAvailable = micCameraAvailable && locationAvailable 135 callbacks.forEach { it.get()?.onFlagLocationChanged(locationAvailable) } 136 } 137 internalUiExecutor.updateListeningState() 138 } 139 } 140 } 141 142 private val cb = object : AppOpsController.Callback { 143 override fun onActiveStateChanged( 144 code: Int, 145 uid: Int, 146 packageName: String, 147 active: Boolean 148 ) { 149 // Check if we care about this code right now 150 if (code in OPS_LOCATION && !locationAvailable) { 151 return 152 } 153 val userId = UserHandle.getUserId(uid) 154 if (userId in currentUserIds || 155 code == AppOpsManager.OP_PHONE_CALL_MICROPHONE || 156 code == AppOpsManager.OP_PHONE_CALL_CAMERA) { 157 logger.logUpdatedItemFromAppOps(code, uid, packageName, active) 158 update(false) 159 } 160 } 161 } 162 163 @VisibleForTesting 164 internal var userTrackerCallback = object : UserTracker.Callback { 165 override fun onUserChanged(newUser: Int, userContext: Context) { 166 update(true) 167 } 168 169 override fun onProfilesChanged(profiles: List<UserInfo>) { 170 update(true) 171 } 172 } 173 174 init { 175 deviceConfigProxy.addOnPropertiesChangedListener( 176 DeviceConfig.NAMESPACE_PRIVACY, 177 uiExecutor, 178 devicePropertiesChangedListener) 179 dumpManager.registerDumpable(TAG, this) 180 } 181 182 private fun unregisterListener() { 183 userTracker.removeCallback(userTrackerCallback) 184 } 185 186 private fun registerReceiver() { 187 userTracker.addCallback(userTrackerCallback, bgExecutor) 188 } 189 190 private fun update(updateUsers: Boolean) { 191 bgExecutor.execute { 192 if (updateUsers) { 193 currentUserIds = userTracker.userProfiles.map { it.id } 194 logger.logCurrentProfilesChanged(currentUserIds) 195 } 196 updateListAndNotifyChanges.run() 197 } 198 } 199 200 /** 201 * Updates listening status based on whether there are callbacks and the indicators are enabled. 202 * 203 * Always listen to all OPS so we don't have to figure out what we should be listening to. We 204 * still have to filter anyway. Updates are filtered in the callback. 205 * 206 * This is only called from private (add/remove)Callback and from the config listener, all in 207 * main thread. 208 */ 209 private fun setListeningState() { 210 val listen = !callbacks.isEmpty() and 211 (micCameraAvailable || locationAvailable) 212 if (listening == listen) return 213 listening = listen 214 if (listening) { 215 appOpsController.addCallback(OPS, cb) 216 registerReceiver() 217 update(true) 218 } else { 219 appOpsController.removeCallback(OPS, cb) 220 unregisterListener() 221 // Make sure that we remove all indicators and notify listeners if we are not 222 // listening anymore due to indicators being disabled 223 update(false) 224 } 225 } 226 227 private fun addCallback(callback: WeakReference<Callback>) { 228 callbacks.add(callback) 229 if (callbacks.isNotEmpty() && !listening) { 230 internalUiExecutor.updateListeningState() 231 } 232 // Notify this callback if we didn't set to listening 233 else if (listening) { 234 internalUiExecutor.execute(NotifyChangesToCallback(callback.get(), privacyList)) 235 } 236 } 237 238 private fun removeCallback(callback: WeakReference<Callback>) { 239 // Removes also if the callback is null 240 callbacks.removeIf { it.get()?.equals(callback.get()) ?: true } 241 if (callbacks.isEmpty()) { 242 internalUiExecutor.updateListeningState() 243 } 244 } 245 246 fun addCallback(callback: Callback) { 247 addCallback(WeakReference(callback)) 248 } 249 250 fun removeCallback(callback: Callback) { 251 removeCallback(WeakReference(callback)) 252 } 253 254 private fun updatePrivacyList() { 255 holdingRunnableCanceler?.run()?.also { 256 holdingRunnableCanceler = null 257 } 258 if (!listening) { 259 privacyList = emptyList() 260 return 261 } 262 val list = appOpsController.getActiveAppOps(true).filter { 263 UserHandle.getUserId(it.uid) in currentUserIds || 264 it.code == AppOpsManager.OP_PHONE_CALL_MICROPHONE || 265 it.code == AppOpsManager.OP_PHONE_CALL_CAMERA 266 }.mapNotNull { toPrivacyItem(it) }.distinct() 267 privacyList = processNewList(list) 268 } 269 270 /** 271 * Figure out which items have not been around for long enough and put them back in the list. 272 * 273 * Also schedule when we should check again to remove expired items. Because we always retrieve 274 * the current list, we have the latest info. 275 * 276 * @param list map of list retrieved from [AppOpsController]. 277 * @return a list that may have added items that should be kept for some time. 278 */ 279 private fun processNewList(list: List<PrivacyItem>): List<PrivacyItem> { 280 logger.logRetrievedPrivacyItemsList(list) 281 282 // Anything earlier than this timestamp can be removed 283 val removeBeforeTime = systemClock.elapsedRealtime() - TIME_TO_HOLD_INDICATORS 284 val mustKeep = privacyList.filter { 285 it.timeStampElapsed > removeBeforeTime && !(it isIn list) 286 } 287 288 // There are items we must keep because they haven't been around for enough time. 289 if (mustKeep.isNotEmpty()) { 290 logger.logPrivacyItemsToHold(mustKeep) 291 val earliestTime = mustKeep.minByOrNull { it.timeStampElapsed }!!.timeStampElapsed 292 293 // Update the list again when the earliest item should be removed. 294 val delay = earliestTime - removeBeforeTime 295 logger.logPrivacyItemsUpdateScheduled(delay) 296 holdingRunnableCanceler = bgExecutor.executeDelayed(updateListAndNotifyChanges, delay) 297 } 298 return list.filter { !it.paused } + mustKeep 299 } 300 301 /** 302 * Ignores the paused status to determine if the element is in the list 303 */ 304 private infix fun PrivacyItem.isIn(list: List<PrivacyItem>): Boolean { 305 return list.any { 306 it.privacyType == privacyType && 307 it.application == application && 308 it.timeStampElapsed == timeStampElapsed 309 } 310 } 311 312 private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? { 313 val type: PrivacyType = when (appOpItem.code) { 314 AppOpsManager.OP_PHONE_CALL_CAMERA, 315 AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA 316 AppOpsManager.OP_COARSE_LOCATION, 317 AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION 318 AppOpsManager.OP_PHONE_CALL_MICROPHONE, 319 AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE 320 else -> return null 321 } 322 if (type == PrivacyType.TYPE_LOCATION && !locationAvailable) { 323 return null 324 } 325 val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid) 326 return PrivacyItem(type, app, appOpItem.timeStartedElapsed, appOpItem.isDisabled) 327 } 328 329 interface Callback { 330 fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) 331 332 @JvmDefault 333 fun onFlagAllChanged(flag: Boolean) {} 334 335 @JvmDefault 336 fun onFlagMicCameraChanged(flag: Boolean) {} 337 338 @JvmDefault 339 fun onFlagLocationChanged(flag: Boolean) {} 340 } 341 342 private class NotifyChangesToCallback( 343 private val callback: Callback?, 344 private val list: List<PrivacyItem> 345 ) : Runnable { 346 override fun run() { 347 callback?.onPrivacyItemsChanged(list) 348 } 349 } 350 351 override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { 352 pw.println("PrivacyItemController state:") 353 pw.println(" Listening: $listening") 354 pw.println(" Current user ids: $currentUserIds") 355 pw.println(" Privacy Items:") 356 privacyList.forEach { 357 pw.print(" ") 358 pw.println(it.toString()) 359 } 360 pw.println(" Callbacks:") 361 callbacks.forEach { 362 it.get()?.let { 363 pw.print(" ") 364 pw.println(it.toString()) 365 } 366 } 367 } 368 369 private inner class MyExecutor( 370 private val delegate: DelayableExecutor 371 ) : Executor { 372 373 private var listeningCanceller: Runnable? = null 374 375 override fun execute(command: Runnable) { 376 delegate.execute(command) 377 } 378 379 fun updateListeningState() { 380 listeningCanceller?.run() 381 listeningCanceller = delegate.executeDelayed({ setListeningState() }, 0L) 382 } 383 } 384 }