Merge changes Ibb968d5c,I38521e9a

* changes:
  [Media TTT] Add the #transferToReceiverTriggered callback.
  [Media TTT] Don't use futures for determining whether a transfer has succeeded or not.
This commit is contained in:
TreeHugger Robot
2022-01-24 19:36:40 +00:00
committed by Android (Google) Code Review
9 changed files with 83 additions and 205 deletions

View File

@@ -60,6 +60,18 @@ interface IDeviceSenderCallback {
oneway void closeToReceiverToEndCast(
in MediaRoute2Info mediaInfo, in DeviceInfo otherDeviceInfo);
/**
* Invoke to notify System UI that a media transfer from this device (the sender) to a receiver
* device has been started.
*
* Important notes:
* - This callback is for *starting* a cast. It should be used when this device is currently
* playing media locally and the media has started being transferred to the receiver device
* instead.
*/
oneway void transferToReceiverTriggered(
in MediaRoute2Info mediaInfo, in DeviceInfo otherDeviceInfo);
/**
* Invoke to notify System UI that the attempted transfer has failed.
*

View File

@@ -21,8 +21,6 @@ import android.content.Context;
import android.view.WindowManager;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.media.MediaDataManager;
import com.android.systemui.media.MediaHierarchyManager;
import com.android.systemui.media.MediaHost;
@@ -33,10 +31,8 @@ import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerR
import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender;
import com.android.systemui.media.taptotransfer.sender.MediaTttSenderService;
import com.android.systemui.statusbar.commandline.CommandRegistry;
import com.android.systemui.util.concurrency.DelayableExecutor;
import java.util.Optional;
import java.util.concurrent.Executor;
import javax.inject.Named;
@@ -89,14 +85,11 @@ public interface MediaModule {
static Optional<MediaTttChipControllerSender> providesMediaTttChipControllerSender(
MediaTttFlags mediaTttFlags,
Context context,
WindowManager windowManager,
@Main Executor mainExecutor,
@Background Executor backgroundExecutor) {
WindowManager windowManager) {
if (!mediaTttFlags.isMediaTttEnabled()) {
return Optional.empty();
}
return Optional.of(new MediaTttChipControllerSender(
context, windowManager, mainExecutor, backgroundExecutor));
return Optional.of(new MediaTttChipControllerSender(context, windowManager));
}
/** */
@@ -120,8 +113,7 @@ public interface MediaModule {
CommandRegistry commandRegistry,
Context context,
MediaTttChipControllerSender mediaTttChipControllerSender,
MediaTttChipControllerReceiver mediaTttChipControllerReceiver,
@Main DelayableExecutor mainExecutor) {
MediaTttChipControllerReceiver mediaTttChipControllerReceiver) {
if (!mediaTttFlags.isMediaTttEnabled()) {
return Optional.empty();
}
@@ -130,8 +122,7 @@ public interface MediaModule {
commandRegistry,
context,
mediaTttChipControllerSender,
mediaTttChipControllerReceiver,
mainExecutor));
mediaTttChipControllerReceiver));
}
/** Inject into MediaTttSenderService. */

View File

@@ -28,7 +28,6 @@ import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
import com.android.systemui.media.taptotransfer.receiver.ChipStateReceiver
import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender
@@ -36,15 +35,13 @@ 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.TransferToReceiverTriggered
import com.android.systemui.media.taptotransfer.sender.TransferSucceeded
import com.android.systemui.shared.mediattt.DeviceInfo
import com.android.systemui.shared.mediattt.IDeviceSenderCallback
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.concurrency.DelayableExecutor
import java.io.PrintWriter
import java.util.concurrent.FutureTask
import javax.inject.Inject
/**
@@ -57,7 +54,6 @@ class MediaTttCommandLineHelper @Inject constructor(
private val context: Context,
private val mediaTttChipControllerSender: MediaTttChipControllerSender,
private val mediaTttChipControllerReceiver: MediaTttChipControllerReceiver,
@Main private val mainExecutor: DelayableExecutor,
) {
private var senderCallback: IDeviceSenderCallback? = null
private val senderServiceConnection = SenderServiceConnection()
@@ -97,22 +93,13 @@ class MediaTttCommandLineHelper @Inject constructor(
senderCallback.closeToReceiverToEndCast(mediaInfo, otherDeviceInfo)
}
}
// TODO(b/203800643): Migrate other commands to invoke the service instead of the
// controller.
TRANSFER_INITIATED_COMMAND_NAME -> {
val futureTask = FutureTask { fakeUndoRunnable }
mediaTttChipControllerSender.displayChip(
TransferInitiated(
appIconDrawable,
APP_ICON_CONTENT_DESCRIPTION,
otherDeviceName,
futureTask
)
)
mainExecutor.executeDelayed({ futureTask.run() }, FUTURE_WAIT_TIME)
TRANSFER_TO_RECEIVER_TRIGGERED_COMMAND_NAME -> {
runOnService { senderCallback ->
senderCallback.transferToReceiverTriggered(mediaInfo, otherDeviceInfo)
}
}
// TODO(b/203800643): Migrate this command to invoke the service instead of the
// controller.
TRANSFER_SUCCEEDED_COMMAND_NAME -> {
mediaTttChipControllerSender.displayChip(
TransferSucceeded(
@@ -132,7 +119,7 @@ class MediaTttCommandLineHelper @Inject constructor(
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_TO_RECEIVER_TRIGGERED_COMMAND_NAME, " +
"$TRANSFER_SUCCEEDED_COMMAND_NAME, " +
TRANSFER_FAILED_COMMAND_NAME
)
@@ -242,12 +229,11 @@ val MOVE_CLOSER_TO_START_CAST_COMMAND_NAME = MoveCloserToStartCast::class.simple
@VisibleForTesting
val MOVE_CLOSER_TO_END_CAST_COMMAND_NAME = MoveCloserToEndCast::class.simpleName!!
@VisibleForTesting
val TRANSFER_INITIATED_COMMAND_NAME = TransferInitiated::class.simpleName!!
val TRANSFER_TO_RECEIVER_TRIGGERED_COMMAND_NAME = TransferToReceiverTriggered::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"
private const val TAG = "MediaTapToTransferCli"

View File

@@ -20,7 +20,6 @@ import android.graphics.drawable.Drawable
import androidx.annotation.StringRes
import com.android.systemui.R
import com.android.systemui.media.taptotransfer.common.MediaTttChipState
import java.util.concurrent.Future
/**
* A class that stores all the information necessary to display the media tap-to-transfer chip on
@@ -72,18 +71,13 @@ class MoveCloserToEndCast(
)
/**
* A state representing that a transfer has been initiated (but not completed).
*
* @property future a future that will be resolved when the transfer has either succeeded or failed.
* If the transfer succeeded, the future can optionally return an undo runnable (see
* [TransferSucceeded.undoRunnable]). [MediaTttChipControllerSender] is responsible for transitioning
* the chip to the [TransferSucceeded] state if the future resolves successfully.
* A state representing that a transfer to the receiver device has been initiated (but not
* completed).
*/
class TransferInitiated(
class TransferToReceiverTriggered(
appIconDrawable: Drawable,
appIconContentDescription: String,
otherDeviceName: String,
val future: Future<Runnable?>
otherDeviceName: String
) : ChipStateSender(
appIconDrawable,
appIconContentDescription,

View File

@@ -23,11 +23,7 @@ import android.view.WindowManager
import android.widget.TextView
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCommon
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
@@ -38,8 +34,6 @@ import javax.inject.Inject
class MediaTttChipControllerSender @Inject constructor(
context: Context,
windowManager: WindowManager,
@Main private val mainExecutor: Executor,
@Background private val backgroundExecutor: Executor,
) : MediaTttChipControllerCommon<ChipStateSender>(
context, windowManager, R.layout.media_ttt_chip
) {
@@ -55,7 +49,7 @@ class MediaTttChipControllerSender @Inject constructor(
}
// Loading
val showLoading = chipState is TransferInitiated
val showLoading = chipState is TransferToReceiverTriggered
currentChipView.requireViewById<View>(R.id.loading).visibility =
if (showLoading) { View.VISIBLE } else { View.GONE }
@@ -77,47 +71,5 @@ class MediaTttChipControllerSender @Inject constructor(
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)
}
}
/**
* Adds the appropriate callbacks to [chipState.future] so that we update the chip correctly
* when the future resolves.
*/
private fun addFutureCallback(chipState: TransferInitiated) {
// Listen to the future on a background thread so we don't occupy the main thread while we
// wait for it to complete.
backgroundExecutor.execute {
try {
val undoRunnable = chipState.future.get(TRANSFER_TIMEOUT_SECONDS, TimeUnit.SECONDS)
// Make UI changes on the main thread
mainExecutor.execute {
displayChip(
TransferSucceeded(
chipState.appIconDrawable,
chipState.appIconContentDescription,
chipState.otherDeviceName,
undoRunnable
)
)
}
} catch (ex: Exception) {
mainExecutor.execute {
displayChip(
TransferFailed(
chipState.appIconDrawable,
chipState.appIconContentDescription,
chipState.otherDeviceName,
)
)
}
}
}
}
}
private const val TRANSFER_TIMEOUT_SECONDS = 10L

View File

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

View File

@@ -26,11 +26,9 @@ import com.android.systemui.shared.mediattt.DeviceInfo
import com.android.systemui.shared.mediattt.IDeviceSenderCallback
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -75,7 +73,6 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
context,
mediaTttChipControllerSender,
mediaTttChipControllerReceiver,
FakeExecutor(FakeSystemClock())
)
}
@@ -138,10 +135,14 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
}
@Test
fun sender_transferInitiated_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getTransferInitiatedCommand())
fun sender_transferToReceiverTriggered_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getTransferToReceiverTriggeredCommand())
verify(mediaTttChipControllerSender).displayChip(any(TransferInitiated::class.java))
assertThat(context.isBound(mediaSenderServiceComponentName)).isTrue()
val deviceInfoCaptor = argumentCaptor<DeviceInfo>()
verify(mediaSenderService).transferToReceiverTriggered(any(), capture(deviceInfoCaptor))
assertThat(deviceInfoCaptor.value!!.name).isEqualTo(DEVICE_NAME)
}
@Test
@@ -194,11 +195,11 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
MOVE_CLOSER_TO_END_CAST_COMMAND_NAME
)
private fun getTransferInitiatedCommand(): Array<String> =
private fun getTransferToReceiverTriggeredCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_SENDER_TAG,
DEVICE_NAME,
TRANSFER_INITIATED_COMMAND_NAME
TRANSFER_TO_RECEIVER_TRIGGERED_COMMAND_NAME
)
private fun getTransferSucceededCommand(): Array<String> =

View File

@@ -26,26 +26,18 @@ import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.SettableFuture
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.concurrent.Future
@SmallTest
class MediaTttChipControllerSenderTest : SysuiTestCase() {
private lateinit var appIconDrawable: Drawable
private lateinit var fakeMainClock: FakeSystemClock
private lateinit var fakeMainExecutor: FakeExecutor
private lateinit var fakeBackgroundClock: FakeSystemClock
private lateinit var fakeBackgroundExecutor: FakeExecutor
private lateinit var controllerSender: MediaTttChipControllerSender
@@ -56,13 +48,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
appIconDrawable = Icon.createWithResource(context, R.drawable.ic_cake).loadDrawable(context)
fakeMainClock = FakeSystemClock()
fakeMainExecutor = FakeExecutor(fakeMainClock)
fakeBackgroundClock = FakeSystemClock()
fakeBackgroundExecutor = FakeExecutor(fakeBackgroundClock)
controllerSender = MediaTttChipControllerSender(
context, windowManager, fakeMainExecutor, fakeBackgroundExecutor
)
controllerSender = MediaTttChipControllerSender(context, windowManager)
}
@Test
@@ -92,13 +78,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
@Test
fun transferInitiated_futureNotResolvedYet_appIcon_loadingIcon_noUndo_noFailureIcon() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
controllerSender.displayChip(transferInitiated(future))
fun transferToReceiverTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() {
controllerSender.displayChip(transferToReceiverTriggered())
// Don't resolve the future in any way and don't run our executors
// Assert we're still in the loading state
val chipView = getChipView()
assertThat(chipView.getAppIconView().drawable).isEqualTo(appIconDrawable)
assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_ICON_CONTENT_DESC)
@@ -108,71 +90,6 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
}
@Test
fun transferInitiated_futureResolvedSuccessfully_switchesToTransferSucceeded() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
val undoRunnable = Runnable { }
controllerSender.displayChip(transferInitiated(future))
future.set(undoRunnable)
fakeBackgroundExecutor.advanceClockToLast()
fakeBackgroundExecutor.runAllReady()
fakeMainExecutor.advanceClockToLast()
val numRun = fakeMainExecutor.runAllReady()
// Assert we ran the future callback
assertThat(numRun).isEqualTo(1)
// Assert that we've moved to the successful state
val chipView = getChipView()
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
}
@Test
fun transferInitiated_futureCancelled_switchesToTransferFailed() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
controllerSender.displayChip(transferInitiated(future))
future.cancel(true)
fakeBackgroundExecutor.advanceClockToLast()
fakeBackgroundExecutor.runAllReady()
fakeMainExecutor.advanceClockToLast()
val numRun = fakeMainExecutor.runAllReady()
// Assert we ran the future callback
assertThat(numRun).isEqualTo(1)
// 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_switchesToTransferFailed() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
controllerSender.displayChip(transferInitiated(future))
// We won't set anything on the future, but we will still run the executors so that we're
// waiting on the future resolving. If we have a bug in our code, then this test will time
// out because we're waiting on the future indefinitely.
fakeBackgroundExecutor.advanceClockToLast()
fakeBackgroundExecutor.runAllReady()
fakeMainExecutor.advanceClockToLast()
val numRun = fakeMainExecutor.runAllReady()
// Assert we eventually decide to not wait for the future anymore
assertThat(numRun).isEqualTo(1)
// 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_deviceName_noLoadingIcon_noFailureIcon() {
controllerSender.displayChip(transferSucceeded())
@@ -227,24 +144,24 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
@Test
fun changeFromCloserToStartToTransferInitiated_loadingIconAppears() {
fun changeFromCloserToStartToTransferTriggered_loadingIconAppears() {
controllerSender.displayChip(moveCloserToStartCast())
controllerSender.displayChip(transferInitiated())
controllerSender.displayChip(transferToReceiverTriggered())
assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
}
@Test
fun changeFromTransferInitiatedToTransferSucceeded_loadingIconDisappears() {
controllerSender.displayChip(transferInitiated())
fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() {
controllerSender.displayChip(transferToReceiverTriggered())
controllerSender.displayChip(transferSucceeded())
assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE)
}
@Test
fun changeFromTransferInitiatedToTransferSucceeded_undoButtonAppears() {
controllerSender.displayChip(transferInitiated())
fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() {
controllerSender.displayChip(transferToReceiverTriggered())
controllerSender.displayChip(transferSucceeded { })
assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE)
@@ -259,8 +176,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
@Test
fun changeFromTransferInitiatedToTransferFailed_failureIconAppears() {
controllerSender.displayChip(transferInitiated())
fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() {
controllerSender.displayChip(transferToReceiverTriggered())
controllerSender.displayChip(transferFailed())
assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE)
@@ -293,9 +210,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
MoveCloserToEndCast(appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME)
/** Helper method providing default parameters to not clutter up the tests. */
private fun transferInitiated(
future: Future<Runnable?> = TEST_FUTURE
) = TransferInitiated(appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME, future)
private fun transferToReceiverTriggered() =
TransferToReceiverTriggered(appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME)
/** Helper method providing default parameters to not clutter up the tests. */
private fun transferSucceeded(
@@ -309,6 +225,3 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
private const val DEVICE_NAME = "My Tablet"
private const val APP_ICON_CONTENT_DESC = "Content description"
// Use a settable future that hasn't yet been set so that we don't immediately switch to the success
// state.
private val TEST_FUTURE: SettableFuture<Runnable?> = SettableFuture.create()

View File

@@ -36,7 +36,7 @@ class MediaTttSenderServiceTest : SysuiTestCase() {
}
@Test
fun closeToReceiverToStartCast_controllerTriggeredWithMoveCloserToStartCastState() {
fun closeToReceiverToStartCast_controllerTriggeredWithCorrectState() {
val name = "Fake name"
callback.closeToReceiverToStartCast(mediaInfo, DeviceInfo(name))
@@ -48,7 +48,7 @@ class MediaTttSenderServiceTest : SysuiTestCase() {
}
@Test
fun closeToReceiverToEndCast_controllerTriggeredWithMoveCloserToEndCastState() {
fun closeToReceiverToEndCast_controllerTriggeredWithCorrectState() {
val name = "Fake name"
callback.closeToReceiverToEndCast(mediaInfo, DeviceInfo(name))
@@ -59,6 +59,18 @@ class MediaTttSenderServiceTest : SysuiTestCase() {
assertThat(chipState.otherDeviceName).isEqualTo(name)
}
@Test
fun transferToReceiverTriggered_controllerTriggeredWithCorrectState() {
val name = "Fake name"
callback.transferToReceiverTriggered(mediaInfo, DeviceInfo(name))
val chipStateCaptor = argumentCaptor<TransferToReceiverTriggered>()
verify(controller).displayChip(capture(chipStateCaptor))
val chipState = chipStateCaptor.value!!
assertThat(chipState.otherDeviceName).isEqualTo(name)
}
@Test
fun transferFailed_controllerTriggeredWithTransferFailedState() {
callback.transferFailed(mediaInfo, DeviceInfo("Fake name"))