1 /* <lambda>null2 * 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.demomode 18 19 import android.content.BroadcastReceiver 20 import android.content.Context 21 import android.content.Intent 22 import android.content.IntentFilter 23 import android.os.Bundle 24 import android.os.UserHandle 25 import android.util.Log 26 import com.android.systemui.Dumpable 27 import com.android.systemui.broadcast.BroadcastDispatcher 28 import com.android.systemui.demomode.DemoMode.ACTION_DEMO 29 import com.android.systemui.dump.DumpManager 30 import com.android.systemui.statusbar.policy.CallbackController 31 import com.android.systemui.util.Assert 32 import com.android.systemui.util.settings.GlobalSettings 33 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 34 import java.io.PrintWriter 35 import kotlinx.coroutines.channels.awaitClose 36 import kotlinx.coroutines.flow.Flow 37 38 /** 39 * Handles system broadcasts for [DemoMode] 40 * 41 * Injected via [DemoModeModule] 42 */ 43 class DemoModeController 44 constructor( 45 private val context: Context, 46 private val dumpManager: DumpManager, 47 private val globalSettings: GlobalSettings, 48 private val broadcastDispatcher: BroadcastDispatcher, 49 ) : CallbackController<DemoMode>, Dumpable { 50 51 // Var updated when the availability tracker changes, or when we enter/exit demo mode in-process 52 var isInDemoMode = false 53 54 var isAvailable = false 55 get() = tracker.isDemoModeAvailable 56 57 private var initialized = false 58 59 private val receivers = mutableListOf<DemoMode>() 60 private val receiverMap: Map<String, MutableList<DemoMode>> 61 62 init { 63 // Don't persist demo mode across restarts. 64 requestFinishDemoMode() 65 66 val m = mutableMapOf<String, MutableList<DemoMode>>() 67 DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) } 68 receiverMap = m 69 } 70 71 fun initialize() { 72 if (initialized) { 73 throw IllegalStateException("Already initialized") 74 } 75 76 initialized = true 77 78 dumpManager.registerNormalDumpable(TAG, this) 79 80 // Due to DemoModeFragment running in systemui:tuner process, we have to observe for 81 // content changes to know if the setting turned on or off 82 tracker.startTracking() 83 84 isInDemoMode = tracker.isInDemoMode 85 86 val demoFilter = IntentFilter() 87 demoFilter.addAction(ACTION_DEMO) 88 89 broadcastDispatcher.registerReceiver( 90 receiver = broadcastReceiver, 91 filter = demoFilter, 92 user = UserHandle.ALL, 93 permission = android.Manifest.permission.DUMP, 94 ) 95 } 96 97 override fun addCallback(listener: DemoMode) { 98 // Register this listener for its commands 99 val commands = listener.demoCommands() 100 101 commands.forEach { command -> 102 if (!receiverMap.containsKey(command)) { 103 throw IllegalStateException( 104 "Command ($command) not recognized. " + "See DemoMode.java for valid commands" 105 ) 106 } 107 108 receiverMap[command]!!.add(listener) 109 } 110 111 synchronized(this) { receivers.add(listener) } 112 113 if (isInDemoMode) { 114 listener.onDemoModeStarted() 115 } 116 } 117 118 override fun removeCallback(listener: DemoMode) { 119 synchronized(this) { 120 listener.demoCommands().forEach { command -> receiverMap[command]!!.remove(listener) } 121 122 receivers.remove(listener) 123 } 124 } 125 126 /** 127 * Create a [Flow] for the stream of demo mode arguments that come in for the given [command] 128 * 129 * This is equivalent of creating a listener manually and adding an event handler for the given 130 * command, like so: 131 * ``` 132 * class Demoable { 133 * private val demoHandler = object : DemoMode { 134 * override fun demoCommands() = listOf(<command>) 135 * 136 * override fun dispatchDemoCommand(command: String, args: Bundle) { 137 * handleDemoCommand(args) 138 * } 139 * } 140 * } 141 * ``` 142 * 143 * @param command The top-level demo mode command you want a stream for 144 */ 145 fun demoFlowForCommand(command: String): Flow<Bundle> = conflatedCallbackFlow { 146 val callback = 147 object : DemoMode { 148 override fun demoCommands(): List<String> = listOf(command) 149 150 override fun dispatchDemoCommand(command: String, args: Bundle) { 151 trySend(args) 152 } 153 } 154 155 addCallback(callback) 156 awaitClose { removeCallback(callback) } 157 } 158 159 private fun setIsDemoModeAllowed(enabled: Boolean) { 160 // Turn off demo mode if it was on 161 if (isInDemoMode && !enabled) { 162 requestFinishDemoMode() 163 } 164 } 165 166 private fun enterDemoMode() { 167 isInDemoMode = true 168 Assert.isMainThread() 169 170 val copy: List<DemoModeCommandReceiver> 171 synchronized(this) { copy = receivers.toList() } 172 173 copy.forEach { r -> r.onDemoModeStarted() } 174 } 175 176 private fun exitDemoMode() { 177 isInDemoMode = false 178 Assert.isMainThread() 179 180 val copy: List<DemoModeCommandReceiver> 181 synchronized(this) { copy = receivers.toList() } 182 183 copy.forEach { r -> r.onDemoModeFinished() } 184 } 185 186 fun dispatchDemoCommand(command: String, args: Bundle) { 187 Assert.isMainThread() 188 if (DEBUG) { 189 Log.d(TAG, "dispatchDemoCommand: $command, args=$args") 190 } 191 192 if (!isAvailable) { 193 return 194 } 195 196 if (command == DemoMode.COMMAND_ENTER) { 197 enterDemoMode() 198 } else if (command == DemoMode.COMMAND_EXIT) { 199 exitDemoMode() 200 } else if (!isInDemoMode) { 201 enterDemoMode() 202 } 203 204 // See? demo mode is easy now, you just notify the listeners when their command is called 205 receiverMap[command]!!.forEach { receiver -> receiver.dispatchDemoCommand(command, args) } 206 } 207 208 override fun dump(pw: PrintWriter, args: Array<out String>) { 209 pw.println("DemoModeController state -") 210 pw.println(" isInDemoMode=$isInDemoMode") 211 pw.println(" isDemoModeAllowed=$isAvailable") 212 val copy: List<DemoMode> 213 synchronized(this) { copy = receivers.toList() } 214 215 // List of all receivers 216 pw.println(" receivers=[${copy.joinToString(", ") { it.logName() }}]") 217 218 // Print out the map of COMMAND -> list of receivers for that command 219 pw.println(" receiverMap= [") 220 receiverMap.entries.forEach { (comm, recv) -> 221 pw.println(" $comm : [${recv.joinToString(", ") {it.logName()}}]") 222 } 223 pw.println(" ]") 224 } 225 226 private val tracker = 227 object : DemoModeAvailabilityTracker(context, globalSettings) { 228 override fun onDemoModeAvailabilityChanged() { 229 setIsDemoModeAllowed(isDemoModeAvailable) 230 } 231 232 override fun onDemoModeStarted() { 233 if (this@DemoModeController.isInDemoMode != isInDemoMode) { 234 enterDemoMode() 235 } 236 } 237 238 override fun onDemoModeFinished() { 239 if (this@DemoModeController.isInDemoMode != isInDemoMode) { 240 exitDemoMode() 241 } 242 } 243 } 244 245 private val broadcastReceiver = 246 object : BroadcastReceiver() { 247 override fun onReceive(context: Context, intent: Intent) { 248 if (DEBUG) { 249 Log.v(TAG, "onReceive: $intent") 250 } 251 252 val action = intent.action 253 if (!ACTION_DEMO.equals(action)) { 254 return 255 } 256 257 val bundle = intent.extras ?: return 258 val command = bundle.getString("command", "").trim().lowercase() 259 if (command.isEmpty()) { 260 return 261 } 262 263 try { 264 dispatchDemoCommand(command, bundle) 265 } catch (t: Throwable) { 266 Log.w(TAG, "Error running demo command, intent=$intent $t") 267 } 268 } 269 } 270 271 fun requestSetDemoModeAllowed(allowed: Boolean) { 272 setGlobal(DEMO_MODE_ALLOWED, if (allowed) 1 else 0) 273 } 274 275 fun requestStartDemoMode() { 276 setGlobal(DEMO_MODE_ON, 1) 277 } 278 279 fun requestFinishDemoMode() { 280 setGlobal(DEMO_MODE_ON, 0) 281 } 282 283 private fun setGlobal(key: String, value: Int) { 284 globalSettings.putInt(key, value) 285 } 286 287 companion object { 288 const val DEMO_MODE_ALLOWED = "sysui_demo_allowed" 289 const val DEMO_MODE_ON = "sysui_tuner_demo_on" 290 } 291 } 292 293 private const val TAG = "DemoModeController" 294 private const val DEBUG = false 295