blob: 45ff963c2a9ffd8fb2766e254330131973dfc9f1 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.demomode
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.os.UserHandle
import android.util.Log
import com.android.systemui.Dumpable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.demomode.DemoMode.ACTION_DEMO
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.util.Assert
import com.android.systemui.util.settings.GlobalSettings
import java.io.PrintWriter
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
/**
* Handles system broadcasts for [DemoMode]
*
* Injected via [DemoModeModule]
*/
class DemoModeController
constructor(
private val context: Context,
private val dumpManager: DumpManager,
private val globalSettings: GlobalSettings,
private val broadcastDispatcher: BroadcastDispatcher,
) : CallbackController<DemoMode>, Dumpable {
// Var updated when the availability tracker changes, or when we enter/exit demo mode in-process
var isInDemoMode = false
var isAvailable = false
get() = tracker.isDemoModeAvailable
private var initialized = false
private val receivers = mutableListOf<DemoMode>()
private val receiverMap: Map<String, MutableList<DemoMode>>
init {
// Don't persist demo mode across restarts.
requestFinishDemoMode()
val m = mutableMapOf<String, MutableList<DemoMode>>()
DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) }
receiverMap = m
}
fun initialize() {
if (initialized) {
throw IllegalStateException("Already initialized")
}
initialized = true
dumpManager.registerNormalDumpable(TAG, this)
// Due to DemoModeFragment running in systemui:tuner process, we have to observe for
// content changes to know if the setting turned on or off
tracker.startTracking()
isInDemoMode = tracker.isInDemoMode
val demoFilter = IntentFilter()
demoFilter.addAction(ACTION_DEMO)
broadcastDispatcher.registerReceiver(
receiver = broadcastReceiver,
filter = demoFilter,
user = UserHandle.ALL,
permission = android.Manifest.permission.DUMP,
)
}
override fun addCallback(listener: DemoMode) {
// Register this listener for its commands
val commands = listener.demoCommands()
commands.forEach { command ->
if (!receiverMap.containsKey(command)) {
throw IllegalStateException(
"Command ($command) not recognized. " + "See DemoMode.java for valid commands"
)
}
receiverMap[command]!!.add(listener)
}
synchronized(this) { receivers.add(listener) }
if (isInDemoMode) {
listener.onDemoModeStarted()
}
}
override fun removeCallback(listener: DemoMode) {
synchronized(this) {
listener.demoCommands().forEach { command -> receiverMap[command]!!.remove(listener) }
receivers.remove(listener)
}
}
/**
* Create a [Flow] for the stream of demo mode arguments that come in for the given [command]
*
* This is equivalent of creating a listener manually and adding an event handler for the given
* command, like so:
* ```
* class Demoable {
* private val demoHandler = object : DemoMode {
* override fun demoCommands() = listOf(<command>)
*
* override fun dispatchDemoCommand(command: String, args: Bundle) {
* handleDemoCommand(args)
* }
* }
* }
* ```
*
* @param command The top-level demo mode command you want a stream for
*/
fun demoFlowForCommand(command: String): Flow<Bundle> = conflatedCallbackFlow {
val callback =
object : DemoMode {
override fun demoCommands(): List<String> = listOf(command)
override fun dispatchDemoCommand(command: String, args: Bundle) {
trySend(args)
}
}
addCallback(callback)
awaitClose { removeCallback(callback) }
}
private fun setIsDemoModeAllowed(enabled: Boolean) {
// Turn off demo mode if it was on
if (isInDemoMode && !enabled) {
requestFinishDemoMode()
}
}
private fun enterDemoMode() {
isInDemoMode = true
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
synchronized(this) { copy = receivers.toList() }
copy.forEach { r -> r.onDemoModeStarted() }
}
private fun exitDemoMode() {
isInDemoMode = false
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
synchronized(this) { copy = receivers.toList() }
copy.forEach { r -> r.onDemoModeFinished() }
}
fun dispatchDemoCommand(command: String, args: Bundle) {
Assert.isMainThread()
if (DEBUG) {
Log.d(TAG, "dispatchDemoCommand: $command, args=$args")
}
if (!isAvailable) {
return
}
if (command == DemoMode.COMMAND_ENTER) {
enterDemoMode()
} else if (command == DemoMode.COMMAND_EXIT) {
exitDemoMode()
} else if (!isInDemoMode) {
enterDemoMode()
}
// See? demo mode is easy now, you just notify the listeners when their command is called
receiverMap[command]!!.forEach { receiver -> receiver.dispatchDemoCommand(command, args) }
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.println("DemoModeController state -")
pw.println(" isInDemoMode=$isInDemoMode")
pw.println(" isDemoModeAllowed=$isAvailable")
pw.print(" receivers=[")
val copy: List<DemoModeCommandReceiver>
synchronized(this) { copy = receivers.toList() }
copy.forEach { recv -> pw.print(" ${recv.javaClass.simpleName}") }
pw.println(" ]")
pw.println(" receiverMap= [")
receiverMap.keys.forEach { command ->
pw.print(" $command : [")
val recvs =
receiverMap[command]!!
.map { receiver -> receiver.javaClass.simpleName }
.joinToString(",")
pw.println("$recvs ]")
}
}
private val tracker =
object : DemoModeAvailabilityTracker(context, globalSettings) {
override fun onDemoModeAvailabilityChanged() {
setIsDemoModeAllowed(isDemoModeAvailable)
}
override fun onDemoModeStarted() {
if (this@DemoModeController.isInDemoMode != isInDemoMode) {
enterDemoMode()
}
}
override fun onDemoModeFinished() {
if (this@DemoModeController.isInDemoMode != isInDemoMode) {
exitDemoMode()
}
}
}
private val broadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (DEBUG) {
Log.v(TAG, "onReceive: $intent")
}
val action = intent.action
if (!ACTION_DEMO.equals(action)) {
return
}
val bundle = intent.extras ?: return
val command = bundle.getString("command", "").trim().lowercase()
if (command.isEmpty()) {
return
}
try {
dispatchDemoCommand(command, bundle)
} catch (t: Throwable) {
Log.w(TAG, "Error running demo command, intent=$intent $t")
}
}
}
fun requestSetDemoModeAllowed(allowed: Boolean) {
setGlobal(DEMO_MODE_ALLOWED, if (allowed) 1 else 0)
}
fun requestStartDemoMode() {
setGlobal(DEMO_MODE_ON, 1)
}
fun requestFinishDemoMode() {
setGlobal(DEMO_MODE_ON, 0)
}
private fun setGlobal(key: String, value: Int) {
globalSettings.putInt(key, value)
}
companion object {
const val DEMO_MODE_ALLOWED = "sysui_demo_allowed"
const val DEMO_MODE_ON = "sysui_tuner_demo_on"
}
}
private const val TAG = "DemoModeController"
private const val DEBUG = false