blob: 22fb4c0dbdb5a8d9b34d4c972b857dcf7b151e23 [file] [log] [blame]
Fabian Kozynski8d06c712018-11-07 10:33:02 -05001/*
2 * Copyright (C) 2018 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
17package com.android.systemui.privacy
18
19import android.app.ActivityManager
20import android.app.AppOpsManager
Fabian Kozynskib5625ac2018-11-21 08:56:55 -050021import android.content.BroadcastReceiver
Fabian Kozynski8d06c712018-11-07 10:33:02 -050022import android.content.Context
Fabian Kozynskib5625ac2018-11-21 08:56:55 -050023import android.content.Intent
24import android.content.IntentFilter
Dave Mankofff4736812019-10-18 17:25:50 -040025import android.os.*
Fabian Kozynski16b26992019-05-06 10:18:41 -040026import android.provider.DeviceConfig
Fabian Kozynskib5625ac2018-11-21 08:56:55 -050027import com.android.internal.annotations.VisibleForTesting
Fabian Kozynski16b26992019-05-06 10:18:41 -040028import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
Dave Mankofff4736812019-10-18 17:25:50 -040029import com.android.systemui.Dumpable
Fabian Kozynski81765392019-02-11 12:38:26 -050030import com.android.systemui.R
Fabian Kozynski8d06c712018-11-07 10:33:02 -050031import com.android.systemui.appops.AppOpItem
32import com.android.systemui.appops.AppOpsController
Dave Mankofff4736812019-10-18 17:25:50 -040033import com.android.systemui.dagger.qualifiers.BgHandler
34import com.android.systemui.dagger.qualifiers.MainHandler
Fabian Kozynskia6ff80b2019-02-12 11:32:44 -050035import java.io.FileDescriptor
36import java.io.PrintWriter
Fabian Kozynski04f83eb2019-01-22 10:38:40 -050037import java.lang.ref.WeakReference
38import javax.inject.Inject
39import javax.inject.Singleton
Fabian Kozynski8d06c712018-11-07 10:33:02 -050040
Fabian Kozynski16b26992019-05-06 10:18:41 -040041fun isPermissionsHubEnabled() = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
42 SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false)
43
Fabian Kozynski04f83eb2019-01-22 10:38:40 -050044@Singleton
Fabian Kozynski81765392019-02-11 12:38:26 -050045class PrivacyItemController @Inject constructor(
Matt Papecb888792019-05-22 15:32:32 -070046 val context: Context,
47 private val appOpsController: AppOpsController,
Dave Mankofff4736812019-10-18 17:25:50 -040048 @MainHandler private val uiHandler: Handler,
49 @BgHandler private val bgHandler: Handler
Fabian Kozynskia6ff80b2019-02-12 11:32:44 -050050) : Dumpable {
Fabian Kozynski8d06c712018-11-07 10:33:02 -050051
Fabian Kozynski16b26992019-05-06 10:18:41 -040052 @VisibleForTesting
53 internal companion object {
Fabian Kozynski8d06c712018-11-07 10:33:02 -050054 val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
55 AppOpsManager.OP_RECORD_AUDIO,
56 AppOpsManager.OP_COARSE_LOCATION,
57 AppOpsManager.OP_FINE_LOCATION)
Fabian Kozynskib5625ac2018-11-21 08:56:55 -050058 val intents = listOf(Intent.ACTION_USER_FOREGROUND,
59 Intent.ACTION_MANAGED_PROFILE_ADDED,
60 Intent.ACTION_MANAGED_PROFILE_REMOVED)
61 const val TAG = "PrivacyItemController"
Fabian Kozynski508422b2018-12-20 10:58:17 -050062 const val SYSTEM_UID = 1000
Fabian Kozynski16b26992019-05-06 10:18:41 -040063 const val MSG_ADD_CALLBACK = 0
64 const val MSG_REMOVE_CALLBACK = 1
65 const val MSG_UPDATE_LISTENING_STATE = 2
Fabian Kozynski8d06c712018-11-07 10:33:02 -050066 }
Fabian Kozynski508422b2018-12-20 10:58:17 -050067
Fabian Kozynskia6ff80b2019-02-12 11:32:44 -050068 @VisibleForTesting
69 internal var privacyList = emptyList<PrivacyItem>()
Fabian Kozynski72090102019-03-04 10:55:55 -050070 @Synchronized get() = field.toList() // Returns a shallow copy of the list
71 @Synchronized set
Fabian Kozynskia6ff80b2019-02-12 11:32:44 -050072
Fabian Kozynski8d06c712018-11-07 10:33:02 -050073 private val userManager = context.getSystemService(UserManager::class.java)
Fabian Kozynskib5625ac2018-11-21 08:56:55 -050074 private var currentUserIds = emptyList<Int>()
Fabian Kozynskib5625ac2018-11-21 08:56:55 -050075 private var listening = false
Fabian Kozynski0d03da32019-01-28 10:35:28 -050076 val systemApp =
77 PrivacyApplication(context.getString(R.string.device_services), SYSTEM_UID, context)
Fabian Kozynski04f83eb2019-01-22 10:38:40 -050078 private val callbacks = mutableListOf<WeakReference<Callback>>()
Fabian Kozynski16b26992019-05-06 10:18:41 -040079 private val messageHandler = H(WeakReference(this), uiHandler.looper)
Fabian Kozynskib5625ac2018-11-21 08:56:55 -050080
Fabian Kozynski8d06c712018-11-07 10:33:02 -050081 private val notifyChanges = Runnable {
Fabian Kozynski72090102019-03-04 10:55:55 -050082 val list = privacyList
83 callbacks.forEach { it.get()?.privacyChanged(list) }
Fabian Kozynski8d06c712018-11-07 10:33:02 -050084 }
Fabian Kozynskib5625ac2018-11-21 08:56:55 -050085
Fabian Kozynski8d06c712018-11-07 10:33:02 -050086 private val updateListAndNotifyChanges = Runnable {
87 updatePrivacyList()
88 uiHandler.post(notifyChanges)
89 }
90
Fabian Kozynski16b26992019-05-06 10:18:41 -040091 private var indicatorsAvailable = isPermissionsHubEnabled()
92 @VisibleForTesting
Matt Papecb888792019-05-22 15:32:32 -070093 internal val devicePropertiesChangedListener =
94 object : DeviceConfig.OnPropertiesChangedListener {
95 override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
96 if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) &&
97 properties.getKeyset().contains(
98 SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED)) {
99 indicatorsAvailable = properties.getBoolean(
100 SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false)
Fabian Kozynski16b26992019-05-06 10:18:41 -0400101 messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
102 messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
103 }
104 }
105 }
106
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500107 private val cb = object : AppOpsController.Callback {
108 override fun onActiveStateChanged(
109 code: Int,
110 uid: Int,
111 packageName: String,
112 active: Boolean
113 ) {
114 val userId = UserHandle.getUserId(uid)
115 if (userId in currentUserIds) {
Fabian Kozynskib5625ac2018-11-21 08:56:55 -0500116 update(false)
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500117 }
118 }
119 }
120
Fabian Kozynskib5625ac2018-11-21 08:56:55 -0500121 @VisibleForTesting
122 internal var userSwitcherReceiver = Receiver()
123 set(value) {
124 context.unregisterReceiver(field)
125 field = value
126 registerReceiver()
127 }
128
Fabian Kozynski16b26992019-05-06 10:18:41 -0400129 init {
Matt Papecb888792019-05-22 15:32:32 -0700130 DeviceConfig.addOnPropertiesChangedListener(
131 DeviceConfig.NAMESPACE_PRIVACY,
132 context.mainExecutor,
133 devicePropertiesChangedListener)
Fabian Kozynski16b26992019-05-06 10:18:41 -0400134 }
135
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500136 private fun unregisterReceiver() {
137 context.unregisterReceiver(userSwitcherReceiver)
Fabian Kozynskib5625ac2018-11-21 08:56:55 -0500138 }
139
140 private fun registerReceiver() {
141 context.registerReceiverAsUser(userSwitcherReceiver, UserHandle.ALL, IntentFilter().apply {
142 intents.forEach {
143 addAction(it)
144 }
145 }, null, null)
146 }
147
148 private fun update(updateUsers: Boolean) {
149 if (updateUsers) {
150 val currentUser = ActivityManager.getCurrentUser()
151 currentUserIds = userManager.getProfiles(currentUser).map { it.id }
152 }
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500153 bgHandler.post(updateListAndNotifyChanges)
154 }
155
Fabian Kozynski16b26992019-05-06 10:18:41 -0400156 /**
157 * Updates listening status based on whether there are callbacks and the indicators are enabled
158 *
159 * This is only called from private (add/remove)Callback and from the config listener, all in
160 * main thread.
161 */
162 private fun setListeningState() {
163 val listen = !callbacks.isEmpty() and indicatorsAvailable
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500164 if (listening == listen) return
165 listening = listen
166 if (listening) {
167 appOpsController.addCallback(OPS, cb)
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500168 registerReceiver()
Fabian Kozynskib5625ac2018-11-21 08:56:55 -0500169 update(true)
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500170 } else {
171 appOpsController.removeCallback(OPS, cb)
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500172 unregisterReceiver()
Fabian Kozynski16b26992019-05-06 10:18:41 -0400173 // Make sure that we remove all indicators and notify listeners if we are not
174 // listening anymore due to indicators being disabled
175 update(false)
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500176 }
177 }
178
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500179 private fun addCallback(callback: WeakReference<Callback>) {
180 callbacks.add(callback)
Fabian Kozynski16b26992019-05-06 10:18:41 -0400181 if (callbacks.isNotEmpty() && !listening) {
182 messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
183 messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
184 }
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500185 // Notify this callback if we didn't set to listening
Fabian Kozynski16b26992019-05-06 10:18:41 -0400186 else if (listening) uiHandler.post(NotifyChangesToCallback(callback.get(), privacyList))
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500187 }
188
189 private fun removeCallback(callback: WeakReference<Callback>) {
190 // Removes also if the callback is null
191 callbacks.removeIf { it.get()?.equals(callback.get()) ?: true }
Fabian Kozynski16b26992019-05-06 10:18:41 -0400192 if (callbacks.isEmpty()) {
193 messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
194 messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
195 }
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500196 }
197
198 fun addCallback(callback: Callback) {
Fabian Kozynski16b26992019-05-06 10:18:41 -0400199 messageHandler.obtainMessage(MSG_ADD_CALLBACK, callback).sendToTarget()
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500200 }
201
202 fun removeCallback(callback: Callback) {
Fabian Kozynski16b26992019-05-06 10:18:41 -0400203 messageHandler.obtainMessage(MSG_REMOVE_CALLBACK, callback).sendToTarget()
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500204 }
205
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500206 private fun updatePrivacyList() {
Fabian Kozynski16b26992019-05-06 10:18:41 -0400207 if (!listening) {
208 privacyList = emptyList()
209 return
210 }
Fabian Kozynski72090102019-03-04 10:55:55 -0500211 val list = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) }
Fabian Kozynski510585d2018-12-19 13:59:17 -0500212 .mapNotNull { toPrivacyItem(it) }.distinct()
Fabian Kozynski72090102019-03-04 10:55:55 -0500213 privacyList = list
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500214 }
215
216 private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? {
217 val type: PrivacyType = when (appOpItem.code) {
218 AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
219 AppOpsManager.OP_COARSE_LOCATION -> PrivacyType.TYPE_LOCATION
220 AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
221 AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
222 else -> return null
223 }
Fabian Kozynski508422b2018-12-20 10:58:17 -0500224 if (appOpItem.uid == SYSTEM_UID) return PrivacyItem(type, systemApp)
Fabian Kozynski0d03da32019-01-28 10:35:28 -0500225 val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid, context)
Fabian Kozynskief124492018-11-02 11:02:11 -0400226 return PrivacyItem(type, app)
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500227 }
228
229 // Used by containing class to get notified of changes
230 interface Callback {
231 fun privacyChanged(privacyItems: List<PrivacyItem>)
232 }
Fabian Kozynskib5625ac2018-11-21 08:56:55 -0500233
234 internal inner class Receiver : BroadcastReceiver() {
235 override fun onReceive(context: Context?, intent: Intent?) {
236 if (intent?.action in intents) {
237 update(true)
238 }
239 }
240 }
Fabian Kozynski04f83eb2019-01-22 10:38:40 -0500241
242 private class NotifyChangesToCallback(
243 private val callback: Callback?,
244 private val list: List<PrivacyItem>
245 ) : Runnable {
246 override fun run() {
247 callback?.privacyChanged(list)
248 }
249 }
Fabian Kozynskia6ff80b2019-02-12 11:32:44 -0500250
251 override fun dump(fd: FileDescriptor?, pw: PrintWriter?, args: Array<out String>?) {
252 pw?.println("PrivacyItemController state:")
253 pw?.println(" Listening: $listening")
254 pw?.println(" Current user ids: $currentUserIds")
255 pw?.println(" Privacy Items:")
256 privacyList.forEach {
257 pw?.print(" ")
258 pw?.println(it.toString())
259 }
260 pw?.println(" Callbacks:")
261 callbacks.forEach {
262 it.get()?.let {
263 pw?.print(" ")
264 pw?.println(it.toString())
265 }
266 }
267 }
Fabian Kozynski16b26992019-05-06 10:18:41 -0400268
269 private class H(
Matt Papecb888792019-05-22 15:32:32 -0700270 private val outerClass: WeakReference<PrivacyItemController>,
271 looper: Looper
Fabian Kozynski16b26992019-05-06 10:18:41 -0400272 ) : Handler(looper) {
273 override fun handleMessage(msg: Message) {
274 super.handleMessage(msg)
275 when (msg.what) {
276 MSG_UPDATE_LISTENING_STATE -> outerClass.get()?.setListeningState()
277
278 MSG_ADD_CALLBACK -> {
279 if (msg.obj !is PrivacyItemController.Callback) return
280 outerClass.get()?.addCallback(
281 WeakReference(msg.obj as PrivacyItemController.Callback))
282 }
283
284 MSG_REMOVE_CALLBACK -> {
285 if (msg.obj !is PrivacyItemController.Callback) return
286 outerClass.get()?.removeCallback(
287 WeakReference(msg.obj as PrivacyItemController.Callback))
288 }
289 else -> {}
290 }
291 }
292 }
Fabian Kozynski8d06c712018-11-07 10:33:02 -0500293}