From 8b3f856cdce1f39073c5ccb62880dba29f642257 Mon Sep 17 00:00:00 2001 From: Caitlin Shkuratov Date: Wed, 8 Mar 2023 17:55:46 +0000 Subject: [PATCH] [Media TTT] Align the timeout with heads up notifications. This reduces the timeout to 5s by default, and 10s for "in progress" actions like loading. The 5s timeout is the same as the heads up notification timeout, since the UIs are in similar places doing similar things. Fixes: 271302820 Test: verify loading chip disappears after 5s Test: verify success chip disappears after 10s Test: atest MediaTttSenderCoordinatorTest Change-Id: I6bf500c55102f2339cd112fc373ba85a88f93d61 --- .../taptotransfer/sender/ChipStateSender.kt | 32 ++++--- .../sender/MediaTttSenderCoordinator.kt | 11 ++- .../sender/MediaTttSenderCoordinatorTest.kt | 87 +++++++++++++++++++ 3 files changed, 119 insertions(+), 11 deletions(-) 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 537dbb93826e5..d3efae427cb02 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 @@ -23,7 +23,6 @@ import androidx.annotation.StringRes import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.common.shared.model.Text -import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS /** * A class enumerating all the possible states of the media tap-to-transfer chip on the sender @@ -34,8 +33,8 @@ import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS * state should not have the chip be displayed. * @property transferStatus the transfer status that the chip state represents. * @property endItem the item that should be displayed in the end section of the chip. - * @property timeout the amount of time this chip should display on the screen before it times out - * and disappears. + * @property timeoutLength how long the chip should display on the screen before it times out and + * disappears. */ enum class ChipStateSender( @StatusBarManager.MediaTransferSenderState val stateInt: Int, @@ -43,7 +42,7 @@ enum class ChipStateSender( @StringRes val stringResId: Int?, val transferStatus: TransferStatus, val endItem: SenderEndItem?, - val timeout: Int = DEFAULT_TIMEOUT_MILLIS, + val timeoutLength: TimeoutLength = TimeoutLength.DEFAULT, ) { /** * A state representing that the two devices are close but not close enough to *start* a cast to @@ -56,6 +55,9 @@ enum class ChipStateSender( R.string.media_move_closer_to_start_cast, transferStatus = TransferStatus.NOT_STARTED, endItem = null, + // Give this view more time in case the loading view takes a bit to come in. (We don't want + // this view to disappear and then the loading view to appear quickly afterwards.) + timeoutLength = TimeoutLength.LONG, ) { override fun isValidNextState(nextState: ChipStateSender): Boolean { return nextState == FAR_FROM_RECEIVER || @@ -75,6 +77,7 @@ enum class ChipStateSender( R.string.media_move_closer_to_end_cast, transferStatus = TransferStatus.NOT_STARTED, endItem = null, + timeoutLength = TimeoutLength.LONG, ) { override fun isValidNextState(nextState: ChipStateSender): Boolean { return nextState == FAR_FROM_RECEIVER || @@ -92,7 +95,9 @@ enum class ChipStateSender( R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.IN_PROGRESS, endItem = SenderEndItem.Loading, - timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS + // Give this view more time in case the succeeded/failed view takes a bit to come in. (We + // don't want this view to disappear and then the next view to appear quickly afterwards.) + timeoutLength = TimeoutLength.LONG, ) { override fun isValidNextState(nextState: ChipStateSender): Boolean { return nextState == FAR_FROM_RECEIVER || @@ -111,7 +116,7 @@ enum class ChipStateSender( R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.IN_PROGRESS, endItem = SenderEndItem.Loading, - timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS + timeoutLength = TimeoutLength.LONG, ) { override fun isValidNextState(nextState: ChipStateSender): Boolean { return nextState == FAR_FROM_RECEIVER || @@ -325,9 +330,16 @@ sealed class SenderEndItem { ) : SenderEndItem() } -// 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 = 30000 +/** Represents how long the chip should be visible before it times out. */ +enum class TimeoutLength { + /** A default timeout used for temporary displays at the top of the screen. */ + DEFAULT, + /** + * A longer timeout. Should be used when the status is pending (e.g. loading), so that the user + * remains informed about the process for longer and so that the UI has more time to resolve the + * pending state before disappearing. + */ + LONG, +} private const val TAG = "ChipStateSender" diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index 6bb6906a0dfc9..c7c72a9cd1203 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -56,6 +56,9 @@ constructor( private val uiEventLogger: MediaTttSenderUiEventLogger, ) : CoreStartable, Dumpable { + // Since the media transfer display is similar to a heads-up notification, use the same timeout. + private val defaultTimeout = context.resources.getInteger(R.integer.heads_up_notification_decay) + // A map to store current chip state per id. private var stateMap: MutableMap = mutableMapOf() @@ -165,6 +168,12 @@ constructor( logger.logPackageNotFound(packageName) } + val timeout = + when (chipStateSender.timeoutLength) { + TimeoutLength.DEFAULT -> defaultTimeout + TimeoutLength.LONG -> 2 * defaultTimeout + } + return ChipbarInfo( // Display the app's icon as the start icon startIcon = icon.toTintedIcon(), @@ -191,7 +200,7 @@ constructor( allowSwipeToDismiss = true, windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER, wakeReason = MediaTttUtils.WAKE_REASON_SENDER, - timeoutMs = chipStateSender.timeout, + timeoutMs = timeout, id = routeInfo.id, priority = ViewPriority.NORMAL, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index db890f6e739be..ca2b1da34766f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -107,6 +107,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { private lateinit var fakeExecutor: FakeExecutor private lateinit var uiEventLoggerFake: UiEventLoggerFake private lateinit var uiEventLogger: MediaTttSenderUiEventLogger + private val defaultTimeout = context.resources.getInteger(R.integer.heads_up_notification_decay) @Before fun setUp() { @@ -1356,6 +1357,92 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) } + @Test + fun almostClose_hasLongTimeout_eventuallyTimesOut() { + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenAnswer { + it.arguments[0] + } + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, + routeInfo, + null, + ) + + // WHEN the default timeout has passed + fakeClock.advanceTime(defaultTimeout + 1L) + + // THEN the view is still on-screen because it has a long timeout + verify(windowManager, never()).removeView(any()) + + // WHEN a very long amount of time has passed + fakeClock.advanceTime(5L * defaultTimeout) + + // THEN the view does time out + verify(windowManager).removeView(any()) + } + + @Test + fun loading_hasLongTimeout_eventuallyTimesOut() { + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenAnswer { + it.arguments[0] + } + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, + routeInfo, + null, + ) + + // WHEN the default timeout has passed + fakeClock.advanceTime(defaultTimeout + 1L) + + // THEN the view is still on-screen because it has a long timeout + verify(windowManager, never()).removeView(any()) + + // WHEN a very long amount of time has passed + fakeClock.advanceTime(5L * defaultTimeout) + + // THEN the view does time out + verify(windowManager).removeView(any()) + } + + @Test + fun succeeded_hasDefaultTimeout() { + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenAnswer { + it.arguments[0] + } + + displayReceiverTriggered() + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + null, + ) + + fakeClock.advanceTime(defaultTimeout + 1L) + + verify(windowManager).removeView(any()) + } + + @Test + fun failed_hasDefaultTimeout() { + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenAnswer { + it.arguments[0] + } + + displayThisDeviceTriggered() + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED, + routeInfo, + null, + ) + + fakeClock.advanceTime(defaultTimeout + 1L) + + verify(windowManager).removeView(any()) + } + private fun getChipbarView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any())