• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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