summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt395
-rw-r--r--packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt71
-rw-r--r--packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt659
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt3
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<ChipbarInfo> providesMediaTttSenderLogger(
@MediaTttSenderLogBuffer LogBuffer buffer
) {
- return new MediaTttLogger("Sender", buffer);
+ return new MediaTttLogger<>("Sender", buffer);
}
@Provides
@SysUISingleton
@MediaTttReceiverLogger
- static MediaTttLogger providesMediaTttReceiverLogger(
+ static MediaTttLogger<ChipReceiverInfo> 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<T : TemporaryViewInfo>(
deviceTypeTag: String,
buffer: LogBuffer
-) : TemporaryViewLogger(buffer, BASE_TAG + deviceTypeTag) {
+) : TemporaryViewLogger<T>(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<out TemporaryViewInfo>
): 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<ChipReceiverInfo>,
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<ChipReceiverInfo, MediaTttLogger>(
+ systemClock: SystemClock,
+) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger<ChipReceiverInfo>>(
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<ChipbarInfo>,
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>,
): 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 <T> 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<T : TemporaryViewInfo, U : TemporaryViewLogger>(
+abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger<T>>(
internal val context: Context,
internal val logger: U,
internal val windowManager: WindowManager,
@@ -55,6 +72,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
private val powerManager: PowerManager,
@LayoutRes private val viewLayoutRes: Int,
private val wakeLockBuilder: WakeLock.Builder,
+ private val systemClock: SystemClock,
) : CoreStartable {
/**
* Window layout params that will be used as a starting point for the [windowLayoutParams] of
@@ -78,27 +96,18 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
*/
internal abstract val windowLayoutParams: WindowManager.LayoutParams
- /** A container for all the display-related objects. Null if the view is not being displayed. */
- private var displayInfo: DisplayInfo? = null
-
- /** A [Runnable] that, when run, will cancel the pending timeout of the view. */
- private var cancelViewTimeout: Runnable? = null
-
/**
- * A wakelock that is acquired when view is displayed and screen off,
- * then released when view is removed.
+ * A list of the currently active views, ordered from highest priority in the beginning to
+ * lowest priority at the end.
+ *
+ * Whenever the current view disappears, the next-priority view will be displayed if it's still
+ * valid.
*/
- private var wakeLock: WakeLock? = null
-
- /** A string that keeps track of wakelock reason once it is acquired till it gets released */
- private var wakeReasonAcquired: String? = null
+ internal val activeViews: MutableList<DisplayInfo> = 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<Pair<String, T>> = ArrayDeque()
+ private fun getCurrentDisplayInfo(): DisplayInfo? {
+ return activeViews.getOrNull(0)
+ }
/**
* Displays the view with the provided [newInfo].
@@ -107,94 +116,139 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
* display the correct information in the view.
* @param onViewTimeout a runnable that runs after the view timeout.
*/
+ @Synchronized
fun displayView(newInfo: T, onViewTimeout: Runnable? = null) {
- val currentDisplayInfo = displayInfo
-
- // Update our list of active devices by removing it if necessary, then adding back at the
- // front of the list
- val id = newInfo.id
- val position = findAndRemoveFromActiveViewsList(id)
- activeViews.addFirst(Pair(id, newInfo))
-
- if (currentDisplayInfo != null &&
- currentDisplayInfo.info.windowTitle == newInfo.windowTitle) {
- // We're already displaying information in the correctly-titled window, so we just need
- // to update the view.
- currentDisplayInfo.info = newInfo
- updateView(currentDisplayInfo.info, currentDisplayInfo.view)
- } else {
- if (currentDisplayInfo != null) {
- // We're already displaying information but that information is under a different
- // window title. So, we need to remove the old window with the old title and add a
- // new window with the new title.
- removeView(
- id,
- removalReason = "New info has new window title: ${newInfo.windowTitle}"
- )
- }
-
- // At this point, we're guaranteed to no longer be displaying a view.
- // So, set up all our callbacks and inflate the view.
- configurationController.addCallback(displayScaleListener)
-
- wakeLock = if (!powerManager.isScreenOn) {
- // If the screen is off, fully wake it so the user can see the view.
- wakeLockBuilder
- .setTag(newInfo.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(newInfo.windowTitle)
- .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK)
- .build()
- }
- wakeLock?.acquire(newInfo.wakeReason)
- wakeReasonAcquired = newInfo.wakeReason
- logger.logViewAddition(id, newInfo.windowTitle)
- inflateAndUpdateView(newInfo)
- }
-
- // Cancel and re-set the view timeout each time we get a new state.
val timeout = accessibilityManager.getRecommendedTimeoutMillis(
newInfo.timeoutMs,
// Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but
// include it just to be safe.
FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS
- )
+ )
+ val timeExpirationMillis = systemClock.currentTimeMillis() + timeout
+
+ val currentDisplayInfo = getCurrentDisplayInfo()
+
+ // We're current displaying a chipbar with the same ID, we just need to update its info
+ if (currentDisplayInfo != null && currentDisplayInfo.info.id == newInfo.id) {
+ val view = checkNotNull(currentDisplayInfo.view) {
+ "First item in activeViews list must have a valid view"
+ }
+ logger.logViewUpdate(newInfo)
+ currentDisplayInfo.info = newInfo
+ currentDisplayInfo.timeExpirationMillis = timeExpirationMillis
+ updateTimeout(currentDisplayInfo, timeout, onViewTimeout)
+ updateView(newInfo, view)
+ return
+ }
+
+ val newDisplayInfo = DisplayInfo(
+ info = newInfo,
+ onViewTimeout = onViewTimeout,
+ timeExpirationMillis = timeExpirationMillis,
+ // Null values will be updated to non-null if/when this view actually gets displayed
+ view = null,
+ wakeLock = null,
+ cancelViewTimeout = null,
+ )
+
+ // We're not displaying anything, so just render this new info
+ if (currentDisplayInfo == null) {
+ addCallbacks()
+ activeViews.add(newDisplayInfo)
+ showNewView(newDisplayInfo, timeout)
+ return
+ }
+
+ // The currently displayed info takes higher priority than the new one.
+ // So, just store the new one in case the current one disappears.
+ if (currentDisplayInfo.info.priority > 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<T : TemporaryViewInfo, U : Tempora
}
/** Removes then re-inflates the view. */
+ @Synchronized
private fun reinflateView() {
- val currentViewInfo = displayInfo ?: return
+ val currentDisplayInfo = getCurrentDisplayInfo() ?: return
- windowManager.removeView(currentViewInfo.view)
- inflateAndUpdateView(currentViewInfo.info)
+ val view = checkNotNull(currentDisplayInfo.view) {
+ "First item in activeViews list must have a valid view"
+ }
+ windowManager.removeView(view)
+ inflateAndUpdateView(currentDisplayInfo)
}
private val displayScaleListener = object : ConfigurationController.ConfigurationListener {
@@ -219,68 +277,109 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
}
}
+ private fun addCallbacks() {
+ configurationController.addCallback(displayScaleListener)
+ }
+
+ private fun removeCallbacks() {
+ configurationController.removeCallback(displayScaleListener)
+ }
+
/**
- * Hides the view given its [id].
+ * Completely removes the view for the given [id], both visually and from our internal store.
*
* @param id the id of the device responsible of displaying the temp view.
* @param removalReason a short string describing why the view was removed (timeout, state
* change, etc.)
*/
+ @Synchronized
fun removeView(id: String, removalReason: String) {
- val currentDisplayInfo = displayInfo ?: return
+ logger.logViewRemoval(id, removalReason)
- val removalPosition = findAndRemoveFromActiveViewsList(id)
- if (removalPosition == null) {
- logger.logViewRemovalIgnored(id, "view not found in the list")
+ val displayInfo = activeViews.firstOrNull { it.info.id == id }
+ if (displayInfo == null) {
+ logger.logViewRemovalIgnored(id, "View not found in list")
return
}
- if (removalPosition != 0) {
- logger.logViewRemovalIgnored(id, "most recent view is being displayed.")
+
+ val currentlyDisplayedView = activeViews[0]
+ // Remove immediately (instead as part of the animation end runnable) so that if a new view
+ // event comes in while this view is animating out, we still display the new view
+ // appropriately.
+ activeViews.remove(displayInfo)
+
+ // No need to time the view out since it's already gone
+ displayInfo.cancelViewTimeout?.run()
+
+ if (displayInfo.view == null) {
+ logger.logViewRemovalIgnored(id, "No view to remove")
return
}
- logger.logViewRemoval(id, removalReason)
- val newViewToDisplay = if (activeViews.isEmpty()) {
- null
- } else {
- activeViews[0].second
+ if (currentlyDisplayedView.info.id != id) {
+ logger.logViewRemovalIgnored(id, "View isn't the currently displayed view")
+ return
}
- val currentView = currentDisplayInfo.view
- animateViewOut(currentView) {
- windowManager.removeView(currentView)
- wakeLock?.release(wakeReasonAcquired)
- }
+ removeViewFromWindow(displayInfo)
- configurationController.removeCallback(displayScaleListener)
- // Re-set to null immediately (instead as part of the animation end runnable) so
- // that if a new view event comes in while this view is animating out, we still display
- // the new view appropriately.
- displayInfo = null
- // No need to time the view out since it's already gone
- cancelViewTimeout?.run()
+ // Prune anything that's already timed out before determining if we should re-display a
+ // different chipbar.
+ removeTimedOutViews()
+ val newViewToDisplay = getCurrentDisplayInfo()
if (newViewToDisplay != null) {
- mainExecutor.executeDelayed({ displayView(newViewToDisplay)}, DISPLAY_VIEW_DELAY)
+ val timeout = newViewToDisplay.timeExpirationMillis - systemClock.currentTimeMillis()
+ // TODO(b/258019006): We may want to have a delay before showing the new view so
+ // that the UI translation looks a bit smoother. But, we expect this to happen
+ // rarely so it may not be worth the extra complexity.
+ showNewView(newViewToDisplay, timeout.toInt())
+ } else {
+ removeCallbacks()
}
}
/**
- * Finds and removes the active view with the given [id] from the stack, or null if there is no
- * active view with that ID
- *
- * @param id that temporary view belonged to.
- *
- * @return index of the view in the stack , otherwise null.
+ * Hides the view from the window, but keeps [displayInfo] around in [activeViews] in case it
+ * should be re-displayed later.
*/
- private fun findAndRemoveFromActiveViewsList(id: String): Int? {
- for (i in 0 until activeViews.size) {
- if (activeViews[i].first == id) {
- activeViews.removeAt(i)
- return i
- }
+ private fun hideView(displayInfo: DisplayInfo) {
+ logger.logViewHidden(displayInfo.info)
+ removeViewFromWindow(displayInfo)
+ }
+
+ private fun removeViewFromWindow(displayInfo: DisplayInfo) {
+ val view = displayInfo.view
+ if (view == null) {
+ logger.logViewRemovalIgnored(displayInfo.info.id, "View is null")
+ return
+ }
+ displayInfo.view = null // Need other places??
+ animateViewOut(view) {
+ windowManager.removeView(view)
+ displayInfo.wakeLock?.release(displayInfo.info.wakeReason)
+ }
+ }
+
+ @Synchronized
+ private fun removeTimedOutViews() {
+ val invalidViews = activeViews
+ .filter { it.timeExpirationMillis <
+ systemClock.currentTimeMillis() + MIN_REQUIRED_TIME_FOR_REDISPLAY }
+
+ invalidViews.forEach {
+ activeViews.remove(it)
+ logger.logViewExpiration(it.info)
+ }
+ }
+
+ @Synchronized
+ private fun removeFromActivesIfNeeded(id: String) {
+ val toRemove = activeViews.find { it.info.id == id }
+ toRemove?.let {
+ it.cancelViewTimeout?.run()
+ activeViews.remove(it)
}
- return null
}
/**
@@ -311,17 +410,47 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
}
/** A container for all the display-related state objects. */
- private inner class DisplayInfo(
- /** The view currently being displayed. */
- val view: ViewGroup,
-
- /** The info currently being displayed. */
+ inner class DisplayInfo(
+ /**
+ * The view currently being displayed.
+ *
+ * Null if this info isn't currently being displayed.
+ */
+ var view: ViewGroup?,
+
+ /** The info that should be displayed if/when this is the highest priority view. */
var info: T,
+
+ /**
+ * The system time at which this display info should expire and never be displayed again.
+ */
+ var timeExpirationMillis: Long,
+
+ /**
+ * The wake lock currently held by this view. Must be released when the view disappears.
+ *
+ * Null if this info isn't currently being displayed.
+ */
+ var wakeLock: WakeLock?,
+
+ /**
+ * See [displayView].
+ */
+ var onViewTimeout: Runnable?,
+
+ /**
+ * A runnable that, when run, will cancel this view's timeout.
+ *
+ * Null if this info isn't currently being displayed.
+ */
+ var cancelViewTimeout: Runnable?,
)
+
+ // TODO(b/258019006): Add a dump method that dumps the currently active views.
}
private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
-const val DISPLAY_VIEW_DELAY = 50L
+private const val MIN_REQUIRED_TIME_FOR_REDISPLAY = 1000
private data class IconInfo(
val iconName: String,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
index df8396051dda..5596cf68b4bc 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
@@ -42,6 +42,20 @@ abstract class TemporaryViewInfo {
* The id of the temporary view.
*/
abstract val id: String
+
+ /** The priority for this view. */
+ abstract val priority: ViewPriority
}
const val DEFAULT_TIMEOUT_MILLIS = 10000
+
+/**
+ * The priority of the view being displayed.
+ *
+ * Must be ordered from lowest priority to highest priority. (CRITICAL is currently the highest
+ * priority.)
+ */
+enum class ViewPriority {
+ NORMAL,
+ CRITICAL,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index 133a384e7e17..ec6965a83b5a 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -20,20 +20,79 @@ import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel
/** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */
-open class TemporaryViewLogger(
+open class TemporaryViewLogger<T : TemporaryViewInfo>(
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<ChipbarInfo, ChipbarLogger>(
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<ChipbarInfo>(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<TemporaryViewInfo>
@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<TemporaryViewInfo>
@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<ChipReceiverInfo>,
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<ChipReceiverInfo>
@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<ChipbarInfo>
@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<ViewInfo>
@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",
)
)
@@ -263,19 +267,69 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
}
@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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<View>()
+ val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+ 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<ViewInfo>,
windowManager: WindowManager,
@Main mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
powerManager: PowerManager,
wakeLockBuilder: WakeLock.Builder,
- ) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger>(
+ systemClock: SystemClock,
+ ) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger<ViewInfo>>(
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<TemporaryViewInfo>
@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