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 }