Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8b3f856c authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[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
parent e7d87a4e
Loading
Loading
Loading
Loading
+22 −10
Original line number Diff line number Diff line
@@ -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"
+10 −1
Original line number Diff line number Diff line
@@ -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<String, ChipStateSender> = 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,
        )
+87 −0
Original line number Diff line number Diff line
@@ -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())