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())
+ }
}