diff --git a/packages/SystemUI/res/drawable/ic_warning.xml b/packages/SystemUI/res/drawable/ic_warning.xml new file mode 100644 index 0000000000000..fbed779ec70f1 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_warning.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml index 2d082dc7d5e2e..a5fdcd9e2671d 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip.xml @@ -28,8 +28,8 @@ @@ -41,23 +41,34 @@ android:textColor="?android:attr/textColorPrimary" /> + + + + 16dp 16sp - 24dp - 20dp + 24dp + 20dp 8dp -8dp + 12dp 100dp diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index a7cd33d92a9ab..3a9159094433b 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2193,6 +2193,8 @@ Move closer to %1$s to play here Playing on %1$s + + Something went wrong Inactive, check app diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderCallback.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderCallback.aidl index a68397deba2aa..9aae7d993185f 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderCallback.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderCallback.aidl @@ -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); } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt index 6142188c7d410..613dfe03433d9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt @@ -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" diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt index 55dffa7513064..e6f4ca54004fb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt @@ -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 +) diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt index 77d3d70fc98c5..6453b79757ad2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt @@ -73,6 +73,11 @@ class MediaTttChipControllerSender @Inject constructor( } undoView.setOnClickListener(undoClickListener) + // Failure + val showFailure = chipState is TransferFailed + currentChipView.requireViewById(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, + ) + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderService.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderService.kt index 9142056842a01..84794cb098f01 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderService.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderService.kt @@ -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) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt index e6673a5fcbea8..be082be974844 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt @@ -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 = + arrayOf( + ADD_CHIP_COMMAND_SENDER_TAG, + DEVICE_NAME, + TRANSFER_FAILED_COMMAND_NAME + ) + class EmptyCommand : Command { override fun execute(pw: PrintWriter, args: List) { } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt index b58eecbfc85af..937d221286d55 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt @@ -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 = 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 = 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 = 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(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" diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderServiceTest.kt index e9ee0bdbd4c91..66e3fe6c5fdaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderServiceTest.kt @@ -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()) + } }