From 6fdfe2a862d9f0e17da014610c9c6c9d9a6c5b33 Mon Sep 17 00:00:00 2001 From: Caitlin Shkuratov Date: Wed, 7 Dec 2022 16:13:51 +0000 Subject: [Chipbar] Enable chipbar to handle chipbars with different priorities. This CL re-writes almost everything about how chipbar works, unfortunately. I realized there were some bugs in the implementation of handling multiple chipbars at once, and with the addition of priorities it made it easier to just start from scratch. Fixes: 261895766 Fixes: 258019006 Test: ttt chipbar then active unlock chipbar -> active unlock chipbar shown, ttt chipbar re-shows after the active unlock chipbar disappears Test: active unlock chipbar then ttt chipbar -> active unlock chipbar still displayed, ttt chipbar shows up after active unlock disappears Test: ttt flow from started -> triggered -> succeeded Test: ttt flow works for multiple IDs Test: ttt chipbar, then active unlock chipbar, then ttt chipbar with updated info -> when active unlock chipbar disappears, ttt chipbar with updated info *and updated timeout* is displayed Test: atest TemporaryViewDisplayControllerTest Change-Id: I7f077f7e05834c34cfd05ab72e307d2798a18ddf --- .../android/systemui/media/dagger/MediaModule.java | 10 +- .../media/taptotransfer/common/MediaTttLogger.kt | 8 +- .../media/taptotransfer/common/MediaTttUtils.kt | 3 +- .../receiver/MediaTttChipControllerReceiver.kt | 9 +- .../sender/MediaTttSenderCoordinator.kt | 6 +- .../TemporaryViewDisplayController.kt | 395 +++++++----- .../systemui/temporarydisplay/TemporaryViewInfo.kt | 14 + .../temporarydisplay/TemporaryViewLogger.kt | 71 ++- .../temporarydisplay/chipbar/ChipbarCoordinator.kt | 3 + .../temporarydisplay/chipbar/ChipbarInfo.kt | 2 + .../temporarydisplay/chipbar/ChipbarLogger.kt | 2 +- .../taptotransfer/common/MediaTttLoggerTest.kt | 3 +- .../taptotransfer/common/MediaTttUtilsTest.kt | 3 +- .../receiver/FakeMediaTttChipControllerReceiver.kt | 5 +- .../receiver/MediaTttChipControllerReceiverTest.kt | 4 +- .../sender/MediaTttSenderCoordinatorTest.kt | 4 +- .../TemporaryViewDisplayControllerTest.kt | 659 +++++++++++++++++++-- .../temporarydisplay/TemporaryViewLoggerTest.kt | 13 +- .../chipbar/ChipbarCoordinatorTest.kt | 3 + .../chipbar/FakeChipbarCoordinator.kt | 3 + 20 files changed, 1019 insertions(+), 201 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index 3e5d337bff9d..bb833df1ff69 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -30,9 +30,11 @@ import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper; import com.android.systemui.media.taptotransfer.MediaTttFlags; import com.android.systemui.media.taptotransfer.common.MediaTttLogger; +import com.android.systemui.media.taptotransfer.receiver.ChipReceiverInfo; import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger; import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger; import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo; import java.util.Optional; @@ -95,19 +97,19 @@ public interface MediaModule { @Provides @SysUISingleton @MediaTttSenderLogger - static MediaTttLogger providesMediaTttSenderLogger( + static MediaTttLogger providesMediaTttSenderLogger( @MediaTttSenderLogBuffer LogBuffer buffer ) { - return new MediaTttLogger("Sender", buffer); + return new MediaTttLogger<>("Sender", buffer); } @Provides @SysUISingleton @MediaTttReceiverLogger - static MediaTttLogger providesMediaTttReceiverLogger( + static MediaTttLogger providesMediaTttReceiverLogger( @MediaTttReceiverLogBuffer LogBuffer buffer ) { - return new MediaTttLogger("Receiver", buffer); + return new MediaTttLogger<>("Receiver", buffer); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt index b55bedda2dc1..8aef9385fe3e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt @@ -18,17 +18,21 @@ package com.android.systemui.media.taptotransfer.common import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.temporarydisplay.TemporaryViewLogger /** * A logger for media tap-to-transfer events. * * @param deviceTypeTag the type of device triggering the logs -- "Sender" or "Receiver". + * + * TODO(b/245610654): We should de-couple the sender and receiver loggers, since they're vastly + * different experiences. */ -class MediaTttLogger( +class MediaTttLogger( deviceTypeTag: String, buffer: LogBuffer -) : TemporaryViewLogger(buffer, BASE_TAG + deviceTypeTag) { +) : TemporaryViewLogger(buffer, BASE_TAG + deviceTypeTag) { /** Logs a change in the chip state for the given [mediaRouteId]. */ fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) { buffer.log( diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt index 009595a6da8b..066c1853818f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt @@ -25,6 +25,7 @@ import com.android.systemui.R import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.TintedIcon +import com.android.systemui.temporarydisplay.TemporaryViewInfo /** Utility methods for media tap-to-transfer. */ class MediaTttUtils { @@ -47,7 +48,7 @@ class MediaTttUtils { fun getIconInfoFromPackageName( context: Context, appPackageName: String?, - logger: MediaTttLogger + logger: MediaTttLogger ): IconInfo { if (appPackageName != null) { val packageManager = context.packageManager diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 1c3a53cbf815..9ca5fad35ca9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -45,8 +45,10 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.TemporaryViewDisplayController import com.android.systemui.temporarydisplay.TemporaryViewInfo +import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.util.animation.AnimationUtil.Companion.frames import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.wakelock.WakeLock import javax.inject.Inject @@ -62,7 +64,7 @@ import javax.inject.Inject open class MediaTttChipControllerReceiver @Inject constructor( private val commandQueue: CommandQueue, context: Context, - @MediaTttReceiverLogger logger: MediaTttLogger, + @MediaTttReceiverLogger logger: MediaTttLogger, windowManager: WindowManager, mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, @@ -73,7 +75,8 @@ open class MediaTttChipControllerReceiver @Inject constructor( private val uiEventLogger: MediaTttReceiverUiEventLogger, private val viewUtil: ViewUtil, wakeLockBuilder: WakeLock.Builder, -) : TemporaryViewDisplayController( + systemClock: SystemClock, +) : TemporaryViewDisplayController>( context, logger, windowManager, @@ -83,6 +86,7 @@ open class MediaTttChipControllerReceiver @Inject constructor( powerManager, R.layout.media_ttt_chip_receiver, wakeLockBuilder, + systemClock, ) { @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS override val windowLayoutParams = commonWindowLayoutParams.apply { @@ -290,4 +294,5 @@ data class ChipReceiverInfo( override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER, override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER, override val id: String, + override val priority: ViewPriority = ViewPriority.NORMAL, ) : TemporaryViewInfo() 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 ec1984d78cf9..9f44d984124f 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 @@ -30,6 +30,7 @@ import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo @@ -46,7 +47,7 @@ constructor( private val chipbarCoordinator: ChipbarCoordinator, private val commandQueue: CommandQueue, private val context: Context, - @MediaTttSenderLogger private val logger: MediaTttLogger, + @MediaTttSenderLogger private val logger: MediaTttLogger, private val mediaTttFlags: MediaTttFlags, private val uiEventLogger: MediaTttSenderUiEventLogger, ) : CoreStartable { @@ -146,7 +147,7 @@ constructor( routeInfo: MediaRoute2Info, undoCallback: IUndoMediaTransferCallback?, context: Context, - logger: MediaTttLogger, + logger: MediaTttLogger, ): ChipbarInfo { val packageName = routeInfo.clientPackageName val otherDeviceName = routeInfo.name.toString() @@ -180,6 +181,7 @@ constructor( wakeReason = MediaTttUtils.WAKE_REASON_SENDER, timeoutMs = chipStateSender.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 ea4020861a09..db7315f311ac 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -34,6 +34,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.wakelock.WakeLock /** @@ -44,8 +45,24 @@ import com.android.systemui.util.wakelock.WakeLock * * The generic type T is expected to contain all the information necessary for the subclasses to * display the view in a certain state, since they receive in [updateView]. + * + * Some information about display ordering: + * + * [ViewPriority] defines different priorities for the incoming views. The incoming view will be + * displayed so long as its priority is equal to or greater than the currently displayed view. + * (Concretely, this means that a [ViewPriority.NORMAL] won't be displayed if a + * [ViewPriority.CRITICAL] is currently displayed. But otherwise, the incoming view will get + * displayed and kick out the old view). + * + * Once the currently displayed view times out, we *may* display a previously requested view if it + * still has enough time left before its own timeout. The same priority ordering applies. + * + * Note: [TemporaryViewInfo.id] is the identifier that we use to determine if a call to + * [displayView] will just update the current view with new information, or display a completely new + * view. This means that you *cannot* change the [TemporaryViewInfo.priority] or + * [TemporaryViewInfo.windowTitle] while using the same ID. */ -abstract class TemporaryViewDisplayController( +abstract class TemporaryViewDisplayController>( internal val context: Context, internal val logger: U, internal val windowManager: WindowManager, @@ -55,6 +72,7 @@ abstract class TemporaryViewDisplayController = mutableListOf() - /** - * A stack of pairs of device id and temporary view info. This is used when there may be - * multiple devices in range, and we want to always display the chip for the most recently - * active device. - */ - internal val activeViews: ArrayDeque> = ArrayDeque() + private fun getCurrentDisplayInfo(): DisplayInfo? { + return activeViews.getOrNull(0) + } /** * Displays the view with the provided [newInfo]. @@ -107,94 +116,139 @@ abstract class TemporaryViewDisplayController newInfo.priority) { + logger.logViewAdditionDelayed(newInfo) + // Remove any old information for this id (if it exists) and re-add it to the list in + // the right priority spot + removeFromActivesIfNeeded(newInfo.id) + var insertIndex = 0 + while (insertIndex < activeViews.size && + activeViews[insertIndex].info.priority > newInfo.priority) { + insertIndex++ + } + activeViews.add(insertIndex, newDisplayInfo) + return + } + + // Else: The newInfo should be displayed and the currentInfo should be hidden + hideView(currentDisplayInfo) + // Remove any old information for this id (if it exists) and put this info at the beginning + removeFromActivesIfNeeded(newDisplayInfo.info.id) + activeViews.add(0, newDisplayInfo) + showNewView(newDisplayInfo, timeout) + } + + private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) { + logger.logViewAddition(newDisplayInfo.info) + createAndAcquireWakeLock(newDisplayInfo) + updateTimeout(newDisplayInfo, timeout, newDisplayInfo.onViewTimeout) + inflateAndUpdateView(newDisplayInfo) + } - // Only cancel timeout of the most recent view displayed, as it will be reset. - if (position == 0) { - cancelViewTimeout?.run() + private fun createAndAcquireWakeLock(displayInfo: DisplayInfo) { + // TODO(b/262009503): Migrate off of isScrenOn, since it's deprecated. + val newWakeLock = if (!powerManager.isScreenOn) { + // If the screen is off, fully wake it so the user can see the view. + wakeLockBuilder + .setTag(displayInfo.info.windowTitle) + .setLevelsAndFlags( + PowerManager.FULL_WAKE_LOCK or + PowerManager.ACQUIRE_CAUSES_WAKEUP + ) + .build() + } else { + // Per b/239426653, we want the view to show over the dream state. + // If the screen is on, using screen bright level will leave screen on the dream + // state but ensure the screen will not go off before wake lock is released. + wakeLockBuilder + .setTag(displayInfo.info.windowTitle) + .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK) + .build() } - cancelViewTimeout = mainExecutor.executeDelayed( + displayInfo.wakeLock = newWakeLock + newWakeLock.acquire(displayInfo.info.wakeReason) + } + + /** + * Creates a runnable that will remove [displayInfo] in [timeout] ms from now. + * + * @param onViewTimeout an optional runnable that will be run if the view times out. + * @return a runnable that, when run, will *cancel* the view's timeout. + */ + private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int, onViewTimeout: Runnable?) { + val cancelViewTimeout = mainExecutor.executeDelayed( { - removeView(id, REMOVAL_REASON_TIMEOUT) + removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT) onViewTimeout?.run() }, timeout.toLong() ) + + displayInfo.onViewTimeout = onViewTimeout + // Cancel old view timeout and re-set it. + displayInfo.cancelViewTimeout?.run() + displayInfo.cancelViewTimeout = cancelViewTimeout } - /** Inflates a new view, updates it with [newInfo], and adds the view to the window. */ - private fun inflateAndUpdateView(newInfo: T) { + /** Inflates a new view, updates it with [DisplayInfo.info], and adds the view to the window. */ + private fun inflateAndUpdateView(displayInfo: DisplayInfo) { + val newInfo = displayInfo.info val newView = LayoutInflater .from(context) .inflate(viewLayoutRes, null) as ViewGroup - val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion) - newViewController.init() + displayInfo.view = newView // We don't need to hold on to the view controller since we never set anything additional // on it -- it will be automatically cleaned up when the view is detached. - val newDisplayInfo = DisplayInfo(newView, newInfo) - displayInfo = newDisplayInfo - updateView(newDisplayInfo.info, newDisplayInfo.view) + val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion) + newViewController.init() + + updateView(newInfo, newView) val paramsWithTitle = WindowManager.LayoutParams().also { it.copyFrom(windowLayoutParams) @@ -206,11 +260,15 @@ abstract class TemporaryViewDisplayController( internal val buffer: LogBuffer, internal val tag: String, ) { - /** Logs that we added the view with the given [id] in a window titled [windowTitle]. */ - fun logViewAddition(id: String, windowTitle: String) { + fun logViewExpiration(info: T) { buffer.log( tag, LogLevel.DEBUG, { - str1 = windowTitle - str2 = id + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name + }, + { "View timeout has already expired; removing. id=$str1 window=$str2 priority=$str3" } + ) + } + + fun logViewUpdate(info: T) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name }, - { "View added. window=$str1 id=$str2" } + { "Existing view updated with new data. id=$str1 window=$str2 priority=$str3" } + ) + } + + fun logViewAdditionDelayed(info: T) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name + }, + { + "New view can't be displayed because higher priority view is currently " + + "displayed. New view id=$str1 window=$str2 priority=$str3" + } + ) + } + + /** Logs that we added the view with the given information. */ + fun logViewAddition(info: T) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name + }, + { "View added. id=$str1 window=$str2 priority=$str3" } + ) + } + + fun logViewHidden(info: T) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name + }, + { + "View hidden in favor of newer view. " + + "Hidden view id=$str1 window=$str2 priority=$str3" + } ) } diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 4d91e35856dc..14ba63a2738f 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.TemporaryViewDisplayController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.wakelock.WakeLock import javax.inject.Inject @@ -77,6 +78,7 @@ open class ChipbarCoordinator @Inject constructor( private val viewUtil: ViewUtil, private val vibratorHelper: VibratorHelper, wakeLockBuilder: WakeLock.Builder, + systemClock: SystemClock, ) : TemporaryViewDisplayController( context, logger, @@ -87,6 +89,7 @@ open class ChipbarCoordinator @Inject constructor( powerManager, R.layout.chipbar, wakeLockBuilder, + systemClock, ) { private lateinit var parent: ChipbarRootView diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt index a3eef8032b3b..dd4bd26e3bcd 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt @@ -22,6 +22,7 @@ import androidx.annotation.AttrRes import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.temporarydisplay.TemporaryViewInfo +import com.android.systemui.temporarydisplay.ViewPriority /** * A container for all the state needed to display a chipbar via [ChipbarCoordinator]. @@ -42,6 +43,7 @@ data class ChipbarInfo( override val wakeReason: String, override val timeoutMs: Int, override val id: String, + override val priority: ViewPriority, ) : TemporaryViewInfo() { companion object { @AttrRes const val DEFAULT_ICON_TINT_ATTR = android.R.attr.textColorPrimary diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt index e477cd68673a..fcfbe0aeedf6 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt @@ -29,7 +29,7 @@ class ChipbarLogger @Inject constructor( @ChipbarLog buffer: LogBuffer, -) : TemporaryViewLogger(buffer, "ChipbarLog") { +) : TemporaryViewLogger(buffer, "ChipbarLog") { /** * Logs that the chipbar was updated to display in a window named [windowTitle], with [text] and * [endItemDesc]. diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt index e009e8651f2a..0e7bf8d9d465 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogcatEchoTracker +import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter @@ -33,7 +34,7 @@ import org.mockito.Mockito.mock class MediaTttLoggerTest : SysuiTestCase() { private lateinit var buffer: LogBuffer - private lateinit var logger: MediaTttLogger + private lateinit var logger: MediaTttLogger @Before fun setUp () { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt index cce3e369c0b8..561867f78e60 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -40,7 +41,7 @@ class MediaTttUtilsTest : SysuiTestCase() { private lateinit var appIconFromPackageName: Drawable @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var applicationInfo: ApplicationInfo - @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var logger: MediaTttLogger @Before fun setUp() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt index 4aa982ed1609..bad3f0374a31 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt @@ -27,13 +27,14 @@ import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.wakelock.WakeLock class FakeMediaTttChipControllerReceiver( commandQueue: CommandQueue, context: Context, - logger: MediaTttLogger, + logger: MediaTttLogger, windowManager: WindowManager, mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, @@ -44,6 +45,7 @@ class FakeMediaTttChipControllerReceiver( uiEventLogger: MediaTttReceiverUiEventLogger, viewUtil: ViewUtil, wakeLockBuilder: WakeLock.Builder, + systemClock: SystemClock, ) : MediaTttChipControllerReceiver( commandQueue, @@ -59,6 +61,7 @@ class FakeMediaTttChipControllerReceiver( uiEventLogger, viewUtil, wakeLockBuilder, + systemClock, ) { override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { // Just bypass the animation in tests diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt index 23f7cdb45026..ffa261a26279 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt @@ -67,7 +67,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { @Mock private lateinit var applicationInfo: ApplicationInfo @Mock - private lateinit var logger: MediaTttLogger + private lateinit var logger: MediaTttLogger @Mock private lateinit var accessibilityManager: AccessibilityManager @Mock @@ -128,6 +128,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { receiverUiEventLogger, viewUtil, fakeWakeLockBuilder, + fakeClock, ) controllerReceiver.start() @@ -155,6 +156,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { receiverUiEventLogger, viewUtil, fakeWakeLockBuilder, + fakeClock, ) controllerReceiver.start() 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 311740e17310..b03a545f787f 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 @@ -45,6 +45,7 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator import com.android.systemui.util.concurrency.FakeExecutor @@ -83,7 +84,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var chipbarLogger: ChipbarLogger - @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var logger: MediaTttLogger @Mock private lateinit var mediaTttFlags: MediaTttFlags @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var powerManager: PowerManager @@ -142,6 +143,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { viewUtil, vibratorHelper, fakeWakeLockBuilder, + fakeClock, ) chipbarCoordinator.start() diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt index 09f0d4a10410..82153d5610a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.wakelock.WakeLock import com.android.systemui.util.wakelock.WakeLockFake import com.google.common.truth.Truth.assertThat @@ -59,7 +60,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { private lateinit var fakeWakeLock: WakeLockFake @Mock - private lateinit var logger: TemporaryViewLogger + private lateinit var logger: TemporaryViewLogger @Mock private lateinit var accessibilityManager: AccessibilityManager @Mock @@ -74,7 +75,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())) - .thenReturn(TIMEOUT_MS.toInt()) + .thenAnswer { it.arguments[0] } fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) @@ -84,14 +85,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { fakeWakeLockBuilder.setWakeLock(fakeWakeLock) underTest = TestController( - context, - logger, - windowManager, - fakeExecutor, - accessibilityManager, - configurationController, - powerManager, - fakeWakeLockBuilder, + context, + logger, + windowManager, + fakeExecutor, + accessibilityManager, + configurationController, + powerManager, + fakeWakeLockBuilder, + fakeClock, ) underTest.start() } @@ -112,14 +114,14 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { @Test fun displayView_logged() { - underTest.displayView( - ViewInfo( - name = "name", - windowTitle = "Fake Window Title", - ) + val info = ViewInfo( + name = "name", + windowTitle = "Fake Window Title", ) - verify(logger).logViewAddition("id", "Fake Window Title") + underTest.displayView(info) + + verify(logger).logViewAddition(info) } @Test @@ -168,10 +170,11 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @Test - fun displayView_twiceWithDifferentWindowTitles_oldViewRemovedNewViewAdded() { + fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() { underTest.displayView( ViewInfo( name = "name", + id = "First", windowTitle = "First Fake Window Title", ) ) @@ -179,6 +182,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.displayView( ViewInfo( name = "name", + id = "Second", windowTitle = "Second Fake Window Title", ) ) @@ -262,20 +266,70 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") } + @Test + fun viewUpdatedWithNewOnViewTimeoutRunnable_newRunnableUsed() { + var runnable1Run = false + underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) { + runnable1Run = true + } + + var runnable2Run = false + underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) { + runnable2Run = true + } + + fakeClock.advanceTime(TIMEOUT_MS + 1) + + assertThat(runnable1Run).isFalse() + assertThat(runnable2Run).isTrue() + } + + @Test + fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() { + underTest.displayView( + ViewInfo( + name = "name", + windowTitle = "First Fake Window Title", + id = "id1" + ) + ) + + underTest.displayView( + ViewInfo( + name = "name", + windowTitle = "Second Fake Window Title", + id = "id2" + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + + verify(windowManager, times(2)).addView(capture(viewCaptor), capture(windowParamsCaptor)) + + assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title") + assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title") + verify(windowManager).removeView(viewCaptor.allValues[0]) + verify(configurationController, never()).removeCallback(any()) + } + @Test fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() { underTest.displayView(ViewInfo("First name", id = "id1")) verify(windowManager).addView(any(), any()) - reset(windowManager) + underTest.displayView(ViewInfo("Second name", id = "id2")) - underTest.removeView("id2", "test reason") verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) + reset(windowManager) - fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1) + underTest.removeView("id2", "test reason") + verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name") @@ -284,6 +338,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { verify(windowManager).removeView(any()) assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) } @Test @@ -291,19 +346,28 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.displayView(ViewInfo("First name", id = "id1")) verify(windowManager).addView(any(), any()) - reset(windowManager) + underTest.displayView(ViewInfo("Second name", id = "id2")) + + verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) + reset(windowManager) + + // WHEN an old view is removed underTest.removeView("id1", "test reason") + // THEN we don't update anything verify(windowManager, never()).removeView(any()) assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") + verify(configurationController, never()).removeCallback(any()) fakeClock.advanceTime(TIMEOUT_MS + 1) verify(windowManager).removeView(any()) assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) } @Test @@ -312,33 +376,31 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.displayView(ViewInfo("Second name", id = "id2")) underTest.displayView(ViewInfo("Third name", id = "id3")) - verify(windowManager).addView(any(), any()) + verify(windowManager, times(3)).addView(any(), any()) + verify(windowManager, times(2)).removeView(any()) reset(windowManager) underTest.removeView("id3", "test reason") verify(windowManager).removeView(any()) - - fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1) - assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") + verify(configurationController, never()).removeCallback(any()) reset(windowManager) underTest.removeView("id2", "test reason") verify(windowManager).removeView(any()) - - fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1) - assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name") + verify(configurationController, never()).removeCallback(any()) reset(windowManager) fakeClock.advanceTime(TIMEOUT_MS + 1) verify(windowManager).removeView(any()) assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) } @Test @@ -347,18 +409,21 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.displayView(ViewInfo("New name", id = "id1")) verify(windowManager).addView(any(), any()) - reset(windowManager) + underTest.displayView(ViewInfo("Second name", id = "id2")) - underTest.removeView("id2", "test reason") verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) + reset(windowManager) - fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1) + underTest.removeView("id2", "test reason") + verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("New name") - assertThat(underTest.activeViews[0].second.name).isEqualTo("New name") + assertThat(underTest.activeViews[0].info.name).isEqualTo("New name") reset(windowManager) fakeClock.advanceTime(TIMEOUT_MS + 1) @@ -368,19 +433,523 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @Test - fun multipleViewsWithDifferentIds_viewsTimeouts_noViewLeftToDisplay() { - underTest.displayView(ViewInfo("First name", id = "id1")) - fakeClock.advanceTime(TIMEOUT_MS / 3) - underTest.displayView(ViewInfo("Second name", id = "id2")) - fakeClock.advanceTime(TIMEOUT_MS / 3) - underTest.displayView(ViewInfo("Third name", id = "id3")) + fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() { + underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000)) + fakeClock.advanceTime(1000) + underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000)) + fakeClock.advanceTime(1000) + underTest.displayView(ViewInfo("Third name", id = "id3", timeoutMs = 20000)) reset(windowManager) - fakeClock.advanceTime(TIMEOUT_MS + 1) + fakeClock.advanceTime(20000 + 1) verify(windowManager).removeView(any()) verify(windowManager, never()).addView(any(), any()) assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) + } + + @Test + fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() { + underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000)) + fakeClock.advanceTime(1000) + underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500)) + + reset(windowManager) + fakeClock.advanceTime(2500 + 1) + // At this point, 3501ms have passed, so id1 only has 499ms left which is not enough. + // So, it shouldn't be displayed. + + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) + } + + @Test + fun lowerThenHigherPriority_higherReplacesLower() { + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun lowerThenHigherPriority_lowerPriorityRedisplayed() { + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 10000 + ) + ) + + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 2000 + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager, times(2)).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("Normal Window Title") + assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Critical Window Title") + verify(windowManager).removeView(viewCaptor.allValues[0]) + + reset(windowManager) + + // WHEN the critical's timeout has expired + fakeClock.advanceTime(2000 + 1) + + // THEN the normal view is re-displayed + verify(windowManager).removeView(viewCaptor.allValues[1]) + verify(windowManager).addView(any(), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() { + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 1000 + ) + ) + + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 2000 + ) + ) + reset(windowManager) + + // WHEN the critical's timeout has expired + fakeClock.advanceTime(2000 + 1) + + // THEN the normal view is not re-displayed since it already timed out + verify(windowManager).removeView(any()) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews).isEmpty() + verify(configurationController).removeCallback(any()) + } + + @Test + fun higherThenLowerPriority_higherStaysDisplayed() { + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + ) + ) + + verify(windowManager, never()).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(2) + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun higherThenLowerPriority_lowerEventuallyDisplayed() { + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 3000, + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 5000, + ) + ) + + verify(windowManager, never()).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(2) + + // WHEN the first critical view has timed out + fakeClock.advanceTime(3000 + 1) + + // THEN the second normal view is displayed + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + assertThat(underTest.activeViews.size).isEqualTo(1) + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() { + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 3000, + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 200, + ) + ) + + verify(windowManager, never()).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // WHEN the first critical view has timed out + fakeClock.advanceTime(3000 + 1) + + // THEN the second normal view is not displayed because it's already timed out + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews).isEmpty() + verify(configurationController).removeCallback(any()) + } + + @Test + fun criticalThenNewCritical_newCriticalDisplayed() { + underTest.displayView( + ViewInfo( + name = "critical 1", + windowTitle = "Critical Window Title 1", + id = "critical1", + priority = ViewPriority.CRITICAL, + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 1") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "critical 2", + windowTitle = "Critical Window Title 2", + id = "critical2", + priority = ViewPriority.CRITICAL, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2") + assertThat(underTest.activeViews.size).isEqualTo(2) + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun normalThenNewNormal_newNormalDisplayed() { + underTest.displayView( + ViewInfo( + name = "normal 1", + windowTitle = "Normal Window Title 1", + id = "normal1", + priority = ViewPriority.NORMAL, + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 1") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "normal 2", + windowTitle = "Normal Window Title 2", + id = "normal2", + priority = ViewPriority.NORMAL, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2") + assertThat(underTest.activeViews.size).isEqualTo(2) + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun lowerPriorityViewUpdatedWhileHigherPriorityDisplayed_eventuallyDisplaysUpdated() { + // First, display a lower priority view + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + // At the end of the test, we'll verify that this information isn't re-displayed. + // Use a super long timeout so that, when we verify it wasn't re-displayed, we know + // that it wasn't because the view just timed out. + timeoutMs = 100000, + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + reset(windowManager) + + // Then, display a higher priority view + fakeClock.advanceTime(1000) + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 3000, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // While the higher priority view is displayed, update the lower priority view with new + // information + fakeClock.advanceTime(1000) + val updatedViewInfo = ViewInfo( + name = "normal with update", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 4000, + ) + underTest.displayView(updatedViewInfo) + + verify(windowManager, never()).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // WHEN the higher priority view times out + fakeClock.advanceTime(2001) + + // THEN the higher priority view disappears and the lower priority view *with the updated + // information* gets displayed. + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + assertThat(underTest.activeViews.size).isEqualTo(1) + assertThat(underTest.mostRecentViewInfo).isEqualTo(updatedViewInfo) + reset(windowManager) + + // WHEN the updated view times out + fakeClock.advanceTime(2001) + + // THEN the old information is never displayed + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(0) + } + + @Test + fun oldViewUpdatedWhileNewViewDisplayed_eventuallyDisplaysUpdated() { + // First, display id1 view + underTest.displayView( + ViewInfo( + name = "name 1", + windowTitle = "Name 1 Title", + id = "id1", + priority = ViewPriority.NORMAL, + // At the end of the test, we'll verify that this information isn't re-displayed. + // Use a super long timeout so that, when we verify it wasn't re-displayed, we know + // that it wasn't because the view just timed out. + timeoutMs = 100000, + ) + ) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Name 1 Title") + reset(windowManager) + + // Then, display a new id2 view + fakeClock.advanceTime(1000) + underTest.displayView( + ViewInfo( + name = "name 2", + windowTitle = "Name 2 Title", + id = "id2", + priority = ViewPriority.NORMAL, + timeoutMs = 3000, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Name 2 Title") + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // While the id2 view is displayed, re-display the id1 view with new information + fakeClock.advanceTime(1000) + val updatedViewInfo = ViewInfo( + name = "name 1 with update", + windowTitle = "Name 1 Title", + id = "id1", + priority = ViewPriority.NORMAL, + timeoutMs = 3000, + ) + underTest.displayView(updatedViewInfo) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Name 1 Title") + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // WHEN the id1 view with new information times out + fakeClock.advanceTime(3001) + + // THEN the id1 view disappears and the old id1 information is never displayed + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(0) + } + + @Test + fun oldViewUpdatedWhileNewViewDisplayed_usesNewTimeout() { + // First, display id1 view + underTest.displayView( + ViewInfo( + name = "name 1", + windowTitle = "Name 1 Title", + id = "id1", + priority = ViewPriority.NORMAL, + timeoutMs = 5000, + ) + ) + + // Then, display a new id2 view + fakeClock.advanceTime(1000) + underTest.displayView( + ViewInfo( + name = "name 2", + windowTitle = "Name 2 Title", + id = "id2", + priority = ViewPriority.NORMAL, + timeoutMs = 3000, + ) + ) + reset(windowManager) + + // While the id2 view is displayed, re-display the id1 view with new information *and a + // longer timeout* + fakeClock.advanceTime(1000) + val updatedViewInfo = ViewInfo( + name = "name 1 with update", + windowTitle = "Name 1 Title", + id = "id1", + priority = ViewPriority.NORMAL, + timeoutMs = 30000, + ) + underTest.displayView(updatedViewInfo) + + val viewCaptor = argumentCaptor() + val windowParamsCaptor = argumentCaptor() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Name 1 Title") + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // WHEN id1's *old* timeout occurs + fakeClock.advanceTime(3001) + + // THEN id1 is still displayed because it was updated with a new timeout + verify(windowManager, never()).removeView(viewCaptor.value) + assertThat(underTest.activeViews.size).isEqualTo(1) } @Test @@ -395,6 +964,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { verify(windowManager).removeView(any()) verify(logger).logViewRemoval(deviceId, reason) + verify(configurationController).removeCallback(any()) } @Test @@ -414,14 +984,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { inner class TestController( context: Context, - logger: TemporaryViewLogger, + logger: TemporaryViewLogger, windowManager: WindowManager, @Main mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, wakeLockBuilder: WakeLock.Builder, - ) : TemporaryViewDisplayController( + systemClock: SystemClock, + ) : TemporaryViewDisplayController>( context, logger, windowManager, @@ -431,6 +1002,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { powerManager, R.layout.chipbar, wakeLockBuilder, + systemClock, ) { var mostRecentViewInfo: ViewInfo? = null @@ -447,12 +1019,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { override fun start() {} } - inner class ViewInfo( + data class ViewInfo( val name: String, override val windowTitle: String = "Window Title", override val wakeReason: String = "WAKE_REASON", - override val timeoutMs: Int = 1, + override val timeoutMs: Int = TIMEOUT_MS.toInt(), override val id: String = "id", + override val priority: ViewPriority = ViewPriority.NORMAL, ) : TemporaryViewInfo() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt index 116b8fe62b37..2e66b205bfd5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt @@ -32,7 +32,7 @@ import org.mockito.Mockito @SmallTest class TemporaryViewLoggerTest : SysuiTestCase() { private lateinit var buffer: LogBuffer - private lateinit var logger: TemporaryViewLogger + private lateinit var logger: TemporaryViewLogger @Before fun setUp() { @@ -44,13 +44,22 @@ class TemporaryViewLoggerTest : SysuiTestCase() { @Test fun logViewAddition_bufferHasLog() { - logger.logViewAddition("test id", "Test Window Title") + val info = + object : TemporaryViewInfo() { + override val id: String = "test id" + override val priority: ViewPriority = ViewPriority.CRITICAL + override val windowTitle: String = "Test Window Title" + override val wakeReason: String = "wake reason" + } + + logger.logViewAddition(info) val stringWriter = StringWriter() buffer.dump(PrintWriter(stringWriter), tailLength = 0) val actualString = stringWriter.toString() assertThat(actualString).contains(TAG) + assertThat(actualString).contains("test id") assertThat(actualString).contains("Test Window Title") } diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 7014f93fba4a..2e4d8e74ad6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -39,6 +39,7 @@ import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -105,6 +106,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { viewUtil, vibratorHelper, fakeWakeLockBuilder, + fakeClock, ) underTest.start() } @@ -408,6 +410,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { wakeReason = WAKE_REASON, timeoutMs = TIMEOUT, id = DEVICE_ID, + priority = ViewPriority.NORMAL, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt index beedf9f337bc..d5167b3890b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt @@ -26,6 +26,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.wakelock.WakeLock @@ -43,6 +44,7 @@ class FakeChipbarCoordinator( viewUtil: ViewUtil, vibratorHelper: VibratorHelper, wakeLockBuilder: WakeLock.Builder, + systemClock: SystemClock, ) : ChipbarCoordinator( context, @@ -57,6 +59,7 @@ class FakeChipbarCoordinator( viewUtil, vibratorHelper, wakeLockBuilder, + systemClock, ) { override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { // Just bypass the animation in tests -- cgit v1.2.3-59-g8ed1b