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 com.android.internal.annotations.VisibleForTesting
20 import com.android.systemui.Dumpable
21 import com.android.systemui.appops.AppOpsController
22 import com.android.systemui.dagger.SysUISingleton
23 import com.android.systemui.dagger.qualifiers.Background
24 import com.android.systemui.dagger.qualifiers.Main
25 import com.android.systemui.dump.DumpManager
26 import com.android.systemui.privacy.logging.PrivacyLogger
27 import com.android.systemui.util.asIndenting
28 import com.android.systemui.util.concurrency.DelayableExecutor
29 import com.android.systemui.util.time.SystemClock
30 import com.android.systemui.util.withIncreasedIndent
31 import java.io.PrintWriter
32 import java.lang.ref.WeakReference
33 import java.util.concurrent.Executor
34 import javax.inject.Inject
35 
36 @SysUISingleton
37 class PrivacyItemController @Inject constructor(
38     @Main uiExecutor: DelayableExecutor,
39     @Background private val bgExecutor: DelayableExecutor,
40     private val privacyConfig: PrivacyConfig,
41     private val privacyItemMonitors: Set<@JvmSuppressWildcards PrivacyItemMonitor>,
42     private val logger: PrivacyLogger,
43     private val systemClock: SystemClock,
44     dumpManager: DumpManager
45 ) : Dumpable {
46 
47     @VisibleForTesting
48     internal companion object {
49         const val TAG = "PrivacyItemController"
50         @VisibleForTesting const val TIME_TO_HOLD_INDICATORS = 5000L
51     }
52 
53     @VisibleForTesting
54     internal var privacyList = emptyList<PrivacyItem>()
55         @Synchronized get() = field.toList() // Returns a shallow copy of the list
56         @Synchronized set
57 
58     private var listening = false
59     private val callbacks = mutableListOf<WeakReference<Callback>>()
60     private val internalUiExecutor = MyExecutor(uiExecutor)
61     private var holdingRunnableCanceler: Runnable? = null
62 
63     val micCameraAvailable
64         get() = privacyConfig.micCameraAvailable
65     val locationAvailable
66         get() = privacyConfig.locationAvailable
67     val allIndicatorsAvailable
68         get() = micCameraAvailable && locationAvailable && privacyConfig.mediaProjectionAvailable
69 
<lambda>null70     private val notifyChanges = Runnable {
71         val list = privacyList
72         callbacks.forEach { it.get()?.onPrivacyItemsChanged(list) }
73     }
74 
<lambda>null75     private val updateListAndNotifyChanges = Runnable {
76         updatePrivacyList()
77         uiExecutor.execute(notifyChanges)
78     }
79 
80     private val optionsCallback = object : PrivacyConfig.Callback {
onFlagLocationChangednull81         override fun onFlagLocationChanged(flag: Boolean) {
82             callbacks.forEach { it.get()?.onFlagLocationChanged(flag) }
83         }
84 
onFlagMicCameraChangednull85         override fun onFlagMicCameraChanged(flag: Boolean) {
86             callbacks.forEach { it.get()?.onFlagMicCameraChanged(flag) }
87         }
88 
onFlagMediaProjectionChangednull89         override fun onFlagMediaProjectionChanged(flag: Boolean) {
90             callbacks.forEach { it.get()?.onFlagMediaProjectionChanged(flag) }
91         }
92     }
93 
94     private val privacyItemMonitorCallback = object : PrivacyItemMonitor.Callback {
onPrivacyItemsChangednull95         override fun onPrivacyItemsChanged() {
96             update()
97         }
98     }
99 
100     init {
101         dumpManager.registerDumpable(TAG, this)
102         privacyConfig.addCallback(optionsCallback)
103     }
104 
updatenull105     private fun update() {
106         bgExecutor.execute {
107             updateListAndNotifyChanges.run()
108         }
109     }
110 
111     /**
112      * Updates listening status based on whether there are callbacks and the indicators are enabled.
113      *
114      * Always listen to all OPS so we don't have to figure out what we should be listening to. We
115      * still have to filter anyway. Updates are filtered in the callback.
116      *
117      * This is only called from private (add/remove)Callback and from the config listener, all in
118      * main thread.
119      */
setListeningStatenull120     private fun setListeningState() {
121         val listen = callbacks.isNotEmpty()
122         if (listening == listen) return
123         listening = listen
124         if (listening) {
125             privacyItemMonitors.forEach { it.startListening(privacyItemMonitorCallback) }
126             update()
127         } else {
128             privacyItemMonitors.forEach { it.stopListening() }
129             // Make sure that we remove all indicators and notify listeners if we are not
130             // listening anymore due to indicators being disabled
131             update()
132         }
133     }
134 
addCallbacknull135     private fun addCallback(callback: WeakReference<Callback>) {
136         callbacks.add(callback)
137         if (callbacks.isNotEmpty() && !listening) {
138             internalUiExecutor.updateListeningState()
139         }
140         // Notify this callback if we didn't set to listening
141         else if (listening) {
142             internalUiExecutor.execute(NotifyChangesToCallback(callback.get(), privacyList))
143         }
144     }
145 
removeCallbacknull146     private fun removeCallback(callback: WeakReference<Callback>) {
147         // Removes also if the callback is null
148         callbacks.removeIf { it.get()?.equals(callback.get()) ?: true }
149         if (callbacks.isEmpty()) {
150             internalUiExecutor.updateListeningState()
151         }
152     }
153 
addCallbacknull154     fun addCallback(callback: Callback) {
155         addCallback(WeakReference(callback))
156     }
157 
removeCallbacknull158     fun removeCallback(callback: Callback) {
159         removeCallback(WeakReference(callback))
160     }
161 
updatePrivacyListnull162     private fun updatePrivacyList() {
163         holdingRunnableCanceler?.run()?.also {
164             holdingRunnableCanceler = null
165         }
166         if (!listening) {
167             privacyList = emptyList()
168             return
169         }
170         val list = privacyItemMonitors.flatMap { it.getActivePrivacyItems() }.distinct()
171         privacyList = processNewList(list)
172     }
173 
174     /**
175      * Figure out which items have not been around for long enough and put them back in the list.
176      *
177      * Also schedule when we should check again to remove expired items. Because we always retrieve
178      * the current list, we have the latest info.
179      *
180      * @param list map of list retrieved from [AppOpsController].
181      * @return a list that may have added items that should be kept for some time.
182      */
processNewListnull183     private fun processNewList(list: List<PrivacyItem>): List<PrivacyItem> {
184         logger.logRetrievedPrivacyItemsList(list)
185 
186         // Anything earlier than this timestamp can be removed
187         val removeBeforeTime = systemClock.elapsedRealtime() - TIME_TO_HOLD_INDICATORS
188         val mustKeep = privacyList.filter {
189             it.timeStampElapsed > removeBeforeTime && !(it isIn list)
190         }
191 
192         // There are items we must keep because they haven't been around for enough time.
193         if (mustKeep.isNotEmpty()) {
194             logger.logPrivacyItemsToHold(mustKeep)
195             val earliestTime = mustKeep.minByOrNull { it.timeStampElapsed }!!.timeStampElapsed
196 
197             // Update the list again when the earliest item should be removed.
198             val delay = earliestTime - removeBeforeTime
199             logger.logPrivacyItemsUpdateScheduled(delay)
200             holdingRunnableCanceler = bgExecutor.executeDelayed(updateListAndNotifyChanges, delay)
201         }
202         return list.filter { !it.paused } + mustKeep
203     }
204 
205     /**
206      * Ignores the paused status to determine if the element is in the list
207      */
isInnull208     private infix fun PrivacyItem.isIn(list: List<PrivacyItem>): Boolean {
209         return list.any {
210             it.privacyType == privacyType &&
211                     it.application == application &&
212                     it.timeStampElapsed == timeStampElapsed
213         }
214     }
215 
216     interface Callback : PrivacyConfig.Callback {
onPrivacyItemsChangednull217         fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>)
218 
219         fun onFlagAllChanged(flag: Boolean) {}
220     }
221 
222     private class NotifyChangesToCallback(
223         private val callback: Callback?,
224         private val list: List<PrivacyItem>
225     ) : Runnable {
runnull226         override fun run() {
227             callback?.onPrivacyItemsChanged(list)
228         }
229     }
230 
dumpnull231     override fun dump(pw: PrintWriter, args: Array<out String>) {
232         val ipw = pw.asIndenting()
233         ipw.println("PrivacyItemController state:")
234         ipw.withIncreasedIndent {
235             ipw.println("Listening: $listening")
236             ipw.println("Privacy Items:")
237             ipw.withIncreasedIndent {
238                 privacyList.forEach {
239                     ipw.println(it.toString())
240                 }
241             }
242 
243             ipw.println("Callbacks:")
244             ipw.withIncreasedIndent {
245                 callbacks.forEach {
246                     it.get()?.let {
247                         ipw.println(it.toString())
248                     }
249                 }
250             }
251 
252             ipw.println("PrivacyItemMonitors:")
253             ipw.withIncreasedIndent {
254                 privacyItemMonitors.forEach {
255                     it.dump(ipw, args)
256                 }
257             }
258         }
259         ipw.flush()
260     }
261 
262     private inner class MyExecutor(
263         private val delegate: DelayableExecutor
264     ) : Executor {
265 
266         private var listeningCanceller: Runnable? = null
267 
executenull268         override fun execute(command: Runnable) {
269             delegate.execute(command)
270         }
271 
updateListeningStatenull272         fun updateListeningState() {
273             listeningCanceller?.run()
274             listeningCanceller = delegate.executeDelayed({ setListeningState() }, 0L)
275         }
276     }
277 }