[Sb refactor] Add a demo command flow method

This CL mainly just adds a single method to DemoModeController,
`demoFlowForCommand(String)`. The rest of the diff is from a combination
of ktlint formatting and writing a test for the flow, which required
some restructuring of the class.

Test: DemoModeControllerTest
Bug: 238425913
Change-Id: I40dd0b42a881d0776e9b19b282496e850a67a981
This commit is contained in:
Evan Laird
2022-12-16 15:57:31 -05:00
parent 81799a75f7
commit 2775b3b39e
7 changed files with 249 additions and 120 deletions

View File

@@ -20,7 +20,7 @@ import android.content.Context
import android.database.ContentObserver
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import com.android.systemui.util.settings.GlobalSettings
/**
* Class to track the availability of [DemoMode]. Use this class to track the availability and
@@ -29,7 +29,10 @@ import android.provider.Settings
* This class works by wrapping a content observer for the relevant keys related to DemoMode
* availability and current on/off state, and triggering callbacks.
*/
abstract class DemoModeAvailabilityTracker(val context: Context) {
abstract class DemoModeAvailabilityTracker(
val context: Context,
val globalSettings: GlobalSettings,
) {
var isInDemoMode = false
var isDemoModeAvailable = false
@@ -41,9 +44,9 @@ abstract class DemoModeAvailabilityTracker(val context: Context) {
fun startTracking() {
val resolver = context.contentResolver
resolver.registerContentObserver(
Settings.Global.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver)
globalSettings.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver)
resolver.registerContentObserver(
Settings.Global.getUriFor(DEMO_MODE_ON), false, onObserver)
globalSettings.getUriFor(DEMO_MODE_ON), false, onObserver)
}
fun stopTracking() {
@@ -57,12 +60,11 @@ abstract class DemoModeAvailabilityTracker(val context: Context) {
abstract fun onDemoModeFinished()
private fun checkIsDemoModeAllowed(): Boolean {
return Settings.Global
.getInt(context.contentResolver, DEMO_MODE_ALLOWED, 0) != 0
return globalSettings.getInt(DEMO_MODE_ALLOWED, 0) != 0
}
private fun checkIsDemoModeOn(): Boolean {
return Settings.Global.getInt(context.contentResolver, DEMO_MODE_ON, 0) != 0
return globalSettings.getInt(DEMO_MODE_ON, 0) != 0
}
private val allowedObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {

View File

@@ -24,22 +24,28 @@ 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(
class DemoModeController
constructor(
private val context: Context,
private val dumpManager: DumpManager,
private val globalSettings: GlobalSettings
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
@@ -58,9 +64,7 @@ class DemoModeController constructor(
requestFinishDemoMode()
val m = mutableMapOf<String, MutableList<DemoMode>>()
DemoMode.COMMANDS.map { command ->
m.put(command, mutableListOf())
}
DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) }
receiverMap = m
}
@@ -71,7 +75,7 @@ class DemoModeController constructor(
initialized = true
dumpManager.registerDumpable(TAG, this)
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
@@ -81,8 +85,13 @@ class DemoModeController constructor(
val demoFilter = IntentFilter()
demoFilter.addAction(ACTION_DEMO)
context.registerReceiverAsUser(broadcastReceiver, UserHandle.ALL, demoFilter,
android.Manifest.permission.DUMP, null, Context.RECEIVER_EXPORTED)
broadcastDispatcher.registerReceiver(
receiver = broadcastReceiver,
filter = demoFilter,
user = UserHandle.ALL,
permission = android.Manifest.permission.DUMP,
)
}
override fun addCallback(listener: DemoMode) {
@@ -91,16 +100,15 @@ class DemoModeController constructor(
commands.forEach { command ->
if (!receiverMap.containsKey(command)) {
throw IllegalStateException("Command ($command) not recognized. " +
"See DemoMode.java for valid commands")
throw IllegalStateException(
"Command ($command) not recognized. " + "See DemoMode.java for valid commands"
)
}
receiverMap[command]!!.add(listener)
}
synchronized(this) {
receivers.add(listener)
}
synchronized(this) { receivers.add(listener) }
if (isInDemoMode) {
listener.onDemoModeStarted()
@@ -109,14 +117,46 @@ class DemoModeController constructor(
override fun removeCallback(listener: DemoMode) {
synchronized(this) {
listener.demoCommands().forEach { command ->
receiverMap[command]!!.remove(listener)
}
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) {
@@ -129,13 +169,9 @@ class DemoModeController constructor(
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
synchronized(this) {
copy = receivers.toList()
}
synchronized(this) { copy = receivers.toList() }
copy.forEach { r ->
r.onDemoModeStarted()
}
copy.forEach { r -> r.onDemoModeStarted() }
}
private fun exitDemoMode() {
@@ -143,18 +179,13 @@ class DemoModeController constructor(
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
synchronized(this) {
copy = receivers.toList()
}
synchronized(this) { copy = receivers.toList() }
copy.forEach { r ->
r.onDemoModeFinished()
}
copy.forEach { r -> r.onDemoModeFinished() }
}
fun dispatchDemoCommand(command: String, args: Bundle) {
Assert.isMainThread()
if (DEBUG) {
Log.d(TAG, "dispatchDemoCommand: $command, args=$args")
}
@@ -172,9 +203,7 @@ class DemoModeController constructor(
}
// See? demo mode is easy now, you just notify the listeners when their command is called
receiverMap[command]!!.forEach { receiver ->
receiver.dispatchDemoCommand(command, args)
}
receiverMap[command]!!.forEach { receiver -> receiver.dispatchDemoCommand(command, args) }
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -183,65 +212,64 @@ class DemoModeController constructor(
pw.println(" isDemoModeAllowed=$isAvailable")
pw.print(" receivers=[")
val copy: List<DemoModeCommandReceiver>
synchronized(this) {
copy = receivers.toList()
}
copy.forEach { recv ->
pw.print(" ${recv.javaClass.simpleName}")
}
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(",")
val recvs =
receiverMap[command]!!
.map { receiver -> receiver.javaClass.simpleName }
.joinToString(",")
pw.println("$recvs ]")
}
}
private val tracker = object : DemoModeAvailabilityTracker(context) {
override fun onDemoModeAvailabilityChanged() {
setIsDemoModeAllowed(isDemoModeAvailable)
}
private val tracker =
object : DemoModeAvailabilityTracker(context, globalSettings) {
override fun onDemoModeAvailabilityChanged() {
setIsDemoModeAllowed(isDemoModeAvailable)
}
override fun onDemoModeStarted() {
if (this@DemoModeController.isInDemoMode != isInDemoMode) {
enterDemoMode()
override fun onDemoModeStarted() {
if (this@DemoModeController.isInDemoMode != isInDemoMode) {
enterDemoMode()
}
}
override fun onDemoModeFinished() {
if (this@DemoModeController.isInDemoMode != isInDemoMode) {
exitDemoMode()
}
}
}
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")
}
}
}
}
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().toLowerCase()
if (command.length == 0) {
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)
@@ -258,10 +286,12 @@ class DemoModeController constructor(
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 DEMO_MODE_ALLOWED = "sysui_demo_allowed"
private const val DEMO_MODE_ON = "sysui_tuner_demo_on"
private const val DEBUG = false

View File

@@ -18,6 +18,7 @@ package com.android.systemui.demomode.dagger;
import android.content.Context;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager;
@@ -37,8 +38,14 @@ public abstract class DemoModeModule {
static DemoModeController provideDemoModeController(
Context context,
DumpManager dumpManager,
GlobalSettings globalSettings) {
DemoModeController dmc = new DemoModeController(context, dumpManager, globalSettings);
GlobalSettings globalSettings,
BroadcastDispatcher broadcastDispatcher
) {
DemoModeController dmc = new DemoModeController(
context,
dumpManager,
globalSettings,
broadcastDispatcher);
dmc.initialize();
return /*run*/dmc;
}

View File

@@ -24,10 +24,8 @@ import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
@@ -35,8 +33,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
@@ -52,27 +48,7 @@ constructor(
demoModeController: DemoModeController,
@Application scope: CoroutineScope,
) {
private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow {
val callback =
object : DemoMode {
override fun demoCommands(): List<String> = listOf(COMMAND_NETWORK)
override fun dispatchDemoCommand(command: String, args: Bundle) {
trySend(args)
}
override fun onDemoModeFinished() {
// Handled elsewhere
}
override fun onDemoModeStarted() {
// Handled elsewhere
}
}
demoModeController.addCallback(callback)
awaitClose { demoModeController.removeCallback(callback) }
}
private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK)
// If the args contains "mobile", then all of the args are relevant. It's just the way demo mode
// commands work and it's a little silly

View File

@@ -33,6 +33,7 @@ import com.android.systemui.R;
import com.android.systemui.demomode.DemoMode;
import com.android.systemui.demomode.DemoModeAvailabilityTracker;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.util.settings.GlobalSettings;
public class DemoModeFragment extends PreferenceFragment implements OnPreferenceChangeListener {
@@ -54,13 +55,15 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference
private SwitchPreference mOnSwitch;
private DemoModeController mDemoModeController;
private GlobalSettings mGlobalSettings;
private Tracker mDemoModeTracker;
// We are the only ones who ever call this constructor, so don't worry about the warning
@SuppressLint("ValidFragment")
public DemoModeFragment(DemoModeController demoModeController) {
public DemoModeFragment(DemoModeController demoModeController, GlobalSettings globalSettings) {
super();
mDemoModeController = demoModeController;
mGlobalSettings = globalSettings;
}
@@ -80,7 +83,7 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference
screen.addPreference(mOnSwitch);
setPreferenceScreen(screen);
mDemoModeTracker = new Tracker(context);
mDemoModeTracker = new Tracker(context, mGlobalSettings);
mDemoModeTracker.startTracking();
updateDemoModeEnabled();
updateDemoModeOn();
@@ -202,8 +205,8 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference
}
private class Tracker extends DemoModeAvailabilityTracker {
Tracker(Context context) {
super(context);
Tracker(Context context, GlobalSettings globalSettings) {
super(context, globalSettings);
}
@Override

View File

@@ -33,6 +33,7 @@ import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.util.settings.GlobalSettings;
import javax.inject.Inject;
@@ -44,12 +45,18 @@ public class TunerActivity extends Activity implements
private final DemoModeController mDemoModeController;
private final TunerService mTunerService;
private final GlobalSettings mGlobalSettings;
@Inject
TunerActivity(DemoModeController demoModeController, TunerService tunerService) {
TunerActivity(
DemoModeController demoModeController,
TunerService tunerService,
GlobalSettings globalSettings
) {
super();
mDemoModeController = demoModeController;
mTunerService = tunerService;
mGlobalSettings = globalSettings;
}
protected void onCreate(Bundle savedInstanceState) {
@@ -69,7 +76,7 @@ public class TunerActivity extends Activity implements
boolean showDemoMode = action != null && action.equals(
"com.android.settings.action.DEMO_MODE");
final PreferenceFragment fragment = showDemoMode
? new DemoModeFragment(mDemoModeController)
? new DemoModeFragment(mDemoModeController, mGlobalSettings)
: new TunerFragment(mTunerService);
getFragmentManager().beginTransaction().replace(R.id.content_frame,
fragment, TAG_TUNER).commit();

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2022 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.Intent
import android.os.Bundle
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode.ACTION_DEMO
import com.android.systemui.demomode.DemoMode.COMMAND_STATUS
import com.android.systemui.dump.DumpManager
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
@SmallTest
class DemoModeControllerTest : SysuiTestCase() {
private lateinit var underTest: DemoModeController
@Mock private lateinit var dumpManager: DumpManager
private val globalSettings = FakeSettings()
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
allowTestableLooperAsMainThread()
MockitoAnnotations.initMocks(this)
globalSettings.putInt(DemoModeController.DEMO_MODE_ALLOWED, 1)
globalSettings.putInt(DemoModeController.DEMO_MODE_ON, 1)
underTest =
DemoModeController(
context = context,
dumpManager = dumpManager,
globalSettings = globalSettings,
broadcastDispatcher = fakeBroadcastDispatcher
)
underTest.initialize()
}
@Test
fun `demo command flow - returns args`() =
testScope.runTest {
var latest: Bundle? = null
val flow = underTest.demoFlowForCommand(TEST_COMMAND)
val job = launch { flow.collect { latest = it } }
sendDemoCommand(args = mapOf("key1" to "val1"))
assertThat(latest!!.getString("key1")).isEqualTo("val1")
sendDemoCommand(args = mapOf("key2" to "val2"))
assertThat(latest!!.getString("key2")).isEqualTo("val2")
job.cancel()
}
private fun sendDemoCommand(command: String? = TEST_COMMAND, args: Map<String, String>) {
val intent = Intent(ACTION_DEMO)
intent.putExtra("command", command)
args.forEach { arg -> intent.putExtra(arg.key, arg.value) }
fakeBroadcastDispatcher.registeredReceivers.forEach { it.onReceive(context, intent) }
}
companion object {
// Use a valid command until we properly fake out everything
const val TEST_COMMAND = COMMAND_STATUS
}
}