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.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 29 import com.android.systemui.demomode.DemoMode.ACTION_DEMO 30 import com.android.systemui.dump.DumpManager 31 import com.android.systemui.statusbar.policy.CallbackController 32 import com.android.systemui.util.Assert 33 import com.android.systemui.util.settings.GlobalSettings 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 pw.print(" receivers=[") 213 val copy: List<DemoModeCommandReceiver> 214 synchronized(this) { copy = receivers.toList() } 215 copy.forEach { recv -> pw.print(" ${recv.javaClass.simpleName}") } 216 pw.println(" ]") 217 pw.println(" receiverMap= [") 218 receiverMap.keys.forEach { command -> 219 pw.print(" $command : [") 220 val recvs = 221 receiverMap[command]!! 222 .map { receiver -> receiver.javaClass.simpleName } 223 .joinToString(",") 224 pw.println("$recvs ]") 225 } 226 } 227 228 private val tracker = 229 object : DemoModeAvailabilityTracker(context, globalSettings) { 230 override fun onDemoModeAvailabilityChanged() { 231 setIsDemoModeAllowed(isDemoModeAvailable) 232 } 233 234 override fun onDemoModeStarted() { 235 if (this@DemoModeController.isInDemoMode != isInDemoMode) { 236 enterDemoMode() 237 } 238 } 239 240 override fun onDemoModeFinished() { 241 if (this@DemoModeController.isInDemoMode != isInDemoMode) { 242 exitDemoMode() 243 } 244 } 245 } 246 247 private val broadcastReceiver = 248 object : BroadcastReceiver() { 249 override fun onReceive(context: Context, intent: Intent) { 250 if (DEBUG) { 251 Log.v(TAG, "onReceive: $intent") 252 } 253 254 val action = intent.action 255 if (!ACTION_DEMO.equals(action)) { 256 return 257 } 258 259 val bundle = intent.extras ?: return 260 val command = bundle.getString("command", "").trim().lowercase() 261 if (command.isEmpty()) { 262 return 263 } 264 265 try { 266 dispatchDemoCommand(command, bundle) 267 } catch (t: Throwable) { 268 Log.w(TAG, "Error running demo command, intent=$intent $t") 269 } 270 } 271 } 272 273 fun requestSetDemoModeAllowed(allowed: Boolean) { 274 setGlobal(DEMO_MODE_ALLOWED, if (allowed) 1 else 0) 275 } 276 277 fun requestStartDemoMode() { 278 setGlobal(DEMO_MODE_ON, 1) 279 } 280 281 fun requestFinishDemoMode() { 282 setGlobal(DEMO_MODE_ON, 0) 283 } 284 285 private fun setGlobal(key: String, value: Int) { 286 globalSettings.putInt(key, value) 287 } 288 289 companion object { 290 const val DEMO_MODE_ALLOWED = "sysui_demo_allowed" 291 const val DEMO_MODE_ON = "sysui_tuner_demo_on" 292 } 293 } 294 295 private const val TAG = "DemoModeController" 296 private const val DEBUG = false 297