• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }