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:
committed by
Android (Google) Code Review
commit
dd38a02c93
@@ -321,8 +321,8 @@
|
||||
-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/gesture/GenericGestureDetector.kt
|
||||
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
|
||||
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
|
||||
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.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/lockscreen/LockscreenSmartspaceController.kt
|
||||
-packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
|
||||
|
||||
@@ -345,6 +345,10 @@ object Flags {
|
||||
// TODO(b/263512203): Tracking Bug
|
||||
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
|
||||
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
|
||||
|
||||
|
||||
@@ -191,15 +191,12 @@ public class LogModule {
|
||||
false /* systrace */);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 a logging buffer for logs related to swipe up gestures. */
|
||||
@Provides
|
||||
@SysUISingleton
|
||||
@SwipeStatusBarAwayLog
|
||||
public static LogBuffer provideSwipeAwayGestureLogBuffer(LogBufferFactory factory) {
|
||||
return factory.create("SwipeStatusBarAwayLog", 30);
|
||||
@SwipeUpLog
|
||||
public static LogBuffer provideSwipeUpLogBuffer(LogBufferFactory factory) {
|
||||
return factory.create("SwipeUpLog", 30);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,10 +27,10 @@ import javax.inject.Qualifier;
|
||||
|
||||
/**
|
||||
* A {@link LogBuffer} for
|
||||
* {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
|
||||
* {@link com.android.systemui.statusbar.gesture.SwipeUpGestureLogger}.
|
||||
*/
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface SwipeStatusBarAwayLog {
|
||||
public @interface SwipeUpLog {
|
||||
}
|
||||
@@ -30,4 +30,8 @@ class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags)
|
||||
/** Check whether the flag for the receiver success state is enabled. */
|
||||
fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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.TemporaryViewDisplayController
|
||||
import com.android.systemui.temporarydisplay.ViewPriority
|
||||
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
|
||||
import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
|
||||
@@ -54,6 +55,7 @@ constructor(
|
||||
|
||||
private var displayedState: ChipStateSender? = null
|
||||
// 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 val commandQueueCallbacks =
|
||||
@@ -102,10 +104,9 @@ constructor(
|
||||
}
|
||||
uiEventLogger.logSenderStateChange(chipState)
|
||||
|
||||
stateMap.put(routeInfo.id, chipState)
|
||||
if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
|
||||
// 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
|
||||
val currentDisplayedState = displayedState ?: return
|
||||
|
||||
@@ -126,7 +127,9 @@ constructor(
|
||||
displayedState = null
|
||||
chipbarCoordinator.removeView(routeInfo.id, removalReason)
|
||||
} else {
|
||||
stateMap[routeInfo.id] = chipState
|
||||
displayedState = chipState
|
||||
chipbarCoordinator.registerListener(displayListener)
|
||||
chipbarCoordinator.displayView(
|
||||
createChipbarInfo(
|
||||
chipState,
|
||||
@@ -135,7 +138,7 @@ constructor(
|
||||
context,
|
||||
logger,
|
||||
)
|
||||
) { stateMap.remove(routeInfo.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +185,7 @@ constructor(
|
||||
}
|
||||
},
|
||||
vibrationEffect = chipStateSender.transferStatus.vibrationEffect,
|
||||
allowSwipeToDismiss = true,
|
||||
windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER,
|
||||
wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
|
||||
timeoutMs = chipStateSender.timeout,
|
||||
@@ -225,4 +229,14 @@ constructor(
|
||||
onClickListener,
|
||||
)
|
||||
}
|
||||
|
||||
private val displayListener =
|
||||
TemporaryViewDisplayController.Listener { id -> removeIdFromStore(id) }
|
||||
|
||||
private fun removeIdFromStore(id: String) {
|
||||
stateMap.remove(id)
|
||||
if (stateMap.isEmpty()) {
|
||||
chipbarCoordinator.unregisterListener(displayListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,90 +17,25 @@
|
||||
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
|
||||
import com.android.systemui.statusbar.window.StatusBarWindowController
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 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].
|
||||
*/
|
||||
/** A class to detect when a user swipes away the status bar. */
|
||||
@SysUISingleton
|
||||
open class SwipeStatusBarAwayGestureHandler @Inject constructor(
|
||||
class SwipeStatusBarAwayGestureHandler
|
||||
@Inject
|
||||
constructor(
|
||||
context: Context,
|
||||
logger: SwipeUpGestureLogger,
|
||||
private val statusBarWindowController: StatusBarWindowController,
|
||||
private val logger: SwipeStatusBarAwayGestureLogger
|
||||
) : GenericGestureDetector(SwipeStatusBarAwayGestureHandler::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 (
|
||||
// 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()
|
||||
) : SwipeUpGestureHandler(context, logger, loggerTag = LOGGER_TAG) {
|
||||
override fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean {
|
||||
// Gesture starts just below the status bar
|
||||
return ev.y >= statusBarWindowController.statusBarHeight &&
|
||||
ev.y <= 3 * statusBarWindowController.statusBarHeight
|
||||
}
|
||||
}
|
||||
|
||||
private const val SWIPE_TIMEOUT_MS: Long = 500
|
||||
private const val LOGGER_TAG = "SwipeStatusBarAway"
|
||||
|
||||
@@ -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
|
||||
@@ -16,49 +16,49 @@
|
||||
|
||||
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.LogLevel
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Log messages for [SwipeStatusBarAwayGestureHandler]. */
|
||||
class SwipeStatusBarAwayGestureLogger @Inject constructor(
|
||||
@SwipeStatusBarAwayLog private val buffer: LogBuffer
|
||||
/** Log messages for [SwipeUpGestureHandler]. */
|
||||
@SysUISingleton
|
||||
class SwipeUpGestureLogger @Inject constructor(
|
||||
@SwipeUpLog private val buffer: LogBuffer,
|
||||
) {
|
||||
fun logGestureDetectionStarted(y: Int) {
|
||||
fun logGestureDetectionStarted(tag: String, y: Int) {
|
||||
buffer.log(
|
||||
TAG,
|
||||
tag,
|
||||
LogLevel.DEBUG,
|
||||
{ int1 = y },
|
||||
{ "Beginning gesture detection. y=$int1" }
|
||||
)
|
||||
}
|
||||
|
||||
fun logGestureDetectionEndedWithoutTriggering(y: Int) {
|
||||
fun logGestureDetectionEndedWithoutTriggering(tag: String, y: Int) {
|
||||
buffer.log(
|
||||
TAG,
|
||||
tag,
|
||||
LogLevel.DEBUG,
|
||||
{ int1 = y },
|
||||
{ "Gesture finished; no swipe up gesture detected. Final y=$int1" }
|
||||
)
|
||||
}
|
||||
|
||||
fun logGestureDetected(y: Int) {
|
||||
fun logGestureDetected(tag: String, y: Int) {
|
||||
buffer.log(
|
||||
TAG,
|
||||
tag,
|
||||
LogLevel.INFO,
|
||||
{ int1 = y },
|
||||
{ "Gesture detected; notifying callbacks. y=$int1" }
|
||||
)
|
||||
}
|
||||
|
||||
fun logInputListeningStarted() {
|
||||
buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening started "})
|
||||
fun logInputListeningStarted(tag: String) {
|
||||
buffer.log(tag, LogLevel.VERBOSE, {}, { "Input listening started "})
|
||||
}
|
||||
|
||||
fun logInputListeningStopped() {
|
||||
buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening stopped "})
|
||||
fun logInputListeningStopped(tag: String) {
|
||||
buffer.log(tag, LogLevel.VERBOSE, {}, { "Input listening stopped "})
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "SwipeStatusBarAwayGestureHandler"
|
||||
@@ -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_TEXT
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.systemui.CoreStartable
|
||||
import com.android.systemui.Dumpable
|
||||
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
|
||||
* valid.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal val activeViews: MutableList<DisplayInfo> = mutableListOf()
|
||||
|
||||
private fun getCurrentDisplayInfo(): DisplayInfo? {
|
||||
internal fun getCurrentDisplayInfo(): DisplayInfo? {
|
||||
return activeViews.getOrNull(0)
|
||||
}
|
||||
|
||||
@@ -119,15 +121,26 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
|
||||
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].
|
||||
*
|
||||
* This method handles inflating and attaching the view, then delegates to [updateView] to
|
||||
* 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) {
|
||||
fun displayView(newInfo: T) {
|
||||
val timeout = accessibilityManager.getRecommendedTimeoutMillis(
|
||||
newInfo.timeoutMs,
|
||||
// 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)
|
||||
currentDisplayInfo.info = newInfo
|
||||
currentDisplayInfo.timeExpirationMillis = timeExpirationMillis
|
||||
updateTimeout(currentDisplayInfo, timeout, onViewTimeout)
|
||||
updateTimeout(currentDisplayInfo, timeout)
|
||||
updateView(newInfo, view)
|
||||
return
|
||||
}
|
||||
|
||||
val newDisplayInfo = DisplayInfo(
|
||||
info = newInfo,
|
||||
onViewTimeout = onViewTimeout,
|
||||
timeExpirationMillis = timeExpirationMillis,
|
||||
// Null values will be updated to non-null if/when this view actually gets displayed
|
||||
view = null,
|
||||
@@ -196,7 +208,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
|
||||
private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) {
|
||||
logger.logViewAddition(newDisplayInfo.info)
|
||||
createAndAcquireWakeLock(newDisplayInfo)
|
||||
updateTimeout(newDisplayInfo, timeout, newDisplayInfo.onViewTimeout)
|
||||
updateTimeout(newDisplayInfo, timeout)
|
||||
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.
|
||||
*
|
||||
* @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?) {
|
||||
private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int) {
|
||||
val cancelViewTimeout = mainExecutor.executeDelayed(
|
||||
{
|
||||
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
|
||||
@@ -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
|
||||
// appropriately.
|
||||
activeViews.remove(displayInfo)
|
||||
listeners.forEach {
|
||||
it.onInfoPermanentlyRemoved(id)
|
||||
}
|
||||
|
||||
// No need to time the view out since it's already gone
|
||||
displayInfo.cancelViewTimeout?.run()
|
||||
@@ -380,6 +392,9 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
|
||||
invalidViews.forEach {
|
||||
activeViews.remove(it)
|
||||
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()
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
inner class DisplayInfo(
|
||||
/**
|
||||
@@ -460,11 +484,6 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
|
||||
*/
|
||||
var wakeLock: WakeLock?,
|
||||
|
||||
/**
|
||||
* See [displayView].
|
||||
*/
|
||||
var onViewTimeout: Runnable?,
|
||||
|
||||
/**
|
||||
* A runnable that, when run, will cancel this view's timeout.
|
||||
*
|
||||
|
||||
@@ -80,6 +80,7 @@ constructor(
|
||||
powerManager: PowerManager,
|
||||
private val falsingManager: FalsingManager,
|
||||
private val falsingCollector: FalsingCollector,
|
||||
private val swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler?,
|
||||
private val viewUtil: ViewUtil,
|
||||
private val vibratorHelper: VibratorHelper,
|
||||
wakeLockBuilder: WakeLock.Builder,
|
||||
@@ -105,6 +106,8 @@ constructor(
|
||||
commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) }
|
||||
|
||||
override fun updateView(newInfo: ChipbarInfo, currentView: ViewGroup) {
|
||||
updateGestureListening()
|
||||
|
||||
logger.logViewUpdate(
|
||||
newInfo.windowTitle,
|
||||
newInfo.text.loadText(context),
|
||||
@@ -228,6 +231,42 @@ constructor(
|
||||
includeMargins = true,
|
||||
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 {
|
||||
@@ -250,3 +289,5 @@ constructor(
|
||||
private const val ANIMATION_IN_DURATION = 500L
|
||||
private const val ANIMATION_OUT_DURATION = 250L
|
||||
@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"
|
||||
|
||||
@@ -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
|
||||
* locales; on the left in RTL locales).
|
||||
* @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(
|
||||
val startIcon: TintedIcon,
|
||||
val text: Text,
|
||||
val endItem: ChipbarEndItem?,
|
||||
val vibrationEffect: VibrationEffect? = null,
|
||||
val allowSwipeToDismiss: Boolean = false,
|
||||
override val windowTitle: String,
|
||||
override val wakeReason: String,
|
||||
override val timeoutMs: Int,
|
||||
|
||||
@@ -46,4 +46,16 @@ constructor(
|
||||
{ "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" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -16,22 +16,38 @@
|
||||
|
||||
package com.android.systemui.temporarydisplay.dagger
|
||||
|
||||
import android.content.Context
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.log.LogBufferFactory
|
||||
import com.android.systemui.media.taptotransfer.MediaTttFlags
|
||||
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.Provides
|
||||
|
||||
@Module
|
||||
interface TemporaryDisplayModule {
|
||||
@Module
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@SysUISingleton
|
||||
@ChipbarLog
|
||||
fun provideChipbarLogBuffer(factory: LogBufferFactory): LogBuffer {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,13 +45,18 @@ import com.android.systemui.plugins.FalsingManager
|
||||
import com.android.systemui.statusbar.CommandQueue
|
||||
import com.android.systemui.statusbar.VibratorHelper
|
||||
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.ChipbarInfo
|
||||
import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger
|
||||
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.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.mock
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import com.android.systemui.util.view.ViewUtil
|
||||
import com.android.systemui.util.wakelock.WakeLockFake
|
||||
@@ -61,6 +66,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.atLeast
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.verify
|
||||
@@ -93,6 +99,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
|
||||
@Mock private lateinit var viewUtil: ViewUtil
|
||||
@Mock private lateinit var windowManager: WindowManager
|
||||
@Mock private lateinit var vibratorHelper: VibratorHelper
|
||||
@Mock private lateinit var swipeHandler: SwipeChipbarAwayGestureHandler
|
||||
private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
|
||||
private lateinit var fakeWakeLock: WakeLockFake
|
||||
private lateinit var chipbarCoordinator: ChipbarCoordinator
|
||||
@@ -143,6 +150,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
|
||||
powerManager,
|
||||
falsingManager,
|
||||
falsingCollector,
|
||||
swipeHandler,
|
||||
viewUtil,
|
||||
vibratorHelper,
|
||||
fakeWakeLockBuilder,
|
||||
@@ -161,9 +169,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
|
||||
)
|
||||
underTest.start()
|
||||
|
||||
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
|
||||
verify(commandQueue).addCallback(callbackCaptor.capture())
|
||||
commandQueueCallback = callbackCaptor.value!!
|
||||
setCommandQueueCallback()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -920,6 +926,172 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
|
||||
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 {
|
||||
val viewCaptor = ArgumentCaptor.forClass(View::class.java)
|
||||
verify(windowManager).addView(viewCaptor.capture(), any())
|
||||
@@ -960,8 +1132,16 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() {
|
||||
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 OTHER_DEVICE_NAME = "My Tablet"
|
||||
private const val BLANK_DEVICE_NAME = " "
|
||||
@@ -969,13 +1149,13 @@ private const val PACKAGE_NAME = "com.android.systemui"
|
||||
private const val TIMEOUT = 10000
|
||||
|
||||
private val routeInfo =
|
||||
MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
|
||||
MediaRoute2Info.Builder(DEFAULT_ID, OTHER_DEVICE_NAME)
|
||||
.addFeature("feature")
|
||||
.setClientPackageName(PACKAGE_NAME)
|
||||
.build()
|
||||
|
||||
private val routeInfoWithBlankDeviceName =
|
||||
MediaRoute2Info.Builder("id", BLANK_DEVICE_NAME)
|
||||
MediaRoute2Info.Builder(DEFAULT_ID, BLANK_DEVICE_NAME)
|
||||
.addFeature("feature")
|
||||
.setClientPackageName(PACKAGE_NAME)
|
||||
.build()
|
||||
|
||||
@@ -50,7 +50,9 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
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.Mockito.`when`
|
||||
import org.mockito.Mockito.eq
|
||||
@@ -83,7 +85,8 @@ class OngoingCallControllerTest : SysuiTestCase() {
|
||||
private lateinit var notifCollectionListener: NotifCollectionListener
|
||||
|
||||
@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 mockActivityStarter: ActivityStarter
|
||||
@Mock private lateinit var mockIActivityManager: IActivityManager
|
||||
|
||||
@@ -159,7 +159,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
underTest.displayView(getState())
|
||||
assertThat(fakeWakeLock.isHeld).isTrue()
|
||||
|
||||
underTest.removeView("id", "test reason")
|
||||
underTest.removeView(DEFAULT_ID, "test reason")
|
||||
|
||||
assertThat(fakeWakeLock.isHeld).isFalse()
|
||||
}
|
||||
@@ -175,6 +175,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(
|
||||
ViewInfo(
|
||||
name = "name",
|
||||
@@ -199,10 +201,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
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])
|
||||
// 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
|
||||
fun displayView_viewDoesNotDisappearsBeforeTimeout() {
|
||||
val listener = registerListener()
|
||||
|
||||
val state = getState()
|
||||
underTest.displayView(state)
|
||||
reset(windowManager)
|
||||
@@ -210,10 +217,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
fakeClock.advanceTime(TIMEOUT_MS - 1)
|
||||
|
||||
verify(windowManager, never()).removeView(any())
|
||||
assertThat(listener.permanentlyRemovedIds).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun displayView_viewDisappearsAfterTimeout() {
|
||||
val listener = registerListener()
|
||||
|
||||
val state = getState()
|
||||
underTest.displayView(state)
|
||||
reset(windowManager)
|
||||
@@ -221,10 +231,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
fakeClock.advanceTime(TIMEOUT_MS + 1)
|
||||
|
||||
verify(windowManager).removeView(any())
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun displayView_calledAgainBeforeTimeout_timeoutReset() {
|
||||
val listener = registerListener()
|
||||
|
||||
// First, display the view
|
||||
val state = getState()
|
||||
underTest.displayView(state)
|
||||
@@ -239,10 +252,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
|
||||
// Verify we didn't hide the view
|
||||
verify(windowManager, never()).removeView(any())
|
||||
assertThat(listener.permanentlyRemovedIds).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun displayView_calledAgainBeforeTimeout_eventuallyTimesOut() {
|
||||
val listener = registerListener()
|
||||
|
||||
// First, display the view
|
||||
val state = getState()
|
||||
underTest.displayView(state)
|
||||
@@ -255,6 +271,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
fakeClock.advanceTime(TIMEOUT_MS + 1)
|
||||
|
||||
verify(windowManager).removeView(any())
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -270,26 +287,10 @@ 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() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(
|
||||
ViewInfo(
|
||||
name = "name",
|
||||
@@ -315,10 +316,16 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
|
||||
verify(windowManager).removeView(viewCaptor.allValues[0])
|
||||
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
|
||||
fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() {
|
||||
fun multipleViewsWithDifferentIds_newViewRemoved_previousViewIsDisplayed() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(ViewInfo("First name", id = "id1"))
|
||||
|
||||
verify(windowManager).addView(any(), any())
|
||||
@@ -329,24 +336,35 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
verify(windowManager).removeView(any())
|
||||
verify(windowManager).addView(any(), any())
|
||||
reset(windowManager)
|
||||
assertThat(listener.permanentlyRemovedIds).isEmpty()
|
||||
|
||||
// WHEN the current view is removed
|
||||
underTest.removeView("id2", "test reason")
|
||||
|
||||
// THEN it's correctly removed
|
||||
verify(windowManager).removeView(any())
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly("id2")
|
||||
|
||||
// And the previous view is correctly added
|
||||
verify(windowManager).addView(any(), any())
|
||||
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
|
||||
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
|
||||
|
||||
// WHEN the previous view times out
|
||||
reset(windowManager)
|
||||
fakeClock.advanceTime(TIMEOUT_MS + 1)
|
||||
|
||||
// THEN it is also removed
|
||||
verify(windowManager).removeView(any())
|
||||
assertThat(underTest.activeViews.size).isEqualTo(0)
|
||||
verify(configurationController).removeCallback(any())
|
||||
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id2", "id1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(ViewInfo("First name", id = "id1"))
|
||||
|
||||
verify(windowManager).addView(any(), any())
|
||||
@@ -361,7 +379,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
// WHEN an old view is removed
|
||||
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())
|
||||
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
|
||||
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
|
||||
@@ -372,10 +391,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
verify(windowManager).removeView(any())
|
||||
assertThat(underTest.activeViews.size).isEqualTo(0)
|
||||
verify(configurationController).removeCallback(any())
|
||||
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id1", "id2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(ViewInfo("First name", id = "id1"))
|
||||
underTest.displayView(ViewInfo("Second name", id = "id2"))
|
||||
underTest.displayView(ViewInfo("Third name", id = "id3"))
|
||||
@@ -387,6 +409,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
underTest.removeView("id3", "test reason")
|
||||
|
||||
verify(windowManager).removeView(any())
|
||||
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3"))
|
||||
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
|
||||
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
|
||||
verify(configurationController, never()).removeCallback(any())
|
||||
@@ -395,6 +418,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
underTest.removeView("id2", "test reason")
|
||||
|
||||
verify(windowManager).removeView(any())
|
||||
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2"))
|
||||
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
|
||||
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
|
||||
verify(configurationController, never()).removeCallback(any())
|
||||
@@ -403,6 +427,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
fakeClock.advanceTime(TIMEOUT_MS + 1)
|
||||
|
||||
verify(windowManager).removeView(any())
|
||||
assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2", "id1"))
|
||||
assertThat(underTest.activeViews.size).isEqualTo(0)
|
||||
verify(configurationController).removeCallback(any())
|
||||
}
|
||||
@@ -438,6 +463,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
|
||||
fakeClock.advanceTime(1000)
|
||||
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000))
|
||||
@@ -451,10 +478,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
verify(windowManager, never()).addView(any(), any())
|
||||
assertThat(underTest.activeViews.size).isEqualTo(0)
|
||||
verify(configurationController).removeCallback(any())
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2", "id3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
|
||||
fakeClock.advanceTime(1000)
|
||||
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500))
|
||||
@@ -467,10 +497,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
verify(windowManager, never()).addView(any(), any())
|
||||
assertThat(underTest.activeViews.size).isEqualTo(0)
|
||||
verify(configurationController).removeCallback(any())
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun lowerThenHigherPriority_higherReplacesLower() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(
|
||||
ViewInfo(
|
||||
name = "normal",
|
||||
@@ -499,10 +532,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
|
||||
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
|
||||
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
|
||||
fun lowerThenHigherPriority_lowerPriorityRedisplayed() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(
|
||||
ViewInfo(
|
||||
name = "normal",
|
||||
@@ -537,6 +575,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
|
||||
// THEN the normal view is re-displayed
|
||||
verify(windowManager).removeView(viewCaptor.allValues[1])
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
|
||||
verify(windowManager).addView(any(), capture(windowParamsCaptor))
|
||||
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
|
||||
verify(configurationController, never()).removeCallback(any())
|
||||
@@ -544,6 +583,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(
|
||||
ViewInfo(
|
||||
name = "normal",
|
||||
@@ -573,6 +614,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
verify(windowManager, never()).addView(any(), any())
|
||||
assertThat(underTest.activeViews).isEmpty()
|
||||
verify(configurationController).removeCallback(any())
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -609,6 +651,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun higherThenLowerPriority_lowerEventuallyDisplayed() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(
|
||||
ViewInfo(
|
||||
name = "critical",
|
||||
@@ -644,6 +688,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
|
||||
// THEN the second normal view is displayed
|
||||
verify(windowManager).removeView(viewCaptor.value)
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
|
||||
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
|
||||
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
|
||||
assertThat(underTest.activeViews.size).isEqualTo(1)
|
||||
@@ -652,6 +697,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(
|
||||
ViewInfo(
|
||||
name = "critical",
|
||||
@@ -691,10 +738,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
verify(windowManager, never()).addView(any(), any())
|
||||
assertThat(underTest.activeViews).isEmpty()
|
||||
verify(configurationController).removeCallback(any())
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun criticalThenNewCritical_newCriticalDisplayed() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(
|
||||
ViewInfo(
|
||||
name = "critical 1",
|
||||
@@ -724,10 +774,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2")
|
||||
assertThat(underTest.activeViews.size).isEqualTo(2)
|
||||
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
|
||||
fun normalThenNewNormal_newNormalDisplayed() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.displayView(
|
||||
ViewInfo(
|
||||
name = "normal 1",
|
||||
@@ -757,6 +812,9 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2")
|
||||
assertThat(underTest.activeViews.size).isEqualTo(2)
|
||||
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
|
||||
@@ -957,25 +1015,103 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeView_viewRemovedAndRemovalLogged() {
|
||||
fun removeView_viewRemovedAndRemovalLoggedAndListenerNotified() {
|
||||
val listener = registerListener()
|
||||
|
||||
// First, add the view
|
||||
underTest.displayView(getState())
|
||||
|
||||
// Then, remove it
|
||||
val reason = "test reason"
|
||||
val deviceId = "id"
|
||||
underTest.removeView(deviceId, reason)
|
||||
underTest.removeView(DEFAULT_ID, reason)
|
||||
|
||||
verify(windowManager).removeView(any())
|
||||
verify(logger).logViewRemoval(deviceId, reason)
|
||||
verify(logger).logViewRemoval(DEFAULT_ID, reason)
|
||||
verify(configurationController).removeCallback(any())
|
||||
assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeView_noAdd_viewNotRemoved() {
|
||||
fun removeView_noAdd_viewNotRemovedAndListenerNotNotified() {
|
||||
val listener = registerListener()
|
||||
|
||||
underTest.removeView("id", "reason")
|
||||
|
||||
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)
|
||||
@@ -1030,9 +1166,17 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
|
||||
override val windowTitle: String = "Window Title",
|
||||
override val wakeReason: String = "WAKE_REASON",
|
||||
override val timeoutMs: Int = TIMEOUT_MS.toInt(),
|
||||
override val id: String = "id",
|
||||
override val id: String = DEFAULT_ID,
|
||||
override val priority: ViewPriority = ViewPriority.NORMAL,
|
||||
) : 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 DEFAULT_ID = "defaultId"
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.os.PowerManager
|
||||
import android.os.VibrationEffect
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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.util.concurrency.FakeExecutor
|
||||
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.time.FakeSystemClock
|
||||
import com.android.systemui.util.view.ViewUtil
|
||||
@@ -54,6 +57,7 @@ import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.MockitoAnnotations
|
||||
@@ -74,6 +78,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
|
||||
@Mock private lateinit var falsingCollector: FalsingCollector
|
||||
@Mock private lateinit var viewUtil: ViewUtil
|
||||
@Mock private lateinit var vibratorHelper: VibratorHelper
|
||||
@Mock private lateinit var swipeGestureHandler: SwipeChipbarAwayGestureHandler
|
||||
private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
|
||||
private lateinit var fakeWakeLock: WakeLockFake
|
||||
private lateinit var fakeClock: FakeSystemClock
|
||||
@@ -106,6 +111,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
|
||||
powerManager,
|
||||
falsingManager,
|
||||
falsingCollector,
|
||||
swipeGestureHandler,
|
||||
viewUtil,
|
||||
vibratorHelper,
|
||||
fakeWakeLockBuilder,
|
||||
@@ -430,17 +436,101 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
|
||||
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(
|
||||
startIcon: Icon,
|
||||
text: Text,
|
||||
endItem: ChipbarEndItem?,
|
||||
vibrationEffect: VibrationEffect? = null,
|
||||
allowSwipeToDismiss: Boolean = false,
|
||||
): ChipbarInfo {
|
||||
return ChipbarInfo(
|
||||
TintedIcon(startIcon, tintAttr = null),
|
||||
text,
|
||||
endItem,
|
||||
vibrationEffect,
|
||||
allowSwipeToDismiss,
|
||||
windowTitle = WINDOW_TITLE,
|
||||
wakeReason = WAKE_REASON,
|
||||
timeoutMs = TIMEOUT,
|
||||
|
||||
@@ -43,6 +43,7 @@ class FakeChipbarCoordinator(
|
||||
powerManager: PowerManager,
|
||||
falsingManager: FalsingManager,
|
||||
falsingCollector: FalsingCollector,
|
||||
swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler,
|
||||
viewUtil: ViewUtil,
|
||||
vibratorHelper: VibratorHelper,
|
||||
wakeLockBuilder: WakeLock.Builder,
|
||||
@@ -59,6 +60,7 @@ class FakeChipbarCoordinator(
|
||||
powerManager,
|
||||
falsingManager,
|
||||
falsingCollector,
|
||||
swipeChipbarAwayGestureHandler,
|
||||
viewUtil,
|
||||
vibratorHelper,
|
||||
wakeLockBuilder,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user