diff options
12 files changed, 170 insertions, 21 deletions
diff --git a/packages/SystemUI/res/drawable/ic_warning.xml b/packages/SystemUI/res/drawable/ic_warning.xml new file mode 100644 index 000000000000..fbed779ec70f --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_warning.xml @@ -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> diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml index 2d082dc7d5e2..a5fdcd9e2671 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip.xml @@ -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" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 99643a763ef7..bbefce630cb0 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -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> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index a7cd33d92a9a..3a9159094433 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -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> 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 a68397deba2a..9aae7d993185 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 6142188c7d41..613dfe03433d 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 55dffa751306..e6f4ca54004f 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 77d3d70fc98c..6453b79757ad 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<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, + ) + ) } } } 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 9142056842a0..84794cb098f0 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 e6673a5fcbea..be082be97484 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 @@ -152,6 +152,14 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { } @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>) { } 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 b58eecbfc85a..937d221286d5 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<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 @@ -204,6 +214,19 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { } @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()) controllerSender.displayChip(transferInitiated()) @@ -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" 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 e9ee0bdbd4c9..66e3fe6c5fda 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<TransferFailed>()) + } } |