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())