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.ActivityManager 20 import android.app.AppOpsManager 21 import android.content.BroadcastReceiver 22 import android.content.Context 23 import android.content.Intent 24 import android.content.IntentFilter 25 import android.os.UserHandle 26 import android.os.UserManager 27 import android.provider.DeviceConfig 28 import com.android.internal.annotations.VisibleForTesting 29 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags 30 import com.android.systemui.Dumpable 31 import com.android.systemui.appops.AppOpItem 32 import com.android.systemui.appops.AppOpsController 33 import com.android.systemui.broadcast.BroadcastDispatcher 34 import com.android.systemui.dagger.qualifiers.Background 35 import com.android.systemui.dagger.qualifiers.Main 36 import com.android.systemui.dump.DumpManager 37 import com.android.systemui.util.DeviceConfigProxy 38 import com.android.systemui.util.concurrency.DelayableExecutor 39 import java.io.FileDescriptor 40 import java.io.PrintWriter 41 import java.lang.ref.WeakReference 42 import java.util.concurrent.Executor 43 import javax.inject.Inject 44 import javax.inject.Singleton 45 46 @Singleton 47 class PrivacyItemController @Inject constructor( 48 private val appOpsController: AppOpsController, 49 @Main uiExecutor: DelayableExecutor, 50 @Background private val bgExecutor: Executor, 51 private val broadcastDispatcher: BroadcastDispatcher, 52 private val deviceConfigProxy: DeviceConfigProxy, 53 private val userManager: UserManager, 54 dumpManager: DumpManager 55 ) : Dumpable { 56 57 @VisibleForTesting 58 internal companion object { 59 val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA, 60 AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO, 61 AppOpsManager.OP_PHONE_CALL_MICROPHONE) 62 val OPS_LOCATION = intArrayOf( 63 AppOpsManager.OP_COARSE_LOCATION, 64 AppOpsManager.OP_FINE_LOCATION) 65 val OPS = OPS_MIC_CAMERA + OPS_LOCATION <lambda>null66 val intentFilter = IntentFilter().apply { 67 addAction(Intent.ACTION_USER_SWITCHED) 68 addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) 69 addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) 70 } 71 const val TAG = "PrivacyItemController" 72 private const val ALL_INDICATORS = 73 SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED 74 private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED 75 } 76 77 @VisibleForTesting 78 internal var privacyList = emptyList<PrivacyItem>() 79 @Synchronized get() = field.toList() // Returns a shallow copy of the list 80 @Synchronized set 81 isAllIndicatorsEnablednull82 fun isAllIndicatorsEnabled(): Boolean { 83 return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, 84 ALL_INDICATORS, false) 85 } 86 isMicCameraEnablednull87 private fun isMicCameraEnabled(): Boolean { 88 return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, 89 MIC_CAMERA, false) 90 } 91 92 private var currentUserIds = emptyList<Int>() 93 private var listening = false 94 private val callbacks = mutableListOf<WeakReference<Callback>>() 95 private val internalUiExecutor = MyExecutor(WeakReference(this), uiExecutor) 96 <lambda>null97 private val notifyChanges = Runnable { 98 val list = privacyList 99 callbacks.forEach { it.get()?.onPrivacyItemsChanged(list) } 100 } 101 <lambda>null102 private val updateListAndNotifyChanges = Runnable { 103 updatePrivacyList() 104 uiExecutor.execute(notifyChanges) 105 } 106 107 var allIndicatorsAvailable = false 108 private set 109 var micCameraAvailable = false 110 private set 111 112 private val devicePropertiesChangedListener = 113 object : DeviceConfig.OnPropertiesChangedListener { onPropertiesChangednull114 override fun onPropertiesChanged(properties: DeviceConfig.Properties) { 115 if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) && 116 (properties.keyset.contains(ALL_INDICATORS) || 117 properties.keyset.contains(MIC_CAMERA))) { 118 119 // Running on the ui executor so can iterate on callbacks 120 if (properties.keyset.contains(ALL_INDICATORS)) { 121 allIndicatorsAvailable = properties.getBoolean(ALL_INDICATORS, false) 122 callbacks.forEach { it.get()?.onFlagAllChanged(allIndicatorsAvailable) } 123 } 124 125 if (properties.keyset.contains(MIC_CAMERA)) { 126 micCameraAvailable = properties.getBoolean(MIC_CAMERA, false) 127 callbacks.forEach { it.get()?.onFlagMicCameraChanged(micCameraAvailable) } 128 } 129 internalUiExecutor.updateListeningState() 130 } 131 } 132 } 133 134 private val cb = object : AppOpsController.Callback { onActiveStateChangednull135 override fun onActiveStateChanged( 136 code: Int, 137 uid: Int, 138 packageName: String, 139 active: Boolean 140 ) { 141 // Check if we care about this code right now 142 if (!allIndicatorsAvailable && code in OPS_LOCATION) { 143 return 144 } 145 val userId = UserHandle.getUserId(uid) 146 if (userId in currentUserIds) { 147 update(false) 148 } 149 } 150 } 151 152 @VisibleForTesting 153 internal var userSwitcherReceiver = Receiver() 154 set(value) { 155 unregisterReceiver() 156 field = value 157 if (listening) registerReceiver() 158 } 159 160 init { 161 dumpManager.registerDumpable(TAG, this) 162 } 163 unregisterReceivernull164 private fun unregisterReceiver() { 165 broadcastDispatcher.unregisterReceiver(userSwitcherReceiver) 166 } 167 registerReceivernull168 private fun registerReceiver() { 169 broadcastDispatcher.registerReceiver(userSwitcherReceiver, intentFilter, 170 null /* handler */, UserHandle.ALL) 171 } 172 updatenull173 private fun update(updateUsers: Boolean) { 174 bgExecutor.execute { 175 if (updateUsers) { 176 val currentUser = ActivityManager.getCurrentUser() 177 currentUserIds = userManager.getProfiles(currentUser).map { it.id } 178 } 179 updateListAndNotifyChanges.run() 180 } 181 } 182 183 /** 184 * Updates listening status based on whether there are callbacks and the indicators are enabled. 185 * 186 * Always listen to all OPS so we don't have to figure out what we should be listening to. We 187 * still have to filter anyway. Updates are filtered in the callback. 188 * 189 * This is only called from private (add/remove)Callback and from the config listener, all in 190 * main thread. 191 */ setListeningStatenull192 private fun setListeningState() { 193 val listen = !callbacks.isEmpty() and (allIndicatorsAvailable || micCameraAvailable) 194 if (listening == listen) return 195 listening = listen 196 if (listening) { 197 appOpsController.addCallback(OPS, cb) 198 registerReceiver() 199 update(true) 200 } else { 201 appOpsController.removeCallback(OPS, cb) 202 unregisterReceiver() 203 // Make sure that we remove all indicators and notify listeners if we are not 204 // listening anymore due to indicators being disabled 205 update(false) 206 } 207 } 208 addCallbacknull209 private fun addCallback(callback: WeakReference<Callback>) { 210 callbacks.add(callback) 211 if (callbacks.isNotEmpty() && !listening) { 212 internalUiExecutor.updateListeningState() 213 } 214 // Notify this callback if we didn't set to listening 215 else if (listening) { 216 internalUiExecutor.execute(NotifyChangesToCallback(callback.get(), privacyList)) 217 } 218 } 219 removeCallbacknull220 private fun removeCallback(callback: WeakReference<Callback>) { 221 // Removes also if the callback is null 222 callbacks.removeIf { it.get()?.equals(callback.get()) ?: true } 223 if (callbacks.isEmpty()) { 224 internalUiExecutor.updateListeningState() 225 } 226 } 227 addCallbacknull228 fun addCallback(callback: Callback) { 229 internalUiExecutor.addCallback(callback) 230 } 231 removeCallbacknull232 fun removeCallback(callback: Callback) { 233 internalUiExecutor.removeCallback(callback) 234 } 235 updatePrivacyListnull236 private fun updatePrivacyList() { 237 if (!listening) { 238 privacyList = emptyList() 239 return 240 } 241 val list = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) } 242 .mapNotNull { toPrivacyItem(it) }.distinct() 243 privacyList = list 244 } 245 toPrivacyItemnull246 private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? { 247 val type: PrivacyType = when (appOpItem.code) { 248 AppOpsManager.OP_PHONE_CALL_CAMERA, 249 AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA 250 AppOpsManager.OP_COARSE_LOCATION, 251 AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION 252 AppOpsManager.OP_PHONE_CALL_MICROPHONE, 253 AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE 254 else -> return null 255 } 256 if (type == PrivacyType.TYPE_LOCATION && !allIndicatorsAvailable) return null 257 val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid) 258 return PrivacyItem(type, app) 259 } 260 261 // Used by containing class to get notified of changes 262 interface Callback { onPrivacyItemsChangednull263 fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) 264 265 @JvmDefault 266 fun onFlagAllChanged(flag: Boolean) {} 267 268 @JvmDefault onFlagMicCameraChangednull269 fun onFlagMicCameraChanged(flag: Boolean) {} 270 } 271 272 internal inner class Receiver : BroadcastReceiver() { onReceivenull273 override fun onReceive(context: Context, intent: Intent) { 274 if (intentFilter.hasAction(intent.action)) { 275 update(true) 276 } 277 } 278 } 279 280 private class NotifyChangesToCallback( 281 private val callback: Callback?, 282 private val list: List<PrivacyItem> 283 ) : Runnable { runnull284 override fun run() { 285 callback?.onPrivacyItemsChanged(list) 286 } 287 } 288 dumpnull289 override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { 290 pw.println("PrivacyItemController state:") 291 pw.println(" Listening: $listening") 292 pw.println(" Current user ids: $currentUserIds") 293 pw.println(" Privacy Items:") 294 privacyList.forEach { 295 pw.print(" ") 296 pw.println(it.toString()) 297 } 298 pw.println(" Callbacks:") 299 callbacks.forEach { 300 it.get()?.let { 301 pw.print(" ") 302 pw.println(it.toString()) 303 } 304 } 305 } 306 307 private class MyExecutor( 308 private val outerClass: WeakReference<PrivacyItemController>, 309 private val delegate: DelayableExecutor 310 ) : Executor { 311 312 private var listeningCanceller: Runnable? = null 313 executenull314 override fun execute(command: Runnable) { 315 delegate.execute(command) 316 } 317 updateListeningStatenull318 fun updateListeningState() { 319 listeningCanceller?.run() 320 listeningCanceller = delegate.executeDelayed({ 321 outerClass.get()?.setListeningState() 322 }, 0L) 323 } 324 addCallbacknull325 fun addCallback(callback: Callback) { 326 outerClass.get()?.addCallback(WeakReference(callback)) 327 } 328 removeCallbacknull329 fun removeCallback(callback: Callback) { 330 outerClass.get()?.removeCallback(WeakReference(callback)) 331 } 332 } 333 }