[Media TTT] Add TransferToThisDeviceSucceeded callback.

Bug: 203800643
Bug: 203800347
Test: verify `adb shell cmd statusbar media-ttt-chip-add-sender Device
TransferToThisDeviceSucceeded` shows the chip
Test: media.taptotransfer tests

Change-Id: Iba98b91617e25126e03c99d03e0a628994e6516a
This commit is contained in:
Caitlin Cassidy
2022-01-20 23:43:33 +00:00
parent 5852540a88
commit ca400ac4a9
8 changed files with 236 additions and 31 deletions

View File

@@ -101,6 +101,22 @@ interface IDeviceSenderService {
in DeviceInfo otherDeviceInfo,
in IUndoTransferCallback undoCallback);
/**
* Invoke to notify System UI that a media transfer from the receiver and back to this device
* (the sender) has finished successfully.
*
* Important notes:
* - This callback is for *ending* a cast. It should be used when media was previously being
* played on the receiver device and has been successfully transferred to play locally on
* this device instead.
*
* @param undoCallback will be invoked if the user chooses to undo this transfer.
*/
oneway void transferToThisDeviceSucceeded(
in MediaRoute2Info mediaInfo,
in DeviceInfo otherDeviceInfo,
in IUndoTransferCallback undoCallback);
/**
* Invoke to notify System UI that the attempted transfer has failed.
*

View File

@@ -36,6 +36,7 @@ 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.TransferToReceiverTriggered
import com.android.systemui.media.taptotransfer.sender.TransferToThisDeviceSucceeded
import com.android.systemui.media.taptotransfer.sender.TransferToThisDeviceTriggered
import com.android.systemui.media.taptotransfer.sender.TransferToReceiverSucceeded
import com.android.systemui.shared.mediattt.DeviceInfo
@@ -108,7 +109,7 @@ class MediaTttCommandLineHelper @Inject constructor(
TRANSFER_TO_RECEIVER_SUCCEEDED_COMMAND_NAME -> {
val undoCallback = object : IUndoTransferCallback.Stub() {
override fun onUndoTriggered() {
Log.i(TAG, "Undo callback triggered")
Log.i(TAG, "Undo transfer to receiver callback triggered")
// The external services that implement this callback would kick off a
// transfer back to this device, so mimic that here.
runOnService { senderService ->
@@ -122,6 +123,23 @@ class MediaTttCommandLineHelper @Inject constructor(
.transferToReceiverSucceeded(mediaInfo, otherDeviceInfo, undoCallback)
}
}
TRANSFER_TO_THIS_DEVICE_SUCCEEDED_COMMAND_NAME -> {
val undoCallback = object : IUndoTransferCallback.Stub() {
override fun onUndoTriggered() {
Log.i(TAG, "Undo transfer to this device callback triggered")
// The external services that implement this callback would kick off a
// transfer back to the receiver, so mimic that here.
runOnService { senderService ->
senderService
.transferToReceiverTriggered(mediaInfo, otherDeviceInfo)
}
}
}
runOnService { senderService ->
senderService
.transferToThisDeviceSucceeded(mediaInfo, otherDeviceInfo, undoCallback)
}
}
TRANSFER_FAILED_COMMAND_NAME -> {
runOnService { senderService ->
senderService.transferFailed(mediaInfo, otherDeviceInfo)
@@ -134,6 +152,7 @@ class MediaTttCommandLineHelper @Inject constructor(
"$TRANSFER_TO_RECEIVER_TRIGGERED_COMMAND_NAME, " +
"$TRANSFER_TO_THIS_DEVICE_TRIGGERED_COMMAND_NAME, " +
"$TRANSFER_TO_RECEIVER_SUCCEEDED_COMMAND_NAME, " +
"$TRANSFER_TO_THIS_DEVICE_SUCCEEDED_COMMAND_NAME, " +
TRANSFER_FAILED_COMMAND_NAME
)
}
@@ -245,6 +264,9 @@ val TRANSFER_TO_THIS_DEVICE_TRIGGERED_COMMAND_NAME =
@VisibleForTesting
val TRANSFER_TO_RECEIVER_SUCCEEDED_COMMAND_NAME = TransferToReceiverSucceeded::class.simpleName!!
@VisibleForTesting
val TRANSFER_TO_THIS_DEVICE_SUCCEEDED_COMMAND_NAME =
TransferToThisDeviceSucceeded::class.simpleName!!
@VisibleForTesting
val TRANSFER_FAILED_COMMAND_NAME = TransferFailed::class.simpleName!!
private const val APP_ICON_CONTENT_DESCRIPTION = "Fake media app icon"

View File

@@ -18,6 +18,7 @@ package com.android.systemui.media.taptotransfer.sender
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import com.android.systemui.R
import com.android.systemui.media.taptotransfer.common.MediaTttChipState
import com.android.systemui.shared.mediattt.IUndoTransferCallback
@@ -37,7 +38,18 @@ sealed class ChipStateSender(
abstract fun getChipTextString(context: Context): String
/** Returns true if the loading icon should be displayed and false otherwise. */
abstract fun showLoading(): Boolean
open fun showLoading(): Boolean = false
/**
* Returns a click listener for the undo button on the chip. Returns null if this chip state
* doesn't have an undo button.
*
* @param controllerSender passed as a parameter in case we want to display a new chip state
* when undo is clicked.
*/
open fun undoClickListener(
controllerSender: MediaTttChipControllerSender
): View.OnClickListener? = null
}
/**
@@ -55,8 +67,6 @@ class MoveCloserToStartCast(
override fun getChipTextString(context: Context): String {
return context.getString(R.string.media_move_closer_to_start_cast, otherDeviceName)
}
override fun showLoading() = false
}
/**
@@ -74,8 +84,6 @@ class MoveCloserToEndCast(
override fun getChipTextString(context: Context): String {
return context.getString(R.string.media_move_closer_to_end_cast, otherDeviceName)
}
override fun showLoading() = false
}
/**
@@ -128,7 +136,66 @@ class TransferToReceiverSucceeded(
return context.getString(R.string.media_transfer_playing_different_device, otherDeviceName)
}
override fun showLoading() = false
override fun undoClickListener(
controllerSender: MediaTttChipControllerSender
): View.OnClickListener? {
if (undoCallback == null) {
return null
}
return View.OnClickListener {
this.undoCallback.onUndoTriggered()
// The external service should eventually send us a TransferToThisDeviceTriggered state,
// but that may take too long to go through the binder and the user may be confused as
// to why the UI hasn't changed yet. So, we immediately change the UI here.
controllerSender.displayChip(
TransferToThisDeviceTriggered(
this.appIconDrawable,
this.appIconContentDescription
)
)
}
}
}
/**
* A state representing that a transfer back to this device has been successfully completed.
*
* @property otherDeviceName the name of the other device involved in the transfer.
* @property undoCallback if present, the callback that should be called when the user clicks the
* undo button. The undo button will only be shown if this is non-null.
*/
class TransferToThisDeviceSucceeded(
appIconDrawable: Drawable,
appIconContentDescription: String,
private val otherDeviceName: String,
val undoCallback: IUndoTransferCallback? = null
) : ChipStateSender(appIconDrawable, appIconContentDescription) {
override fun getChipTextString(context: Context): String {
return context.getString(R.string.media_transfer_playing_this_device)
}
override fun undoClickListener(
controllerSender: MediaTttChipControllerSender
): View.OnClickListener? {
if (undoCallback == null) {
return null
}
return View.OnClickListener {
this.undoCallback.onUndoTriggered()
// The external service should eventually send us a TransferToReceiverTriggered state,
// but that may take too long to go through the binder and the user may be confused as
// to why the UI hasn't changed yet. So, we immediately change the UI here.
controllerSender.displayChip(
TransferToReceiverTriggered(
this.appIconDrawable,
this.appIconContentDescription,
this.otherDeviceName
)
)
}
}
}
/** A state representing that a transfer has failed. */
@@ -139,6 +206,4 @@ class TransferFailed(
override fun getChipTextString(context: Context): String {
return context.getString(R.string.media_transfer_failed)
}
override fun showLoading() = false
}

View File

@@ -53,30 +53,10 @@ class MediaTttChipControllerSender @Inject constructor(
if (chipState.showLoading()) { View.VISIBLE } else { View.GONE }
// Undo
val undoClickListener: View.OnClickListener? =
if (chipState is TransferToReceiverSucceeded && chipState.undoCallback != null)
View.OnClickListener {
chipState.undoCallback.onUndoTriggered()
// The external service should eventually send us a
// TransferToThisDeviceTriggered state, but that may take too long to go through
// the binder and the user may be confused as to why the UI hasn't changed yet.
// So, we immediately change the UI here.
displayChip(
TransferToThisDeviceTriggered(
chipState.appIconDrawable,
chipState.appIconContentDescription
)
)
}
else
null
val undoView = currentChipView.requireViewById<View>(R.id.undo)
undoView.visibility = if (undoClickListener != null) {
View.VISIBLE
} else {
View.GONE
}
val undoClickListener = chipState.undoClickListener(this)
undoView.setOnClickListener(undoClickListener)
undoView.visibility = if (undoClickListener != null) { View.VISIBLE } else { View.GONE }
// Failure
val showFailure = chipState is TransferFailed

View File

@@ -78,6 +78,16 @@ class MediaTttSenderService @Inject constructor(
mediaInfo, otherDeviceInfo, undoCallback
)
}
override fun transferToThisDeviceSucceeded(
mediaInfo: MediaRoute2Info,
otherDeviceInfo: DeviceInfo,
undoCallback: IUndoTransferCallback
) {
this@MediaTttSenderService.transferToThisDeviceSucceeded(
mediaInfo, otherDeviceInfo, undoCallback
)
}
}
// TODO(b/203800643): Use the app icon from the media info instead of a fake one.
@@ -146,4 +156,16 @@ class MediaTttSenderService @Inject constructor(
)
controller.displayChip(chipState)
}
private fun transferToThisDeviceSucceeded(
mediaInfo: MediaRoute2Info, otherDeviceInfo: DeviceInfo, undoCallback: IUndoTransferCallback
) {
val chipState = TransferToThisDeviceSucceeded(
appIconDrawable = fakeAppIconDrawable,
appIconContentDescription = mediaInfo.name.toString(),
otherDeviceName = otherDeviceInfo.name,
undoCallback = undoCallback
)
controller.displayChip(chipState)
}
}

View File

@@ -166,6 +166,18 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
assertThat(deviceInfoCaptor.value!!.name).isEqualTo(DEVICE_NAME)
}
@Test
fun sender_transferToThisDeviceSucceeded_chipDisplayWithCorrectState() {
commandRegistry.onShellCommand(pw, getTransferToThisDeviceSucceededCommand())
assertThat(context.isBound(mediaSenderServiceComponentName)).isTrue()
val deviceInfoCaptor = argumentCaptor<DeviceInfo>()
verify(mediaSenderService)
.transferToThisDeviceSucceeded(any(), capture(deviceInfoCaptor), any())
assertThat(deviceInfoCaptor.value!!.name).isEqualTo(DEVICE_NAME)
}
@Test
fun sender_transferFailed_serviceCallbackCalled() {
commandRegistry.onShellCommand(pw, getTransferFailedCommand())
@@ -230,6 +242,13 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
TRANSFER_TO_RECEIVER_SUCCEEDED_COMMAND_NAME
)
private fun getTransferToThisDeviceSucceededCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_SENDER_TAG,
DEVICE_NAME,
TRANSFER_TO_THIS_DEVICE_SUCCEEDED_COMMAND_NAME
)
private fun getTransferFailedCommand(): Array<String> =
arrayOf(
ADD_CHIP_COMMAND_SENDER_TAG,

View File

@@ -169,6 +169,67 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
.isEqualTo(transferToThisDeviceTriggered().getChipTextString(context))
}
@Test
fun transferToThisDeviceSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() {
val state = transferToThisDeviceSucceeded()
controllerSender.displayChip(state)
val chipView = getChipView()
assertThat(chipView.getAppIconView().drawable).isEqualTo(appIconDrawable)
assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_ICON_CONTENT_DESC)
assertThat(chipView.getChipText()).isEqualTo(state.getChipTextString(context))
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
}
@Test
fun transferToThisDeviceSucceeded_nullUndoRunnable_noUndo() {
controllerSender.displayChip(transferToThisDeviceSucceeded(undoCallback = null))
val chipView = getChipView()
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
}
@Test
fun transferToThisDeviceSucceeded_withUndoRunnable_undoWithClick() {
val undoCallback = object : IUndoTransferCallback.Stub() {
override fun onUndoTriggered() {}
}
controllerSender.displayChip(transferToThisDeviceSucceeded(undoCallback))
val chipView = getChipView()
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue()
}
@Test
fun transferToThisDeviceSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() {
var undoCallbackCalled = false
val undoCallback = object : IUndoTransferCallback.Stub() {
override fun onUndoTriggered() {
undoCallbackCalled = true
}
}
controllerSender.displayChip(transferToThisDeviceSucceeded(undoCallback))
getChipView().getUndoButton().performClick()
assertThat(undoCallbackCalled).isTrue()
}
@Test
fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() {
val undoCallback = object : IUndoTransferCallback.Stub() {
override fun onUndoTriggered() {}
}
controllerSender.displayChip(transferToThisDeviceSucceeded(undoCallback))
getChipView().getUndoButton().performClick()
assertThat(getChipView().getChipText())
.isEqualTo(transferToReceiverTriggered().getChipTextString(context))
}
@Test
fun transferFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() {
val state = transferFailed()
@@ -269,6 +330,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME, undoCallback
)
/** Helper method providing default parameters to not clutter up the tests. */
private fun transferToThisDeviceSucceeded(undoCallback: IUndoTransferCallback? = null) =
TransferToThisDeviceSucceeded(
appIconDrawable, APP_ICON_CONTENT_DESC, DEVICE_NAME, undoCallback
)
/** Helper method providing default parameters to not clutter up the tests. */
private fun transferFailed() = TransferFailed(appIconDrawable, APP_ICON_CONTENT_DESC)
}

View File

@@ -94,6 +94,20 @@ class MediaTttSenderServiceTest : SysuiTestCase() {
assertThat(chipState.undoCallback).isEqualTo(undoCallback)
}
@Test
fun transferToThisDeviceSucceeded_controllerTriggeredWithCorrectState() {
val undoCallback = object : IUndoTransferCallback.Stub() {
override fun onUndoTriggered() {}
}
service.transferToThisDeviceSucceeded(mediaInfo, DeviceInfo("name"), undoCallback)
val chipStateCaptor = argumentCaptor<TransferToThisDeviceSucceeded>()
verify(controller).displayChip(capture(chipStateCaptor))
val chipState = chipStateCaptor.value!!
assertThat(chipState.undoCallback).isEqualTo(undoCallback)
}
@Test
fun transferFailed_controllerTriggeredWithTransferFailedState() {
service.transferFailed(mediaInfo, DeviceInfo("Fake name"))