[Media TTT] Refactor the command line interface into its own class and

use sealed classes for the chip states.

The sealed class doesn't do anything in this CL, but it will become
useful in future CLs.

Sample command: adb shell cmd statusbar media-ttt-chip-add Tablet TransferInitiated

Bug: 203800327
Test: MediaTttCommandLineHelperTest, MediaTttChipControllerTest
Test: Re-tested adb commands and they still trigger the correct UIs
Change-Id: Iffd53c5648dac0307d282158122365613fb7b653
This commit is contained in:
Caitlin Cassidy
2021-12-06 17:14:45 +00:00
parent 5913ddb213
commit 733f7fb1bb
7 changed files with 322 additions and 139 deletions

View File

@@ -24,6 +24,7 @@ import com.android.systemui.SystemUIAppComponentFactory;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardSliceProvider;
import com.android.systemui.media.taptotransfer.MediaTttChipController;
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
import com.android.systemui.people.PeopleProvider;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.unfold.SysUIUnfoldComponent;
@@ -133,6 +134,7 @@ public interface SysUIComponent {
getNaturalRotationUnfoldProgressProvider().ifPresent(o -> o.init());
// No init method needed, just needs to be gotten so that it's created.
getMediaTttChipController();
getMediaTttCommandLineHelper();
}
/**
@@ -182,6 +184,9 @@ public interface SysUIComponent {
/** */
Optional<MediaTttChipController> getMediaTttChipController();
/** */
Optional<MediaTttCommandLineHelper> getMediaTttCommandLineHelper();
/**
* Member injection into the supplied argument.
*/

View File

@@ -25,6 +25,7 @@ import com.android.systemui.media.MediaHierarchyManager;
import com.android.systemui.media.MediaHost;
import com.android.systemui.media.MediaHostStatesManager;
import com.android.systemui.media.taptotransfer.MediaTttChipController;
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
import com.android.systemui.media.taptotransfer.MediaTttFlags;
import com.android.systemui.statusbar.commandline.CommandRegistry;
@@ -78,11 +79,23 @@ public interface MediaModule {
static Optional<MediaTttChipController> providesMediaTttChipController(
MediaTttFlags mediaTttFlags,
Context context,
CommandRegistry commandRegistry,
WindowManager windowManager) {
if (!mediaTttFlags.isMediaTttEnabled()) {
return Optional.empty();
}
return Optional.of(new MediaTttChipController(commandRegistry, context, windowManager));
return Optional.of(new MediaTttChipController(context, windowManager));
}
/** */
@Provides
@SysUISingleton
static Optional<MediaTttCommandLineHelper> providesMediaTttCommandLineHelper(
MediaTttFlags mediaTttFlags,
CommandRegistry commandRegistry,
MediaTttChipController mediaTttChipController) {
if (!mediaTttFlags.isMediaTttEnabled()) {
return Optional.empty();
}
return Optional.of(new MediaTttCommandLineHelper(commandRegistry, mediaTttChipController));
}
}

View File

@@ -24,13 +24,8 @@ import android.view.View
import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import java.io.PrintWriter
import javax.inject.Inject
/**
@@ -41,14 +36,9 @@ import javax.inject.Inject
*/
@SysUISingleton
class MediaTttChipController @Inject constructor(
commandRegistry: CommandRegistry,
private val context: Context,
private val windowManager: WindowManager,
) {
init {
commandRegistry.registerCommand(ADD_CHIP_COMMAND_TAG) { AddChipCommand() }
commandRegistry.registerCommand(REMOVE_CHIP_COMMAND_TAG) { RemoveChipCommand() }
}
private val windowLayoutParams = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.WRAP_CONTENT
@@ -65,7 +55,8 @@ class MediaTttChipController @Inject constructor(
/** The chip view currently being displayed. Null if the chip is not being displayed. */
private var chipView: LinearLayout? = null
private fun displayChip(chipType: ChipType, otherDeviceName: String) {
/** Displays the chip view for the given state. */
fun displayChip(chipState: MediaTttChipState) {
val oldChipView = chipView
if (chipView == null) {
chipView = LayoutInflater
@@ -76,16 +67,16 @@ class MediaTttChipController @Inject constructor(
// Text
currentChipView.requireViewById<TextView>(R.id.text).apply {
text = context.getString(chipType.chipText, otherDeviceName)
text = context.getString(chipState.chipText, chipState.otherDeviceName)
}
// Loading
val showLoading = chipType == ChipType.TRANSFER_INITIATED
val showLoading = chipState is TransferInitiated
currentChipView.requireViewById<View>(R.id.loading).visibility =
if (showLoading) { View.VISIBLE } else { View.GONE }
// Undo
val showUndo = chipType == ChipType.TRANSFER_SUCCEEDED
val showUndo = chipState is TransferSucceeded
currentChipView.requireViewById<View>(R.id.undo).visibility =
if (showUndo) { View.VISIBLE } else { View.GONE }
@@ -94,53 +85,10 @@ class MediaTttChipController @Inject constructor(
}
}
private fun removeChip() {
/** Hides the chip. */
fun removeChip() {
if (chipView == null) { return }
windowManager.removeView(chipView)
chipView = null
}
@VisibleForTesting
enum class ChipType(
@StringRes internal val chipText: Int
) {
MOVE_CLOSER_TO_TRANSFER(R.string.media_move_closer_to_transfer),
TRANSFER_INITIATED(R.string.media_transfer_playing),
TRANSFER_SUCCEEDED(R.string.media_transfer_playing),
}
inner class AddChipCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
val chipTypeArg = args[1]
ChipType.values().forEach {
if (it.name == chipTypeArg) {
displayChip(it, otherDeviceName = args[0])
return
}
}
pw.println("Chip type must be one of " +
ChipType.values().map { it.name }.reduce { acc, s -> "$acc, $s" })
}
override fun help(pw: PrintWriter) {
pw.println(
"Usage: adb shell cmd statusbar $ADD_CHIP_COMMAND_TAG <deviceName> <chipType>"
)
}
}
inner class RemoveChipCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) = removeChip()
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $REMOVE_CHIP_COMMAND_TAG")
}
}
companion object {
@VisibleForTesting
const val ADD_CHIP_COMMAND_TAG = "media-ttt-chip-add"
@VisibleForTesting
const val REMOVE_CHIP_COMMAND_TAG = "media-ttt-chip-remove"
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2021 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.media.taptotransfer
import androidx.annotation.StringRes
import com.android.systemui.R
/**
* A class that stores all the information necessary to display the media tap-to-transfer chip in
* certain states.
*
* This is a sealed class where each subclass represents a specific chip state. Each subclass can
* contain additional information that is necessary for only that state.
*/
sealed class MediaTttChipState(
/** A string resource for the text that the chip should display. */
@StringRes internal val chipText: Int,
/** The name of the other device involved in the transfer. */
internal val otherDeviceName: String
)
/**
* A state representing that the two devices are close but not close enough to initiate a transfer.
* The chip will instruct the user to move closer in order to initiate the transfer.
*/
class MoveCloserToTransfer(
otherDeviceName: String
) : MediaTttChipState(R.string.media_move_closer_to_transfer, otherDeviceName)
/**
* A state representing that a transfer has been initiated (but not completed).
*/
class TransferInitiated(
otherDeviceName: String
) : MediaTttChipState(R.string.media_transfer_playing, otherDeviceName)
/**
* A state representing that a transfer has been successfully completed.
*/
class TransferSucceeded(
otherDeviceName: String
) : MediaTttChipState(R.string.media_transfer_playing, otherDeviceName)

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2021 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.media.taptotransfer
import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import java.io.PrintWriter
import javax.inject.Inject
/**
* A helper class to test the media tap-to-transfer chip via the command line. See inner classes for
* command usages.
*/
@SysUISingleton
class MediaTttCommandLineHelper @Inject constructor(
commandRegistry: CommandRegistry,
private val mediaTttChipController: MediaTttChipController
) {
init {
commandRegistry.registerCommand(ADD_CHIP_COMMAND_TAG) { AddChipCommand() }
commandRegistry.registerCommand(REMOVE_CHIP_COMMAND_TAG) { RemoveChipCommand() }
}
inner class AddChipCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
val otherDeviceName = args[0]
when (args[1]) {
MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME -> {
mediaTttChipController.displayChip(MoveCloserToTransfer(otherDeviceName))
}
TRANSFER_INITIATED_COMMAND_NAME -> {
mediaTttChipController.displayChip(TransferInitiated(otherDeviceName))
}
TRANSFER_SUCCEEDED_COMMAND_NAME -> {
mediaTttChipController.displayChip(TransferSucceeded(otherDeviceName))
}
else -> {
pw.println("Chip type must be one of " +
"$MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME, " +
"$TRANSFER_INITIATED_COMMAND_NAME, " +
TRANSFER_SUCCEEDED_COMMAND_NAME
)
}
}
}
override fun help(pw: PrintWriter) {
pw.println(
"Usage: adb shell cmd statusbar $ADD_CHIP_COMMAND_TAG <deviceName> <chipStatus>"
)
}
}
inner class RemoveChipCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
mediaTttChipController.removeChip()
}
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $REMOVE_CHIP_COMMAND_TAG")
}
}
}
@VisibleForTesting
const val ADD_CHIP_COMMAND_TAG = "media-ttt-chip-add"
@VisibleForTesting
const val REMOVE_CHIP_COMMAND_TAG = "media-ttt-chip-remove"
@VisibleForTesting
val MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME = MoveCloserToTransfer::class.simpleName!!
@VisibleForTesting
val TRANSFER_INITIATED_COMMAND_NAME = TransferInitiated::class.simpleName!!
@VisibleForTesting
val TRANSFER_SUCCEEDED_COMMAND_NAME = TransferSucceeded::class.simpleName!!

View File

@@ -23,8 +23,6 @@ import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -35,83 +33,58 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.io.PrintWriter
import java.io.StringWriter
import java.util.concurrent.Executor
@SmallTest
class MediaTttChipControllerTest : SysuiTestCase() {
private lateinit var mediaTttChipController: MediaTttChipController
private val inlineExecutor = Executor { command -> command.run() }
private val commandRegistry = CommandRegistry(context, inlineExecutor)
private val pw = PrintWriter(StringWriter())
@Mock
private lateinit var windowManager: WindowManager
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
mediaTttChipController = MediaTttChipController(commandRegistry, context, windowManager)
}
@Test(expected = IllegalStateException::class)
fun constructor_addCommmandAlreadyRegistered() {
// Since creating the chip controller should automatically register the add command, it
// should throw when registering it again.
commandRegistry.registerCommand(
MediaTttChipController.ADD_CHIP_COMMAND_TAG
) { EmptyCommand() }
}
@Test(expected = IllegalStateException::class)
fun constructor_removeCommmandAlreadyRegistered() {
// Since creating the chip controller should automatically register the remove command, it
// should throw when registering it again.
commandRegistry.registerCommand(
MediaTttChipController.REMOVE_CHIP_COMMAND_TAG
) { EmptyCommand() }
mediaTttChipController = MediaTttChipController(context, windowManager)
}
@Test
fun addChipCommand_chipAdded() {
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
fun displayChip_chipAdded() {
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
verify(windowManager).addView(any(), any())
}
@Test
fun addChipCommand_twice_chipNotAddedTwice() {
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
fun displayChip_twice_chipNotAddedTwice() {
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
reset(windowManager)
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
verify(windowManager, never()).addView(any(), any())
}
@Test
fun removeChipCommand_chipRemoved() {
fun removeChip_chipRemoved() {
// First, add the chip
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
// Then, remove it
commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.REMOVE_CHIP_COMMAND_TAG))
mediaTttChipController.removeChip()
verify(windowManager).removeView(any())
}
@Test
fun removeChipCommand_noAdd_viewNotRemoved() {
commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.REMOVE_CHIP_COMMAND_TAG))
fun removeChip_noAdd_viewNotRemoved() {
mediaTttChipController.removeChip()
verify(windowManager, never()).removeView(any())
}
@Test
fun moveCloserToTransfer_chipTextContainsDeviceName_noLoadingIcon_noUndo() {
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
val chipView = getChipView()
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
@@ -121,7 +94,7 @@ class MediaTttChipControllerTest : SysuiTestCase() {
@Test
fun transferInitiated_chipTextContainsDeviceName_loadingIcon_noUndo() {
commandRegistry.onShellCommand(pw, getTransferInitiatedCommand())
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME))
val chipView = getChipView()
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
@@ -131,7 +104,7 @@ class MediaTttChipControllerTest : SysuiTestCase() {
@Test
fun transferSucceeded_chipTextContainsDeviceName_noLoadingIcon_undo() {
commandRegistry.onShellCommand(pw, getTransferSucceededCommand())
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME))
val chipView = getChipView()
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
@@ -141,57 +114,36 @@ class MediaTttChipControllerTest : SysuiTestCase() {
@Test
fun changeFromCloserToTransferToTransferInitiated_loadingIconAppears() {
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
commandRegistry.onShellCommand(pw, getTransferInitiatedCommand())
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME))
assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
}
@Test
fun changeFromTransferInitiatedToTransferSucceeded_loadingIconDisappears() {
commandRegistry.onShellCommand(pw, getTransferInitiatedCommand())
commandRegistry.onShellCommand(pw, getTransferSucceededCommand())
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME))
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME))
assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE)
}
@Test
fun changeFromTransferInitiatedToTransferSucceeded_undoButtonAppears() {
commandRegistry.onShellCommand(pw, getTransferInitiatedCommand())
commandRegistry.onShellCommand(pw, getTransferSucceededCommand())
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME))
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME))
assertThat(getChipView().getUndoButtonVisibility()).isEqualTo(View.VISIBLE)
}
@Test
fun changeFromTransferSucceededToMoveCloser_undoButtonDisappears() {
commandRegistry.onShellCommand(pw, getTransferSucceededCommand())
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME))
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
assertThat(getChipView().getUndoButtonVisibility()).isEqualTo(View.GONE)
}
private fun getMoveCloserToTransferCommand(): Array<String> =
arrayOf(
MediaTttChipController.ADD_CHIP_COMMAND_TAG,
DEVICE_NAME,
MediaTttChipController.ChipType.MOVE_CLOSER_TO_TRANSFER.name
)
private fun getTransferInitiatedCommand(): Array<String> =
arrayOf(
MediaTttChipController.ADD_CHIP_COMMAND_TAG,
DEVICE_NAME,
MediaTttChipController.ChipType.TRANSFER_INITIATED.name
)
private fun getTransferSucceededCommand(): Array<String> =
arrayOf(
MediaTttChipController.ADD_CHIP_COMMAND_TAG,
DEVICE_NAME,
MediaTttChipController.ChipType.TRANSFER_SUCCEEDED.name
)
private fun LinearLayout.getChipText(): String =
(this.requireViewById<TextView>(R.id.text)).text as String
@@ -206,14 +158,6 @@ class MediaTttChipControllerTest : SysuiTestCase() {
verify(windowManager).addView(viewCaptor.capture(), any())
return viewCaptor.value as LinearLayout
}
class EmptyCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
}
override fun help(pw: PrintWriter) {
}
}
}
private const val DEVICE_NAME = "My Tablet"

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2021 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.media.taptotransfer
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.mockito.any
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.io.PrintWriter
import java.io.StringWriter
import java.util.concurrent.Executor
@SmallTest
class MediaTttCommandLineHelperTest : SysuiTestCase() {
private val inlineExecutor = Executor { command -> command.run() }
private val commandRegistry = CommandRegistry(context, inlineExecutor)
private val pw = PrintWriter(StringWriter())
private lateinit var mediaTttCommandLineHelper: MediaTttCommandLineHelper
@Mock
private lateinit var mediaTttChipController: MediaTttChipController
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
mediaTttCommandLineHelper =
MediaTttCommandLineHelper(commandRegistry, mediaTttChipController)
}
@Test(expected = IllegalStateException::class)
fun constructor_addCommandAlreadyRegistered() {
// Since creating the chip controller should automatically register the add command, it
// should throw when registering it again.
commandRegistry.registerCommand(
ADD_CHIP_COMMAND_TAG
) { EmptyCommand() }
}
@Test(expected = IllegalStateException::class)
fun constructor_removeCommandAlreadyRegistered() {
// Since creating the chip controller should automatically register the remove command, it
// should throw when registering it again.
commandRegistry.registerCommand(
REMOVE_CHIP_COMMAND_TAG
) { EmptyCommand() }
}
@Test
fun moveCloserToTransfer_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
verify(mediaTttChipController).displayChip(any(MoveCloserToTransfer::class.java))
}
@Test
fun transferInitiated_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getTransferInitiatedCommand())
verify(mediaTttChipController).displayChip(any(TransferInitiated::class.java))
}
@Test
fun transferSucceeded_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getTransferSucceededCommand())
verify(mediaTttChipController).displayChip(any(TransferSucceeded::class.java))
}
@Test
fun removeCommand_chipRemoved() {
commandRegistry.onShellCommand(pw, arrayOf(REMOVE_CHIP_COMMAND_TAG))
verify(mediaTttChipController).removeChip()
}
private fun getMoveCloserToTransferCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_TAG,
DEVICE_NAME,
MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME
)
private fun getTransferInitiatedCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_TAG,
DEVICE_NAME,
TRANSFER_INITIATED_COMMAND_NAME
)
private fun getTransferSucceededCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_TAG,
DEVICE_NAME,
TRANSFER_SUCCEEDED_COMMAND_NAME
)
class EmptyCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
}
override fun help(pw: PrintWriter) {
}
}
}
private const val DEVICE_NAME = "My Tablet"