[Media TTT] Add error state.

Screenshot of chip is in the linked bug.

Fixes: 211493953
Test: manual: verify `adb shell cmd statusbar media-ttt-chip-add-sender
Tablet TransferFailed` triggers failed chip
Test: media.taptotransfer tests

Change-Id: Ic4074394af1bf619581bad479eb6828ff04368d1
This commit is contained in:
Caitlin Cassidy
2022-01-13 19:06:41 +00:00
parent b8bcb11dd7
commit a0cf54232c
12 changed files with 170 additions and 21 deletions

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M12,12.5zM1,21L12,2l11,19zM11,15h2v-5h-2zM12,18q0.425,0 0.713,-0.288Q13,17.425 13,17t-0.287,-0.712Q12.425,16 12,16t-0.713,0.288Q11,16.575 11,17t0.287,0.712Q11.575,18 12,18zM4.45,19h15.1L12,6z"/>
</vector>

View File

@@ -28,8 +28,8 @@
<com.android.internal.widget.CachingIconView
android:id="@+id/app_icon"
android:layout_width="@dimen/media_ttt_icon_size"
android:layout_height="@dimen/media_ttt_icon_size"
android:layout_width="@dimen/media_ttt_app_icon_size"
android:layout_height="@dimen/media_ttt_app_icon_size"
android:layout_marginEnd="12dp"
/>
@@ -41,23 +41,34 @@
android:textColor="?android:attr/textColorPrimary"
/>
<!-- At most one of [loading, failure_icon, undo] will be visible at a time. -->
<ProgressBar
android:id="@+id/loading"
android:indeterminate="true"
android:layout_width="@dimen/media_ttt_loading_size"
android:layout_height="@dimen/media_ttt_loading_size"
android:layout_marginStart="12dp"
android:layout_width="@dimen/media_ttt_status_icon_size"
android:layout_height="@dimen/media_ttt_status_icon_size"
android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant"
style="?android:attr/progressBarStyleSmall"
/>
<ImageView
android:id="@+id/failure_icon"
android:layout_width="@dimen/media_ttt_status_icon_size"
android:layout_height="@dimen/media_ttt_status_icon_size"
android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
android:src="@drawable/ic_warning"
android:tint="@color/GM2_red_500"
/>
<TextView
android:id="@+id/undo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/media_transfer_undo"
android:textColor="?androidprv:attr/textColorOnAccent"
android:layout_marginStart="12dp"
android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
android:textSize="@dimen/media_ttt_text_size"
android:paddingStart="@dimen/media_ttt_chip_outer_padding"
android:paddingEnd="@dimen/media_ttt_chip_outer_padding"

View File

@@ -980,10 +980,11 @@
<!-- Media tap-to-transfer chip for sender device -->
<dimen name="media_ttt_chip_outer_padding">16dp</dimen>
<dimen name="media_ttt_text_size">16sp</dimen>
<dimen name="media_ttt_icon_size">24dp</dimen>
<dimen name="media_ttt_loading_size">20dp</dimen>
<dimen name="media_ttt_app_icon_size">24dp</dimen>
<dimen name="media_ttt_status_icon_size">20dp</dimen>
<dimen name="media_ttt_undo_button_vertical_padding">8dp</dimen>
<dimen name="media_ttt_undo_button_vertical_negative_margin">-8dp</dimen>
<dimen name="media_ttt_last_item_start_margin">12dp</dimen>
<!-- Media tap-to-transfer chip for receiver device -->
<dimen name="media_ttt_chip_size_receiver">100dp</dimen>

View File

@@ -2193,6 +2193,8 @@
<string name="media_move_closer_to_end_cast">Move closer to <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g> to play here</string>
<!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] -->
<string name="media_transfer_playing">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
<!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIMIT=50] -->
<string name="media_transfer_failed">Something went wrong</string>
<!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
<string name="controls_error_timeout">Inactive, check app</string>

View File

@@ -59,4 +59,12 @@ interface IDeviceSenderCallback {
*/
oneway void closeToReceiverToEndCast(
in MediaRoute2Info mediaInfo, in DeviceInfo otherDeviceInfo);
/**
* Invoke to notify System UI that the attempted transfer has failed.
*
* This callback will be used for both the transfer that should've *started* playing the media
* on the receiver and the transfer that should've *ended* the playing on the receiver.
*/
oneway void transferFailed(in MediaRoute2Info mediaInfo, in DeviceInfo otherDeviceInfo);
}

View File

@@ -35,6 +35,7 @@ import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSen
import com.android.systemui.media.taptotransfer.sender.MediaTttSenderService
import com.android.systemui.media.taptotransfer.sender.MoveCloserToEndCast
import com.android.systemui.media.taptotransfer.sender.MoveCloserToStartCast
import com.android.systemui.media.taptotransfer.sender.TransferFailed
import com.android.systemui.media.taptotransfer.sender.TransferInitiated
import com.android.systemui.media.taptotransfer.sender.TransferSucceeded
import com.android.systemui.shared.mediattt.DeviceInfo
@@ -122,12 +123,18 @@ class MediaTttCommandLineHelper @Inject constructor(
)
)
}
TRANSFER_FAILED_COMMAND_NAME -> {
runOnService { senderCallback ->
senderCallback.transferFailed(mediaInfo, otherDeviceInfo)
}
}
else -> {
pw.println("Chip type must be one of " +
"$MOVE_CLOSER_TO_START_CAST_COMMAND_NAME, " +
"$MOVE_CLOSER_TO_END_CAST_COMMAND_NAME, " +
"$TRANSFER_INITIATED_COMMAND_NAME, " +
TRANSFER_SUCCEEDED_COMMAND_NAME
"$TRANSFER_SUCCEEDED_COMMAND_NAME, " +
TRANSFER_FAILED_COMMAND_NAME
)
}
}
@@ -238,6 +245,8 @@ val MOVE_CLOSER_TO_END_CAST_COMMAND_NAME = MoveCloserToEndCast::class.simpleName
val TRANSFER_INITIATED_COMMAND_NAME = TransferInitiated::class.simpleName!!
@VisibleForTesting
val TRANSFER_SUCCEEDED_COMMAND_NAME = TransferSucceeded::class.simpleName!!
@VisibleForTesting
val TRANSFER_FAILED_COMMAND_NAME = TransferFailed::class.simpleName!!
private const val FUTURE_WAIT_TIME = 2000L
private const val APP_ICON_CONTENT_DESCRIPTION = "Fake media app icon"

View File

@@ -107,3 +107,17 @@ class TransferSucceeded(
R.string.media_transfer_playing,
otherDeviceName
)
/** A state representing that a transfer has failed. */
class TransferFailed(
appIconDrawable: Drawable,
appIconContentDescription: String,
// TODO(b/211493953): The failed chip doesn't need [otherDeviceName] so we may want to remove
// [otherDeviceName] from the superclass [ChipStateSender].
otherDeviceName: String,
) : ChipStateSender(
appIconDrawable,
appIconContentDescription,
R.string.media_transfer_failed,
otherDeviceName
)

View File

@@ -73,6 +73,11 @@ class MediaTttChipControllerSender @Inject constructor(
}
undoView.setOnClickListener(undoClickListener)
// Failure
val showFailure = chipState is TransferFailed
currentChipView.requireViewById<View>(R.id.failure_icon).visibility =
if (showFailure) { View.VISIBLE } else { View.GONE }
// Future handling
if (chipState is TransferInitiated) {
addFutureCallback(chipState)
@@ -101,9 +106,14 @@ class MediaTttChipControllerSender @Inject constructor(
)
}
} catch (ex: Exception) {
// TODO(b/203800327): Maybe show a failure chip here if UX decides we need one.
mainExecutor.execute {
removeChip()
displayChip(
TransferFailed(
chipState.appIconDrawable,
chipState.appIconContentDescription,
chipState.otherDeviceName,
)
)
}
}
}

View File

@@ -49,6 +49,12 @@ class MediaTttSenderService @Inject constructor(
) {
this@MediaTttSenderService.closeToReceiverToEndCast(mediaInfo, otherDeviceInfo)
}
override fun transferFailed(
mediaInfo: MediaRoute2Info, otherDeviceInfo: DeviceInfo
) {
this@MediaTttSenderService.transferFailed(mediaInfo, otherDeviceInfo)
}
}
// TODO(b/203800643): Use the app icon from the media info instead of a fake one.
@@ -78,4 +84,13 @@ class MediaTttSenderService @Inject constructor(
)
controller.displayChip(chipState)
}
private fun transferFailed(mediaInfo: MediaRoute2Info, otherDeviceInfo: DeviceInfo) {
val chipState = TransferFailed(
appIconDrawable = fakeAppIconDrawable,
appIconContentDescription = mediaInfo.name.toString(),
otherDeviceName = otherDeviceInfo.name
)
controller.displayChip(chipState)
}
}

View File

@@ -151,6 +151,14 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
verify(mediaTttChipControllerSender).displayChip(any(TransferSucceeded::class.java))
}
@Test
fun sender_transferFailed_serviceCallbackCalled() {
commandRegistry.onShellCommand(pw, getTransferFailedCommand())
assertThat(context.isBound(mediaSenderServiceComponentName)).isTrue()
verify(mediaSenderService).transferFailed(any(), any())
}
@Test
fun sender_removeCommand_chipRemoved() {
commandRegistry.onShellCommand(pw, arrayOf(REMOVE_CHIP_COMMAND_SENDER_TAG))
@@ -200,6 +208,13 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
TRANSFER_SUCCEEDED_COMMAND_NAME
)
private fun getTransferFailedCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_SENDER_TAG,
DEVICE_NAME,
TRANSFER_FAILED_COMMAND_NAME
)
class EmptyCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
}

View File

@@ -66,7 +66,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
@Test
fun moveCloserToStartCast_appIcon_chipTextContainsDeviceName_noLoadingIcon_noUndo() {
fun moveCloserToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
controllerSender.displayChip(moveCloserToStartCast())
val chipView = getChipView()
@@ -75,10 +75,11 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
}
@Test
fun moveCloserToEndCast_appIcon_chipTextContainsDeviceName_noLoadingIcon_noUndo() {
fun moveCloserToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
controllerSender.displayChip(moveCloserToEndCast())
val chipView = getChipView()
@@ -87,10 +88,11 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
}
@Test
fun transferInitiated_futureNotResolvedYet_appIcon_loadingIcon_noUndo() {
fun transferInitiated_futureNotResolvedYet_appIcon_loadingIcon_noUndo_noFailureIcon() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
controllerSender.displayChip(transferInitiated(future))
@@ -103,6 +105,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
}
@Test
@@ -128,7 +131,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
@Test
fun transferInitiated_futureCancelled_chipRemoved() {
fun transferInitiated_futureCancelled_switchesToTransferFailed() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
controllerSender.displayChip(transferInitiated(future))
@@ -141,12 +144,15 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
// Assert we ran the future callback
assertThat(numRun).isEqualTo(1)
// Assert that we've hidden the chip
verify(windowManager).removeView(any())
// Assert that we've moved to the failed state
val chipView = getChipView()
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE)
}
@Test
fun transferInitiated_futureNotResolvedAfterTimeout_chipRemoved() {
fun transferInitiated_futureNotResolvedAfterTimeout_switchesToTransferFailed() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
controllerSender.displayChip(transferInitiated(future))
@@ -160,12 +166,15 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
// Assert we eventually decide to not wait for the future anymore
assertThat(numRun).isEqualTo(1)
// Assert we've hidden the chip
verify(windowManager).removeView(any())
// Assert that we've moved to the failed state
val chipView = getChipView()
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE)
}
@Test
fun transferSucceeded_appIcon_chipTextContainsDeviceName_noLoadingIcon() {
fun transferSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() {
controllerSender.displayChip(transferSucceeded())
val chipView = getChipView()
@@ -173,6 +182,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_ICON_CONTENT_DESC)
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
}
@Test
@@ -203,6 +213,19 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
assertThat(runnableRun).isTrue()
}
@Test
fun transferFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() {
controllerSender.displayChip(transferFailed())
val chipView = getChipView()
assertThat(chipView.getAppIconView().drawable).isEqualTo(appIconDrawable)
assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_ICON_CONTENT_DESC)
assertThat(chipView.getChipText()).doesNotContain(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE)
}
@Test
fun changeFromCloserToStartToTransferInitiated_loadingIconAppears() {
controllerSender.displayChip(moveCloserToStartCast())
@@ -235,6 +258,14 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE)
}
@Test
fun changeFromTransferInitiatedToTransferFailed_failureIconAppears() {
controllerSender.displayChip(transferInitiated())
controllerSender.displayChip(transferFailed())
assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE)
}
private fun LinearLayout.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
private fun LinearLayout.getChipText(): String =
@@ -245,6 +276,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
private fun LinearLayout.getUndoButton(): View = this.requireViewById(R.id.undo)
private fun LinearLayout.getFailureIcon(): View = this.requireViewById(R.id.failure_icon)
private fun getChipView(): LinearLayout {
val viewCaptor = ArgumentCaptor.forClass(View::class.java)
verify(windowManager).addView(viewCaptor.capture(), any())
@@ -268,6 +301,10 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
private fun transferSucceeded(
undoRunnable: Runnable? = null
) = TransferSucceeded(appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME, undoRunnable)
/** Helper method providing default parameters to not clutter up the tests. */
private fun transferFailed() =
TransferFailed(appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME)
}
private const val DEVICE_NAME = "My Tablet"

View File

@@ -5,6 +5,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.shared.mediattt.DeviceInfo
import com.android.systemui.shared.mediattt.IDeviceSenderCallback
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.google.common.truth.Truth.assertThat
@@ -57,4 +58,11 @@ class MediaTttSenderServiceTest : SysuiTestCase() {
val chipState = chipStateCaptor.value!!
assertThat(chipState.otherDeviceName).isEqualTo(name)
}
@Test
fun transferFailed_controllerTriggeredWithTransferFailedState() {
callback.transferFailed(mediaInfo, DeviceInfo("Fake name"))
verify(controller).displayChip(any<TransferFailed>())
}
}