diff options
| author | 2022-12-16 15:57:31 -0500 | |
|---|---|---|
| committer | 2022-12-21 15:19:21 -0500 | |
| commit | 2775b3b39e9eaee328130482688dd1f810b83e6a (patch) | |
| tree | 78b115ad063126a909bf145bdecda407996fd9d2 | |
| parent | 81799a75f78c2df0ea29f457b737df4d80716325 (diff) | |
[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
7 files changed, 247 insertions, 118 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt index 16f415070b45..c746efdbbd30 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt @@ -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())) { diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt index 000bbe6afc50..84f83f1ae956 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt +++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt @@ -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) - } - - override fun onDemoModeStarted() { - if (this@DemoModeController.isInDemoMode != isInDemoMode) { - enterDemoMode() + private val tracker = + object : DemoModeAvailabilityTracker(context, globalSettings) { + override fun onDemoModeAvailabilityChanged() { + setIsDemoModeAllowed(isDemoModeAvailable) } - } - override fun onDemoModeFinished() { - if (this@DemoModeController.isInDemoMode != isInDemoMode) { - exitDemoMode() + override fun onDemoModeStarted() { + if (this@DemoModeController.isInDemoMode != isInDemoMode) { + enterDemoMode() + } } - } - } - 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 + override fun onDemoModeFinished() { + if (this@DemoModeController.isInDemoMode != isInDemoMode) { + exitDemoMode() + } } + } - 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().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) @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java index de9affb5c748..b84fa5af4d8d 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java +++ b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java @@ -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; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt index a1ae8ed7329a..d4ddb856eecf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java index 1f444340653d..246488600eef 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java index 3231aecdc4b2..32ecb6786a51 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java @@ -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(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt new file mode 100644 index 000000000000..87c66b5a98ac --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt @@ -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 + } +} |