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