diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderService.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderService.aidl index 3b9950930cbec..67259f4bb9399 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderService.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/mediattt/IDeviceSenderService.aidl @@ -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. * 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 a2ab281b7ab1b..da93e925ac842 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt @@ -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" 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 47969f2efcf12..c656df2e0a356 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 @@ -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 } 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 30a2809f3c42d..453e3d627bc8b 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 @@ -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(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 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 8431e2598eef1..8d9d7a9f9ce84 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 @@ -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) + } } 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 f2e224705e59c..9b163058ab32f 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 @@ -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() + 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 = + arrayOf( + ADD_CHIP_COMMAND_SENDER_TAG, + DEVICE_NAME, + TRANSFER_TO_THIS_DEVICE_SUCCEEDED_COMMAND_NAME + ) + private fun getTransferFailedCommand(): Array = arrayOf( ADD_CHIP_COMMAND_SENDER_TAG, 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 319b692d004cd..509ae337abb8a 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 @@ -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) } 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 177a1c275fba7..e7304d49f2469 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 @@ -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() + verify(controller).displayChip(capture(chipStateCaptor)) + + val chipState = chipStateCaptor.value!! + assertThat(chipState.undoCallback).isEqualTo(undoCallback) + } + @Test fun transferFailed_controllerTriggeredWithTransferFailedState() { service.transferFailed(mediaInfo, DeviceInfo("Fake name"))