Merge "[Chipbar] Enable chipbar to handle chipbars with different priorities." into tm-qpr-dev

This commit is contained in:
Caitlin Shkuratov
2022-12-14 19:48:49 +00:00
committed by Android (Google) Code Review
20 changed files with 1023 additions and 205 deletions

View File

@@ -30,9 +30,11 @@ import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper; import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
import com.android.systemui.media.taptotransfer.MediaTttFlags; import com.android.systemui.media.taptotransfer.MediaTttFlags;
import com.android.systemui.media.taptotransfer.common.MediaTttLogger; 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.receiver.MediaTttReceiverLogger;
import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger; import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger;
import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.plugins.log.LogBuffer;
import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo;
import java.util.Optional; import java.util.Optional;
@@ -95,19 +97,19 @@ public interface MediaModule {
@Provides @Provides
@SysUISingleton @SysUISingleton
@MediaTttSenderLogger @MediaTttSenderLogger
static MediaTttLogger providesMediaTttSenderLogger( static MediaTttLogger<ChipbarInfo> providesMediaTttSenderLogger(
@MediaTttSenderLogBuffer LogBuffer buffer @MediaTttSenderLogBuffer LogBuffer buffer
) { ) {
return new MediaTttLogger("Sender", buffer); return new MediaTttLogger<>("Sender", buffer);
} }
@Provides @Provides
@SysUISingleton @SysUISingleton
@MediaTttReceiverLogger @MediaTttReceiverLogger
static MediaTttLogger providesMediaTttReceiverLogger( static MediaTttLogger<ChipReceiverInfo> providesMediaTttReceiverLogger(
@MediaTttReceiverLogBuffer LogBuffer buffer @MediaTttReceiverLogBuffer LogBuffer buffer
) { ) {
return new MediaTttLogger("Receiver", buffer); return new MediaTttLogger<>("Receiver", buffer);
} }
/** */ /** */

View File

@@ -18,17 +18,21 @@ package com.android.systemui.media.taptotransfer.common
import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.android.systemui.temporarydisplay.TemporaryViewLogger import com.android.systemui.temporarydisplay.TemporaryViewLogger
/** /**
* A logger for media tap-to-transfer events. * A logger for media tap-to-transfer events.
* *
* @param deviceTypeTag the type of device triggering the logs -- "Sender" or "Receiver". * @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, deviceTypeTag: String,
buffer: LogBuffer buffer: LogBuffer
) : TemporaryViewLogger(buffer, BASE_TAG + deviceTypeTag) { ) : TemporaryViewLogger<T>(buffer, BASE_TAG + deviceTypeTag) {
/** Logs a change in the chip state for the given [mediaRouteId]. */ /** Logs a change in the chip state for the given [mediaRouteId]. */
fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) { fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) {
buffer.log( buffer.log(

View File

@@ -25,6 +25,7 @@ import com.android.systemui.R
import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.common.shared.model.TintedIcon
import com.android.systemui.temporarydisplay.TemporaryViewInfo
/** Utility methods for media tap-to-transfer. */ /** Utility methods for media tap-to-transfer. */
class MediaTttUtils { class MediaTttUtils {
@@ -47,7 +48,7 @@ class MediaTttUtils {
fun getIconInfoFromPackageName( fun getIconInfoFromPackageName(
context: Context, context: Context,
appPackageName: String?, appPackageName: String?,
logger: MediaTttLogger logger: MediaTttLogger<out TemporaryViewInfo>
): IconInfo { ): IconInfo {
if (appPackageName != null) { if (appPackageName != null) {
val packageManager = context.packageManager val packageManager = context.packageManager

View File

@@ -45,8 +45,10 @@ import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.TemporaryViewInfo 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.animation.AnimationUtil.Companion.frames
import com.android.systemui.util.concurrency.DelayableExecutor 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.view.ViewUtil
import com.android.systemui.util.wakelock.WakeLock import com.android.systemui.util.wakelock.WakeLock
import javax.inject.Inject import javax.inject.Inject
@@ -62,7 +64,7 @@ import javax.inject.Inject
open class MediaTttChipControllerReceiver @Inject constructor( open class MediaTttChipControllerReceiver @Inject constructor(
private val commandQueue: CommandQueue, private val commandQueue: CommandQueue,
context: Context, context: Context,
@MediaTttReceiverLogger logger: MediaTttLogger, @MediaTttReceiverLogger logger: MediaTttLogger<ChipReceiverInfo>,
windowManager: WindowManager, windowManager: WindowManager,
mainExecutor: DelayableExecutor, mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager, accessibilityManager: AccessibilityManager,
@@ -73,7 +75,8 @@ open class MediaTttChipControllerReceiver @Inject constructor(
private val uiEventLogger: MediaTttReceiverUiEventLogger, private val uiEventLogger: MediaTttReceiverUiEventLogger,
private val viewUtil: ViewUtil, private val viewUtil: ViewUtil,
wakeLockBuilder: WakeLock.Builder, wakeLockBuilder: WakeLock.Builder,
) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger>( systemClock: SystemClock,
) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger<ChipReceiverInfo>>(
context, context,
logger, logger,
windowManager, windowManager,
@@ -83,6 +86,7 @@ open class MediaTttChipControllerReceiver @Inject constructor(
powerManager, powerManager,
R.layout.media_ttt_chip_receiver, R.layout.media_ttt_chip_receiver,
wakeLockBuilder, wakeLockBuilder,
systemClock,
) { ) {
@SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
override val windowLayoutParams = commonWindowLayoutParams.apply { override val windowLayoutParams = commonWindowLayoutParams.apply {
@@ -290,4 +294,5 @@ data class ChipReceiverInfo(
override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER, override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER,
override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER, override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER,
override val id: String, override val id: String,
override val priority: ViewPriority = ViewPriority.NORMAL,
) : TemporaryViewInfo() ) : TemporaryViewInfo()

View File

@@ -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.MediaTttLogger
import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.media.taptotransfer.common.MediaTttUtils
import com.android.systemui.statusbar.CommandQueue 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.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
@@ -46,7 +47,7 @@ constructor(
private val chipbarCoordinator: ChipbarCoordinator, private val chipbarCoordinator: ChipbarCoordinator,
private val commandQueue: CommandQueue, private val commandQueue: CommandQueue,
private val context: Context, private val context: Context,
@MediaTttSenderLogger private val logger: MediaTttLogger, @MediaTttSenderLogger private val logger: MediaTttLogger<ChipbarInfo>,
private val mediaTttFlags: MediaTttFlags, private val mediaTttFlags: MediaTttFlags,
private val uiEventLogger: MediaTttSenderUiEventLogger, private val uiEventLogger: MediaTttSenderUiEventLogger,
) : CoreStartable { ) : CoreStartable {
@@ -146,7 +147,7 @@ constructor(
routeInfo: MediaRoute2Info, routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?, undoCallback: IUndoMediaTransferCallback?,
context: Context, context: Context,
logger: MediaTttLogger, logger: MediaTttLogger<ChipbarInfo>,
): ChipbarInfo { ): ChipbarInfo {
val packageName = routeInfo.clientPackageName val packageName = routeInfo.clientPackageName
val otherDeviceName = routeInfo.name.toString() val otherDeviceName = routeInfo.name.toString()
@@ -180,6 +181,7 @@ constructor(
wakeReason = MediaTttUtils.WAKE_REASON_SENDER, wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
timeoutMs = chipStateSender.timeout, timeoutMs = chipStateSender.timeout,
id = routeInfo.id, id = routeInfo.id,
priority = ViewPriority.NORMAL,
) )
} }

View File

@@ -34,6 +34,7 @@ import com.android.systemui.CoreStartable
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
import com.android.systemui.util.wakelock.WakeLock 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 * 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]. * 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 context: Context,
internal val logger: U, internal val logger: U,
internal val windowManager: WindowManager, internal val windowManager: WindowManager,
@@ -55,6 +72,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
private val powerManager: PowerManager, private val powerManager: PowerManager,
@LayoutRes private val viewLayoutRes: Int, @LayoutRes private val viewLayoutRes: Int,
private val wakeLockBuilder: WakeLock.Builder, private val wakeLockBuilder: WakeLock.Builder,
private val systemClock: SystemClock,
) : CoreStartable { ) : CoreStartable {
/** /**
* Window layout params that will be used as a starting point for the [windowLayoutParams] of * 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 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, * A list of the currently active views, ordered from highest priority in the beginning to
* then released when view is removed. * 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 internal val activeViews: MutableList<DisplayInfo> = mutableListOf()
/** A string that keeps track of wakelock reason once it is acquired till it gets released */ private fun getCurrentDisplayInfo(): DisplayInfo? {
private var wakeReasonAcquired: String? = null return activeViews.getOrNull(0)
}
/**
* 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()
/** /**
* Displays the view with the provided [newInfo]. * 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. * display the correct information in the view.
* @param onViewTimeout a runnable that runs after the view timeout. * @param onViewTimeout a runnable that runs after the view timeout.
*/ */
@Synchronized
fun displayView(newInfo: T, onViewTimeout: Runnable? = null) { 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( val timeout = accessibilityManager.getRecommendedTimeoutMillis(
newInfo.timeoutMs, newInfo.timeoutMs,
// Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but // Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but
// include it just to be safe. // include it just to be safe.
FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS
) )
val timeExpirationMillis = systemClock.currentTimeMillis() + timeout
// Only cancel timeout of the most recent view displayed, as it will be reset. val currentDisplayInfo = getCurrentDisplayInfo()
if (position == 0) {
cancelViewTimeout?.run() // 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
} }
cancelViewTimeout = mainExecutor.executeDelayed(
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)
}
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()
}
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() onViewTimeout?.run()
}, },
timeout.toLong() 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. */ /** Inflates a new view, updates it with [DisplayInfo.info], and adds the view to the window. */
private fun inflateAndUpdateView(newInfo: T) { private fun inflateAndUpdateView(displayInfo: DisplayInfo) {
val newInfo = displayInfo.info
val newView = LayoutInflater val newView = LayoutInflater
.from(context) .from(context)
.inflate(viewLayoutRes, null) as ViewGroup .inflate(viewLayoutRes, null) as ViewGroup
val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion) displayInfo.view = newView
newViewController.init()
// We don't need to hold on to the view controller since we never set anything additional // 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. // on it -- it will be automatically cleaned up when the view is detached.
val newDisplayInfo = DisplayInfo(newView, newInfo) val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion)
displayInfo = newDisplayInfo newViewController.init()
updateView(newDisplayInfo.info, newDisplayInfo.view)
updateView(newInfo, newView)
val paramsWithTitle = WindowManager.LayoutParams().also { val paramsWithTitle = WindowManager.LayoutParams().also {
it.copyFrom(windowLayoutParams) it.copyFrom(windowLayoutParams)
@@ -206,11 +260,15 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
} }
/** Removes then re-inflates the view. */ /** Removes then re-inflates the view. */
@Synchronized
private fun reinflateView() { private fun reinflateView() {
val currentViewInfo = displayInfo ?: return val currentDisplayInfo = getCurrentDisplayInfo() ?: return
windowManager.removeView(currentViewInfo.view) val view = checkNotNull(currentDisplayInfo.view) {
inflateAndUpdateView(currentViewInfo.info) "First item in activeViews list must have a valid view"
}
windowManager.removeView(view)
inflateAndUpdateView(currentDisplayInfo)
} }
private val displayScaleListener = object : ConfigurationController.ConfigurationListener { 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 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 * @param removalReason a short string describing why the view was removed (timeout, state
* change, etc.) * change, etc.)
*/ */
@Synchronized
fun removeView(id: String, removalReason: String) { fun removeView(id: String, removalReason: String) {
val currentDisplayInfo = displayInfo ?: return
val removalPosition = findAndRemoveFromActiveViewsList(id)
if (removalPosition == null) {
logger.logViewRemovalIgnored(id, "view not found in the list")
return
}
if (removalPosition != 0) {
logger.logViewRemovalIgnored(id, "most recent view is being displayed.")
return
}
logger.logViewRemoval(id, removalReason) logger.logViewRemoval(id, removalReason)
val newViewToDisplay = if (activeViews.isEmpty()) { val displayInfo = activeViews.firstOrNull { it.info.id == id }
null if (displayInfo == null) {
} else { logger.logViewRemovalIgnored(id, "View not found in list")
activeViews[0].second return
} }
val currentView = currentDisplayInfo.view val currentlyDisplayedView = activeViews[0]
animateViewOut(currentView) { // Remove immediately (instead as part of the animation end runnable) so that if a new view
windowManager.removeView(currentView) // event comes in while this view is animating out, we still display the new view
wakeLock?.release(wakeReasonAcquired) // appropriately.
} activeViews.remove(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 // No need to time the view out since it's already gone
cancelViewTimeout?.run() displayInfo.cancelViewTimeout?.run()
if (displayInfo.view == null) {
logger.logViewRemovalIgnored(id, "No view to remove")
return
}
if (currentlyDisplayedView.info.id != id) {
logger.logViewRemovalIgnored(id, "View isn't the currently displayed view")
return
}
removeViewFromWindow(displayInfo)
// Prune anything that's already timed out before determining if we should re-display a
// different chipbar.
removeTimedOutViews()
val newViewToDisplay = getCurrentDisplayInfo()
if (newViewToDisplay != null) { 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 * Hides the view from the window, but keeps [displayInfo] around in [activeViews] in case it
* active view with that ID * should be re-displayed later.
*
* @param id that temporary view belonged to.
*
* @return index of the view in the stack , otherwise null.
*/ */
private fun findAndRemoveFromActiveViewsList(id: String): Int? { private fun hideView(displayInfo: DisplayInfo) {
for (i in 0 until activeViews.size) { logger.logViewHidden(displayInfo.info)
if (activeViews[i].first == id) { removeViewFromWindow(displayInfo)
activeViews.removeAt(i) }
return i
} 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. */ /** A container for all the display-related state objects. */
private inner class DisplayInfo( inner class DisplayInfo(
/** The view currently being displayed. */ /**
val view: ViewGroup, * The view currently being displayed.
*
* Null if this info isn't currently being displayed.
*/
var view: ViewGroup?,
/** The info currently being displayed. */ /** The info that should be displayed if/when this is the highest priority view. */
var info: T, 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" 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( private data class IconInfo(
val iconName: String, val iconName: String,

View File

@@ -42,6 +42,20 @@ abstract class TemporaryViewInfo {
* The id of the temporary view. * The id of the temporary view.
*/ */
abstract val id: String abstract val id: String
/** The priority for this view. */
abstract val priority: ViewPriority
} }
const val DEFAULT_TIMEOUT_MILLIS = 10000 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,
}

View File

@@ -20,20 +20,79 @@ import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel import com.android.systemui.plugins.log.LogLevel
/** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */ /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */
open class TemporaryViewLogger( open class TemporaryViewLogger<T : TemporaryViewInfo>(
internal val buffer: LogBuffer, internal val buffer: LogBuffer,
internal val tag: String, internal val tag: String,
) { ) {
/** Logs that we added the view with the given [id] in a window titled [windowTitle]. */ fun logViewExpiration(info: T) {
fun logViewAddition(id: String, windowTitle: String) {
buffer.log( buffer.log(
tag, tag,
LogLevel.DEBUG, LogLevel.DEBUG,
{ {
str1 = windowTitle str1 = info.id
str2 = id str2 = info.windowTitle
str3 = info.priority.name
}, },
{ "View added. window=$str1 id=$str2" } { "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
},
{ "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"
}
) )
} }

View File

@@ -43,6 +43,7 @@ import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.util.concurrency.DelayableExecutor 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.view.ViewUtil
import com.android.systemui.util.wakelock.WakeLock import com.android.systemui.util.wakelock.WakeLock
import javax.inject.Inject import javax.inject.Inject
@@ -77,6 +78,7 @@ open class ChipbarCoordinator @Inject constructor(
private val viewUtil: ViewUtil, private val viewUtil: ViewUtil,
private val vibratorHelper: VibratorHelper, private val vibratorHelper: VibratorHelper,
wakeLockBuilder: WakeLock.Builder, wakeLockBuilder: WakeLock.Builder,
systemClock: SystemClock,
) : TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>( ) : TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>(
context, context,
logger, logger,
@@ -87,6 +89,7 @@ open class ChipbarCoordinator @Inject constructor(
powerManager, powerManager,
R.layout.chipbar, R.layout.chipbar,
wakeLockBuilder, wakeLockBuilder,
systemClock,
) { ) {
private lateinit var parent: ChipbarRootView private lateinit var parent: ChipbarRootView

View File

@@ -22,6 +22,7 @@ import androidx.annotation.AttrRes
import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.Text
import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.common.shared.model.TintedIcon
import com.android.systemui.temporarydisplay.TemporaryViewInfo 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]. * 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 wakeReason: String,
override val timeoutMs: Int, override val timeoutMs: Int,
override val id: String, override val id: String,
override val priority: ViewPriority,
) : TemporaryViewInfo() { ) : TemporaryViewInfo() {
companion object { companion object {
@AttrRes const val DEFAULT_ICON_TINT_ATTR = android.R.attr.textColorPrimary @AttrRes const val DEFAULT_ICON_TINT_ATTR = android.R.attr.textColorPrimary

View File

@@ -29,7 +29,7 @@ class ChipbarLogger
@Inject @Inject
constructor( constructor(
@ChipbarLog buffer: LogBuffer, @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 * Logs that the chipbar was updated to display in a window named [windowTitle], with [text] and
* [endItemDesc]. * [endItemDesc].

View File

@@ -22,6 +22,7 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.log.LogBufferFactory import com.android.systemui.log.LogBufferFactory
import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.plugins.log.LogcatEchoTracker
import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
@@ -33,7 +34,7 @@ import org.mockito.Mockito.mock
class MediaTttLoggerTest : SysuiTestCase() { class MediaTttLoggerTest : SysuiTestCase() {
private lateinit var buffer: LogBuffer private lateinit var buffer: LogBuffer
private lateinit var logger: MediaTttLogger private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
@Before @Before
fun setUp () { fun setUp () {

View File

@@ -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
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Icon
import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Before import org.junit.Before
@@ -40,7 +41,7 @@ class MediaTttUtilsTest : SysuiTestCase() {
private lateinit var appIconFromPackageName: Drawable private lateinit var appIconFromPackageName: Drawable
@Mock private lateinit var packageManager: PackageManager @Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var applicationInfo: ApplicationInfo @Mock private lateinit var applicationInfo: ApplicationInfo
@Mock private lateinit var logger: MediaTttLogger @Mock private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
@Before @Before
fun setUp() { fun setUp() {

View File

@@ -27,13 +27,14 @@ import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor 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.view.ViewUtil
import com.android.systemui.util.wakelock.WakeLock import com.android.systemui.util.wakelock.WakeLock
class FakeMediaTttChipControllerReceiver( class FakeMediaTttChipControllerReceiver(
commandQueue: CommandQueue, commandQueue: CommandQueue,
context: Context, context: Context,
logger: MediaTttLogger, logger: MediaTttLogger<ChipReceiverInfo>,
windowManager: WindowManager, windowManager: WindowManager,
mainExecutor: DelayableExecutor, mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager, accessibilityManager: AccessibilityManager,
@@ -44,6 +45,7 @@ class FakeMediaTttChipControllerReceiver(
uiEventLogger: MediaTttReceiverUiEventLogger, uiEventLogger: MediaTttReceiverUiEventLogger,
viewUtil: ViewUtil, viewUtil: ViewUtil,
wakeLockBuilder: WakeLock.Builder, wakeLockBuilder: WakeLock.Builder,
systemClock: SystemClock,
) : ) :
MediaTttChipControllerReceiver( MediaTttChipControllerReceiver(
commandQueue, commandQueue,
@@ -59,6 +61,7 @@ class FakeMediaTttChipControllerReceiver(
uiEventLogger, uiEventLogger,
viewUtil, viewUtil,
wakeLockBuilder, wakeLockBuilder,
systemClock,
) { ) {
override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
// Just bypass the animation in tests // Just bypass the animation in tests

View File

@@ -67,7 +67,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
@Mock @Mock
private lateinit var applicationInfo: ApplicationInfo private lateinit var applicationInfo: ApplicationInfo
@Mock @Mock
private lateinit var logger: MediaTttLogger private lateinit var logger: MediaTttLogger<ChipReceiverInfo>
@Mock @Mock
private lateinit var accessibilityManager: AccessibilityManager private lateinit var accessibilityManager: AccessibilityManager
@Mock @Mock
@@ -128,6 +128,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
receiverUiEventLogger, receiverUiEventLogger,
viewUtil, viewUtil,
fakeWakeLockBuilder, fakeWakeLockBuilder,
fakeClock,
) )
controllerReceiver.start() controllerReceiver.start()
@@ -155,6 +156,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
receiverUiEventLogger, receiverUiEventLogger,
viewUtil, viewUtil,
fakeWakeLockBuilder, fakeWakeLockBuilder,
fakeClock,
) )
controllerReceiver.start() controllerReceiver.start()

View File

@@ -45,6 +45,7 @@ import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator 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.ChipbarLogger
import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
@@ -83,7 +84,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
@Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingManager: FalsingManager
@Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var falsingCollector: FalsingCollector
@Mock private lateinit var chipbarLogger: ChipbarLogger @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 mediaTttFlags: MediaTttFlags
@Mock private lateinit var packageManager: PackageManager @Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var powerManager: PowerManager @Mock private lateinit var powerManager: PowerManager
@@ -142,6 +143,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
viewUtil, viewUtil,
vibratorHelper, vibratorHelper,
fakeWakeLockBuilder, fakeWakeLockBuilder,
fakeClock,
) )
chipbarCoordinator.start() chipbarCoordinator.start()

View File

@@ -35,6 +35,7 @@ import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.capture
import com.android.systemui.util.time.FakeSystemClock 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.WakeLock
import com.android.systemui.util.wakelock.WakeLockFake import com.android.systemui.util.wakelock.WakeLockFake
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
@@ -59,7 +60,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
private lateinit var fakeWakeLock: WakeLockFake private lateinit var fakeWakeLock: WakeLockFake
@Mock @Mock
private lateinit var logger: TemporaryViewLogger private lateinit var logger: TemporaryViewLogger<ViewInfo>
@Mock @Mock
private lateinit var accessibilityManager: AccessibilityManager private lateinit var accessibilityManager: AccessibilityManager
@Mock @Mock
@@ -74,7 +75,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())) whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any()))
.thenReturn(TIMEOUT_MS.toInt()) .thenAnswer { it.arguments[0] }
fakeClock = FakeSystemClock() fakeClock = FakeSystemClock()
fakeExecutor = FakeExecutor(fakeClock) fakeExecutor = FakeExecutor(fakeClock)
@@ -84,14 +85,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeWakeLockBuilder.setWakeLock(fakeWakeLock) fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
underTest = TestController( underTest = TestController(
context, context,
logger, logger,
windowManager, windowManager,
fakeExecutor, fakeExecutor,
accessibilityManager, accessibilityManager,
configurationController, configurationController,
powerManager, powerManager,
fakeWakeLockBuilder, fakeWakeLockBuilder,
fakeClock,
) )
underTest.start() underTest.start()
} }
@@ -112,14 +114,14 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun displayView_logged() { fun displayView_logged() {
underTest.displayView( val info = ViewInfo(
ViewInfo( name = "name",
name = "name", windowTitle = "Fake Window Title",
windowTitle = "Fake Window Title",
)
) )
verify(logger).logViewAddition("id", "Fake Window Title") underTest.displayView(info)
verify(logger).logViewAddition(info)
} }
@Test @Test
@@ -168,10 +170,11 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
} }
@Test @Test
fun displayView_twiceWithDifferentWindowTitles_oldViewRemovedNewViewAdded() { fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() {
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "name", name = "name",
id = "First",
windowTitle = "First Fake Window Title", windowTitle = "First Fake Window Title",
) )
) )
@@ -179,6 +182,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "name", name = "name",
id = "Second",
windowTitle = "Second Fake Window Title", windowTitle = "Second Fake Window Title",
) )
) )
@@ -262,20 +266,70 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") 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<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 @Test
fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() { fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() {
underTest.displayView(ViewInfo("First name", id = "id1")) underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
reset(windowManager) reset(windowManager)
underTest.displayView(ViewInfo("Second name", id = "id2")) underTest.displayView(ViewInfo("Second name", id = "id2"))
verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any())
reset(windowManager)
underTest.removeView("id2", "test reason") underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
@@ -284,6 +338,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
} }
@Test @Test
@@ -291,19 +346,28 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.displayView(ViewInfo("First name", id = "id1")) underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
reset(windowManager) reset(windowManager)
underTest.displayView(ViewInfo("Second name", id = "id2")) 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") underTest.removeView("id1", "test reason")
// THEN we don't update anything
verify(windowManager, never()).removeView(any()) verify(windowManager, never()).removeView(any())
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2") assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
verify(configurationController, never()).removeCallback(any())
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
} }
@Test @Test
@@ -312,33 +376,31 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.displayView(ViewInfo("Second name", id = "id2")) underTest.displayView(ViewInfo("Second name", id = "id2"))
underTest.displayView(ViewInfo("Third name", id = "id3")) 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) reset(windowManager)
underTest.removeView("id3", "test reason") underTest.removeView("id3", "test reason")
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2") assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
verify(configurationController, never()).removeCallback(any())
reset(windowManager) reset(windowManager)
underTest.removeView("id2", "test reason") underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
verify(configurationController, never()).removeCallback(any())
reset(windowManager) reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
} }
@Test @Test
@@ -347,18 +409,21 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.displayView(ViewInfo("New name", id = "id1")) underTest.displayView(ViewInfo("New name", id = "id1"))
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
reset(windowManager) reset(windowManager)
underTest.displayView(ViewInfo("Second name", id = "id2")) underTest.displayView(ViewInfo("Second name", id = "id2"))
verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any())
reset(windowManager)
underTest.removeView("id2", "test reason") underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("New name") 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) reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
@@ -368,19 +433,523 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
} }
@Test @Test
fun multipleViewsWithDifferentIds_viewsTimeouts_noViewLeftToDisplay() { fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() {
underTest.displayView(ViewInfo("First name", id = "id1")) underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
fakeClock.advanceTime(TIMEOUT_MS / 3) fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Second name", id = "id2")) underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000))
fakeClock.advanceTime(TIMEOUT_MS / 3) fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Third name", id = "id3")) underTest.displayView(ViewInfo("Third name", id = "id3", timeoutMs = 20000))
reset(windowManager) reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(20000 + 1)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
verify(windowManager, never()).addView(any(), any()) verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews.size).isEqualTo(0) 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 @Test
@@ -395,6 +964,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
verify(logger).logViewRemoval(deviceId, reason) verify(logger).logViewRemoval(deviceId, reason)
verify(configurationController).removeCallback(any())
} }
@Test @Test
@@ -414,14 +984,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
inner class TestController( inner class TestController(
context: Context, context: Context,
logger: TemporaryViewLogger, logger: TemporaryViewLogger<ViewInfo>,
windowManager: WindowManager, windowManager: WindowManager,
@Main mainExecutor: DelayableExecutor, @Main mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager, accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController, configurationController: ConfigurationController,
powerManager: PowerManager, powerManager: PowerManager,
wakeLockBuilder: WakeLock.Builder, wakeLockBuilder: WakeLock.Builder,
) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger>( systemClock: SystemClock,
) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger<ViewInfo>>(
context, context,
logger, logger,
windowManager, windowManager,
@@ -431,6 +1002,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
powerManager, powerManager,
R.layout.chipbar, R.layout.chipbar,
wakeLockBuilder, wakeLockBuilder,
systemClock,
) { ) {
var mostRecentViewInfo: ViewInfo? = null var mostRecentViewInfo: ViewInfo? = null
@@ -447,12 +1019,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
override fun start() {} override fun start() {}
} }
inner class ViewInfo( data class ViewInfo(
val name: String, val name: String,
override val windowTitle: String = "Window Title", override val windowTitle: String = "Window Title",
override val wakeReason: String = "WAKE_REASON", 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 id: String = "id",
override val priority: ViewPriority = ViewPriority.NORMAL,
) : TemporaryViewInfo() ) : TemporaryViewInfo()
} }

View File

@@ -32,7 +32,7 @@ import org.mockito.Mockito
@SmallTest @SmallTest
class TemporaryViewLoggerTest : SysuiTestCase() { class TemporaryViewLoggerTest : SysuiTestCase() {
private lateinit var buffer: LogBuffer private lateinit var buffer: LogBuffer
private lateinit var logger: TemporaryViewLogger private lateinit var logger: TemporaryViewLogger<TemporaryViewInfo>
@Before @Before
fun setUp() { fun setUp() {
@@ -44,13 +44,22 @@ class TemporaryViewLoggerTest : SysuiTestCase() {
@Test @Test
fun logViewAddition_bufferHasLog() { 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() val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0) buffer.dump(PrintWriter(stringWriter), tailLength = 0)
val actualString = stringWriter.toString() val actualString = stringWriter.toString()
assertThat(actualString).contains(TAG) assertThat(actualString).contains(TAG)
assertThat(actualString).contains("test id")
assertThat(actualString).contains("Test Window Title") assertThat(actualString).contains("Test Window Title")
} }

View File

@@ -39,6 +39,7 @@ import com.android.systemui.common.shared.model.TintedIcon
import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController 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.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
@@ -105,6 +106,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
viewUtil, viewUtil,
vibratorHelper, vibratorHelper,
fakeWakeLockBuilder, fakeWakeLockBuilder,
fakeClock,
) )
underTest.start() underTest.start()
} }
@@ -408,6 +410,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
wakeReason = WAKE_REASON, wakeReason = WAKE_REASON,
timeoutMs = TIMEOUT, timeoutMs = TIMEOUT,
id = DEVICE_ID, id = DEVICE_ID,
priority = ViewPriority.NORMAL,
) )
} }

View File

@@ -26,6 +26,7 @@ import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor 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.view.ViewUtil
import com.android.systemui.util.wakelock.WakeLock import com.android.systemui.util.wakelock.WakeLock
@@ -43,6 +44,7 @@ class FakeChipbarCoordinator(
viewUtil: ViewUtil, viewUtil: ViewUtil,
vibratorHelper: VibratorHelper, vibratorHelper: VibratorHelper,
wakeLockBuilder: WakeLock.Builder, wakeLockBuilder: WakeLock.Builder,
systemClock: SystemClock,
) : ) :
ChipbarCoordinator( ChipbarCoordinator(
context, context,
@@ -57,6 +59,7 @@ class FakeChipbarCoordinator(
viewUtil, viewUtil,
vibratorHelper, vibratorHelper,
wakeLockBuilder, wakeLockBuilder,
systemClock,
) { ) {
override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
// Just bypass the animation in tests // Just bypass the animation in tests