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.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);
}
/** */

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.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(

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.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

View File

@@ -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()

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.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,
)
}

View File

@@ -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
internal val activeViews: MutableList<DisplayInfo> = mutableListOf()
/** A string that keeps track of wakelock reason once it is acquired till it gets released */
private var wakeReasonAcquired: String? = null
/**
* 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
// Only cancel timeout of the most recent view displayed, as it will be reset.
if (position == 0) {
cancelViewTimeout?.run()
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
}
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()
},
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
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)
val newViewToDisplay = if (activeViews.isEmpty()) {
null
} else {
activeViews[0].second
val displayInfo = activeViews.firstOrNull { it.info.id == id }
if (displayInfo == null) {
logger.logViewRemovalIgnored(id, "View not found in list")
return
}
val currentView = currentDisplayInfo.view
animateViewOut(currentView) {
windowManager.removeView(currentView)
wakeLock?.release(wakeReasonAcquired)
}
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)
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()
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) {
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,
inner class DisplayInfo(
/**
* 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,
/**
* 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,

View File

@@ -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,
}

View File

@@ -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 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.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

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.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

View File

@@ -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].

View File

@@ -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 () {

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.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() {

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.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

View File

@@ -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()

View File

@@ -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()

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.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",
)
)
@@ -262,20 +266,70 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
}
@Test
fun viewUpdatedWithNewOnViewTimeoutRunnable_newRunnableUsed() {
var runnable1Run = false
underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
runnable1Run = true
}
var runnable2Run = false
underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
runnable2Run = true
}
fakeClock.advanceTime(TIMEOUT_MS + 1)
assertThat(runnable1Run).isFalse()
assertThat(runnable2Run).isTrue()
}
@Test
fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() {
underTest.displayView(
ViewInfo(
name = "name",
windowTitle = "First Fake Window Title",
id = "id1"
)
)
underTest.displayView(
ViewInfo(
name = "name",
windowTitle = "Second Fake Window Title",
id = "id2"
)
)
val viewCaptor = argumentCaptor<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"))
verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any())
reset(windowManager)
underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
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"))
verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any())
reset(windowManager)
underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
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()
}

View File

@@ -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")
}

View File

@@ -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,
)
}

View File

@@ -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