diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt index 59607c4b6e31f..9c4b39d9cb771 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt @@ -95,8 +95,8 @@ abstract class MediaTttChipControllerCommon( // Cancel and re-set the chip timeout each time we get a new state. cancelChipViewTimeout?.run() cancelChipViewTimeout = mainExecutor.executeDelayed( - { removeChip(REASON_TIMEOUT) }, - TIMEOUT_MILLIS + { removeChip(MediaTttRemovalReason.REASON_TIMEOUT) }, + chipState.getTimeoutMs() ) } @@ -106,10 +106,7 @@ abstract class MediaTttChipControllerCommon( * @param removalReason a short string describing why the chip was removed (timeout, state * change, etc.) */ - fun removeChip(removalReason: String) { - // TODO(b/203800347): We may not want to hide the chip if we're currently in a - // TransferTriggered state: Once the user has initiated the transfer, they should be able - // to move away from the receiver device but still see the status of the transfer. + open fun removeChip(removalReason: String) { if (chipView == null) { return } logger.logChipRemoval(removalReason) tapGestureDetector.removeOnGestureDetectedCallback(TAG) @@ -148,7 +145,7 @@ abstract class MediaTttChipControllerCommon( // If the tap is within the chip bounds, we shouldn't hide the chip (in case users think the // chip is tappable). if (!viewUtil.touchIsWithinView(view, e.x, e.y)) { - removeChip(REASON_SCREEN_TAP) + removeChip(MediaTttRemovalReason.REASON_SCREEN_TAP) } } } @@ -157,8 +154,9 @@ abstract class MediaTttChipControllerCommon( // UpdateMediaTapToTransferReceiverDisplayTest private const val WINDOW_TITLE = "Media Transfer Chip View" private val TAG = MediaTttChipControllerCommon::class.simpleName!! -@VisibleForTesting -const val TIMEOUT_MILLIS = 3000L -private const val REASON_TIMEOUT = "TIMEOUT" -private const val REASON_SCREEN_TAP = "SCREEN_TAP" +object MediaTttRemovalReason { + const val REASON_TIMEOUT = "TIMEOUT" + const val REASON_SCREEN_TAP = "SCREEN_TAP" +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipState.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipState.kt index 2da48cef792a4..6f6018170f986 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipState.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipState.kt @@ -52,6 +52,14 @@ open class MediaTttChipState( null } } + + /** + * Returns the amount of time this chip should display on the screen before it times out and + * disappears. [MediaTttChipControllerCommon] will ensure that the timeout resets each time we + * receive a new state. + */ + open fun getTimeoutMs(): Long = DEFAULT_TIMEOUT_MILLIS } +private const val DEFAULT_TIMEOUT_MILLIS = 3000L private val TAG = MediaTttChipState::class.simpleName!! 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 9b537fb48ba98..22424a4e9c74f 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 @@ -97,6 +97,7 @@ class TransferToReceiverTriggered( } override fun showLoading() = true + override fun getTimeoutMs() = TRANSFER_TRIGGERED_TIMEOUT_MILLIS } /** @@ -111,6 +112,7 @@ class TransferToThisDeviceTriggered( } override fun showLoading() = true + override fun getTimeoutMs() = TRANSFER_TRIGGERED_TIMEOUT_MILLIS } /** @@ -194,3 +196,8 @@ class TransferFailed( return context.getString(R.string.media_transfer_failed) } } + +// Give the Transfer*Triggered states a longer timeout since those states represent an active +// process and we should keep the user informed about it as long as possible (but don't allow it to +// continue indefinitely). +private const val TRANSFER_TRIGGERED_TIMEOUT_MILLIS = 15000L \ No newline at end of file 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 5737b15323cd9..da2aac4d5ad7a 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 @@ -30,6 +30,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCommon import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.media.taptotransfer.common.MediaTttRemovalReason import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.gesture.TapGestureDetector import com.android.systemui.util.concurrency.DelayableExecutor @@ -58,6 +59,8 @@ class MediaTttChipControllerSender @Inject constructor( tapGestureDetector, R.layout.media_ttt_chip ) { + private var currentlyDisplayedChipState: ChipStateSender? = null + private val commandQueueCallbacks = object : CommandQueue.Callbacks { override fun updateMediaTapToTransferSenderDisplay( @StatusBarManager.MediaTransferSenderState displayState: Int, @@ -115,6 +118,8 @@ class MediaTttChipControllerSender @Inject constructor( /** Displays the chip view for the given state. */ override fun updateChipView(chipState: ChipStateSender, currentChipView: ViewGroup) { + currentlyDisplayedChipState = chipState + // App icon setIcon(chipState, currentChipView) @@ -139,6 +144,18 @@ class MediaTttChipControllerSender @Inject constructor( if (showFailure) { View.VISIBLE } else { View.GONE } } + override fun removeChip(removalReason: String) { + // Don't remove the chip if we're mid-transfer since the user should still be able to + // see the status of the transfer. (But do remove it if it's finally timed out.) + if ((currentlyDisplayedChipState is TransferToReceiverTriggered || + currentlyDisplayedChipState is TransferToThisDeviceTriggered) + && removalReason != MediaTttRemovalReason.REASON_TIMEOUT) { + return + } + super.removeChip(removalReason) + currentlyDisplayedChipState = null + } + private fun stateIntToString(@StatusBarManager.MediaTransferSenderState state: Int): String { return when(state) { StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST -> diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt index 62bb2ce70acad..adb59eca1e083 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt @@ -96,20 +96,22 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { @Test fun displayChip_chipDoesNotDisappearsBeforeTimeout() { - controllerCommon.displayChip(getState()) + val state = getState() + controllerCommon.displayChip(state) reset(windowManager) - fakeClock.advanceTime(TIMEOUT_MILLIS - 1) + fakeClock.advanceTime(state.getTimeoutMs() - 1) verify(windowManager, never()).removeView(any()) } @Test fun displayChip_chipDisappearsAfterTimeout() { - controllerCommon.displayChip(getState()) + val state = getState() + controllerCommon.displayChip(state) reset(windowManager) - fakeClock.advanceTime(TIMEOUT_MILLIS + 1) + fakeClock.advanceTime(state.getTimeoutMs() + 1) verify(windowManager).removeView(any()) } @@ -117,7 +119,8 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { @Test fun displayChip_calledAgainBeforeTimeout_timeoutReset() { // First, display the chip - controllerCommon.displayChip(getState()) + val state = getState() + controllerCommon.displayChip(state) // After some time, re-display the chip val waitTime = 1000L @@ -125,7 +128,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { controllerCommon.displayChip(getState()) // Wait until the timeout for the first display would've happened - fakeClock.advanceTime(TIMEOUT_MILLIS - waitTime + 1) + fakeClock.advanceTime(state.getTimeoutMs() - waitTime + 1) // Verify we didn't hide the chip verify(windowManager, never()).removeView(any()) @@ -134,14 +137,15 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { @Test fun displayChip_calledAgainBeforeTimeout_eventuallyTimesOut() { // First, display the chip - controllerCommon.displayChip(getState()) + val state = getState() + controllerCommon.displayChip(state) // After some time, re-display the chip fakeClock.advanceTime(1000L) controllerCommon.displayChip(getState()) // Ensure we still hide the chip eventually - fakeClock.advanceTime(TIMEOUT_MILLIS + 1) + fakeClock.advanceTime(state.getTimeoutMs() + 1) verify(windowManager).removeView(any()) } 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 367e2e462d955..fd1d76a5d02d3 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 @@ -72,6 +72,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { private lateinit var commandQueue: CommandQueue private lateinit var commandQueueCallback: CommandQueue.Callbacks private lateinit var fakeAppIconDrawable: Drawable + private lateinit var fakeClock: FakeSystemClock + private lateinit var fakeExecutor: FakeExecutor @Before fun setUp() { @@ -85,13 +87,16 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { )).thenReturn(applicationInfo) context.setMockPackageManager(packageManager) + fakeClock = FakeSystemClock() + fakeExecutor = FakeExecutor(fakeClock) + controllerSender = MediaTttChipControllerSender( commandQueue, context, logger, windowManager, viewUtil, - FakeExecutor(FakeSystemClock()), + fakeExecutor, TapGestureDetector(context) ) @@ -475,6 +480,52 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE) } + @Test + fun transferToReceiverTriggeredThenRemoveChip_chipStillDisplayed() { + controllerSender.displayChip(transferToReceiverTriggered()) + fakeClock.advanceTime(1000L) + + controllerSender.removeChip("fakeRemovalReason") + fakeExecutor.runAllReady() + + verify(windowManager, never()).removeView(any()) + } + + @Test + fun transferToReceiverTriggeredThenFarFromReceiver_eventuallyTimesOut() { + val state = transferToReceiverTriggered() + controllerSender.displayChip(state) + fakeClock.advanceTime(1000L) + controllerSender.removeChip("fakeRemovalReason") + + fakeClock.advanceTime(state.getTimeoutMs() + 1) + + verify(windowManager).removeView(any()) + } + + @Test + fun transferToThisDeviceTriggeredThenRemoveChip_chipStillDisplayed() { + controllerSender.displayChip(transferToThisDeviceTriggered()) + fakeClock.advanceTime(1000L) + + controllerSender.removeChip("fakeRemovalReason") + fakeExecutor.runAllReady() + + verify(windowManager, never()).removeView(any()) + } + + @Test + fun transferToThisDeviceTriggeredThenFarFromReceiver_eventuallyTimesOut() { + val state = transferToThisDeviceTriggered() + controllerSender.displayChip(state) + fakeClock.advanceTime(1000L) + controllerSender.removeChip("fakeRemovalReason") + + fakeClock.advanceTime(state.getTimeoutMs() + 1) + + verify(windowManager).removeView(any()) + } + private fun LinearLayout.getAppIconView() = this.requireViewById(R.id.app_icon) private fun LinearLayout.getChipText(): String =