diff --git a/packages/SystemUI/res/drawable/media_ttt_chip_background.xml b/packages/SystemUI/res/drawable/chipbar_background.xml similarity index 92% rename from packages/SystemUI/res/drawable/media_ttt_chip_background.xml rename to packages/SystemUI/res/drawable/chipbar_background.xml index 3abf4d72c5386..57221776a32ff 100644 --- a/packages/SystemUI/res/drawable/media_ttt_chip_background.xml +++ b/packages/SystemUI/res/drawable/chipbar_background.xml @@ -17,6 +17,6 @@ - + diff --git a/packages/SystemUI/res/drawable/media_ttt_undo_background.xml b/packages/SystemUI/res/drawable/chipbar_end_button_background.xml similarity index 93% rename from packages/SystemUI/res/drawable/media_ttt_undo_background.xml rename to packages/SystemUI/res/drawable/chipbar_end_button_background.xml index 3e2e4f055b14e..80c7207a35b61 100644 --- a/packages/SystemUI/res/drawable/media_ttt_undo_background.xml +++ b/packages/SystemUI/res/drawable/chipbar_end_button_background.xml @@ -20,7 +20,7 @@ android:color="?android:textColorPrimary"> - + diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index 0ff944c2becf5..a3171784903f8 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -29,8 +29,8 @@ android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:padding="@dimen/media_ttt_chip_outer_padding" - android:background="@drawable/media_ttt_chip_background" + android:padding="@dimen/chipbar_outer_padding" + android:background="@drawable/chipbar_background" android:layout_marginTop="20dp" android:layout_marginStart="@dimen/notification_side_paddings" android:layout_marginEnd="@dimen/notification_side_paddings" @@ -43,8 +43,8 @@ @@ -54,47 +54,46 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:textSize="@dimen/media_ttt_text_size" - android:textColor="?android:attr/textColorPrimary" + android:textSize="@dimen/chipbar_text_size" + android:textColor="@android:color/system_accent2_900" android:alpha="0.0" /> - diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index ac4c4929bab2a..3763b17471975 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -161,6 +161,7 @@ #F28B82 #EA4335 + #B3261E #C5221F #8AB4F8 diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 35fc9b69e1f44..97a7d8faa5815 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1111,14 +1111,15 @@ 22dp 12dp - - 16dp - 16sp - 24dp - 20dp - 8dp - -8dp - 12dp + + + 16dp + 16sp + 24dp + 20dp + 8dp + -8dp + 12dp 112dp 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/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index 1065d3349930d..59122aff7f216 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -274,6 +274,7 @@ abstract class TemporaryViewDisplayController( { "Removal of view with id=$str2 is ignored because $str1" } ) } + + fun logViewAddedToWindowManager(info: T, view: View) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = view.javaClass.name + int1 = view.getIdForLogging() + }, + { + "Adding view to window manager. " + + "id=$str1 window=$str2 view=$str3(id=${Integer.toHexString(int1)})" + } + ) + } + + fun logViewRemovedFromWindowManager(info: T, view: View, isReinflation: Boolean = false) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = view.javaClass.name + int1 = view.getIdForLogging() + bool1 = isReinflation + }, + { + "Removing view from window manager${if (bool1) " due to reinflation" else ""}. " + + "id=$str1 window=$str2 view=$str3(id=${Integer.toHexString(int1)})" + } + ) + } + + companion object { + private fun View.getIdForLogging(): Int { + // The identityHashCode is guaranteed to be constant for the lifetime of the object. + return System.identityHashCode(this) + } + } } 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())