Merge changes from topic "caitlinshk-media-ttt-displayview" into tm-qpr-dev

* changes:
  [Media TTT] Allow swiping up to dismiss the chipbar.
  [Media TTT] Make the SwipeUpGestureHandler generic.
  [Media TTT] Re-name the status bar gesture handler to be more generic.
  [Media TTT] Use a listener pattern to notify about view removals.
This commit is contained in:
Caitlin Shkuratov
2023-01-19 22:21:59 +00:00
committed by Android (Google) Code Review
21 changed files with 902 additions and 157 deletions

View File

@@ -321,8 +321,8 @@
-packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
-packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt -packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt -packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt -packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt -packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt
-packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt -packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
-packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt -packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt

View File

@@ -345,6 +345,10 @@ object Flags {
// TODO(b/263512203): Tracking Bug // TODO(b/263512203): Tracking Bug
val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true) val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true)
// TODO(b/265813373): Tracking Bug
val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE =
unreleasedFlag(912, "media_ttt_dismiss_gesture", teamfood = true)
// 1000 - dock // 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")

View File

@@ -191,15 +191,12 @@ public class LogModule {
false /* systrace */); false /* systrace */);
} }
/** /** Provides a logging buffer for logs related to swipe up gestures. */
* Provides a logging buffer for logs related to swiping away the status bar while in immersive
* mode. See {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
*/
@Provides @Provides
@SysUISingleton @SysUISingleton
@SwipeStatusBarAwayLog @SwipeUpLog
public static LogBuffer provideSwipeAwayGestureLogBuffer(LogBufferFactory factory) { public static LogBuffer provideSwipeUpLogBuffer(LogBufferFactory factory) {
return factory.create("SwipeStatusBarAwayLog", 30); return factory.create("SwipeUpLog", 30);
} }
/** /**

View File

@@ -27,10 +27,10 @@ import javax.inject.Qualifier;
/** /**
* A {@link LogBuffer} for * A {@link LogBuffer} for
* {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}. * {@link com.android.systemui.statusbar.gesture.SwipeUpGestureLogger}.
*/ */
@Qualifier @Qualifier
@Documented @Documented
@Retention(RUNTIME) @Retention(RUNTIME)
public @interface SwipeStatusBarAwayLog { public @interface SwipeUpLog {
} }

View File

@@ -30,4 +30,8 @@ class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags)
/** Check whether the flag for the receiver success state is enabled. */ /** Check whether the flag for the receiver success state is enabled. */
fun isMediaTttReceiverSuccessRippleEnabled(): Boolean = fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE) featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
/** True if the media transfer chip can be dismissed via a gesture. */
fun isMediaTttDismissGestureEnabled(): Boolean =
featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE)
} }

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.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.ViewPriority 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
@@ -54,6 +55,7 @@ constructor(
private var displayedState: ChipStateSender? = null private var displayedState: ChipStateSender? = null
// A map to store current chip state per id. // A map to store current chip state per id.
// TODO(b/265455911): Log whenever we add or remove from the store.
private var stateMap: MutableMap<String, ChipStateSender> = mutableMapOf() private var stateMap: MutableMap<String, ChipStateSender> = mutableMapOf()
private val commandQueueCallbacks = private val commandQueueCallbacks =
@@ -102,10 +104,9 @@ constructor(
} }
uiEventLogger.logSenderStateChange(chipState) uiEventLogger.logSenderStateChange(chipState)
stateMap.put(routeInfo.id, chipState)
if (chipState == ChipStateSender.FAR_FROM_RECEIVER) { if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
// No need to store the state since it is the default state // No need to store the state since it is the default state
stateMap.remove(routeInfo.id) removeIdFromStore(routeInfo.id)
// Return early if we're not displaying a chip anyway // Return early if we're not displaying a chip anyway
val currentDisplayedState = displayedState ?: return val currentDisplayedState = displayedState ?: return
@@ -126,7 +127,9 @@ constructor(
displayedState = null displayedState = null
chipbarCoordinator.removeView(routeInfo.id, removalReason) chipbarCoordinator.removeView(routeInfo.id, removalReason)
} else { } else {
stateMap[routeInfo.id] = chipState
displayedState = chipState displayedState = chipState
chipbarCoordinator.registerListener(displayListener)
chipbarCoordinator.displayView( chipbarCoordinator.displayView(
createChipbarInfo( createChipbarInfo(
chipState, chipState,
@@ -135,7 +138,7 @@ constructor(
context, context,
logger, logger,
) )
) { stateMap.remove(routeInfo.id) } )
} }
} }
@@ -182,6 +185,7 @@ constructor(
} }
}, },
vibrationEffect = chipStateSender.transferStatus.vibrationEffect, vibrationEffect = chipStateSender.transferStatus.vibrationEffect,
allowSwipeToDismiss = true,
windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER, windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER,
wakeReason = MediaTttUtils.WAKE_REASON_SENDER, wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
timeoutMs = chipStateSender.timeout, timeoutMs = chipStateSender.timeout,
@@ -225,4 +229,14 @@ constructor(
onClickListener, onClickListener,
) )
} }
private val displayListener =
TemporaryViewDisplayController.Listener { id -> removeIdFromStore(id) }
private fun removeIdFromStore(id: String) {
stateMap.remove(id)
if (stateMap.isEmpty()) {
chipbarCoordinator.unregisterListener(displayListener)
}
}
} }

View File

@@ -17,90 +17,25 @@
package com.android.systemui.statusbar.gesture package com.android.systemui.statusbar.gesture
import android.content.Context import android.content.Context
import android.view.InputEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.MotionEvent.ACTION_CANCEL
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
import android.view.MotionEvent.ACTION_UP
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.statusbar.window.StatusBarWindowController
import javax.inject.Inject import javax.inject.Inject
/** /** A class to detect when a user swipes away the status bar. */
* A class to detect when a user swipes away the status bar. To be notified when the swipe away
* gesture is detected, add a callback via [addOnGestureDetectedCallback].
*/
@SysUISingleton @SysUISingleton
open class SwipeStatusBarAwayGestureHandler @Inject constructor( class SwipeStatusBarAwayGestureHandler
@Inject
constructor(
context: Context, context: Context,
logger: SwipeUpGestureLogger,
private val statusBarWindowController: StatusBarWindowController, private val statusBarWindowController: StatusBarWindowController,
private val logger: SwipeStatusBarAwayGestureLogger ) : SwipeUpGestureHandler(context, logger, loggerTag = LOGGER_TAG) {
) : GenericGestureDetector(SwipeStatusBarAwayGestureHandler::class.simpleName!!) { override fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean {
// Gesture starts just below the status bar
private var startY: Float = 0f return ev.y >= statusBarWindowController.statusBarHeight &&
private var startTime: Long = 0L ev.y <= 3 * statusBarWindowController.statusBarHeight
private var monitoringCurrentTouch: Boolean = false
private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
com.android.internal.R.dimen.system_gestures_start_threshold
)
override fun onInputEvent(ev: InputEvent) {
if (ev !is MotionEvent) {
return
}
when (ev.actionMasked) {
ACTION_DOWN -> {
if (
// Gesture starts just below the status bar
ev.y >= statusBarWindowController.statusBarHeight
&& ev.y <= 3 * statusBarWindowController.statusBarHeight
) {
logger.logGestureDetectionStarted(ev.y.toInt())
startY = ev.y
startTime = ev.eventTime
monitoringCurrentTouch = true
} else {
monitoringCurrentTouch = false
}
}
ACTION_MOVE -> {
if (!monitoringCurrentTouch) {
return
}
if (
// Gesture is up
ev.y < startY
// Gesture went far enough
&& (startY - ev.y) >= swipeDistanceThreshold
// Gesture completed quickly enough
&& (ev.eventTime - startTime) < SWIPE_TIMEOUT_MS
) {
monitoringCurrentTouch = false
logger.logGestureDetected(ev.y.toInt())
onGestureDetected(ev)
}
}
ACTION_CANCEL, ACTION_UP -> {
if (monitoringCurrentTouch) {
logger.logGestureDetectionEndedWithoutTriggering(ev.y.toInt())
}
monitoringCurrentTouch = false
}
}
}
override fun startGestureListening() {
super.startGestureListening()
logger.logInputListeningStarted()
}
override fun stopGestureListening() {
super.stopGestureListening()
logger.logInputListeningStopped()
} }
} }
private const val SWIPE_TIMEOUT_MS: Long = 500 private const val LOGGER_TAG = "SwipeStatusBarAway"

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.gesture
import android.content.Context
import android.view.InputEvent
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_CANCEL
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
import android.view.MotionEvent.ACTION_UP
import com.android.systemui.dagger.SysUISingleton
/**
* A class to detect a generic "swipe up" gesture. To be notified when the swipe up gesture is
* detected, add a callback via [addOnGestureDetectedCallback].
*/
@SysUISingleton
abstract class SwipeUpGestureHandler(
context: Context,
private val logger: SwipeUpGestureLogger,
private val loggerTag: String,
) : GenericGestureDetector(SwipeUpGestureHandler::class.simpleName!!) {
private var startY: Float = 0f
private var startTime: Long = 0L
private var monitoringCurrentTouch: Boolean = false
private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
com.android.internal.R.dimen.system_gestures_start_threshold
)
override fun onInputEvent(ev: InputEvent) {
if (ev !is MotionEvent) {
return
}
when (ev.actionMasked) {
ACTION_DOWN -> {
if (
startOfGestureIsWithinBounds(ev)
) {
logger.logGestureDetectionStarted(loggerTag, ev.y.toInt())
startY = ev.y
startTime = ev.eventTime
monitoringCurrentTouch = true
} else {
monitoringCurrentTouch = false
}
}
ACTION_MOVE -> {
if (!monitoringCurrentTouch) {
return
}
if (
// Gesture is up
ev.y < startY &&
// Gesture went far enough
(startY - ev.y) >= swipeDistanceThreshold &&
// Gesture completed quickly enough
(ev.eventTime - startTime) < SWIPE_TIMEOUT_MS
) {
monitoringCurrentTouch = false
logger.logGestureDetected(loggerTag, ev.y.toInt())
onGestureDetected(ev)
}
}
ACTION_CANCEL, ACTION_UP -> {
if (monitoringCurrentTouch) {
logger.logGestureDetectionEndedWithoutTriggering(loggerTag, ev.y.toInt())
}
monitoringCurrentTouch = false
}
}
}
/**
* Returns true if the [ACTION_DOWN] event falls within bounds for this specific swipe-up
* gesture.
*
* Implementations must override this method to specify what part(s) of the screen are valid
* locations for the swipe up gesture to start at.
*/
abstract fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean
override fun startGestureListening() {
super.startGestureListening()
logger.logInputListeningStarted(loggerTag)
}
override fun stopGestureListening() {
super.stopGestureListening()
logger.logInputListeningStopped(loggerTag)
}
}
private const val SWIPE_TIMEOUT_MS: Long = 500

View File

@@ -16,49 +16,49 @@
package com.android.systemui.statusbar.gesture package com.android.systemui.statusbar.gesture
import com.android.systemui.log.dagger.SwipeStatusBarAwayLog import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.dagger.SwipeUpLog
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 javax.inject.Inject import javax.inject.Inject
/** Log messages for [SwipeStatusBarAwayGestureHandler]. */ /** Log messages for [SwipeUpGestureHandler]. */
class SwipeStatusBarAwayGestureLogger @Inject constructor( @SysUISingleton
@SwipeStatusBarAwayLog private val buffer: LogBuffer class SwipeUpGestureLogger @Inject constructor(
@SwipeUpLog private val buffer: LogBuffer,
) { ) {
fun logGestureDetectionStarted(y: Int) { fun logGestureDetectionStarted(tag: String, y: Int) {
buffer.log( buffer.log(
TAG, tag,
LogLevel.DEBUG, LogLevel.DEBUG,
{ int1 = y }, { int1 = y },
{ "Beginning gesture detection. y=$int1" } { "Beginning gesture detection. y=$int1" }
) )
} }
fun logGestureDetectionEndedWithoutTriggering(y: Int) { fun logGestureDetectionEndedWithoutTriggering(tag: String, y: Int) {
buffer.log( buffer.log(
TAG, tag,
LogLevel.DEBUG, LogLevel.DEBUG,
{ int1 = y }, { int1 = y },
{ "Gesture finished; no swipe up gesture detected. Final y=$int1" } { "Gesture finished; no swipe up gesture detected. Final y=$int1" }
) )
} }
fun logGestureDetected(y: Int) { fun logGestureDetected(tag: String, y: Int) {
buffer.log( buffer.log(
TAG, tag,
LogLevel.INFO, LogLevel.INFO,
{ int1 = y }, { int1 = y },
{ "Gesture detected; notifying callbacks. y=$int1" } { "Gesture detected; notifying callbacks. y=$int1" }
) )
} }
fun logInputListeningStarted() { fun logInputListeningStarted(tag: String) {
buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening started "}) buffer.log(tag, LogLevel.VERBOSE, {}, { "Input listening started "})
} }
fun logInputListeningStopped() { fun logInputListeningStopped(tag: String) {
buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening stopped "}) buffer.log(tag, LogLevel.VERBOSE, {}, { "Input listening stopped "})
} }
} }
private const val TAG = "SwipeStatusBarAwayGestureHandler"

View File

@@ -31,6 +31,7 @@ import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS
import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS
import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import com.android.systemui.CoreStartable import com.android.systemui.CoreStartable
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
@@ -108,9 +109,10 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
* Whenever the current view disappears, the next-priority view will be displayed if it's still * Whenever the current view disappears, the next-priority view will be displayed if it's still
* valid. * valid.
*/ */
@VisibleForTesting
internal val activeViews: MutableList<DisplayInfo> = mutableListOf() internal val activeViews: MutableList<DisplayInfo> = mutableListOf()
private fun getCurrentDisplayInfo(): DisplayInfo? { internal fun getCurrentDisplayInfo(): DisplayInfo? {
return activeViews.getOrNull(0) return activeViews.getOrNull(0)
} }
@@ -119,15 +121,26 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
dumpManager.registerNormalDumpable(this) dumpManager.registerNormalDumpable(this)
} }
private val listeners: MutableSet<Listener> = mutableSetOf()
/** Registers a listener. */
fun registerListener(listener: Listener) {
listeners.add(listener)
}
/** Unregisters a listener. */
fun unregisterListener(listener: Listener) {
listeners.remove(listener)
}
/** /**
* Displays the view with the provided [newInfo]. * Displays the view with the provided [newInfo].
* *
* This method handles inflating and attaching the view, then delegates to [updateView] to * This method handles inflating and attaching the view, then delegates to [updateView] to
* display the correct information in the view. * display the correct information in the view.
* @param onViewTimeout a runnable that runs after the view timeout.
*/ */
@Synchronized @Synchronized
fun displayView(newInfo: T, onViewTimeout: Runnable? = null) { fun displayView(newInfo: T) {
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
@@ -146,14 +159,13 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
logger.logViewUpdate(newInfo) logger.logViewUpdate(newInfo)
currentDisplayInfo.info = newInfo currentDisplayInfo.info = newInfo
currentDisplayInfo.timeExpirationMillis = timeExpirationMillis currentDisplayInfo.timeExpirationMillis = timeExpirationMillis
updateTimeout(currentDisplayInfo, timeout, onViewTimeout) updateTimeout(currentDisplayInfo, timeout)
updateView(newInfo, view) updateView(newInfo, view)
return return
} }
val newDisplayInfo = DisplayInfo( val newDisplayInfo = DisplayInfo(
info = newInfo, info = newInfo,
onViewTimeout = onViewTimeout,
timeExpirationMillis = timeExpirationMillis, timeExpirationMillis = timeExpirationMillis,
// Null values will be updated to non-null if/when this view actually gets displayed // Null values will be updated to non-null if/when this view actually gets displayed
view = null, view = null,
@@ -196,7 +208,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) { private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) {
logger.logViewAddition(newDisplayInfo.info) logger.logViewAddition(newDisplayInfo.info)
createAndAcquireWakeLock(newDisplayInfo) createAndAcquireWakeLock(newDisplayInfo)
updateTimeout(newDisplayInfo, timeout, newDisplayInfo.onViewTimeout) updateTimeout(newDisplayInfo, timeout)
inflateAndUpdateView(newDisplayInfo) inflateAndUpdateView(newDisplayInfo)
} }
@@ -227,19 +239,16 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
/** /**
* Creates a runnable that will remove [displayInfo] in [timeout] ms from now. * 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. * @return a runnable that, when run, will *cancel* the view's timeout.
*/ */
private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int, onViewTimeout: Runnable?) { private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int) {
val cancelViewTimeout = mainExecutor.executeDelayed( val cancelViewTimeout = mainExecutor.executeDelayed(
{ {
removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT) removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT)
onViewTimeout?.run()
}, },
timeout.toLong() timeout.toLong()
) )
displayInfo.onViewTimeout = onViewTimeout
// Cancel old view timeout and re-set it. // Cancel old view timeout and re-set it.
displayInfo.cancelViewTimeout?.run() displayInfo.cancelViewTimeout?.run()
displayInfo.cancelViewTimeout = cancelViewTimeout displayInfo.cancelViewTimeout = cancelViewTimeout
@@ -317,6 +326,9 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
// event comes in while this view is animating out, we still display the new view // event comes in while this view is animating out, we still display the new view
// appropriately. // appropriately.
activeViews.remove(displayInfo) activeViews.remove(displayInfo)
listeners.forEach {
it.onInfoPermanentlyRemoved(id)
}
// No need to time the view out since it's already gone // No need to time the view out since it's already gone
displayInfo.cancelViewTimeout?.run() displayInfo.cancelViewTimeout?.run()
@@ -380,6 +392,9 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
invalidViews.forEach { invalidViews.forEach {
activeViews.remove(it) activeViews.remove(it)
logger.logViewExpiration(it.info) logger.logViewExpiration(it.info)
listeners.forEach { listener ->
listener.onInfoPermanentlyRemoved(it.info.id)
}
} }
} }
@@ -436,6 +451,15 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
onAnimationEnd.run() onAnimationEnd.run()
} }
/** A listener interface to be notified of various view events. */
fun interface Listener {
/**
* Called whenever a [DisplayInfo] with the given [id] has been removed and will never be
* displayed again (unless another call to [updateView] is made).
*/
fun onInfoPermanentlyRemoved(id: String)
}
/** A container for all the display-related state objects. */ /** A container for all the display-related state objects. */
inner class DisplayInfo( inner class DisplayInfo(
/** /**
@@ -460,11 +484,6 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
*/ */
var wakeLock: WakeLock?, var wakeLock: WakeLock?,
/**
* See [displayView].
*/
var onViewTimeout: Runnable?,
/** /**
* A runnable that, when run, will cancel this view's timeout. * A runnable that, when run, will cancel this view's timeout.
* *

View File

@@ -80,6 +80,7 @@ constructor(
powerManager: PowerManager, powerManager: PowerManager,
private val falsingManager: FalsingManager, private val falsingManager: FalsingManager,
private val falsingCollector: FalsingCollector, private val falsingCollector: FalsingCollector,
private val swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler?,
private val viewUtil: ViewUtil, private val viewUtil: ViewUtil,
private val vibratorHelper: VibratorHelper, private val vibratorHelper: VibratorHelper,
wakeLockBuilder: WakeLock.Builder, wakeLockBuilder: WakeLock.Builder,
@@ -105,6 +106,8 @@ constructor(
commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) } commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) }
override fun updateView(newInfo: ChipbarInfo, currentView: ViewGroup) { override fun updateView(newInfo: ChipbarInfo, currentView: ViewGroup) {
updateGestureListening()
logger.logViewUpdate( logger.logViewUpdate(
newInfo.windowTitle, newInfo.windowTitle,
newInfo.text.loadText(context), newInfo.text.loadText(context),
@@ -228,6 +231,42 @@ constructor(
includeMargins = true, includeMargins = true,
onAnimationEnd, onAnimationEnd,
) )
updateGestureListening()
}
private fun updateGestureListening() {
if (swipeChipbarAwayGestureHandler == null) {
return
}
val currentDisplayInfo = getCurrentDisplayInfo()
if (currentDisplayInfo != null && currentDisplayInfo.info.allowSwipeToDismiss) {
swipeChipbarAwayGestureHandler.setViewFetcher { currentDisplayInfo.view }
swipeChipbarAwayGestureHandler.addOnGestureDetectedCallback(TAG) {
onSwipeUpGestureDetected()
}
} else {
swipeChipbarAwayGestureHandler.resetViewFetcher()
swipeChipbarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
}
}
private fun onSwipeUpGestureDetected() {
val currentDisplayInfo = getCurrentDisplayInfo()
if (currentDisplayInfo == null) {
logger.logSwipeGestureError(id = null, errorMsg = "No info is being displayed")
return
}
if (!currentDisplayInfo.info.allowSwipeToDismiss) {
logger.logSwipeGestureError(
id = currentDisplayInfo.info.id,
errorMsg = "This view prohibits swipe-to-dismiss",
)
return
}
removeView(currentDisplayInfo.info.id, SWIPE_UP_GESTURE_REASON)
updateGestureListening()
} }
private fun ViewGroup.getInnerView(): ViewGroup { private fun ViewGroup.getInnerView(): ViewGroup {
@@ -250,3 +289,5 @@ constructor(
private const val ANIMATION_IN_DURATION = 500L private const val ANIMATION_IN_DURATION = 500L
private const val ANIMATION_OUT_DURATION = 250L private const val ANIMATION_OUT_DURATION = 250L
@IdRes private val INFO_TAG = R.id.tag_chipbar_info @IdRes private val INFO_TAG = R.id.tag_chipbar_info
private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED"
private const val TAG = "ChipbarCoordinator"

View File

@@ -33,12 +33,14 @@ import com.android.systemui.temporarydisplay.ViewPriority
* @property endItem an optional end item to display at the end of the chipbar (on the right in LTR * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR
* locales; on the left in RTL locales). * locales; on the left in RTL locales).
* @property vibrationEffect an optional vibration effect when the chipbar is displayed * @property vibrationEffect an optional vibration effect when the chipbar is displayed
* @property allowSwipeToDismiss true if users are allowed to swipe up to dismiss this chipbar.
*/ */
data class ChipbarInfo( data class ChipbarInfo(
val startIcon: TintedIcon, val startIcon: TintedIcon,
val text: Text, val text: Text,
val endItem: ChipbarEndItem?, val endItem: ChipbarEndItem?,
val vibrationEffect: VibrationEffect? = null, val vibrationEffect: VibrationEffect? = null,
val allowSwipeToDismiss: Boolean = false,
override val windowTitle: String, override val windowTitle: String,
override val wakeReason: String, override val wakeReason: String,
override val timeoutMs: Int, override val timeoutMs: Int,

View File

@@ -46,4 +46,16 @@ constructor(
{ "Chipbar updated. window=$str1 text=$str2 endItem=$str3" } { "Chipbar updated. window=$str1 text=$str2 endItem=$str3" }
) )
} }
fun logSwipeGestureError(id: String?, errorMsg: String) {
buffer.log(
tag,
LogLevel.WARNING,
{
str1 = id
str2 = errorMsg
},
{ "Chipbar swipe gesture detected for incorrect state. id=$str1 error=$str2" }
)
}
} }

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.temporarydisplay.chipbar
import android.content.Context
import android.view.MotionEvent
import android.view.View
import com.android.systemui.statusbar.gesture.SwipeUpGestureHandler
import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger
import com.android.systemui.util.boundsOnScreen
/**
* A class to detect when a user has swiped the chipbar away.
*
* Effectively [SysUISingleton]. But, this shouldn't be created if the gesture isn't enabled. See
* [TemporaryDisplayModule.provideSwipeChipbarAwayGestureHandler].
*/
class SwipeChipbarAwayGestureHandler(
context: Context,
logger: SwipeUpGestureLogger,
) : SwipeUpGestureHandler(context, logger, loggerTag = LOGGER_TAG) {
private var viewFetcher: () -> View? = { null }
override fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean {
val view = viewFetcher.invoke() ?: return false
// Since chipbar is in its own window, we need to use [boundsOnScreen] to get an accurate
// bottom. ([view.bottom] would be relative to its window, which would be too small.)
val viewBottom = view.boundsOnScreen.bottom
// Allow the gesture to start a bit below the chipbar
return ev.y <= 1.5 * viewBottom
}
/**
* Sets a fetcher that returns the current chipbar view. The fetcher will be invoked whenever a
* gesture starts to determine if the gesture is near the chipbar.
*/
fun setViewFetcher(fetcher: () -> View?) {
viewFetcher = fetcher
}
/** Removes the current view fetcher. */
fun resetViewFetcher() {
viewFetcher = { null }
}
}
private const val LOGGER_TAG = "SwipeChipbarAway"

View File

@@ -16,22 +16,38 @@
package com.android.systemui.temporarydisplay.dagger package com.android.systemui.temporarydisplay.dagger
import android.content.Context
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBufferFactory import com.android.systemui.log.LogBufferFactory
import com.android.systemui.media.taptotransfer.MediaTttFlags
import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger
import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@Module @Module
interface TemporaryDisplayModule { interface TemporaryDisplayModule {
@Module
companion object { companion object {
@JvmStatic
@Provides @Provides
@SysUISingleton @SysUISingleton
@ChipbarLog @ChipbarLog
fun provideChipbarLogBuffer(factory: LogBufferFactory): LogBuffer { fun provideChipbarLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create("ChipbarLog", 40) return factory.create("ChipbarLog", 40)
} }
@Provides
@SysUISingleton
fun provideSwipeChipbarAwayGestureHandler(
mediaTttFlags: MediaTttFlags,
context: Context,
logger: SwipeUpGestureLogger,
): SwipeChipbarAwayGestureHandler? {
return if (mediaTttFlags.isMediaTttDismissGestureEnabled()) {
SwipeChipbarAwayGestureHandler(context, logger)
} else {
null
}
}
} }
} }

View File

@@ -45,13 +45,18 @@ import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.CommandQueue 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.TemporaryViewDisplayController
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.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.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler
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.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.view.ViewUtil
import com.android.systemui.util.wakelock.WakeLockFake import com.android.systemui.util.wakelock.WakeLockFake
@@ -61,6 +66,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.atLeast
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.reset import org.mockito.Mockito.reset
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
@@ -93,6 +99,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
@Mock private lateinit var viewUtil: ViewUtil @Mock private lateinit var viewUtil: ViewUtil
@Mock private lateinit var windowManager: WindowManager @Mock private lateinit var windowManager: WindowManager
@Mock private lateinit var vibratorHelper: VibratorHelper @Mock private lateinit var vibratorHelper: VibratorHelper
@Mock private lateinit var swipeHandler: SwipeChipbarAwayGestureHandler
private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
private lateinit var fakeWakeLock: WakeLockFake private lateinit var fakeWakeLock: WakeLockFake
private lateinit var chipbarCoordinator: ChipbarCoordinator private lateinit var chipbarCoordinator: ChipbarCoordinator
@@ -143,6 +150,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
powerManager, powerManager,
falsingManager, falsingManager,
falsingCollector, falsingCollector,
swipeHandler,
viewUtil, viewUtil,
vibratorHelper, vibratorHelper,
fakeWakeLockBuilder, fakeWakeLockBuilder,
@@ -161,9 +169,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
) )
underTest.start() underTest.start()
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) setCommandQueueCallback()
verify(commandQueue).addCallback(callbackCaptor.capture())
commandQueueCallback = callbackCaptor.value!!
} }
@Test @Test
@@ -920,6 +926,172 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
} }
@Test
fun newState_viewListenerRegistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
// Re-set the command queue callback since we've created a new [MediaTttSenderCoordinator]
// with a new callback.
setCommandQueueCallback()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
routeInfo,
null,
)
verify(mockChipbarCoordinator).registerListener(any())
}
@Test
fun onInfoPermanentlyRemoved_viewListenerUnregistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
setCommandQueueCallback()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
routeInfo,
null,
)
val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
// WHEN the listener is notified that the view has been removed
listenerCaptor.value.onInfoPermanentlyRemoved(DEFAULT_ID)
// THEN the media coordinator unregisters the listener
verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value)
}
@Test
fun onInfoPermanentlyRemoved_wrongId_viewListenerNotUnregistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
setCommandQueueCallback()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
routeInfo,
null,
)
val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
// WHEN the listener is notified that a different view has been removed
listenerCaptor.value.onInfoPermanentlyRemoved("differentViewId")
// THEN the media coordinator doesn't unregister the listener
verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value)
}
@Test
fun farFromReceiverState_viewListenerUnregistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
setCommandQueueCallback()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
routeInfo,
null,
)
val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
// WHEN we go to the FAR_FROM_RECEIVER state
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
routeInfo,
null
)
// THEN the media coordinator unregisters the listener
verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value)
}
@Test
fun statesWithDifferentIds_onInfoPermanentlyRemovedForOneId_viewListenerNotUnregistered() {
val mockChipbarCoordinator = mock<ChipbarCoordinator>()
underTest =
MediaTttSenderCoordinator(
mockChipbarCoordinator,
commandQueue,
context,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
setCommandQueueCallback()
// WHEN there are two different media transfers with different IDs
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
MediaRoute2Info.Builder("route1", OTHER_DEVICE_NAME)
.addFeature("feature")
.setClientPackageName(PACKAGE_NAME)
.build(),
null,
)
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
MediaRoute2Info.Builder("route2", OTHER_DEVICE_NAME)
.addFeature("feature")
.setClientPackageName(PACKAGE_NAME)
.build(),
null,
)
val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
verify(mockChipbarCoordinator, atLeast(1)).registerListener(capture(listenerCaptor))
// THEN one of them is removed
listenerCaptor.value.onInfoPermanentlyRemoved("route1")
// THEN the media coordinator doesn't unregister the listener (since route2 is still active)
verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value)
}
private fun getChipbarView(): ViewGroup { private fun getChipbarView(): ViewGroup {
val viewCaptor = ArgumentCaptor.forClass(View::class.java) val viewCaptor = ArgumentCaptor.forClass(View::class.java)
verify(windowManager).addView(viewCaptor.capture(), any()) verify(windowManager).addView(viewCaptor.capture(), any())
@@ -960,8 +1132,16 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
null null
) )
} }
private fun setCommandQueueCallback() {
val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
verify(commandQueue).addCallback(capture(callbackCaptor))
commandQueueCallback = callbackCaptor.value
reset(commandQueue)
}
} }
private const val DEFAULT_ID = "defaultId"
private const val APP_NAME = "Fake app name" private const val APP_NAME = "Fake app name"
private const val OTHER_DEVICE_NAME = "My Tablet" private const val OTHER_DEVICE_NAME = "My Tablet"
private const val BLANK_DEVICE_NAME = " " private const val BLANK_DEVICE_NAME = " "
@@ -969,13 +1149,13 @@ private const val PACKAGE_NAME = "com.android.systemui"
private const val TIMEOUT = 10000 private const val TIMEOUT = 10000
private val routeInfo = private val routeInfo =
MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) MediaRoute2Info.Builder(DEFAULT_ID, OTHER_DEVICE_NAME)
.addFeature("feature") .addFeature("feature")
.setClientPackageName(PACKAGE_NAME) .setClientPackageName(PACKAGE_NAME)
.build() .build()
private val routeInfoWithBlankDeviceName = private val routeInfoWithBlankDeviceName =
MediaRoute2Info.Builder("id", BLANK_DEVICE_NAME) MediaRoute2Info.Builder(DEFAULT_ID, BLANK_DEVICE_NAME)
.addFeature("feature") .addFeature("feature")
.setClientPackageName(PACKAGE_NAME) .setClientPackageName(PACKAGE_NAME)
.build() .build()

View File

@@ -50,7 +50,9 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.* import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.nullable
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.Mockito.eq import org.mockito.Mockito.eq
@@ -83,7 +85,8 @@ class OngoingCallControllerTest : SysuiTestCase() {
private lateinit var notifCollectionListener: NotifCollectionListener private lateinit var notifCollectionListener: NotifCollectionListener
@Mock private lateinit var mockOngoingCallFlags: OngoingCallFlags @Mock private lateinit var mockOngoingCallFlags: OngoingCallFlags
@Mock private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler:
SwipeStatusBarAwayGestureHandler
@Mock private lateinit var mockOngoingCallListener: OngoingCallListener @Mock private lateinit var mockOngoingCallListener: OngoingCallListener
@Mock private lateinit var mockActivityStarter: ActivityStarter @Mock private lateinit var mockActivityStarter: ActivityStarter
@Mock private lateinit var mockIActivityManager: IActivityManager @Mock private lateinit var mockIActivityManager: IActivityManager

View File

@@ -159,7 +159,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.displayView(getState()) underTest.displayView(getState())
assertThat(fakeWakeLock.isHeld).isTrue() assertThat(fakeWakeLock.isHeld).isTrue()
underTest.removeView("id", "test reason") underTest.removeView(DEFAULT_ID, "test reason")
assertThat(fakeWakeLock.isHeld).isFalse() assertThat(fakeWakeLock.isHeld).isFalse()
} }
@@ -175,6 +175,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() { fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "name", name = "name",
@@ -199,10 +201,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title") assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title")
assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title") assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
verify(windowManager).removeView(viewCaptor.allValues[0]) verify(windowManager).removeView(viewCaptor.allValues[0])
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
} }
@Test @Test
fun displayView_viewDoesNotDisappearsBeforeTimeout() { fun displayView_viewDoesNotDisappearsBeforeTimeout() {
val listener = registerListener()
val state = getState() val state = getState()
underTest.displayView(state) underTest.displayView(state)
reset(windowManager) reset(windowManager)
@@ -210,10 +217,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS - 1) fakeClock.advanceTime(TIMEOUT_MS - 1)
verify(windowManager, never()).removeView(any()) verify(windowManager, never()).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEmpty()
} }
@Test @Test
fun displayView_viewDisappearsAfterTimeout() { fun displayView_viewDisappearsAfterTimeout() {
val listener = registerListener()
val state = getState() val state = getState()
underTest.displayView(state) underTest.displayView(state)
reset(windowManager) reset(windowManager)
@@ -221,10 +231,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
} }
@Test @Test
fun displayView_calledAgainBeforeTimeout_timeoutReset() { fun displayView_calledAgainBeforeTimeout_timeoutReset() {
val listener = registerListener()
// First, display the view // First, display the view
val state = getState() val state = getState()
underTest.displayView(state) underTest.displayView(state)
@@ -239,10 +252,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// Verify we didn't hide the view // Verify we didn't hide the view
verify(windowManager, never()).removeView(any()) verify(windowManager, never()).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEmpty()
} }
@Test @Test
fun displayView_calledAgainBeforeTimeout_eventuallyTimesOut() { fun displayView_calledAgainBeforeTimeout_eventuallyTimesOut() {
val listener = registerListener()
// First, display the view // First, display the view
val state = getState() val state = getState()
underTest.displayView(state) underTest.displayView(state)
@@ -255,6 +271,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
} }
@Test @Test
@@ -270,26 +287,10 @@ 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 @Test
fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() { fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "name", name = "name",
@@ -315,10 +316,16 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title") assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
verify(windowManager).removeView(viewCaptor.allValues[0]) verify(windowManager).removeView(viewCaptor.allValues[0])
verify(configurationController, never()).removeCallback(any()) verify(configurationController, never()).removeCallback(any())
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
} }
@Test @Test
fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() { fun multipleViewsWithDifferentIds_newViewRemoved_previousViewIsDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1")) underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
@@ -329,24 +336,35 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
reset(windowManager) reset(windowManager)
assertThat(listener.permanentlyRemovedIds).isEmpty()
// WHEN the current view is removed
underTest.removeView("id2", "test reason") underTest.removeView("id2", "test reason")
// THEN it's correctly removed
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("id2")
// And the previous view is correctly added
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
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")
// WHEN the previous view times out
reset(windowManager) reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
// THEN it is also removed
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id2", "id1"))
} }
@Test @Test
fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() { fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1")) underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any()) verify(windowManager).addView(any(), any())
@@ -361,7 +379,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// WHEN an old view is removed // WHEN an old view is removed
underTest.removeView("id1", "test reason") underTest.removeView("id1", "test reason")
// THEN we don't update anything // THEN we don't update anything except the listener
assertThat(listener.permanentlyRemovedIds).containsExactly("id1")
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")
@@ -372,10 +391,13 @@ 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()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id1", "id2"))
} }
@Test @Test
fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() { fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1")) underTest.displayView(ViewInfo("First name", id = "id1"))
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"))
@@ -387,6 +409,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.removeView("id3", "test reason") underTest.removeView("id3", "test reason")
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3"))
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()) verify(configurationController, never()).removeCallback(any())
@@ -395,6 +418,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.removeView("id2", "test reason") underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2"))
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()) verify(configurationController, never()).removeCallback(any())
@@ -403,6 +427,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
fakeClock.advanceTime(TIMEOUT_MS + 1) fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2", "id1"))
assertThat(underTest.activeViews.size).isEqualTo(0) assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
} }
@@ -438,6 +463,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() { fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000)) underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
fakeClock.advanceTime(1000) fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000)) underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000))
@@ -451,10 +478,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
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()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2", "id3")
} }
@Test @Test
fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() { fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() {
val listener = registerListener()
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000)) underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
fakeClock.advanceTime(1000) fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500)) underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500))
@@ -467,10 +497,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
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()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
} }
@Test @Test
fun lowerThenHigherPriority_higherReplacesLower() { fun lowerThenHigherPriority_higherReplacesLower() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "normal", name = "normal",
@@ -499,10 +532,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
verify(configurationController, never()).removeCallback(any()) verify(configurationController, never()).removeCallback(any())
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
} }
@Test @Test
fun lowerThenHigherPriority_lowerPriorityRedisplayed() { fun lowerThenHigherPriority_lowerPriorityRedisplayed() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "normal", name = "normal",
@@ -537,6 +575,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// THEN the normal view is re-displayed // THEN the normal view is re-displayed
verify(windowManager).removeView(viewCaptor.allValues[1]) verify(windowManager).removeView(viewCaptor.allValues[1])
assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
verify(windowManager).addView(any(), capture(windowParamsCaptor)) verify(windowManager).addView(any(), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
verify(configurationController, never()).removeCallback(any()) verify(configurationController, never()).removeCallback(any())
@@ -544,6 +583,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() { fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "normal", name = "normal",
@@ -573,6 +614,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any()) verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews).isEmpty() assertThat(underTest.activeViews).isEmpty()
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
} }
@Test @Test
@@ -609,6 +651,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun higherThenLowerPriority_lowerEventuallyDisplayed() { fun higherThenLowerPriority_lowerEventuallyDisplayed() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "critical", name = "critical",
@@ -644,6 +688,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// THEN the second normal view is displayed // THEN the second normal view is displayed
verify(windowManager).removeView(viewCaptor.value) verify(windowManager).removeView(viewCaptor.value)
assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
assertThat(underTest.activeViews.size).isEqualTo(1) assertThat(underTest.activeViews.size).isEqualTo(1)
@@ -652,6 +697,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
@Test @Test
fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() { fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "critical", name = "critical",
@@ -691,10 +738,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
verify(windowManager, never()).addView(any(), any()) verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews).isEmpty() assertThat(underTest.activeViews).isEmpty()
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
} }
@Test @Test
fun criticalThenNewCritical_newCriticalDisplayed() { fun criticalThenNewCritical_newCriticalDisplayed() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "critical 1", name = "critical 1",
@@ -724,10 +774,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2") assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2")
assertThat(underTest.activeViews.size).isEqualTo(2) assertThat(underTest.activeViews.size).isEqualTo(2)
verify(configurationController, never()).removeCallback(any()) verify(configurationController, never()).removeCallback(any())
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
} }
@Test @Test
fun normalThenNewNormal_newNormalDisplayed() { fun normalThenNewNormal_newNormalDisplayed() {
val listener = registerListener()
underTest.displayView( underTest.displayView(
ViewInfo( ViewInfo(
name = "normal 1", name = "normal 1",
@@ -757,6 +812,9 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2") assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2")
assertThat(underTest.activeViews.size).isEqualTo(2) assertThat(underTest.activeViews.size).isEqualTo(2)
verify(configurationController, never()).removeCallback(any()) verify(configurationController, never()).removeCallback(any())
// Since the controller is still storing the older view in case it'll get re-displayed
// later, the listener shouldn't be notified
assertThat(listener.permanentlyRemovedIds).isEmpty()
} }
@Test @Test
@@ -957,25 +1015,103 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
} }
@Test @Test
fun removeView_viewRemovedAndRemovalLogged() { fun removeView_viewRemovedAndRemovalLoggedAndListenerNotified() {
val listener = registerListener()
// First, add the view // First, add the view
underTest.displayView(getState()) underTest.displayView(getState())
// Then, remove it // Then, remove it
val reason = "test reason" val reason = "test reason"
val deviceId = "id" underTest.removeView(DEFAULT_ID, reason)
underTest.removeView(deviceId, reason)
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
verify(logger).logViewRemoval(deviceId, reason) verify(logger).logViewRemoval(DEFAULT_ID, reason)
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
} }
@Test @Test
fun removeView_noAdd_viewNotRemoved() { fun removeView_noAdd_viewNotRemovedAndListenerNotNotified() {
val listener = registerListener()
underTest.removeView("id", "reason") underTest.removeView("id", "reason")
verify(windowManager, never()).removeView(any()) verify(windowManager, never()).removeView(any())
assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun listenerRegistered_notifiedOnRemoval() {
val listener = registerListener()
underTest.displayView(getState())
underTest.removeView(DEFAULT_ID, "reason")
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
fun listenerRegistered_notifiedOnTimedOutEvenWhenNotDisplayed() {
val listener = registerListener()
underTest.displayView(
ViewInfo(
id = "id1",
name = "name1",
timeoutMs = 3000,
),
)
// Display a second view
underTest.displayView(
ViewInfo(
id = "id2",
name = "name2",
timeoutMs = 2500,
),
)
// WHEN the second view times out
fakeClock.advanceTime(2501)
// THEN the listener is notified of both IDs, since id2 timed out and id1 doesn't have
// enough time left to be redisplayed
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
}
@Test
fun multipleListeners_allNotified() {
val listener1 = registerListener()
val listener2 = registerListener()
val listener3 = registerListener()
underTest.displayView(getState())
underTest.removeView(DEFAULT_ID, "reason")
assertThat(listener1.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
assertThat(listener2.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
assertThat(listener3.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
fun sameListenerRegisteredMultipleTimes_onlyNotifiedOnce() {
val listener = registerListener()
underTest.registerListener(listener)
underTest.registerListener(listener)
underTest.displayView(getState())
underTest.removeView(DEFAULT_ID, "reason")
assertThat(listener.permanentlyRemovedIds).hasSize(1)
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
private fun registerListener(): Listener {
return Listener().also {
underTest.registerListener(it)
}
} }
private fun getState(name: String = "name") = ViewInfo(name) private fun getState(name: String = "name") = ViewInfo(name)
@@ -1030,9 +1166,17 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
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 = TIMEOUT_MS.toInt(), override val timeoutMs: Int = TIMEOUT_MS.toInt(),
override val id: String = "id", override val id: String = DEFAULT_ID,
override val priority: ViewPriority = ViewPriority.NORMAL, override val priority: ViewPriority = ViewPriority.NORMAL,
) : TemporaryViewInfo() ) : TemporaryViewInfo()
inner class Listener : TemporaryViewDisplayController.Listener {
val permanentlyRemovedIds = mutableListOf<String>()
override fun onInfoPermanentlyRemoved(id: String) {
permanentlyRemovedIds.add(id)
}
}
} }
private const val TIMEOUT_MS = 10000L private const val TIMEOUT_MS = 10000L
private const val DEFAULT_ID = "defaultId"

View File

@@ -20,6 +20,7 @@ import android.os.PowerManager
import android.os.VibrationEffect import android.os.VibrationEffect
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
@@ -43,6 +44,8 @@ import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.ViewPriority 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.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.view.ViewUtil
@@ -54,6 +57,7 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@@ -74,6 +78,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
@Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var falsingCollector: FalsingCollector
@Mock private lateinit var viewUtil: ViewUtil @Mock private lateinit var viewUtil: ViewUtil
@Mock private lateinit var vibratorHelper: VibratorHelper @Mock private lateinit var vibratorHelper: VibratorHelper
@Mock private lateinit var swipeGestureHandler: SwipeChipbarAwayGestureHandler
private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
private lateinit var fakeWakeLock: WakeLockFake private lateinit var fakeWakeLock: WakeLockFake
private lateinit var fakeClock: FakeSystemClock private lateinit var fakeClock: FakeSystemClock
@@ -106,6 +111,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
powerManager, powerManager,
falsingManager, falsingManager,
falsingCollector, falsingCollector,
swipeGestureHandler,
viewUtil, viewUtil,
vibratorHelper, vibratorHelper,
fakeWakeLockBuilder, fakeWakeLockBuilder,
@@ -430,17 +436,101 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("new title text"), any()) verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("new title text"), any())
} }
@Test
fun swipeToDismiss_false_neverListensForGesture() {
underTest.displayView(
createChipbarInfo(
Icon.Resource(R.drawable.ic_cake, contentDescription = null),
Text.Loaded("title text"),
endItem = ChipbarEndItem.Loading,
allowSwipeToDismiss = false,
)
)
verify(swipeGestureHandler, never()).addOnGestureDetectedCallback(any(), any())
}
@Test
fun swipeToDismiss_true_listensForGesture() {
underTest.displayView(
createChipbarInfo(
Icon.Resource(R.drawable.ic_cake, contentDescription = null),
Text.Loaded("title text"),
endItem = ChipbarEndItem.Loading,
allowSwipeToDismiss = true,
)
)
verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), any())
}
@Test
fun swipeToDismiss_swipeOccurs_viewDismissed() {
underTest.displayView(
createChipbarInfo(
Icon.Resource(R.drawable.ic_cake, contentDescription = null),
Text.Loaded("title text"),
endItem = ChipbarEndItem.Loading,
allowSwipeToDismiss = true,
)
)
val view = getChipbarView()
val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor))
callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0))
verify(windowManager).removeView(view)
}
@Test
fun swipeToDismiss_viewUpdatedToFalse_swipeOccurs_viewNotDismissed() {
underTest.displayView(
createChipbarInfo(
Icon.Resource(R.drawable.ic_cake, contentDescription = null),
Text.Loaded("title text"),
endItem = ChipbarEndItem.Loading,
allowSwipeToDismiss = true,
)
)
val view = getChipbarView()
val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor))
// WHEN the view is updated to not allow swipe-to-dismiss
underTest.displayView(
createChipbarInfo(
Icon.Resource(R.drawable.ic_cake, contentDescription = null),
Text.Loaded("title text"),
endItem = ChipbarEndItem.Loading,
allowSwipeToDismiss = false,
)
)
// THEN the callback is removed
verify(swipeGestureHandler).removeOnGestureDetectedCallback(any())
// And WHEN the old callback is invoked
callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0))
// THEN it is ignored and view isn't removed
verify(windowManager, never()).removeView(view)
}
private fun createChipbarInfo( private fun createChipbarInfo(
startIcon: Icon, startIcon: Icon,
text: Text, text: Text,
endItem: ChipbarEndItem?, endItem: ChipbarEndItem?,
vibrationEffect: VibrationEffect? = null, vibrationEffect: VibrationEffect? = null,
allowSwipeToDismiss: Boolean = false,
): ChipbarInfo { ): ChipbarInfo {
return ChipbarInfo( return ChipbarInfo(
TintedIcon(startIcon, tintAttr = null), TintedIcon(startIcon, tintAttr = null),
text, text,
endItem, endItem,
vibrationEffect, vibrationEffect,
allowSwipeToDismiss,
windowTitle = WINDOW_TITLE, windowTitle = WINDOW_TITLE,
wakeReason = WAKE_REASON, wakeReason = WAKE_REASON,
timeoutMs = TIMEOUT, timeoutMs = TIMEOUT,

View File

@@ -43,6 +43,7 @@ class FakeChipbarCoordinator(
powerManager: PowerManager, powerManager: PowerManager,
falsingManager: FalsingManager, falsingManager: FalsingManager,
falsingCollector: FalsingCollector, falsingCollector: FalsingCollector,
swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler,
viewUtil: ViewUtil, viewUtil: ViewUtil,
vibratorHelper: VibratorHelper, vibratorHelper: VibratorHelper,
wakeLockBuilder: WakeLock.Builder, wakeLockBuilder: WakeLock.Builder,
@@ -59,6 +60,7 @@ class FakeChipbarCoordinator(
powerManager, powerManager,
falsingManager, falsingManager,
falsingCollector, falsingCollector,
swipeChipbarAwayGestureHandler,
viewUtil, viewUtil,
vibratorHelper, vibratorHelper,
wakeLockBuilder, wakeLockBuilder,

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.temporarydisplay.chipbar
import android.graphics.Rect
import android.view.MotionEvent
import android.view.View
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@SmallTest
class SwipeChipbarAwayGestureHandlerTest : SysuiTestCase() {
private lateinit var underTest: SwipeChipbarAwayGestureHandler
@Before
fun setUp() {
underTest = SwipeChipbarAwayGestureHandler(context, mock())
}
@Test
fun startOfGestureIsWithinBounds_noViewFetcher_returnsFalse() {
assertThat(underTest.startOfGestureIsWithinBounds(createMotionEvent())).isFalse()
}
@Test
fun startOfGestureIsWithinBounds_usesViewFetcher_aboveBottom_returnsTrue() {
val view = createMockView()
underTest.setViewFetcher { view }
val motionEvent = createMotionEvent(y = VIEW_BOTTOM - 100f)
assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue()
}
@Test
fun startOfGestureIsWithinBounds_usesViewFetcher_slightlyBelowBottom_returnsTrue() {
val view = createMockView()
underTest.setViewFetcher { view }
val motionEvent = createMotionEvent(y = VIEW_BOTTOM + 20f)
assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue()
}
@Test
fun startOfGestureIsWithinBounds_usesViewFetcher_tooFarDown_returnsFalse() {
val view = createMockView()
underTest.setViewFetcher { view }
val motionEvent = createMotionEvent(y = VIEW_BOTTOM * 4f)
assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isFalse()
}
@Test
fun startOfGestureIsWithinBounds_viewFetcherReset_returnsFalse() {
val view = createMockView()
underTest.setViewFetcher { view }
val motionEvent = createMotionEvent(y = VIEW_BOTTOM - 100f)
assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue()
underTest.resetViewFetcher()
assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isFalse()
}
private fun createMotionEvent(y: Float = 0f): MotionEvent {
return MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, y, 0)
}
private fun createMockView(): View {
return mock<View>().also {
doAnswer { invocation ->
val out: Rect = invocation.getArgument(0)
out.set(0, 0, 0, VIEW_BOTTOM)
null
}
.whenever(it)
.getBoundsOnScreen(any())
}
}
private companion object {
const val VIEW_BOTTOM = 455
}
}