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 b22ad66ab67a6..34e5aef99e8c5 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2158,8 +2158,12 @@ Undo Move closer to play on %1$s + + 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 484791df053eb..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 @@ -43,4 +43,28 @@ interface IDeviceSenderCallback { */ oneway void closeToReceiverToStartCast( in MediaRoute2Info mediaInfo, in DeviceInfo otherDeviceInfo); + + /** + * Invoke to notify System UI that this device (the sender) is close to a receiver device, so + * the user can potentially *end* a cast on the receiver device if the user moves this device a + * bit closer. + * + * Important notes: + * - When this callback triggers, the device is close enough to inform the user that + * transferring is an option, but the device is *not* close enough to actually initiate a + * transfer yet. + * - This callback is for *ending* a cast. It should be used when media is currently being + * played on the receiver device and the media should be transferred to play locally + * instead. + */ + 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 460d38f45b4d9..613dfe03433d9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt @@ -33,7 +33,9 @@ import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerR import com.android.systemui.media.taptotransfer.receiver.ChipStateReceiver import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender 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 @@ -90,6 +92,11 @@ class MediaTttCommandLineHelper @Inject constructor( senderCallback.closeToReceiverToStartCast(mediaInfo, otherDeviceInfo) } } + MOVE_CLOSER_TO_END_CAST_COMMAND_NAME -> { + runOnService { senderCallback -> + senderCallback.closeToReceiverToEndCast(mediaInfo, otherDeviceInfo) + } + } // TODO(b/203800643): Migrate other commands to invoke the service instead of the // controller. @@ -116,11 +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 ) } } @@ -226,9 +240,13 @@ const val REMOVE_CHIP_COMMAND_RECEIVER_TAG = "media-ttt-chip-remove-receiver" @VisibleForTesting val MOVE_CLOSER_TO_START_CAST_COMMAND_NAME = MoveCloserToStartCast::class.simpleName!! @VisibleForTesting +val MOVE_CLOSER_TO_END_CAST_COMMAND_NAME = MoveCloserToEndCast::class.simpleName!! +@VisibleForTesting 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 dd434e7756fbe..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 @@ -55,6 +55,22 @@ class MoveCloserToStartCast( otherDeviceName ) +/** + * A state representing that the two devices are close but not close enough to *end* a cast that's + * currently occurring the receiver device. The chip will instruct the user to move closer in order + * to initiate the transfer from the receiver and back onto this device (the original sender). + */ +class MoveCloserToEndCast( + appIconDrawable: Drawable, + appIconContentDescription: String, + otherDeviceName: String, +) : ChipStateSender( + appIconDrawable, + appIconContentDescription, + R.string.media_move_closer_to_end_cast, + otherDeviceName +) + /** * A state representing that a transfer has been initiated (but not completed). * @@ -91,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 b56a69903ea4f..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 @@ -43,6 +43,18 @@ class MediaTttSenderService @Inject constructor( ) { this@MediaTttSenderService.closeToReceiverToStartCast(mediaInfo, otherDeviceInfo) } + + override fun closeToReceiverToEndCast( + mediaInfo: MediaRoute2Info, otherDeviceInfo: DeviceInfo + ) { + 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. @@ -63,4 +75,22 @@ class MediaTttSenderService @Inject constructor( ) controller.displayChip(chipState) } + + private fun closeToReceiverToEndCast(mediaInfo: MediaRoute2Info, otherDeviceInfo: DeviceInfo) { + val chipState = MoveCloserToEndCast( + appIconDrawable = fakeAppIconDrawable, + appIconContentDescription = mediaInfo.name.toString(), + otherDeviceName = otherDeviceInfo.name + ) + 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 4839bdea1b701..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 @@ -126,6 +126,17 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { assertThat(deviceInfoCaptor.value!!.name).isEqualTo(DEVICE_NAME) } + @Test + fun sender_moveCloserToEndCast_serviceCallbackCalled() { + commandRegistry.onShellCommand(pw, getMoveCloserToEndCastCommand()) + + assertThat(context.isBound(mediaSenderServiceComponentName)).isTrue() + + val deviceInfoCaptor = argumentCaptor() + verify(mediaSenderService).closeToReceiverToEndCast(any(), capture(deviceInfoCaptor)) + assertThat(deviceInfoCaptor.value!!.name).isEqualTo(DEVICE_NAME) + } + @Test fun sender_transferInitiated_chipDisplayWithCorrectState() { commandRegistry.onShellCommand(pw, getTransferInitiatedCommand()) @@ -140,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)) @@ -168,6 +187,13 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { MOVE_CLOSER_TO_START_CAST_COMMAND_NAME ) + private fun getMoveCloserToEndCastCommand(): Array = + arrayOf( + ADD_CHIP_COMMAND_SENDER_TAG, + DEVICE_NAME, + MOVE_CLOSER_TO_END_CAST_COMMAND_NAME + ) + private fun getTransferInitiatedCommand(): Array = arrayOf( ADD_CHIP_COMMAND_SENDER_TAG, @@ -182,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 ecc4c46634b98..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,24 @@ 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 moveCloserToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { + controllerSender.displayChip(moveCloserToEndCast()) + + val chipView = getChipView() + assertThat(chipView.getAppIconView().drawable).isEqualTo(appIconDrawable) + assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_ICON_CONTENT_DESC) + 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_noFailureIcon() { val future: SettableFuture = SettableFuture.create() controllerSender.displayChip(transferInitiated(future)) @@ -91,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 @@ -116,7 +131,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { } @Test - fun transferInitiated_futureCancelled_chipRemoved() { + fun transferInitiated_futureCancelled_switchesToTransferFailed() { val future: SettableFuture = SettableFuture.create() controllerSender.displayChip(transferInitiated(future)) @@ -129,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)) @@ -148,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() @@ -161,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 @@ -191,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()) @@ -223,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 = @@ -233,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()) @@ -243,6 +288,10 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { private fun moveCloserToStartCast() = MoveCloserToStartCast(appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME) + /** Helper method providing default parameters to not clutter up the tests. */ + private fun moveCloserToEndCast() = + MoveCloserToEndCast(appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME) + /** Helper method providing default parameters to not clutter up the tests. */ private fun transferInitiated( future: Future = TEST_FUTURE @@ -252,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 8f64698a5a6c1..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 @@ -45,4 +46,23 @@ class MediaTttSenderServiceTest : SysuiTestCase() { val chipState = chipStateCaptor.value!! assertThat(chipState.otherDeviceName).isEqualTo(name) } + + @Test + fun closeToReceiverToEndCast_controllerTriggeredWithMoveCloserToEndCastState() { + val name = "Fake name" + callback.closeToReceiverToEndCast(mediaInfo, DeviceInfo(name)) + + val chipStateCaptor = argumentCaptor() + verify(controller).displayChip(capture(chipStateCaptor)) + + val chipState = chipStateCaptor.value!! + assertThat(chipState.otherDeviceName).isEqualTo(name) + } + + @Test + fun transferFailed_controllerTriggeredWithTransferFailedState() { + callback.transferFailed(mediaInfo, DeviceInfo("Fake name")) + + verify(controller).displayChip(any()) + } }