Merge "[Media TTT] Refactor the command line interface into its own class and use sealed classes for the chip states."

This commit is contained in:
Caitlin Cassidy
2021-12-10 15:33:13 +00:00
committed by Android (Google) Code Review
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"