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 }