Merge "[multishade] Deletes unused multi-shade code." into udc-qpr-dev

This commit is contained in:
Ale Nijamkin
2023-07-21 18:01:06 +00:00
committed by Android (Google) Code Review
31 changed files with 76 additions and 4462 deletions

View File

@@ -1,849 +0,0 @@
/*
* 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.compose.swipeable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import com.android.compose.swipeable.SwipeableDefaults.AnimationSpec
import com.android.compose.swipeable.SwipeableDefaults.StandardResistanceFactor
import com.android.compose.swipeable.SwipeableDefaults.VelocityThreshold
import com.android.compose.swipeable.SwipeableDefaults.resistanceConfig
import com.android.compose.ui.util.lerp
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.sign
import kotlin.math.sin
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
/**
* State of the [swipeable] modifier.
*
* This contains necessary information about any ongoing swipe or animation and provides methods to
* change the state either immediately or by starting an animation. To create and remember a
* [SwipeableState] with the default animation clock, use [rememberSwipeableState].
*
* @param initialValue The initial value of the state.
* @param animationSpec The default animation that will be used to animate to a new state.
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
*
* TODO(b/272311106): this is a fork from material. Unfork it when Swipeable.kt reaches material3.
*/
@Stable
open class SwipeableState<T>(
initialValue: T,
internal val animationSpec: AnimationSpec<Float> = AnimationSpec,
internal val confirmStateChange: (newValue: T) -> Boolean = { true }
) {
/**
* The current value of the state.
*
* If no swipe or animation is in progress, this corresponds to the anchor at which the
* [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
* the last anchor at which the [swipeable] was settled before the swipe or animation started.
*/
var currentValue: T by mutableStateOf(initialValue)
private set
/** Whether the state is currently animating. */
var isAnimationRunning: Boolean by mutableStateOf(false)
private set
/**
* The current position (in pixels) of the [swipeable].
*
* You should use this state to offset your content accordingly. The recommended way is to use
* `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
*/
val offset: State<Float>
get() = offsetState
/** The amount by which the [swipeable] has been swiped past its bounds. */
val overflow: State<Float>
get() = overflowState
// Use `Float.NaN` as a placeholder while the state is uninitialised.
private val offsetState = mutableStateOf(0f)
private val overflowState = mutableStateOf(0f)
// the source of truth for the "real"(non ui) position
// basically position in bounds + overflow
private val absoluteOffset = mutableStateOf(0f)
// current animation target, if animating, otherwise null
private val animationTarget = mutableStateOf<Float?>(null)
internal var anchors by mutableStateOf(emptyMap<Float, T>())
private val latestNonEmptyAnchorsFlow: Flow<Map<Float, T>> =
snapshotFlow { anchors }.filter { it.isNotEmpty() }.take(1)
internal var minBound = Float.NEGATIVE_INFINITY
internal var maxBound = Float.POSITIVE_INFINITY
internal fun ensureInit(newAnchors: Map<Float, T>) {
if (anchors.isEmpty()) {
// need to do initial synchronization synchronously :(
val initialOffset = newAnchors.getOffset(currentValue)
requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
offsetState.value = initialOffset
absoluteOffset.value = initialOffset
}
}
internal suspend fun processNewAnchors(oldAnchors: Map<Float, T>, newAnchors: Map<Float, T>) {
if (oldAnchors.isEmpty()) {
// If this is the first time that we receive anchors, then we need to initialise
// the state so we snap to the offset associated to the initial value.
minBound = newAnchors.keys.minOrNull()!!
maxBound = newAnchors.keys.maxOrNull()!!
val initialOffset = newAnchors.getOffset(currentValue)
requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
snapInternalToOffset(initialOffset)
} else if (newAnchors != oldAnchors) {
// If we have received new anchors, then the offset of the current value might
// have changed, so we need to animate to the new offset. If the current value
// has been removed from the anchors then we animate to the closest anchor
// instead. Note that this stops any ongoing animation.
minBound = Float.NEGATIVE_INFINITY
maxBound = Float.POSITIVE_INFINITY
val animationTargetValue = animationTarget.value
// if we're in the animation already, let's find it a new home
val targetOffset =
if (animationTargetValue != null) {
// first, try to map old state to the new state
val oldState = oldAnchors[animationTargetValue]
val newState = newAnchors.getOffset(oldState)
// return new state if exists, or find the closes one among new anchors
newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!!
} else {
// we're not animating, proceed by finding the new anchors for an old value
val actualOldValue = oldAnchors[offset.value]
val value = if (actualOldValue == currentValue) currentValue else actualOldValue
newAnchors.getOffset(value)
?: newAnchors.keys.minByOrNull { abs(it - offset.value) }!!
}
try {
animateInternalToOffset(targetOffset, animationSpec)
} catch (c: CancellationException) {
// If the animation was interrupted for any reason, snap as a last resort.
snapInternalToOffset(targetOffset)
} finally {
currentValue = newAnchors.getValue(targetOffset)
minBound = newAnchors.keys.minOrNull()!!
maxBound = newAnchors.keys.maxOrNull()!!
}
}
}
internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
internal var velocityThreshold by mutableStateOf(0f)
internal var resistance: ResistanceConfig? by mutableStateOf(null)
internal val draggableState = DraggableState {
val newAbsolute = absoluteOffset.value + it
val clamped = newAbsolute.coerceIn(minBound, maxBound)
val overflow = newAbsolute - clamped
val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f
offsetState.value = clamped + resistanceDelta
overflowState.value = overflow
absoluteOffset.value = newAbsolute
}
private suspend fun snapInternalToOffset(target: Float) {
draggableState.drag { dragBy(target - absoluteOffset.value) }
}
private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec<Float>) {
draggableState.drag {
var prevValue = absoluteOffset.value
animationTarget.value = target
isAnimationRunning = true
try {
Animatable(prevValue).animateTo(target, spec) {
dragBy(this.value - prevValue)
prevValue = this.value
}
} finally {
animationTarget.value = null
isAnimationRunning = false
}
}
}
/**
* The target value of the state.
*
* If a swipe is in progress, this is the value that the [swipeable] would animate to if the
* swipe finished. If an animation is running, this is the target value of that animation.
* Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
*/
val targetValue: T
get() {
// TODO(calintat): Track current velocity (b/149549482) and use that here.
val target =
animationTarget.value
?: computeTarget(
offset = offset.value,
lastValue = anchors.getOffset(currentValue) ?: offset.value,
anchors = anchors.keys,
thresholds = thresholds,
velocity = 0f,
velocityThreshold = Float.POSITIVE_INFINITY
)
return anchors[target] ?: currentValue
}
/**
* Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details.
*
* If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`.
*/
val progress: SwipeProgress<T>
get() {
val bounds = findBounds(offset.value, anchors.keys)
val from: T
val to: T
val fraction: Float
when (bounds.size) {
0 -> {
from = currentValue
to = currentValue
fraction = 1f
}
1 -> {
from = anchors.getValue(bounds[0])
to = anchors.getValue(bounds[0])
fraction = 1f
}
else -> {
val (a, b) =
if (direction > 0f) {
bounds[0] to bounds[1]
} else {
bounds[1] to bounds[0]
}
from = anchors.getValue(a)
to = anchors.getValue(b)
fraction = (offset.value - a) / (b - a)
}
}
return SwipeProgress(from, to, fraction)
}
/**
* The direction in which the [swipeable] is moving, relative to the current [currentValue].
*
* This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is
* moving from right to left or bottom to top, or 0f if no swipe or animation is in progress.
*/
val direction: Float
get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f
/**
* Set the state without any animation and suspend until it's set
*
* @param targetValue The new target value to set [currentValue] to.
*/
suspend fun snapTo(targetValue: T) {
latestNonEmptyAnchorsFlow.collect { anchors ->
val targetOffset = anchors.getOffset(targetValue)
requireNotNull(targetOffset) { "The target value must have an associated anchor." }
snapInternalToOffset(targetOffset)
currentValue = targetValue
}
}
/**
* Set the state to the target value by starting an animation.
*
* @param targetValue The new value to animate to.
* @param anim The animation that will be used to animate to the new value.
*/
suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec) {
latestNonEmptyAnchorsFlow.collect { anchors ->
try {
val targetOffset = anchors.getOffset(targetValue)
requireNotNull(targetOffset) { "The target value must have an associated anchor." }
animateInternalToOffset(targetOffset, anim)
} finally {
val endOffset = absoluteOffset.value
val endValue =
anchors
// fighting rounding error once again, anchor should be as close as 0.5
// pixels
.filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f }
.values
.firstOrNull()
?: currentValue
currentValue = endValue
}
}
}
/**
* Perform fling with settling to one of the anchors which is determined by the given
* [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
* since it will settle at the anchor.
*
* In general cases, [swipeable] flings by itself when being swiped. This method is to be used
* for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
* trigger settling fling when the child scroll container reaches the bound.
*
* @param velocity velocity to fling and settle with
* @return the reason fling ended
*/
suspend fun performFling(velocity: Float) {
latestNonEmptyAnchorsFlow.collect { anchors ->
val lastAnchor = anchors.getOffset(currentValue)!!
val targetValue =
computeTarget(
offset = offset.value,
lastValue = lastAnchor,
anchors = anchors.keys,
thresholds = thresholds,
velocity = velocity,
velocityThreshold = velocityThreshold
)
val targetState = anchors[targetValue]
if (targetState != null && confirmStateChange(targetState)) animateTo(targetState)
// If the user vetoed the state change, rollback to the previous state.
else animateInternalToOffset(lastAnchor, animationSpec)
}
}
/**
* Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
* gesture flow.
*
* Note: This method performs generic drag and it won't settle to any particular anchor, *
* leaving swipeable in between anchors. When done dragging, [performFling] must be called as
* well to ensure swipeable will settle at the anchor.
*
* In general cases, [swipeable] drags by itself when being swiped. This method is to be used
* for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
* force drag when the child scroll container reaches the bound.
*
* @param delta delta in pixels to drag by
* @return the amount of [delta] consumed
*/
fun performDrag(delta: Float): Float {
val potentiallyConsumed = absoluteOffset.value + delta
val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
val deltaToConsume = clamped - absoluteOffset.value
if (abs(deltaToConsume) > 0) {
draggableState.dispatchRawDelta(deltaToConsume)
}
return deltaToConsume
}
companion object {
/** The default [Saver] implementation for [SwipeableState]. */
fun <T : Any> Saver(
animationSpec: AnimationSpec<Float>,
confirmStateChange: (T) -> Boolean
) =
Saver<SwipeableState<T>, T>(
save = { it.currentValue },
restore = { SwipeableState(it, animationSpec, confirmStateChange) }
)
}
}
/**
* Collects information about the ongoing swipe or animation in [swipeable].
*
* To access this information, use [SwipeableState.progress].
*
* @param from The state corresponding to the anchor we are moving away from.
* @param to The state corresponding to the anchor we are moving towards.
* @param fraction The fraction that the current position represents between [from] and [to]. Must
* be between `0` and `1`.
*/
@Immutable
class SwipeProgress<T>(
val from: T,
val to: T,
/*@FloatRange(from = 0.0, to = 1.0)*/
val fraction: Float
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SwipeProgress<*>) return false
if (from != other.from) return false
if (to != other.to) return false
if (fraction != other.fraction) return false
return true
}
override fun hashCode(): Int {
var result = from?.hashCode() ?: 0
result = 31 * result + (to?.hashCode() ?: 0)
result = 31 * result + fraction.hashCode()
return result
}
override fun toString(): String {
return "SwipeProgress(from=$from, to=$to, fraction=$fraction)"
}
}
/**
* Create and [remember] a [SwipeableState] with the default animation clock.
*
* @param initialValue The initial value of the state.
* @param animationSpec The default animation that will be used to animate to a new state.
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
*/
@Composable
fun <T : Any> rememberSwipeableState(
initialValue: T,
animationSpec: AnimationSpec<Float> = AnimationSpec,
confirmStateChange: (newValue: T) -> Boolean = { true }
): SwipeableState<T> {
return rememberSaveable(
saver =
SwipeableState.Saver(
animationSpec = animationSpec,
confirmStateChange = confirmStateChange
)
) {
SwipeableState(
initialValue = initialValue,
animationSpec = animationSpec,
confirmStateChange = confirmStateChange
)
}
}
/**
* Create and [remember] a [SwipeableState] which is kept in sync with another state, i.e.:
* 1. Whenever the [value] changes, the [SwipeableState] will be animated to that new value.
* 2. Whenever the value of the [SwipeableState] changes (e.g. after a swipe), the owner of the
* [value] will be notified to update their state to the new value of the [SwipeableState] by
* invoking [onValueChange]. If the owner does not update their state to the provided value for
* some reason, then the [SwipeableState] will perform a rollback to the previous, correct value.
*/
@Composable
internal fun <T : Any> rememberSwipeableStateFor(
value: T,
onValueChange: (T) -> Unit,
animationSpec: AnimationSpec<Float> = AnimationSpec
): SwipeableState<T> {
val swipeableState = remember {
SwipeableState(
initialValue = value,
animationSpec = animationSpec,
confirmStateChange = { true }
)
}
val forceAnimationCheck = remember { mutableStateOf(false) }
LaunchedEffect(value, forceAnimationCheck.value) {
if (value != swipeableState.currentValue) {
swipeableState.animateTo(value)
}
}
DisposableEffect(swipeableState.currentValue) {
if (value != swipeableState.currentValue) {
onValueChange(swipeableState.currentValue)
forceAnimationCheck.value = !forceAnimationCheck.value
}
onDispose {}
}
return swipeableState
}
/**
* Enable swipe gestures between a set of predefined states.
*
* To use this, you must provide a map of anchors (in pixels) to states (of type [T]). Note that
* this map cannot be empty and cannot have two anchors mapped to the same state.
*
* When a swipe is detected, the offset of the [SwipeableState] will be updated with the swipe
* delta. You should use this offset to move your content accordingly (see `Modifier.offsetPx`).
* When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
* reached, the value of the [SwipeableState] will also be updated to the state corresponding to the
* new anchor. The target anchor is calculated based on the provided positional [thresholds].
*
* Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe
* past these bounds, a resistance effect will be applied by default. The amount of resistance at
* each edge is specified by the [resistance] config. To disable all resistance, set it to `null`.
*
* For an example of a [swipeable] with three states, see:
*
* @param T The type of the state.
* @param state The state of the [swipeable].
* @param anchors Pairs of anchors and states, used to map anchors to states and vice versa.
* @param thresholds Specifies where the thresholds between the states are. The thresholds will be
* used to determine which state to animate to when swiping stops. This is represented as a lambda
* that takes two states and returns the threshold between them in the form of a
* [ThresholdConfig]. Note that the order of the states corresponds to the swipe direction.
* @param orientation The orientation in which the [swipeable] can be swiped.
* @param enabled Whether this [swipeable] is enabled and should react to the user's input.
* @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom swipe
* will behave like bottom to top, and a left to right swipe will behave like right to left.
* @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
* [Modifier.draggable].
* @param resistance Controls how much resistance will be applied when swiping past the bounds.
* @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed in
* order to animate to the next state, even if the positional [thresholds] have not been reached.
* @sample androidx.compose.material.samples.SwipeableSample
*/
fun <T> Modifier.swipeable(
state: SwipeableState<T>,
anchors: Map<Float, T>,
orientation: Orientation,
enabled: Boolean = true,
reverseDirection: Boolean = false,
interactionSource: MutableInteractionSource? = null,
thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
velocityThreshold: Dp = VelocityThreshold
) =
composed(
inspectorInfo =
debugInspectorInfo {
name = "swipeable"
properties["state"] = state
properties["anchors"] = anchors
properties["orientation"] = orientation
properties["enabled"] = enabled
properties["reverseDirection"] = reverseDirection
properties["interactionSource"] = interactionSource
properties["thresholds"] = thresholds
properties["resistance"] = resistance
properties["velocityThreshold"] = velocityThreshold
}
) {
require(anchors.isNotEmpty()) { "You must have at least one anchor." }
require(anchors.values.distinct().count() == anchors.size) {
"You cannot have two anchors mapped to the same state."
}
val density = LocalDensity.current
state.ensureInit(anchors)
LaunchedEffect(anchors, state) {
val oldAnchors = state.anchors
state.anchors = anchors
state.resistance = resistance
state.thresholds = { a, b ->
val from = anchors.getValue(a)
val to = anchors.getValue(b)
with(thresholds(from, to)) { density.computeThreshold(a, b) }
}
with(density) { state.velocityThreshold = velocityThreshold.toPx() }
state.processNewAnchors(oldAnchors, anchors)
}
Modifier.draggable(
orientation = orientation,
enabled = enabled,
reverseDirection = reverseDirection,
interactionSource = interactionSource,
startDragImmediately = state.isAnimationRunning,
onDragStopped = { velocity -> launch { state.performFling(velocity) } },
state = state.draggableState
)
}
/**
* Interface to compute a threshold between two anchors/states in a [swipeable].
*
* To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold].
*/
@Stable
interface ThresholdConfig {
/** Compute the value of the threshold (in pixels), once the values of the anchors are known. */
fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
}
/**
* A fixed threshold will be at an [offset] away from the first anchor.
*
* @param offset The offset (in dp) that the threshold will be at.
*/
@Immutable
data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
return fromValue + offset.toPx() * sign(toValue - fromValue)
}
}
/**
* A fractional threshold will be at a [fraction] of the way between the two anchors.
*
* @param fraction The fraction (between 0 and 1) that the threshold will be at.
*/
@Immutable
data class FractionalThreshold(
/*@FloatRange(from = 0.0, to = 1.0)*/
private val fraction: Float
) : ThresholdConfig {
override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
return lerp(fromValue, toValue, fraction)
}
}
/**
* Specifies how resistance is calculated in [swipeable].
*
* There are two things needed to calculate resistance: the resistance basis determines how much
* overflow will be consumed to achieve maximum resistance, and the resistance factor determines the
* amount of resistance (the larger the resistance factor, the stronger the resistance).
*
* The resistance basis is usually either the size of the component which [swipeable] is applied to,
* or the distance between the minimum and maximum anchors. For a constructor in which the
* resistance basis defaults to the latter, consider using [resistanceConfig].
*
* You may specify different resistance factors for each bound. Consider using one of the default
* resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user has
* run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe this
* right now. Also, you can set either factor to 0 to disable resistance at that bound.
*
* @param basis Specifies the maximum amount of overflow that will be consumed. Must be positive.
* @param factorAtMin The factor by which to scale the resistance at the minimum bound. Must not be
* negative.
* @param factorAtMax The factor by which to scale the resistance at the maximum bound. Must not be
* negative.
*/
@Immutable
class ResistanceConfig(
/*@FloatRange(from = 0.0, fromInclusive = false)*/
val basis: Float,
/*@FloatRange(from = 0.0)*/
val factorAtMin: Float = StandardResistanceFactor,
/*@FloatRange(from = 0.0)*/
val factorAtMax: Float = StandardResistanceFactor
) {
fun computeResistance(overflow: Float): Float {
val factor = if (overflow < 0) factorAtMin else factorAtMax
if (factor == 0f) return 0f
val progress = (overflow / basis).coerceIn(-1f, 1f)
return basis / factor * sin(progress * PI.toFloat() / 2)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ResistanceConfig) return false
if (basis != other.basis) return false
if (factorAtMin != other.factorAtMin) return false
if (factorAtMax != other.factorAtMax) return false
return true
}
override fun hashCode(): Int {
var result = basis.hashCode()
result = 31 * result + factorAtMin.hashCode()
result = 31 * result + factorAtMax.hashCode()
return result
}
override fun toString(): String {
return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)"
}
}
/**
* Given an offset x and a set of anchors, return a list of anchors:
* 1. [ ] if the set of anchors is empty,
* 2. [ x' ] if x is equal to one of the anchors, accounting for a small rounding error, where x' is
* x rounded to the exact value of the matching anchor,
* 3. [ min ] if min is the minimum anchor and x < min,
* 4. [ max ] if max is the maximum anchor and x > max, or
* 5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal.
*/
private fun findBounds(offset: Float, anchors: Set<Float>): List<Float> {
// Find the anchors the target lies between with a little bit of rounding error.
val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
return when {
a == null ->
// case 1 or 3
listOfNotNull(b)
b == null ->
// case 4
listOf(a)
a == b ->
// case 2
// Can't return offset itself here since it might not be exactly equal
// to the anchor, despite being considered an exact match.
listOf(a)
else ->
// case 5
listOf(a, b)
}
}
private fun computeTarget(
offset: Float,
lastValue: Float,
anchors: Set<Float>,
thresholds: (Float, Float) -> Float,
velocity: Float,
velocityThreshold: Float
): Float {
val bounds = findBounds(offset, anchors)
return when (bounds.size) {
0 -> lastValue
1 -> bounds[0]
else -> {
val lower = bounds[0]
val upper = bounds[1]
if (lastValue <= offset) {
// Swiping from lower to upper (positive).
if (velocity >= velocityThreshold) {
return upper
} else {
val threshold = thresholds(lower, upper)
if (offset < threshold) lower else upper
}
} else {
// Swiping from upper to lower (negative).
if (velocity <= -velocityThreshold) {
return lower
} else {
val threshold = thresholds(upper, lower)
if (offset > threshold) upper else lower
}
}
}
}
}
private fun <T> Map<Float, T>.getOffset(state: T): Float? {
return entries.firstOrNull { it.value == state }?.key
}
/** Contains useful defaults for [swipeable] and [SwipeableState]. */
object SwipeableDefaults {
/** The default animation used by [SwipeableState]. */
val AnimationSpec = SpringSpec<Float>()
/** The default velocity threshold (1.8 dp per millisecond) used by [swipeable]. */
val VelocityThreshold = 125.dp
/** A stiff resistance factor which indicates that swiping isn't available right now. */
const val StiffResistanceFactor = 20f
/** A standard resistance factor which indicates that the user has run out of things to see. */
const val StandardResistanceFactor = 10f
/**
* The default resistance config used by [swipeable].
*
* This returns `null` if there is one anchor. If there are at least two anchors, it returns a
* [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
*/
fun resistanceConfig(
anchors: Set<Float>,
factorAtMin: Float = StandardResistanceFactor,
factorAtMax: Float = StandardResistanceFactor
): ResistanceConfig? {
return if (anchors.size <= 1) {
null
} else {
val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
ResistanceConfig(basis, factorAtMin, factorAtMax)
}
}
}
// temp default nested scroll connection for swipeables which desire as an opt in
// revisit in b/174756744 as all types will have their own specific connection probably
internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection
get() =
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (delta < 0 && source == NestedScrollSource.Drag) {
performDrag(delta).toOffset()
} else {
Offset.Zero
}
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return if (source == NestedScrollSource.Drag) {
performDrag(available.toFloat()).toOffset()
} else {
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = Offset(available.x, available.y).toFloat()
return if (toFling < 0 && offset.value > minBound) {
performFling(velocity = toFling)
// since we go to the anchor with tween settling, consume all for the best UX
available
} else {
Velocity.Zero
}
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
performFling(velocity = Offset(available.x, available.y).toFloat())
return available
}
private fun Float.toOffset(): Offset = Offset(0f, this)
private fun Offset.toFloat(): Float = this.y
}

View File

@@ -21,13 +21,11 @@ import android.content.Context
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is *not* available. */
object ComposeFacade : BaseComposeFacade {
@@ -53,14 +51,6 @@ object ComposeFacade : BaseComposeFacade {
throwComposeUnavailableError()
}
override fun createMultiShadeView(
context: Context,
viewModel: MultiShadeViewModel,
clock: SystemClock,
): View {
throwComposeUnavailableError()
}
override fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,

View File

@@ -23,8 +23,6 @@ import androidx.activity.compose.setContent
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
import com.android.compose.theme.PlatformTheme
import com.android.systemui.multishade.ui.composable.MultiShade
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -34,7 +32,6 @@ import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is available. */
object ComposeFacade : BaseComposeFacade {
@@ -60,23 +57,6 @@ object ComposeFacade : BaseComposeFacade {
}
}
override fun createMultiShadeView(
context: Context,
viewModel: MultiShadeViewModel,
clock: SystemClock,
): View {
return ComposeView(context).apply {
setContent {
PlatformTheme {
MultiShade(
viewModel = viewModel,
clock = clock,
)
}
}
}
}
override fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,

View File

@@ -1,145 +0,0 @@
/*
* 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.multishade.ui.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.IntSize
import com.android.systemui.R
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.QuickSettings
import com.android.systemui.statusbar.ui.composable.StatusBar
import com.android.systemui.util.time.SystemClock
@Composable
fun MultiShade(
viewModel: MultiShadeViewModel,
clock: SystemClock,
modifier: Modifier = Modifier,
) {
val isScrimEnabled: Boolean by viewModel.isScrimEnabled.collectAsState()
val scrimAlpha: Float by viewModel.scrimAlpha.collectAsState()
// TODO(b/273298030): find a different way to get the height constraint from its parent.
BoxWithConstraints(modifier = modifier) {
val maxHeightPx = with(LocalDensity.current) { maxHeight.toPx() }
Scrim(
modifier = Modifier.fillMaxSize(),
remoteTouch = viewModel::onScrimTouched,
alpha = { scrimAlpha },
isScrimEnabled = isScrimEnabled,
)
Shade(
viewModel = viewModel.leftShade,
currentTimeMillis = clock::elapsedRealtime,
containerHeightPx = maxHeightPx,
modifier = Modifier.align(Alignment.TopStart),
) {
Column {
StatusBar()
Notifications()
}
}
Shade(
viewModel = viewModel.rightShade,
currentTimeMillis = clock::elapsedRealtime,
containerHeightPx = maxHeightPx,
modifier = Modifier.align(Alignment.TopEnd),
) {
Column {
StatusBar()
QuickSettings()
}
}
Shade(
viewModel = viewModel.singleShade,
currentTimeMillis = clock::elapsedRealtime,
containerHeightPx = maxHeightPx,
modifier = Modifier,
) {
Column {
StatusBar()
Notifications()
QuickSettings()
}
}
}
}
@Composable
private fun Scrim(
remoteTouch: (ProxiedInputModel) -> Unit,
alpha: () -> Float,
isScrimEnabled: Boolean,
modifier: Modifier = Modifier,
) {
var size by remember { mutableStateOf(IntSize.Zero) }
Box(
modifier =
modifier
.graphicsLayer { this.alpha = alpha() }
.background(colorResource(R.color.opaque_scrim))
.fillMaxSize()
.onSizeChanged { size = it }
.then(
if (isScrimEnabled) {
Modifier.pointerInput(Unit) {
detectTapGestures(onTap = { remoteTouch(ProxiedInputModel.OnTap) })
}
.pointerInput(Unit) {
detectVerticalDragGestures(
onVerticalDrag = { change, dragAmount ->
remoteTouch(
ProxiedInputModel.OnDrag(
xFraction = change.position.x / size.width,
yDragAmountPx = dragAmount,
)
)
},
onDragEnd = { remoteTouch(ProxiedInputModel.OnDragEnd) },
onDragCancel = { remoteTouch(ProxiedInputModel.OnDragCancel) }
)
}
} else {
Modifier
}
)
)
}

View File

@@ -1,336 +0,0 @@
/*
* 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.multishade.ui.composable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.modifiers.height
import com.android.compose.modifiers.padding
import com.android.compose.swipeable.FixedThreshold
import com.android.compose.swipeable.SwipeableState
import com.android.compose.swipeable.ThresholdConfig
import com.android.compose.swipeable.rememberSwipeableState
import com.android.compose.swipeable.swipeable
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.ui.viewmodel.ShadeViewModel
import kotlin.math.min
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
/**
* Renders a shade (container and content).
*
* This should be allowed to grow to fill the width and height of its container.
*
* @param viewModel The view-model for this shade.
* @param currentTimeMillis A provider for the current time, in milliseconds.
* @param containerHeightPx The height of the container that this shade is being shown in, in
* pixels.
* @param modifier The Modifier.
* @param content The content of the shade.
*/
@Composable
fun Shade(
viewModel: ShadeViewModel,
currentTimeMillis: () -> Long,
containerHeightPx: Float,
modifier: Modifier = Modifier,
content: @Composable () -> Unit = {},
) {
val isVisible: Boolean by viewModel.isVisible.collectAsState()
if (!isVisible) {
return
}
val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
ReportNonProxiedInput(viewModel, interactionSource)
val swipeableState = rememberSwipeableState(initialValue = ShadeState.FullyCollapsed)
HandleForcedCollapse(viewModel, swipeableState)
HandleProxiedInput(viewModel, swipeableState, currentTimeMillis)
ReportShadeExpansion(viewModel, swipeableState, containerHeightPx)
val isSwipingEnabled: Boolean by viewModel.isSwipingEnabled.collectAsState()
val collapseThreshold: Float by viewModel.swipeCollapseThreshold.collectAsState()
val expandThreshold: Float by viewModel.swipeExpandThreshold.collectAsState()
val width: ShadeViewModel.Size by viewModel.width.collectAsState()
val density = LocalDensity.current
val anchors: Map<Float, ShadeState> =
remember(containerHeightPx) { swipeableAnchors(containerHeightPx) }
ShadeContent(
shadeHeightPx = { swipeableState.offset.value },
overstretch = { swipeableState.overflow.value / containerHeightPx },
isSwipingEnabled = isSwipingEnabled,
swipeableState = swipeableState,
interactionSource = interactionSource,
anchors = anchors,
thresholds = { _, to ->
swipeableThresholds(
to = to,
swipeCollapseThreshold = collapseThreshold.fractionToDp(density, containerHeightPx),
swipeExpandThreshold = expandThreshold.fractionToDp(density, containerHeightPx),
)
},
modifier = modifier.shadeWidth(width, density),
content = content,
)
}
/**
* Draws the content of the shade.
*
* @param shadeHeightPx Provider for the current expansion of the shade, in pixels, where `0` is
* fully collapsed.
* @param overstretch Provider for the current amount of vertical "overstretch" that the shade
* should be rendered with. This is `0` or a positive number that is a percentage of the total
* height of the shade when fully expanded. A value of `0` means that the shade is not stretched
* at all.
* @param isSwipingEnabled Whether swiping inside the shade is enabled or not.
* @param swipeableState The state to use for the [swipeable] modifier, allowing external control in
* addition to direct control (proxied user input in addition to non-proxied/direct user input).
* @param anchors A map of [ShadeState] keyed by the vertical position, in pixels, where that state
* occurs; this is used to configure the [swipeable] modifier.
* @param thresholds Function that returns the [ThresholdConfig] for going from one [ShadeState] to
* another. This controls how the [swipeable] decides which [ShadeState] to animate to once the
* user lets go of the shade; e.g. does it animate to fully collapsed or fully expanded.
* @param content The content to render inside the shade.
* @param modifier The [Modifier].
*/
@Composable
private fun ShadeContent(
shadeHeightPx: () -> Float,
overstretch: () -> Float,
isSwipingEnabled: Boolean,
swipeableState: SwipeableState<ShadeState>,
interactionSource: MutableInteractionSource,
anchors: Map<Float, ShadeState>,
thresholds: (from: ShadeState, to: ShadeState) -> ThresholdConfig,
modifier: Modifier = Modifier,
content: @Composable () -> Unit = {},
) {
/**
* Returns a function that takes in [Density] and returns the current padding around the shade
* content.
*/
fun padding(
shadeHeightPx: () -> Float,
): Density.() -> Int {
return {
min(
12.dp.toPx().roundToInt(),
shadeHeightPx().roundToInt(),
)
}
}
Surface(
shape = RoundedCornerShape(32.dp),
modifier =
modifier
.fillMaxWidth()
.height { shadeHeightPx().roundToInt() }
.padding(
horizontal = padding(shadeHeightPx),
vertical = padding(shadeHeightPx),
)
.graphicsLayer {
// Applies the vertical over-stretching of the shade content that may happen if
// the user keep dragging down when the shade is already fully-expanded.
transformOrigin = transformOrigin.copy(pivotFractionY = 0f)
this.scaleY = 1 + overstretch().coerceAtLeast(0f)
}
.swipeable(
enabled = isSwipingEnabled,
state = swipeableState,
interactionSource = interactionSource,
anchors = anchors,
thresholds = thresholds,
orientation = Orientation.Vertical,
),
content = content,
)
}
/** Funnels current shade expansion values into the view-model. */
@Composable
private fun ReportShadeExpansion(
viewModel: ShadeViewModel,
swipeableState: SwipeableState<ShadeState>,
containerHeightPx: Float,
) {
LaunchedEffect(swipeableState.offset, containerHeightPx) {
snapshotFlow { swipeableState.offset.value / containerHeightPx }
.collect { expansion -> viewModel.onExpansionChanged(expansion) }
}
}
/** Funnels drag gesture start and end events into the view-model. */
@Composable
private fun ReportNonProxiedInput(
viewModel: ShadeViewModel,
interactionSource: InteractionSource,
) {
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect {
when (it) {
is DragInteraction.Start -> {
viewModel.onDragStarted()
}
is DragInteraction.Stop -> {
viewModel.onDragEnded()
}
}
}
}
}
/** When told to force collapse, collapses the shade. */
@Composable
private fun HandleForcedCollapse(
viewModel: ShadeViewModel,
swipeableState: SwipeableState<ShadeState>,
) {
LaunchedEffect(viewModel) {
viewModel.isForceCollapsed.collect {
launch { swipeableState.animateTo(ShadeState.FullyCollapsed) }
}
}
}
/**
* Handles proxied input (input originating outside of the UI of the shade) by driving the
* [SwipeableState] accordingly.
*/
@Composable
private fun HandleProxiedInput(
viewModel: ShadeViewModel,
swipeableState: SwipeableState<ShadeState>,
currentTimeMillis: () -> Long,
) {
val velocityTracker: VelocityTracker = remember { VelocityTracker() }
LaunchedEffect(viewModel) {
viewModel.proxiedInput.collect {
when (it) {
is ProxiedInputModel.OnDrag -> {
velocityTracker.addPosition(
timeMillis = currentTimeMillis.invoke(),
position = Offset(0f, it.yDragAmountPx),
)
swipeableState.performDrag(it.yDragAmountPx)
}
is ProxiedInputModel.OnDragEnd -> {
launch {
val velocity = velocityTracker.calculateVelocity().y
velocityTracker.resetTracking()
// We use a VelocityTracker to keep a record of how fast the pointer was
// moving such that we know how far to fling the shade when the gesture
// ends. Flinging the SwipeableState using performFling is required after
// one or more calls to performDrag such that the swipeable settles into one
// of the states. Without doing that, the shade would remain unmoving in an
// in-between state on the screen.
swipeableState.performFling(velocity)
}
}
is ProxiedInputModel.OnDragCancel -> {
launch {
velocityTracker.resetTracking()
swipeableState.animateTo(swipeableState.progress.from)
}
}
else -> Unit
}
}
}
}
/**
* Converts the [Float] (which is assumed to be a fraction between `0` and `1`) to a value in dp.
*
* @param density The [Density] of the display.
* @param wholePx The whole amount that the given [Float] is a fraction of.
* @return The dp size that's a fraction of the whole amount.
*/
private fun Float.fractionToDp(density: Density, wholePx: Float): Dp {
return with(density) { (this@fractionToDp * wholePx).toDp() }
}
private fun Modifier.shadeWidth(
size: ShadeViewModel.Size,
density: Density,
): Modifier {
return then(
when (size) {
is ShadeViewModel.Size.Fraction -> Modifier.fillMaxWidth(size.fraction)
is ShadeViewModel.Size.Pixels -> Modifier.width(with(density) { size.pixels.toDp() })
}
)
}
/** Returns the pixel positions for each of the supported shade states. */
private fun swipeableAnchors(containerHeightPx: Float): Map<Float, ShadeState> {
return mapOf(
0f to ShadeState.FullyCollapsed,
containerHeightPx to ShadeState.FullyExpanded,
)
}
/**
* Returns the [ThresholdConfig] for how far the shade should be expanded or collapsed such that it
* actually completes the expansion or collapse after the user lifts their pointer.
*/
private fun swipeableThresholds(
to: ShadeState,
swipeExpandThreshold: Dp,
swipeCollapseThreshold: Dp,
): ThresholdConfig {
return FixedThreshold(
when (to) {
ShadeState.FullyExpanded -> swipeExpandThreshold
ShadeState.FullyCollapsed -> swipeCollapseThreshold
}
)
}
/** Enumerates the shade UI states for [SwipeableState]. */
private enum class ShadeState {
FullyCollapsed,
FullyExpanded,
}

View File

@@ -21,13 +21,11 @@ import android.content.Context
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.util.time.SystemClock
/**
* A facade to interact with Compose, when it is available.
@@ -63,13 +61,6 @@ interface BaseComposeFacade {
qsVisibilityLifecycleOwner: LifecycleOwner,
): View
/** Create a [View] to represent [viewModel] on screen. */
fun createMultiShadeView(
context: Context,
viewModel: MultiShadeViewModel,
clock: SystemClock,
): View
/** Create a [View] to represent [viewModel] on screen. */
fun createSceneContainerView(
context: Context,

View File

@@ -77,7 +77,7 @@ object Flags {
// TODO(b/278873737): Tracking Bug
@JvmField
val LOAD_NOTIFICATIONS_BEFORE_THE_USER_SWITCH_IS_COMPLETE =
releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete")
releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete")
// TODO(b/277338665): Tracking Bug
@JvmField
@@ -92,11 +92,7 @@ object Flags {
// TODO(b/288326013): Tracking Bug
@JvmField
val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION =
unreleasedFlag(
288326013,
"notification_async_hybrid_view_inflation",
teamfood = false
)
unreleasedFlag(288326013, "notification_async_hybrid_view_inflation", teamfood = false)
@JvmField
val ANIMATED_NOTIFICATION_SHADE_INSETS =
@@ -104,18 +100,17 @@ object Flags {
// TODO(b/268005230): Tracking Bug
@JvmField
val SENSITIVE_REVEAL_ANIM =
unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
val SENSITIVE_REVEAL_ANIM = unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
// TODO(b/280783617): Tracking Bug
@Keep
@JvmField
val BUILDER_EXTRAS_OVERRIDE =
sysPropBooleanFlag(
128,
"persist.sysui.notification.builder_extras_override",
default = false
)
sysPropBooleanFlag(
128,
"persist.sysui.notification.builder_extras_override",
default = false
)
// 200 - keyguard/lockscreen
// ** Flag retired **
@@ -133,16 +128,17 @@ object Flags {
// TODO(b/254512676): Tracking Bug
@JvmField
val LOCKSCREEN_CUSTOM_CLOCKS = resourceBooleanFlag(
207,
R.bool.config_enableLockScreenCustomClocks,
"lockscreen_custom_clocks"
)
val LOCKSCREEN_CUSTOM_CLOCKS =
resourceBooleanFlag(
207,
R.bool.config_enableLockScreenCustomClocks,
"lockscreen_custom_clocks"
)
// TODO(b/275694445): Tracking Bug
@JvmField
val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING = releasedFlag(208,
"lockscreen_without_secure_lock_when_dreaming")
val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING =
releasedFlag(208, "lockscreen_without_secure_lock_when_dreaming")
// TODO(b/286092087): Tracking Bug
@JvmField
@@ -156,8 +152,7 @@ object Flags {
* Whether the clock on a wide lock screen should use the new "stepping" animation for moving
* the digits when the clock moves.
*/
@JvmField
val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
@JvmField val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
/**
* Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
@@ -183,13 +178,11 @@ object Flags {
@JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
// TODO(b/262780002): Tracking Bug
@JvmField
val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
@JvmField val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
// flag for controlling auto pin confirmation and material u shapes in bouncer
@JvmField
val AUTO_PIN_CONFIRMATION =
releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
val AUTO_PIN_CONFIRMATION = releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
// TODO(b/262859270): Tracking Bug
@JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded")
@@ -206,20 +199,11 @@ object Flags {
/** Whether the long-press gesture to open wallpaper picker is enabled. */
// TODO(b/266242192): Tracking Bug
@JvmField
val LOCK_SCREEN_LONG_PRESS_ENABLED =
releasedFlag(
228,
"lock_screen_long_press_enabled"
)
val LOCK_SCREEN_LONG_PRESS_ENABLED = releasedFlag(228, "lock_screen_long_press_enabled")
/** Enables UI updates for AI wallpapers in the wallpaper picker. */
// TODO(b/267722622): Tracking Bug
@JvmField
val WALLPAPER_PICKER_UI_FOR_AIWP =
releasedFlag(
229,
"wallpaper_picker_ui_for_aiwp"
)
@JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag(229, "wallpaper_picker_ui_for_aiwp")
/** Whether to use a new data source for intents to run on keyguard dismissal. */
// TODO(b/275069969): Tracking bug.
@@ -239,27 +223,21 @@ object Flags {
/** Provide new auth messages on the bouncer. */
// TODO(b/277961132): Tracking bug.
@JvmField
val REVAMPED_BOUNCER_MESSAGES =
unreleasedFlag(234, "revamped_bouncer_messages")
@JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag(234, "revamped_bouncer_messages")
/** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
// TODO(b/279794160): Tracking bug.
@JvmField
val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
@JvmField val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
/** Keyguard Migration */
/** Migrate the indication area to the new keyguard root view. */
// TODO(b/280067944): Tracking bug.
@JvmField
val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area")
@JvmField val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area")
/**
* Migrate the bottom area to the new keyguard root view.
* Because there is no such thing as a "bottom area" after this, this also breaks it up into
* many smaller, modular pieces.
* Migrate the bottom area to the new keyguard root view. Because there is no such thing as a
* "bottom area" after this, this also breaks it up into many smaller, modular pieces.
*/
// TODO(b/290652751): Tracking bug.
@JvmField
@@ -268,36 +246,29 @@ object Flags {
/** Whether to listen for fingerprint authentication over keyguard occluding activities. */
// TODO(b/283260512): Tracking bug.
@JvmField
val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
@JvmField val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
/** Flag meant to guard the talkback fix for the KeyguardIndicationTextView */
// TODO(b/286563884): Tracking bug
@JvmField
val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
@JvmField val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
// TODO(b/287268101): Tracking bug.
@JvmField
val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
@JvmField val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
/** Migrate the lock icon view to the new keyguard root view. */
// TODO(b/286552209): Tracking bug.
@JvmField
val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
@JvmField val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
// TODO(b/288276738): Tracking bug.
@JvmField
val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
@JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
/** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */
// TODO(b/288074305): Tracking bug.
@JvmField
val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
@JvmField val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
/** Migrate the status view from the notification panel to keyguard root view. */
// TODO(b/291767565): Tracking bug.
@JvmField
val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view")
@JvmField val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view")
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@@ -316,8 +287,7 @@ object Flags {
// TODO(b/270223352): Tracking Bug
@JvmField
val HIDE_SMARTSPACE_ON_DREAM_OVERLAY =
releasedFlag(404, "hide_smartspace_on_dream_overlay")
val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = releasedFlag(404, "hide_smartspace_on_dream_overlay")
// TODO(b/271460958): Tracking Bug
@JvmField
@@ -357,8 +327,7 @@ object Flags {
/** Enables Font Scaling Quick Settings tile */
// TODO(b/269341316): Tracking Bug
@JvmField
val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
@JvmField val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
/** Enables new QS Edit Mode visual refresh */
// TODO(b/269787742): Tracking Bug
@@ -367,13 +336,11 @@ object Flags {
// 600- status bar
// TODO(b/265892345): Tracking Bug
val PLUG_IN_STATUS_BAR_CHIP = releasedFlag(265892345, "plug_in_status_bar_chip")
// TODO(b/280426085): Tracking Bug
@JvmField
val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
@JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
// 700 - dialer/calls
// TODO(b/254512734): Tracking Bug
@@ -441,8 +408,8 @@ object Flags {
// TODO(b/273509374): Tracking Bug
@JvmField
val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS = releasedFlag(1006,
"always_show_home_controls_on_dreams")
val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS =
releasedFlag(1006, "always_show_home_controls_on_dreams")
// 1100 - windowing
@Keep
@@ -490,11 +457,7 @@ object Flags {
@Keep
@JvmField
val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
sysPropBooleanFlag(
1110,
"persist.wm.debug.enable_pip_keep_clear_algorithm",
default = true
)
sysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", default = true)
// TODO(b/256873975): Tracking Bug
@JvmField
@@ -532,7 +495,8 @@ object Flags {
// TODO(b/273443374): Tracking Bug
@Keep
@JvmField val LOCKSCREEN_LIVE_WALLPAPER =
@JvmField
val LOCKSCREEN_LIVE_WALLPAPER =
sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = true)
// TODO(b/281648899): Tracking bug
@@ -547,7 +511,6 @@ object Flags {
val ENABLE_PIP2_IMPLEMENTATION =
sysPropBooleanFlag(1119, "persist.wm.debug.enable_pip2_implementation", default = false)
// 1200 - predictive back
@Keep
@JvmField
@@ -573,8 +536,7 @@ object Flags {
unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true)
// TODO(b/270987164): Tracking Bug
@JvmField
val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
@JvmField val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
// TODO(b/263826204): Tracking Bug
@JvmField
@@ -597,8 +559,7 @@ object Flags {
unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
// TODO(b/273800936): Tracking Bug
@JvmField
val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
@JvmField val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
// 1300 - screenshots
// TODO(b/264916608): Tracking Bug
@@ -623,16 +584,12 @@ object Flags {
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
// TODO(b/278714186) Tracking Bug
@JvmField val CLIPBOARD_IMAGE_TIMEOUT =
unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
@JvmField
val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
// TODO(b/279405451): Tracking Bug
@JvmField
val CLIPBOARD_SHARED_TRANSITIONS = unreleasedFlag(1703, "clipboard_shared_transitions")
// 1800 - shade container
// TODO(b/265944639): Tracking Bug
@JvmField val DUAL_SHADE = unreleasedFlag(1801, "dual_shade")
// TODO(b/283300105): Tracking Bug
@JvmField val SCENE_CONTAINER = unreleasedFlag(1802, "scene_container")
@@ -642,19 +599,15 @@ object Flags {
// 2000 - device controls
@Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels")
@JvmField
val APP_PANELS_ALL_APPS_ALLOWED =
releasedFlag(2001, "app_panels_all_apps_allowed")
@JvmField val APP_PANELS_ALL_APPS_ALLOWED = releasedFlag(2001, "app_panels_all_apps_allowed")
@JvmField
val CONTROLS_MANAGEMENT_NEW_FLOWS =
releasedFlag(2002, "controls_management_new_flows")
val CONTROLS_MANAGEMENT_NEW_FLOWS = releasedFlag(2002, "controls_management_new_flows")
// Enables removing app from Home control panel as a part of a new flow
// TODO(b/269132640): Tracking Bug
@JvmField
val APP_PANELS_REMOVE_APPS_ALLOWED =
releasedFlag(2003, "app_panels_remove_apps_allowed")
val APP_PANELS_REMOVE_APPS_ALLOWED = releasedFlag(2003, "app_panels_remove_apps_allowed")
// 2200 - biometrics (udfps, sfps, BiometricPrompt, etc.)
// TODO(b/259264861): Tracking Bug
@@ -665,11 +618,9 @@ object Flags {
// 2300 - stylus
@JvmField val TRACK_STYLUS_EVER_USED = releasedFlag(2300, "track_stylus_ever_used")
@JvmField val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui")
@JvmField
val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui")
@JvmField
val ENABLE_USI_BATTERY_NOTIFICATIONS =
releasedFlag(2302, "enable_usi_battery_notifications")
val ENABLE_USI_BATTERY_NOTIFICATIONS = releasedFlag(2302, "enable_usi_battery_notifications")
@JvmField val ENABLE_STYLUS_EDUCATION = releasedFlag(2303, "enable_stylus_education")
// 2400 - performance tools and debugging info
@@ -681,11 +632,10 @@ object Flags {
// TODO(b/283071711): Tracking bug
@JvmField
val TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK =
unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
// TODO:(b/283203305): Tracking bug
@JvmField
val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
@JvmField val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
// 2700 - unfold transitions
// TODO(b/265764985): Tracking Bug
@@ -708,27 +658,21 @@ object Flags {
@JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = releasedFlag(2600, "shortcut_list_search_layout")
// TODO(b/259428678): Tracking Bug
@JvmField
val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
@JvmField val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
// TODO(b/277192623): Tracking Bug
@JvmField
val KEYBOARD_EDUCATION =
unreleasedFlag(2603, "keyboard_education", teamfood = false)
@JvmField val KEYBOARD_EDUCATION = unreleasedFlag(2603, "keyboard_education", teamfood = false)
// TODO(b/277201412): Tracking Bug
@JvmField
val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION =
releasedFlag(2805, "split_shade_subpixel_optimization")
val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION = releasedFlag(2805, "split_shade_subpixel_optimization")
// TODO(b/288868056): Tracking Bug
@JvmField
val PARTIAL_SCREEN_SHARING_TASK_SWITCHER =
unreleasedFlag(288868056, "pss_task_switcher")
val PARTIAL_SCREEN_SHARING_TASK_SWITCHER = unreleasedFlag(288868056, "pss_task_switcher")
// TODO(b/278761837): Tracking Bug
@JvmField
val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
@JvmField val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
// 2900 - Zero Jank fixes. Naming convention is: zj_<bug number>_<cuj name>
@@ -744,23 +688,20 @@ object Flags {
unreleasedFlag(3000, name = "enable_lockscreen_wallpaper_dream")
// TODO(b/283084712): Tracking Bug
@JvmField
val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
@JvmField val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
// TODO(b/283447257): Tracking bug
@JvmField
val BIGPICTURE_NOTIFICATION_LAZY_LOADING =
unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
// TODO(b/283740863): Tracking Bug
@JvmField
val ENABLE_NEW_PRIVACY_DIALOG =
unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = false)
unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = false)
// TODO(b/289573946): Tracking Bug
@JvmField
val PRECOMPUTED_TEXT =
unreleasedFlag(289573946, "precomputed_text")
@JvmField val PRECOMPUTED_TEXT = unreleasedFlag(289573946, "precomputed_text")
// 2900 - CentralSurfaces-related flags
@@ -773,6 +714,5 @@ object Flags {
// TODO(b/290213663): Tracking Bug
@JvmField
val ONE_WAY_HAPTICS_API_MIGRATION =
unreleasedFlag(3100, "oneway_haptics_api_migration")
val ONE_WAY_HAPTICS_API_MIGRATION = unreleasedFlag(3100, "oneway_haptics_api_migration")
}

View File

@@ -1,28 +0,0 @@
/*
* 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.multishade.data.model
import com.android.systemui.multishade.shared.model.ShadeId
/** Models the current interaction with one of the shades. */
data class MultiShadeInteractionModel(
/** The ID of the shade that the user is currently interacting with. */
val shadeId: ShadeId,
/** Whether the interaction is proxied (as in: coming from an external app or different UI). */
val isProxied: Boolean,
)

View File

@@ -1,47 +0,0 @@
/*
* 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.multishade.data.remoteproxy
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
/**
* Acts as a hub for routing proxied user input into the multi shade system.
*
* "Proxied" user input is coming through a proxy; typically from an external app or different UI.
* In other words: it's not user input that's occurring directly on the shade UI itself. This class
* is that proxy.
*/
@Singleton
class MultiShadeInputProxy @Inject constructor() {
private val _proxiedTouch =
MutableSharedFlow<ProxiedInputModel>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val proxiedInput: Flow<ProxiedInputModel> = _proxiedTouch.asSharedFlow()
fun onProxiedInput(proxiedInput: ProxiedInputModel) {
_proxiedTouch.tryEmit(proxiedInput)
}
}

View File

@@ -1,157 +0,0 @@
/*
* 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.multishade.data.repository
import android.content.Context
import androidx.annotation.FloatRange
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.shared.model.ShadeConfig
import com.android.systemui.multishade.shared.model.ShadeId
import com.android.systemui.multishade.shared.model.ShadeModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/** Encapsulates application state for all shades. */
@SysUISingleton
class MultiShadeRepository
@Inject
constructor(
@Application private val applicationContext: Context,
inputProxy: MultiShadeInputProxy,
) {
/**
* Remote input coming from sources outside of system UI (for example, swiping down on the
* Launcher or from the status bar).
*/
val proxiedInput: Flow<ProxiedInputModel> = inputProxy.proxiedInput
/** Width of the left-hand side shade, in pixels. */
private val leftShadeWidthPx =
applicationContext.resources.getDimensionPixelSize(R.dimen.left_shade_width)
/** Width of the right-hand side shade, in pixels. */
private val rightShadeWidthPx =
applicationContext.resources.getDimensionPixelSize(R.dimen.right_shade_width)
/**
* The amount that the user must swipe up when the shade is fully expanded to automatically
* collapse once the user lets go of the shade. If the user swipes less than this amount, the
* shade will automatically revert back to fully expanded once the user stops swiping.
*
* This is a fraction between `0` and `1`.
*/
private val swipeCollapseThreshold =
checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_collapse_threshold))
/**
* The amount that the user must swipe down when the shade is fully collapsed to automatically
* expand once the user lets go of the shade. If the user swipes less than this amount, the
* shade will automatically revert back to fully collapsed once the user stops swiping.
*
* This is a fraction between `0` and `1`.
*/
private val swipeExpandThreshold =
checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_expand_threshold))
/**
* Maximum opacity when the scrim that shows up behind the dual shades is fully visible.
*
* This is a fraction between `0` and `1`.
*/
private val dualShadeScrimAlpha =
checkInBounds(applicationContext.resources.getFloat(R.dimen.dual_shade_scrim_alpha))
/** The current configuration of the shade system. */
val shadeConfig: StateFlow<ShadeConfig> =
MutableStateFlow(
if (applicationContext.resources.getBoolean(R.bool.dual_shade_enabled)) {
ShadeConfig.DualShadeConfig(
leftShadeWidthPx = leftShadeWidthPx,
rightShadeWidthPx = rightShadeWidthPx,
swipeCollapseThreshold = swipeCollapseThreshold,
swipeExpandThreshold = swipeExpandThreshold,
splitFraction =
applicationContext.resources.getFloat(
R.dimen.dual_shade_split_fraction
),
scrimAlpha = dualShadeScrimAlpha,
)
} else {
ShadeConfig.SingleShadeConfig(
swipeCollapseThreshold = swipeCollapseThreshold,
swipeExpandThreshold = swipeExpandThreshold,
)
}
)
.asStateFlow()
private val _forceCollapseAll = MutableStateFlow(false)
/** Whether all shades should be collapsed. */
val forceCollapseAll: StateFlow<Boolean> = _forceCollapseAll.asStateFlow()
private val _shadeInteraction = MutableStateFlow<MultiShadeInteractionModel?>(null)
/** The current shade interaction or `null` if no shade is interacted with currently. */
val shadeInteraction: StateFlow<MultiShadeInteractionModel?> = _shadeInteraction.asStateFlow()
private val stateByShade = mutableMapOf<ShadeId, MutableStateFlow<ShadeModel>>()
/** The model for the shade with the given ID. */
fun getShade(
shadeId: ShadeId,
): StateFlow<ShadeModel> {
return getMutableShade(shadeId).asStateFlow()
}
/** Sets the expansion amount for the shade with the given ID. */
fun setExpansion(
shadeId: ShadeId,
@FloatRange(from = 0.0, to = 1.0) expansion: Float,
) {
getMutableShade(shadeId).let { mutableState ->
mutableState.value = mutableState.value.copy(expansion = expansion)
}
}
/** Sets whether all shades should be immediately forced to collapse. */
fun setForceCollapseAll(isForced: Boolean) {
_forceCollapseAll.value = isForced
}
/** Sets the current shade interaction; use `null` if no shade is interacted with currently. */
fun setShadeInteraction(shadeInteraction: MultiShadeInteractionModel?) {
_shadeInteraction.value = shadeInteraction
}
private fun getMutableShade(id: ShadeId): MutableStateFlow<ShadeModel> {
return stateByShade.getOrPut(id) { MutableStateFlow(ShadeModel(id)) }
}
/** Asserts that the given [Float] is in the range of `0` and `1`, inclusive. */
private fun checkInBounds(float: Float): Float {
check(float in 0f..1f) { "$float isn't between 0 and 1." }
return float
}
}

View File

@@ -1,327 +0,0 @@
/*
* 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.multishade.domain.interactor
import androidx.annotation.FloatRange
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.data.repository.MultiShadeRepository
import com.android.systemui.multishade.shared.math.isZero
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.shared.model.ShadeConfig
import com.android.systemui.multishade.shared.model.ShadeId
import com.android.systemui.multishade.shared.model.ShadeModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.yield
/** Encapsulates business logic related to interactions with the multi-shade system. */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class MultiShadeInteractor
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
private val repository: MultiShadeRepository,
private val inputProxy: MultiShadeInputProxy,
) {
/** The current configuration of the shade system. */
val shadeConfig: StateFlow<ShadeConfig> = repository.shadeConfig
/** The expansion of the shade that's most expanded. */
val maxShadeExpansion: Flow<Float> =
repository.shadeConfig.flatMapLatest { shadeConfig ->
combine(allShades(shadeConfig)) { shadeModels ->
shadeModels.maxOfOrNull { it.expansion } ?: 0f
}
}
/** Whether any shade is expanded, even a little bit. */
val isAnyShadeExpanded: Flow<Boolean> =
maxShadeExpansion.map { maxExpansion -> !maxExpansion.isZero() }.distinctUntilChanged()
/**
* A _processed_ version of the proxied input flow.
*
* All internal dependencies on the proxied input flow *must* use this one for two reasons:
* 1. It's a [SharedFlow] so we only do the upstream work once, no matter how many usages we
* actually have.
* 2. It actually does some preprocessing as the proxied input events stream through, handling
* common things like recording the current state of the system based on incoming input
* events.
*/
private val processedProxiedInput: SharedFlow<ProxiedInputModel> =
combine(
repository.shadeConfig,
repository.proxiedInput.distinctUntilChanged(),
::Pair,
)
.map { (shadeConfig, proxiedInput) ->
if (proxiedInput !is ProxiedInputModel.OnTap) {
// If the user is interacting with any other gesture type (for instance,
// dragging),
// we no longer want to force collapse all shades.
repository.setForceCollapseAll(false)
}
when (proxiedInput) {
is ProxiedInputModel.OnDrag -> {
val affectedShadeId = affectedShadeId(shadeConfig, proxiedInput.xFraction)
// This might be the start of a new drag gesture, let's update our
// application
// state to record that fact.
onUserInteractionStarted(
shadeId = affectedShadeId,
isProxied = true,
)
}
is ProxiedInputModel.OnTap -> {
// Tapping outside any shade collapses all shades. This code path is not hit
// for
// taps that happen _inside_ a shade as that input event is directly applied
// through the UI and is, hence, not a proxied input.
collapseAll()
}
else -> Unit
}
proxiedInput
}
.shareIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
replay = 1,
)
/** Whether the shade with the given ID should be visible. */
fun isVisible(shadeId: ShadeId): Flow<Boolean> {
return repository.shadeConfig.map { shadeConfig -> shadeConfig.shadeIds.contains(shadeId) }
}
/** Whether direct user input is allowed on the shade with the given ID. */
fun isNonProxiedInputAllowed(shadeId: ShadeId): Flow<Boolean> {
return combine(
isForceCollapsed(shadeId),
repository.shadeInteraction,
::Pair,
)
.map { (isForceCollapsed, shadeInteraction) ->
!isForceCollapsed && shadeInteraction?.isProxied != true
}
}
/** Whether the shade with the given ID is forced to collapse. */
fun isForceCollapsed(shadeId: ShadeId): Flow<Boolean> {
return combine(
repository.forceCollapseAll,
repository.shadeInteraction.map { it?.shadeId },
::Pair,
)
.map { (collapseAll, userInteractedShadeIdOrNull) ->
val counterpartShadeIdOrNull =
when (shadeId) {
ShadeId.SINGLE -> null
ShadeId.LEFT -> ShadeId.RIGHT
ShadeId.RIGHT -> ShadeId.LEFT
}
when {
// If all shades have been told to collapse (by a tap outside, for example),
// then this shade is collapsed.
collapseAll -> true
// A shade that doesn't have a counterpart shade cannot be force-collapsed by
// interactions on the counterpart shade.
counterpartShadeIdOrNull == null -> false
// If the current user interaction is on the counterpart shade, then this shade
// should be force-collapsed.
else -> userInteractedShadeIdOrNull == counterpartShadeIdOrNull
}
}
}
/**
* Proxied input affecting the shade with the given ID. This is input coming from sources
* outside of system UI (for example, swiping down on the Launcher or from the status bar) or
* outside the UI of any shade (for example, the scrim that's shown behind the shades).
*/
fun proxiedInput(shadeId: ShadeId): Flow<ProxiedInputModel?> {
return combine(
processedProxiedInput,
isForceCollapsed(shadeId).distinctUntilChanged(),
repository.shadeInteraction,
::Triple,
)
.map { (proxiedInput, isForceCollapsed, shadeInteraction) ->
when {
// If the shade is force-collapsed, we ignored proxied input on it.
isForceCollapsed -> null
// If the proxied input does not belong to this shade, ignore it.
shadeInteraction?.shadeId != shadeId -> null
// If there is ongoing non-proxied user input on any shade, ignore the
// proxied input.
!shadeInteraction.isProxied -> null
// Otherwise, send the proxied input downstream.
else -> proxiedInput
}
}
.onEach { proxiedInput ->
// We use yield() to make sure that the following block of code happens _after_
// downstream collectors had a chance to process the proxied input. Otherwise, we
// might change our state to clear the current UserInteraction _before_ those
// downstream collectors get a chance to process the proxied input, which will make
// them ignore it (since they ignore proxied input when the current user interaction
// doesn't match their shade).
yield()
if (
proxiedInput is ProxiedInputModel.OnDragEnd ||
proxiedInput is ProxiedInputModel.OnDragCancel
) {
onUserInteractionEnded(shadeId = shadeId, isProxied = true)
}
}
}
/** Sets the expansion amount for the shade with the given ID. */
fun setExpansion(
shadeId: ShadeId,
@FloatRange(from = 0.0, to = 1.0) expansion: Float,
) {
repository.setExpansion(shadeId, expansion)
}
/** Collapses all shades. */
fun collapseAll() {
repository.setForceCollapseAll(true)
}
/**
* Notifies that a new non-proxied interaction may have started. Safe to call multiple times for
* the same interaction as it won't overwrite an existing interaction.
*
* Existing interactions can be cleared by calling [onUserInteractionEnded].
*/
fun onUserInteractionStarted(shadeId: ShadeId) {
onUserInteractionStarted(
shadeId = shadeId,
isProxied = false,
)
}
/**
* Notifies that the current non-proxied interaction has ended.
*
* Safe to call multiple times, even if there's no current interaction or even if the current
* interaction doesn't belong to the given shade or is proxied as the code is a no-op unless
* there's a match between the parameters and the current interaction.
*/
fun onUserInteractionEnded(
shadeId: ShadeId,
) {
onUserInteractionEnded(
shadeId = shadeId,
isProxied = false,
)
}
fun sendProxiedInput(proxiedInput: ProxiedInputModel) {
inputProxy.onProxiedInput(proxiedInput)
}
/**
* Notifies that a new interaction may have started. Safe to call multiple times for the same
* interaction as it won't overwrite an existing interaction.
*
* Existing interactions can be cleared by calling [onUserInteractionEnded].
*/
private fun onUserInteractionStarted(
shadeId: ShadeId,
isProxied: Boolean,
) {
if (repository.shadeInteraction.value != null) {
return
}
repository.setShadeInteraction(
MultiShadeInteractionModel(
shadeId = shadeId,
isProxied = isProxied,
)
)
}
/**
* Notifies that the current interaction has ended.
*
* Safe to call multiple times, even if there's no current interaction or even if the current
* interaction doesn't belong to the given shade or [isProxied] value as the code is a no-op
* unless there's a match between the parameters and the current interaction.
*/
private fun onUserInteractionEnded(
shadeId: ShadeId,
isProxied: Boolean,
) {
repository.shadeInteraction.value?.let { (interactionShadeId, isInteractionProxied) ->
if (shadeId == interactionShadeId && isProxied == isInteractionProxied) {
repository.setShadeInteraction(null)
}
}
}
/**
* Returns the ID of the shade that's affected by user input at a given coordinate.
*
* @param config The shade configuration being used.
* @param xFraction The horizontal position of the user input as a fraction along the width of
* its container where `0` is all the way to the left and `1` is all the way to the right.
*/
private fun affectedShadeId(
config: ShadeConfig,
@FloatRange(from = 0.0, to = 1.0) xFraction: Float,
): ShadeId {
return if (config is ShadeConfig.DualShadeConfig) {
if (xFraction <= config.splitFraction) {
ShadeId.LEFT
} else {
ShadeId.RIGHT
}
} else {
ShadeId.SINGLE
}
}
/** Returns the list of flows of all the shades in the given configuration. */
private fun allShades(
config: ShadeConfig,
): List<Flow<ShadeModel>> {
return config.shadeIds.map { shadeId -> repository.getShade(shadeId) }
}
}

View File

@@ -1,288 +0,0 @@
/*
* 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.multishade.domain.interactor
import android.content.Context
import android.view.MotionEvent
import android.view.ViewConfiguration
import com.android.systemui.classifier.Classifier
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.multishade.shared.math.isZero
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.shade.ShadeController
import javax.inject.Inject
import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
* Encapsulates business logic to handle [MotionEvent]-based user input.
*
* This class is meant purely for the legacy `View`-based system to be able to pass `MotionEvent`s
* into the newer multi-shade framework for processing.
*/
@SysUISingleton
class MultiShadeMotionEventInteractor
@Inject
constructor(
@Application private val applicationContext: Context,
@Application private val applicationScope: CoroutineScope,
private val multiShadeInteractor: MultiShadeInteractor,
featureFlags: FeatureFlags,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val falsingManager: FalsingManager,
private val shadeController: ShadeController,
) {
init {
if (featureFlags.isEnabled(Flags.DUAL_SHADE)) {
applicationScope.launch {
multiShadeInteractor.isAnyShadeExpanded.collect {
if (!it && !shadeController.isKeyguard) {
shadeController.makeExpandedInvisible()
} else {
shadeController.makeExpandedVisible(false)
}
}
}
}
}
private val isAnyShadeExpanded: StateFlow<Boolean> =
multiShadeInteractor.isAnyShadeExpanded.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
private val isBouncerShowing: StateFlow<Boolean> =
keyguardTransitionInteractor
.transitionValue(state = KeyguardState.PRIMARY_BOUNCER)
.map { !it.isZero() }
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
private var interactionState: InteractionState? = null
/**
* Returns `true` if the given [MotionEvent] and the rest of events in this gesture should be
* passed to this interactor's [onTouchEvent] method.
*
* Note: the caller should continue to pass [MotionEvent] instances into this method, even if it
* returns `false` as the gesture may be intercepted mid-stream.
*/
fun shouldIntercept(event: MotionEvent): Boolean {
if (isAnyShadeExpanded.value) {
// If any shade is expanded, we assume that touch handling outside the shades is handled
// by the scrim that appears behind the shades. No need to intercept anything here.
return false
}
if (isBouncerShowing.value) {
return false
}
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
// Record where the pointer was placed and which pointer it was.
interactionState =
InteractionState(
initialX = event.x,
initialY = event.y,
currentY = event.y,
pointerId = event.getPointerId(0),
isDraggingHorizontally = false,
isDraggingShade = false,
)
false
}
MotionEvent.ACTION_MOVE -> {
onMove(event)
// We want to intercept the rest of the gesture if we're dragging the shade.
isDraggingShade()
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL ->
// Make sure that we intercept the up or cancel if we're dragging the shade, to
// handle drag end or cancel.
isDraggingShade()
else -> false
}
}
/**
* Notifies that a [MotionEvent] in a series of events of a gesture that was intercepted due to
* the result of [shouldIntercept] has been received.
*
* @param event The [MotionEvent] to handle.
* @param viewWidthPx The width of the view, in pixels.
* @return `true` if the event was consumed, `false` otherwise.
*/
fun onTouchEvent(event: MotionEvent, viewWidthPx: Int): Boolean {
return when (event.actionMasked) {
MotionEvent.ACTION_MOVE -> {
interactionState?.let {
if (it.isDraggingShade) {
val pointerIndex = event.findPointerIndex(it.pointerId)
val previousY = it.currentY
val currentY = event.getY(pointerIndex)
interactionState = it.copy(currentY = currentY)
val yDragAmountPx = currentY - previousY
if (yDragAmountPx != 0f) {
multiShadeInteractor.sendProxiedInput(
ProxiedInputModel.OnDrag(
xFraction = event.x / viewWidthPx,
yDragAmountPx = yDragAmountPx,
)
)
}
true
} else {
onMove(event)
isDraggingShade()
}
}
?: false
}
MotionEvent.ACTION_UP -> {
if (isDraggingShade()) {
// We finished dragging the shade. Record that so the multi-shade framework can
// issue a fling, if the velocity reached in the drag was high enough, for
// example.
multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragEnd)
if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
multiShadeInteractor.collapseAll()
}
}
interactionState = null
true
}
MotionEvent.ACTION_POINTER_UP -> {
val removedPointerId = event.getPointerId(event.actionIndex)
if (removedPointerId == interactionState?.pointerId && event.pointerCount > 1) {
// We removed the original pointer but there must be another pointer because the
// gesture is still ongoing. Let's switch to that pointer.
interactionState =
event.firstUnremovedPointerId(removedPointerId)?.let { replacementPointerId
->
interactionState?.copy(
pointerId = replacementPointerId,
// We want to update the currentY of our state so that the
// transition to the next pointer doesn't report a big jump between
// the Y coordinate of the removed pointer and the Y coordinate of
// the replacement pointer.
currentY = event.getY(replacementPointerId),
)
}
}
true
}
MotionEvent.ACTION_CANCEL -> {
if (isDraggingShade()) {
// Our drag gesture was canceled by the system. This happens primarily in one of
// two occasions: (a) the parent view has decided to intercept the gesture
// itself and/or route it to a different child view or (b) the pointer has
// traveled beyond the bounds of our view and/or the touch display. Either way,
// we pass the cancellation event to the multi-shade framework to record it.
// Doing that allows the multi-shade framework to know that the gesture ended to
// allow new gestures to be accepted.
multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragCancel)
if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
multiShadeInteractor.collapseAll()
}
}
interactionState = null
true
}
else -> false
}
}
/**
* Handles [MotionEvent.ACTION_MOVE] and sets whether or not we are dragging shade in our
* current interaction
*
* @param event The [MotionEvent] to handle.
*/
private fun onMove(event: MotionEvent) {
interactionState?.let {
val pointerIndex = event.findPointerIndex(it.pointerId)
val currentX = event.getX(pointerIndex)
val currentY = event.getY(pointerIndex)
if (!it.isDraggingHorizontally && !it.isDraggingShade) {
val xDistanceTravelled = currentX - it.initialX
val yDistanceTravelled = currentY - it.initialY
val touchSlop = ViewConfiguration.get(applicationContext).scaledTouchSlop
interactionState =
when {
yDistanceTravelled > touchSlop -> it.copy(isDraggingShade = true)
abs(xDistanceTravelled) > touchSlop ->
it.copy(isDraggingHorizontally = true)
else -> interactionState
}
}
}
}
private data class InteractionState(
val initialX: Float,
val initialY: Float,
val currentY: Float,
val pointerId: Int,
/** Whether the current gesture is dragging horizontally. */
val isDraggingHorizontally: Boolean,
/** Whether the current gesture is dragging the shade vertically. */
val isDraggingShade: Boolean,
)
private fun isDraggingShade(): Boolean {
return interactionState?.isDraggingShade ?: false
}
/**
* Returns the index of the first pointer that is not [removedPointerId] or `null`, if there is
* no other pointer.
*/
private fun MotionEvent.firstUnremovedPointerId(removedPointerId: Int): Int? {
return (0 until pointerCount)
.firstOrNull { pointerIndex ->
val pointerId = getPointerId(pointerIndex)
pointerId != removedPointerId
}
?.let { pointerIndex -> getPointerId(pointerIndex) }
}
}

View File

@@ -1,27 +0,0 @@
/*
* 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.multishade.shared.math
import androidx.annotation.VisibleForTesting
import kotlin.math.abs
/** Returns `true` if this [Float] is within [epsilon] of `0`. */
fun Float.isZero(epsilon: Float = EPSILON): Boolean {
return abs(this) < epsilon
}
@VisibleForTesting private const val EPSILON = 0.0001f

View File

@@ -1,50 +0,0 @@
/*
* 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.multishade.shared.model
import androidx.annotation.FloatRange
/**
* Models a part of an ongoing proxied user input gesture.
*
* "Proxied" user input is coming through a proxy; typically from an external app or different UI.
* In other words: it's not user input that's occurring directly on the shade UI itself.
*/
sealed class ProxiedInputModel {
/** The user is dragging their pointer. */
data class OnDrag(
/**
* The relative position of the pointer as a fraction of its container width where `0` is
* all the way to the left and `1` is all the way to the right.
*/
@FloatRange(from = 0.0, to = 1.0) val xFraction: Float,
/** The amount that the pointer was dragged, in pixels. */
val yDragAmountPx: Float,
) : ProxiedInputModel()
/** The user finished dragging by lifting up their pointer. */
object OnDragEnd : ProxiedInputModel()
/**
* The drag gesture has been canceled. Usually because the pointer exited the draggable area.
*/
object OnDragCancel : ProxiedInputModel()
/** The user has tapped (clicked). */
object OnTap : ProxiedInputModel()
}

View File

@@ -1,79 +0,0 @@
/*
* 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.multishade.shared.model
import androidx.annotation.FloatRange
/** Enumerates the various possible configurations of the shade system. */
sealed class ShadeConfig(
/** IDs of the shade(s) in this configuration. */
open val shadeIds: List<ShadeId>,
/**
* The amount that the user must swipe up when the shade is fully expanded to automatically
* collapse once the user lets go of the shade. If the user swipes less than this amount, the
* shade will automatically revert back to fully expanded once the user stops swiping.
*/
@FloatRange(from = 0.0, to = 1.0) open val swipeCollapseThreshold: Float,
/**
* The amount that the user must swipe down when the shade is fully collapsed to automatically
* expand once the user lets go of the shade. If the user swipes less than this amount, the
* shade will automatically revert back to fully collapsed once the user stops swiping.
*/
@FloatRange(from = 0.0, to = 1.0) open val swipeExpandThreshold: Float,
) {
/** There is a single shade. */
data class SingleShadeConfig(
@FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
@FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
) :
ShadeConfig(
shadeIds = listOf(ShadeId.SINGLE),
swipeCollapseThreshold = swipeCollapseThreshold,
swipeExpandThreshold = swipeExpandThreshold,
)
/** There are two shades arranged side-by-side. */
data class DualShadeConfig(
/** Width of the left-hand side shade. */
val leftShadeWidthPx: Int,
/** Width of the right-hand side shade. */
val rightShadeWidthPx: Int,
@FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
@FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
/**
* The position of the "split" between interaction areas for each of the shades, as a
* fraction of the width of the container.
*
* Interactions that occur on the start-side (left-hand side in left-to-right languages like
* English) affect the start-side shade. Interactions that occur on the end-side (right-hand
* side in left-to-right languages like English) affect the end-side shade.
*/
@FloatRange(from = 0.0, to = 1.0) val splitFraction: Float,
/** Maximum opacity when the scrim that shows up behind the dual shades is fully visible. */
@FloatRange(from = 0.0, to = 1.0) val scrimAlpha: Float,
) :
ShadeConfig(
shadeIds = listOf(ShadeId.LEFT, ShadeId.RIGHT),
swipeCollapseThreshold = swipeCollapseThreshold,
swipeExpandThreshold = swipeExpandThreshold,
)
}

View File

@@ -1,28 +0,0 @@
/*
* 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.multishade.shared.model
/** Enumerates all known shade IDs. */
enum class ShadeId {
/** ID of the shade on the left in dual shade configurations. */
LEFT,
/** ID of the shade on the right in dual shade configurations. */
RIGHT,
/** ID of the single shade in single shade configurations. */
SINGLE,
}

View File

@@ -1,26 +0,0 @@
/*
* 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.multishade.shared.model
import androidx.annotation.FloatRange
/** Models the current state of a shade. */
data class ShadeModel(
val id: ShadeId,
@FloatRange(from = 0.0, to = 1.0) val expansion: Float = 0f,
)

View File

@@ -1,71 +0,0 @@
/*
* 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.multishade.ui.view
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.compose.ComposeFacade
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.util.time.SystemClock
import kotlinx.coroutines.launch
/**
* View that hosts the multi-shade system and acts as glue between legacy code and the
* implementation.
*/
class MultiShadeView(
context: Context,
attrs: AttributeSet?,
) :
FrameLayout(
context,
attrs,
) {
fun init(
interactor: MultiShadeInteractor,
clock: SystemClock,
) {
repeatWhenAttached {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
addView(
ComposeFacade.createMultiShadeView(
context = context,
viewModel =
MultiShadeViewModel(
viewModelScope = this,
interactor = interactor,
),
clock = clock,
)
)
}
// Here when destroyed.
removeAllViews()
}
}
}
}

View File

@@ -1,108 +0,0 @@
/*
* 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.multishade.ui.viewmodel
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.shared.model.ShadeConfig
import com.android.systemui.multishade.shared.model.ShadeId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** Models UI state for UI that supports multi (or single) shade. */
@OptIn(ExperimentalCoroutinesApi::class)
class MultiShadeViewModel(
viewModelScope: CoroutineScope,
private val interactor: MultiShadeInteractor,
) {
/** Models UI state for the single shade. */
val singleShade =
ShadeViewModel(
viewModelScope,
ShadeId.SINGLE,
interactor,
)
/** Models UI state for the shade on the left-hand side. */
val leftShade =
ShadeViewModel(
viewModelScope,
ShadeId.LEFT,
interactor,
)
/** Models UI state for the shade on the right-hand side. */
val rightShade =
ShadeViewModel(
viewModelScope,
ShadeId.RIGHT,
interactor,
)
/** The amount of alpha that the scrim should have. This is a value between `0` and `1`. */
val scrimAlpha: StateFlow<Float> =
combine(
interactor.maxShadeExpansion,
interactor.shadeConfig
.map { it as? ShadeConfig.DualShadeConfig }
.map { dualShadeConfigOrNull -> dualShadeConfigOrNull?.scrimAlpha ?: 0f },
::Pair,
)
.map { (anyShadeExpansion, scrimAlpha) ->
(anyShadeExpansion * scrimAlpha).coerceIn(0f, 1f)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = 0f,
)
/** Whether the scrim should accept touch events. */
val isScrimEnabled: StateFlow<Boolean> =
interactor.shadeConfig
.flatMapLatest { shadeConfig ->
when (shadeConfig) {
// In the dual shade configuration, the scrim is enabled when the expansion is
// greater than zero on any one of the shades.
is ShadeConfig.DualShadeConfig -> interactor.isAnyShadeExpanded
// No scrim in the single shade configuration.
is ShadeConfig.SingleShadeConfig -> flowOf(false)
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = false,
)
/** Notifies that the scrim has been touched. */
fun onScrimTouched(proxiedInput: ProxiedInputModel) {
if (!isScrimEnabled.value) {
return
}
interactor.sendProxiedInput(proxiedInput)
}
}

View File

@@ -1,150 +0,0 @@
/*
* 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.multishade.ui.viewmodel
import androidx.annotation.FloatRange
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.shared.model.ShadeConfig
import com.android.systemui.multishade.shared.model.ShadeId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** Models UI state for a single shade. */
class ShadeViewModel(
viewModelScope: CoroutineScope,
private val shadeId: ShadeId,
private val interactor: MultiShadeInteractor,
) {
/** Whether the shade is visible. */
val isVisible: StateFlow<Boolean> =
interactor
.isVisible(shadeId)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = false,
)
/** Whether swiping on the shade UI is currently enabled. */
val isSwipingEnabled: StateFlow<Boolean> =
interactor
.isNonProxiedInputAllowed(shadeId)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = false,
)
/** Whether the shade must be collapsed immediately. */
val isForceCollapsed: Flow<Boolean> =
interactor.isForceCollapsed(shadeId).distinctUntilChanged()
/** The width of the shade. */
val width: StateFlow<Size> =
interactor.shadeConfig
.map { shadeWidth(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = shadeWidth(interactor.shadeConfig.value),
)
/**
* The amount that the user must swipe up when the shade is fully expanded to automatically
* collapse once the user lets go of the shade. If the user swipes less than this amount, the
* shade will automatically revert back to fully expanded once the user stops swiping.
*/
val swipeCollapseThreshold: StateFlow<Float> =
interactor.shadeConfig
.map { it.swipeCollapseThreshold }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = interactor.shadeConfig.value.swipeCollapseThreshold,
)
/**
* The amount that the user must swipe down when the shade is fully collapsed to automatically
* expand once the user lets go of the shade. If the user swipes less than this amount, the
* shade will automatically revert back to fully collapsed once the user stops swiping.
*/
val swipeExpandThreshold: StateFlow<Float> =
interactor.shadeConfig
.map { it.swipeExpandThreshold }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = interactor.shadeConfig.value.swipeExpandThreshold,
)
/**
* Proxied input affecting the shade. This is input coming from sources outside of system UI
* (for example, swiping down on the Launcher or from the status bar) or outside the UI of any
* shade (for example, the scrim that's shown behind the shades).
*/
val proxiedInput: Flow<ProxiedInputModel?> =
interactor
.proxiedInput(shadeId)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null,
)
/** Notifies that the expansion amount for the shade has changed. */
fun onExpansionChanged(
expansion: Float,
) {
interactor.setExpansion(shadeId, expansion.coerceIn(0f, 1f))
}
/** Notifies that a drag gesture has started. */
fun onDragStarted() {
interactor.onUserInteractionStarted(shadeId)
}
/** Notifies that a drag gesture has ended. */
fun onDragEnded() {
interactor.onUserInteractionEnded(shadeId = shadeId)
}
private fun shadeWidth(shadeConfig: ShadeConfig): Size {
return when (shadeId) {
ShadeId.LEFT ->
Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.leftShadeWidthPx ?: 0)
ShadeId.RIGHT ->
Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.rightShadeWidthPx ?: 0)
ShadeId.SINGLE -> Size.Fraction(1f)
}
}
sealed class Size {
data class Fraction(
@FloatRange(from = 0.0, to = 1.0) val fraction: Float,
) : Size()
data class Pixels(
val pixels: Int,
) : Size()
}
}

View File

@@ -147,7 +147,6 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
import com.android.systemui.model.SysUiState;
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
@@ -365,7 +364,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private KeyguardBottomAreaView mKeyguardBottomArea;
private boolean mExpanding;
private boolean mSplitShadeEnabled;
private boolean mDualShadeEnabled;
/** The bottom padding reserved for elements of the keyguard measuring notifications. */
private float mKeyguardNotificationBottomPadding;
/**
@@ -599,7 +597,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final KeyguardInteractor mKeyguardInteractor;
private final KeyguardViewConfigurator mKeyguardViewConfigurator;
private final @Nullable MultiShadeInteractor mMultiShadeInteractor;
private final CoroutineDispatcher mMainDispatcher;
private boolean mIsAnyMultiShadeExpanded;
private boolean mIsOcclusionTransitionRunning = false;
@@ -735,7 +732,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
@Main CoroutineDispatcher mainDispatcher,
KeyguardTransitionInteractor keyguardTransitionInteractor,
Provider<MultiShadeInteractor> multiShadeInteractorProvider,
DumpManager dumpManager,
KeyguardLongPressViewModel keyguardLongPressViewModel,
KeyguardInteractor keyguardInteractor,
@@ -839,8 +835,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mFeatureFlags = featureFlags;
mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE);
mTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_FEATURES);
mDualShadeEnabled = mFeatureFlags.isEnabled(Flags.DUAL_SHADE);
mMultiShadeInteractor = mDualShadeEnabled ? multiShadeInteractorProvider.get() : null;
mFalsingCollector = falsingCollector;
mPowerManager = powerManager;
mWakeUpCoordinator = coordinator;
@@ -1079,11 +1073,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
controller.setup(mNotificationContainerParent));
if (mDualShadeEnabled) {
collectFlow(mView, mMultiShadeInteractor.isAnyShadeExpanded(),
mMultiShadeExpansionConsumer, mMainDispatcher);
}
// Dreaming->Lockscreen
collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
mDreamingToLockscreenTransition, mMainDispatcher);

View File

@@ -31,9 +31,6 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.AuthKeyguardMessageArea;
@@ -46,7 +43,6 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder;
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dock.DockManager;
import com.android.systemui.flags.FeatureFlags;
@@ -57,9 +53,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
import com.android.systemui.log.BouncerLogger;
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor;
import com.android.systemui.multishade.ui.view.MultiShadeView;
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
import com.android.systemui.statusbar.DragDownHelper;
@@ -83,7 +76,6 @@ import java.util.Optional;
import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Provider;
/**
* Controller for {@link NotificationShadeWindowView}.
@@ -135,7 +127,6 @@ public class NotificationShadeWindowViewController {
step.getTransitionState() == TransitionState.RUNNING;
};
private final SystemClock mClock;
private final @Nullable MultiShadeMotionEventInteractor mMultiShadeMotionEventInteractor;
@Inject
public NotificationShadeWindowViewController(
@@ -167,9 +158,7 @@ public class NotificationShadeWindowViewController {
KeyguardTransitionInteractor keyguardTransitionInteractor,
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
FeatureFlags featureFlags,
Provider<MultiShadeInteractor> multiShadeInteractorProvider,
SystemClock clock,
Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider,
BouncerMessageInteractor bouncerMessageInteractor,
BouncerLogger bouncerLogger) {
mLockscreenShadeTransitionController = transitionController;
@@ -219,17 +208,6 @@ public class NotificationShadeWindowViewController {
progressProvider -> progressProvider.addCallback(
mDisableSubpixelTextTransitionListener));
}
if (ComposeFacade.INSTANCE.isComposeAvailable()
&& featureFlags.isEnabled(Flags.DUAL_SHADE)) {
mMultiShadeMotionEventInteractor = multiShadeMotionEventInteractorProvider.get();
final ViewStub multiShadeViewStub = mView.findViewById(R.id.multi_shade_stub);
if (multiShadeViewStub != null) {
final MultiShadeView multiShadeView = (MultiShadeView) multiShadeViewStub.inflate();
multiShadeView.init(multiShadeInteractorProvider.get(), clock);
}
} else {
mMultiShadeMotionEventInteractor = null;
}
}
/**
@@ -395,10 +373,7 @@ public class NotificationShadeWindowViewController {
return true;
}
if (mMultiShadeMotionEventInteractor != null) {
// This interactor is not null only if the dual shade feature is enabled.
return mMultiShadeMotionEventInteractor.shouldIntercept(ev);
} else if (mNotificationPanelViewController.isFullyExpanded()
if (mNotificationPanelViewController.isFullyExpanded()
&& mDragDownHelper.isDragDownEnabled()
&& !mService.isBouncerShowing()
&& !mStatusBarStateController.isDozing()) {
@@ -428,10 +403,7 @@ public class NotificationShadeWindowViewController {
return true;
}
if (mMultiShadeMotionEventInteractor != null) {
// This interactor is not null only if the dual shade feature is enabled.
return mMultiShadeMotionEventInteractor.onTouchEvent(ev, mView.getWidth());
} else if (mDragDownHelper.isDragDownEnabled()
if (mDragDownHelper.isDragDownEnabled()
|| mDragDownHelper.isDraggingDown()) {
// we still want to finish our drag down gesture when locking the screen
return mDragDownHelper.onTouchEvent(ev) || handled;

View File

@@ -1,191 +0,0 @@
/*
* 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.multishade.data.repository
import android.content.Context
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.shared.model.ShadeConfig
import com.android.systemui.multishade.shared.model.ShadeId
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class MultiShadeRepositoryTest : SysuiTestCase() {
private lateinit var inputProxy: MultiShadeInputProxy
@Before
fun setUp() {
inputProxy = MultiShadeInputProxy()
}
@Test
fun proxiedInput() = runTest {
val underTest = create()
val latest: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
assertWithMessage("proxiedInput should start with null").that(latest).isNull()
inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
assertThat(latest).isEqualTo(ProxiedInputModel.OnTap)
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 100f))
assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 100f))
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 120f))
assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 120f))
inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
assertThat(latest).isEqualTo(ProxiedInputModel.OnDragEnd)
}
@Test
fun shadeConfig_dualShadeEnabled() = runTest {
overrideResource(R.bool.dual_shade_enabled, true)
val underTest = create()
val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
assertThat(shadeConfig).isInstanceOf(ShadeConfig.DualShadeConfig::class.java)
}
@Test
fun shadeConfig_dualShadeNotEnabled() = runTest {
overrideResource(R.bool.dual_shade_enabled, false)
val underTest = create()
val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
assertThat(shadeConfig).isInstanceOf(ShadeConfig.SingleShadeConfig::class.java)
}
@Test
fun forceCollapseAll() = runTest {
val underTest = create()
val forceCollapseAll: Boolean? by collectLastValue(underTest.forceCollapseAll)
assertWithMessage("forceCollapseAll should start as false!")
.that(forceCollapseAll)
.isFalse()
underTest.setForceCollapseAll(true)
assertThat(forceCollapseAll).isTrue()
underTest.setForceCollapseAll(false)
assertThat(forceCollapseAll).isFalse()
}
@Test
fun shadeInteraction() = runTest {
val underTest = create()
val shadeInteraction: MultiShadeInteractionModel? by
collectLastValue(underTest.shadeInteraction)
assertWithMessage("shadeInteraction should start as null!").that(shadeInteraction).isNull()
underTest.setShadeInteraction(
MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false)
)
assertThat(shadeInteraction)
.isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false))
underTest.setShadeInteraction(
MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true)
)
assertThat(shadeInteraction)
.isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true))
underTest.setShadeInteraction(null)
assertThat(shadeInteraction).isNull()
}
@Test
fun expansion() = runTest {
val underTest = create()
val leftExpansion: Float? by
collectLastValue(underTest.getShade(ShadeId.LEFT).map { it.expansion })
val rightExpansion: Float? by
collectLastValue(underTest.getShade(ShadeId.RIGHT).map { it.expansion })
val singleExpansion: Float? by
collectLastValue(underTest.getShade(ShadeId.SINGLE).map { it.expansion })
assertWithMessage("expansion should start as 0!").that(leftExpansion).isZero()
assertWithMessage("expansion should start as 0!").that(rightExpansion).isZero()
assertWithMessage("expansion should start as 0!").that(singleExpansion).isZero()
underTest.setExpansion(
shadeId = ShadeId.LEFT,
0.4f,
)
assertThat(leftExpansion).isEqualTo(0.4f)
assertThat(rightExpansion).isEqualTo(0f)
assertThat(singleExpansion).isEqualTo(0f)
underTest.setExpansion(
shadeId = ShadeId.RIGHT,
0.73f,
)
assertThat(leftExpansion).isEqualTo(0.4f)
assertThat(rightExpansion).isEqualTo(0.73f)
assertThat(singleExpansion).isEqualTo(0f)
underTest.setExpansion(
shadeId = ShadeId.LEFT,
0.1f,
)
underTest.setExpansion(
shadeId = ShadeId.SINGLE,
0.88f,
)
assertThat(leftExpansion).isEqualTo(0.1f)
assertThat(rightExpansion).isEqualTo(0.73f)
assertThat(singleExpansion).isEqualTo(0.88f)
}
private fun create(): MultiShadeRepository {
return create(
context = context,
inputProxy = inputProxy,
)
}
companion object {
fun create(
context: Context,
inputProxy: MultiShadeInputProxy,
): MultiShadeRepository {
return MultiShadeRepository(
applicationContext = context,
inputProxy = inputProxy,
)
}
}
}

View File

@@ -1,323 +0,0 @@
/*
* 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.multishade.domain.interactor
import android.content.Context
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.data.repository.MultiShadeRepositoryTest
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.shared.model.ShadeId
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class MultiShadeInteractorTest : SysuiTestCase() {
private lateinit var testScope: TestScope
private lateinit var inputProxy: MultiShadeInputProxy
@Before
fun setUp() {
testScope = TestScope()
inputProxy = MultiShadeInputProxy()
}
@Test
fun maxShadeExpansion() =
testScope.runTest {
val underTest = create()
val maxShadeExpansion: Float? by collectLastValue(underTest.maxShadeExpansion)
assertWithMessage("maxShadeExpansion must start with 0.0!")
.that(maxShadeExpansion)
.isEqualTo(0f)
underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
assertThat(maxShadeExpansion).isEqualTo(0.441f)
underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
assertThat(maxShadeExpansion).isEqualTo(0.442f)
underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
assertThat(maxShadeExpansion).isEqualTo(0.441f)
underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
assertThat(maxShadeExpansion).isEqualTo(0f)
}
@Test
fun isAnyShadeExpanded() =
testScope.runTest {
val underTest = create()
val isAnyShadeExpanded: Boolean? by collectLastValue(underTest.isAnyShadeExpanded)
assertWithMessage("isAnyShadeExpanded must start with false!")
.that(isAnyShadeExpanded)
.isFalse()
underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
assertThat(isAnyShadeExpanded).isTrue()
underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
assertThat(isAnyShadeExpanded).isTrue()
underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
assertThat(isAnyShadeExpanded).isTrue()
underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
assertThat(isAnyShadeExpanded).isFalse()
}
@Test
fun isVisible_dualShadeConfig() =
testScope.runTest {
overrideResource(R.bool.dual_shade_enabled, true)
val underTest = create()
val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
val isRightShadeVisible: Boolean? by
collectLastValue(underTest.isVisible(ShadeId.RIGHT))
val isSingleShadeVisible: Boolean? by
collectLastValue(underTest.isVisible(ShadeId.SINGLE))
assertThat(isLeftShadeVisible).isTrue()
assertThat(isRightShadeVisible).isTrue()
assertThat(isSingleShadeVisible).isFalse()
}
@Test
fun isVisible_singleShadeConfig() =
testScope.runTest {
overrideResource(R.bool.dual_shade_enabled, false)
val underTest = create()
val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
val isRightShadeVisible: Boolean? by
collectLastValue(underTest.isVisible(ShadeId.RIGHT))
val isSingleShadeVisible: Boolean? by
collectLastValue(underTest.isVisible(ShadeId.SINGLE))
assertThat(isLeftShadeVisible).isFalse()
assertThat(isRightShadeVisible).isFalse()
assertThat(isSingleShadeVisible).isTrue()
}
@Test
fun isNonProxiedInputAllowed() =
testScope.runTest {
val underTest = create()
val isLeftShadeNonProxiedInputAllowed: Boolean? by
collectLastValue(underTest.isNonProxiedInputAllowed(ShadeId.LEFT))
assertWithMessage("isNonProxiedInputAllowed should start as true!")
.that(isLeftShadeNonProxiedInputAllowed)
.isTrue()
// Need to collect proxied input so the flows become hot as the gesture cancelation code
// logic sits in side the proxiedInput flow for each shade.
collectLastValue(underTest.proxiedInput(ShadeId.LEFT))
collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
// Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
// the
// same shade.
inputProxy.onProxiedInput(
ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
)
assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
// Registering the end of the proxied interaction re-allows it.
inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
// Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
// disallowing non-proxied input on the LEFT shade.
inputProxy.onProxiedInput(
ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
)
assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
// Registering the end of the interaction on the RIGHT shade re-allows it.
inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
}
@Test
fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
testScope.runTest {
val underTest = create()
val isLeftShadeForceCollapsed: Boolean? by
collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
val isRightShadeForceCollapsed: Boolean? by
collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
val isSingleShadeForceCollapsed: Boolean? by
collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
assertWithMessage("isForceCollapsed should start as false!")
.that(isLeftShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isRightShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isSingleShadeForceCollapsed)
.isFalse()
// Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
// shade.
underTest.onUserInteractionStarted(ShadeId.RIGHT)
assertThat(isLeftShadeForceCollapsed).isTrue()
assertThat(isRightShadeForceCollapsed).isFalse()
assertThat(isSingleShadeForceCollapsed).isFalse()
// Registering the end of the interaction on the RIGHT shade re-allows it.
underTest.onUserInteractionEnded(ShadeId.RIGHT)
assertThat(isLeftShadeForceCollapsed).isFalse()
assertThat(isRightShadeForceCollapsed).isFalse()
assertThat(isSingleShadeForceCollapsed).isFalse()
// Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
// shade.
underTest.onUserInteractionStarted(ShadeId.LEFT)
assertThat(isLeftShadeForceCollapsed).isFalse()
assertThat(isRightShadeForceCollapsed).isTrue()
assertThat(isSingleShadeForceCollapsed).isFalse()
// Registering the end of the interaction on the LEFT shade re-allows it.
underTest.onUserInteractionEnded(ShadeId.LEFT)
assertThat(isLeftShadeForceCollapsed).isFalse()
assertThat(isRightShadeForceCollapsed).isFalse()
assertThat(isSingleShadeForceCollapsed).isFalse()
}
@Test
fun collapseAll() =
testScope.runTest {
val underTest = create()
val isLeftShadeForceCollapsed: Boolean? by
collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
val isRightShadeForceCollapsed: Boolean? by
collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
val isSingleShadeForceCollapsed: Boolean? by
collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
assertWithMessage("isForceCollapsed should start as false!")
.that(isLeftShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isRightShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isSingleShadeForceCollapsed)
.isFalse()
underTest.collapseAll()
assertThat(isLeftShadeForceCollapsed).isTrue()
assertThat(isRightShadeForceCollapsed).isTrue()
assertThat(isSingleShadeForceCollapsed).isTrue()
// Receiving proxied input on that's not a tap gesture, on the left-hand side resets the
// "collapse all". Note that now the RIGHT shade is force-collapsed because we're
// interacting with the LEFT shade.
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 0f))
assertThat(isLeftShadeForceCollapsed).isFalse()
assertThat(isRightShadeForceCollapsed).isTrue()
assertThat(isSingleShadeForceCollapsed).isFalse()
}
@Test
fun onTapOutside_collapsesAll() =
testScope.runTest {
val underTest = create()
val isLeftShadeForceCollapsed: Boolean? by
collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
val isRightShadeForceCollapsed: Boolean? by
collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
val isSingleShadeForceCollapsed: Boolean? by
collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
assertWithMessage("isForceCollapsed should start as false!")
.that(isLeftShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isRightShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isSingleShadeForceCollapsed)
.isFalse()
inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
assertThat(isLeftShadeForceCollapsed).isTrue()
assertThat(isRightShadeForceCollapsed).isTrue()
assertThat(isSingleShadeForceCollapsed).isTrue()
}
@Test
fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
testScope.runTest {
val underTest = create()
val proxiedInput: ProxiedInputModel? by
collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
underTest.onUserInteractionStarted(shadeId = ShadeId.RIGHT)
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
assertThat(proxiedInput).isNull()
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
assertThat(proxiedInput).isNull()
underTest.onUserInteractionEnded(shadeId = ShadeId.RIGHT)
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
assertThat(proxiedInput).isNotNull()
}
private fun create(): MultiShadeInteractor {
return create(
testScope = testScope,
context = context,
inputProxy = inputProxy,
)
}
companion object {
fun create(
testScope: TestScope,
context: Context,
inputProxy: MultiShadeInputProxy,
): MultiShadeInteractor {
return MultiShadeInteractor(
applicationScope = testScope.backgroundScope,
repository =
MultiShadeRepositoryTest.create(
context = context,
inputProxy = inputProxy,
),
inputProxy = inputProxy,
)
}
}
}

View File

@@ -1,530 +0,0 @@
/*
* 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.multishade.domain.interactor
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.data.repository.MultiShadeRepository
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.shared.model.ShadeId
import com.android.systemui.shade.ShadeController
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
private lateinit var underTest: MultiShadeMotionEventInteractor
private lateinit var testScope: TestScope
private lateinit var motionEvents: MutableSet<MotionEvent>
private lateinit var repository: MultiShadeRepository
private lateinit var interactor: MultiShadeInteractor
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
private lateinit var falsingManager: FalsingManagerFake
@Mock private lateinit var shadeController: ShadeController
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
testScope = TestScope()
motionEvents = mutableSetOf()
val inputProxy = MultiShadeInputProxy()
repository =
MultiShadeRepository(
applicationContext = context,
inputProxy = inputProxy,
)
interactor =
MultiShadeInteractor(
applicationScope = testScope.backgroundScope,
repository = repository,
inputProxy = inputProxy,
)
val featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.DUAL_SHADE, true)
keyguardTransitionRepository = FakeKeyguardTransitionRepository()
falsingManager = FalsingManagerFake()
underTest =
MultiShadeMotionEventInteractor(
applicationContext = context,
applicationScope = testScope.backgroundScope,
multiShadeInteractor = interactor,
featureFlags = featureFlags,
keyguardTransitionInteractor =
KeyguardTransitionInteractorFactory.create(
scope = TestScope().backgroundScope,
repository = keyguardTransitionRepository,
)
.keyguardTransitionInteractor,
falsingManager = falsingManager,
shadeController = shadeController,
)
}
@After
fun tearDown() {
motionEvents.forEach { motionEvent -> motionEvent.recycle() }
}
@Test
fun listenForIsAnyShadeExpanded_expanded_makesWindowViewVisible() =
testScope.runTest {
whenever(shadeController.isKeyguard).thenReturn(false)
repository.setExpansion(ShadeId.LEFT, 0.1f)
val expanded by collectLastValue(interactor.isAnyShadeExpanded)
assertThat(expanded).isTrue()
verify(shadeController).makeExpandedVisible(anyBoolean())
}
@Test
fun listenForIsAnyShadeExpanded_collapsed_makesWindowViewInvisible() =
testScope.runTest {
whenever(shadeController.isKeyguard).thenReturn(false)
repository.setForceCollapseAll(true)
val expanded by collectLastValue(interactor.isAnyShadeExpanded)
assertThat(expanded).isFalse()
verify(shadeController).makeExpandedInvisible()
}
@Test
fun listenForIsAnyShadeExpanded_collapsedOnKeyguard_makesWindowViewVisible() =
testScope.runTest {
whenever(shadeController.isKeyguard).thenReturn(true)
repository.setForceCollapseAll(true)
val expanded by collectLastValue(interactor.isAnyShadeExpanded)
assertThat(expanded).isFalse()
verify(shadeController).makeExpandedVisible(anyBoolean())
}
@Test
fun shouldIntercept_initialDown_returnsFalse() =
testScope.runTest {
assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))).isFalse()
}
@Test
fun shouldIntercept_moveBelowTouchSlop_returnsFalse() =
testScope.runTest {
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
assertThat(
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_MOVE,
y = touchSlop - 1f,
)
)
)
.isFalse()
}
@Test
fun shouldIntercept_moveAboveTouchSlop_returnsTrue() =
testScope.runTest {
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
assertThat(
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_MOVE,
y = touchSlop + 1f,
)
)
)
.isTrue()
}
@Test
fun shouldIntercept_moveAboveTouchSlop_butHorizontalFirst_returnsFalse() =
testScope.runTest {
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
assertThat(
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_MOVE,
x = touchSlop + 1f,
)
)
)
.isFalse()
assertThat(
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_MOVE,
y = touchSlop + 1f,
)
)
)
.isFalse()
}
@Test
fun shouldIntercept_up_afterMovedAboveTouchSlop_returnsTrue() =
testScope.runTest {
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f))
assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isTrue()
}
@Test
fun shouldIntercept_cancel_afterMovedAboveTouchSlop_returnsTrue() =
testScope.runTest {
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f))
assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isTrue()
}
@Test
fun shouldIntercept_moveAboveTouchSlopAndUp_butShadeExpanded_returnsFalse() =
testScope.runTest {
repository.setExpansion(ShadeId.LEFT, 0.1f)
runCurrent()
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
assertThat(
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_MOVE,
y = touchSlop + 1f,
)
)
)
.isFalse()
assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse()
}
@Test
fun shouldIntercept_moveAboveTouchSlopAndCancel_butShadeExpanded_returnsFalse() =
testScope.runTest {
repository.setExpansion(ShadeId.LEFT, 0.1f)
runCurrent()
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
assertThat(
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_MOVE,
y = touchSlop + 1f,
)
)
)
.isFalse()
assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse()
}
@Test
fun shouldIntercept_moveAboveTouchSlopAndUp_butBouncerShowing_returnsFalse() =
testScope.runTest {
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.PRIMARY_BOUNCER,
value = 0.1f,
transitionState = TransitionState.STARTED,
)
)
runCurrent()
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
assertThat(
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_MOVE,
y = touchSlop + 1f,
)
)
)
.isFalse()
assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse()
}
@Test
fun shouldIntercept_moveAboveTouchSlopAndCancel_butBouncerShowing_returnsFalse() =
testScope.runTest {
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.PRIMARY_BOUNCER,
value = 0.1f,
transitionState = TransitionState.STARTED,
)
)
runCurrent()
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
assertThat(
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_MOVE,
y = touchSlop + 1f,
)
)
)
.isFalse()
assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse()
}
@Test
fun tap_doesNotSendProxiedInput() =
testScope.runTest {
val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
}
@Test
fun dragBelowTouchSlop_doesNotSendProxiedInput() =
testScope.runTest {
val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop - 1f))
underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
}
@Test
fun dragShadeAboveTouchSlopAndUp() =
testScope.runTest {
val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_DOWN,
x = 100f, // left shade
)
)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
val yDragAmountPx = touchSlop + 1f
val moveEvent =
motionEvent(
MotionEvent.ACTION_MOVE,
x = 100f, // left shade
y = yDragAmountPx,
)
assertThat(underTest.shouldIntercept(moveEvent)).isTrue()
underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
assertThat(leftShadeProxiedInput)
.isEqualTo(
ProxiedInputModel.OnDrag(
xFraction = 0.1f,
yDragAmountPx = yDragAmountPx,
)
)
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
val upEvent = motionEvent(MotionEvent.ACTION_UP)
assertThat(underTest.shouldIntercept(upEvent)).isTrue()
underTest.onTouchEvent(upEvent, viewWidthPx = 1000)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
}
@Test
fun dragShadeAboveTouchSlopAndCancel() =
testScope.runTest {
val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_DOWN,
x = 900f, // right shade
)
)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
val yDragAmountPx = touchSlop + 1f
val moveEvent =
motionEvent(
MotionEvent.ACTION_MOVE,
x = 900f, // right shade
y = yDragAmountPx,
)
assertThat(underTest.shouldIntercept(moveEvent)).isTrue()
underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput)
.isEqualTo(
ProxiedInputModel.OnDrag(
xFraction = 0.9f,
yDragAmountPx = yDragAmountPx,
)
)
assertThat(singleShadeProxiedInput).isNull()
val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL)
assertThat(underTest.shouldIntercept(cancelEvent)).isTrue()
underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
}
@Test
fun dragUp_withUp_doesNotShowShade() =
testScope.runTest {
val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_DOWN,
x = 100f, // left shade
)
)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
val yDragAmountPx = -(touchSlop + 1f) // dragging up
val moveEvent =
motionEvent(
MotionEvent.ACTION_MOVE,
x = 100f, // left shade
y = yDragAmountPx,
)
assertThat(underTest.shouldIntercept(moveEvent)).isFalse()
underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
val upEvent = motionEvent(MotionEvent.ACTION_UP)
assertThat(underTest.shouldIntercept(upEvent)).isFalse()
underTest.onTouchEvent(upEvent, viewWidthPx = 1000)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
}
@Test
fun dragUp_withCancel_falseTouch_showsThenHidesBouncer() =
testScope.runTest {
val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
underTest.shouldIntercept(
motionEvent(
MotionEvent.ACTION_DOWN,
x = 900f, // right shade
)
)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
val yDragAmountPx = -(touchSlop + 1f) // drag up
val moveEvent =
motionEvent(
MotionEvent.ACTION_MOVE,
x = 900f, // right shade
y = yDragAmountPx,
)
assertThat(underTest.shouldIntercept(moveEvent)).isFalse()
underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
falsingManager.setIsFalseTouch(true)
val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL)
assertThat(underTest.shouldIntercept(cancelEvent)).isFalse()
underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
}
private fun TestScope.motionEvent(
action: Int,
downTime: Long = currentTime,
eventTime: Long = currentTime,
x: Float = 0f,
y: Float = 0f,
): MotionEvent {
val motionEvent = MotionEvent.obtain(downTime, eventTime, action, x, y, 0)
motionEvents.add(motionEvent)
return motionEvent
}
}

View File

@@ -1,68 +0,0 @@
/*
* 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.multishade.shared.math
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@SmallTest
@RunWith(JUnit4::class)
class MathTest : SysuiTestCase() {
@Test
fun isZero_zero_true() {
assertThat(0f.isZero(epsilon = EPSILON)).isTrue()
}
@Test
fun isZero_belowPositiveEpsilon_true() {
assertThat((EPSILON * 0.999999f).isZero(epsilon = EPSILON)).isTrue()
}
@Test
fun isZero_aboveNegativeEpsilon_true() {
assertThat((EPSILON * -0.999999f).isZero(epsilon = EPSILON)).isTrue()
}
@Test
fun isZero_positiveEpsilon_false() {
assertThat(EPSILON.isZero(epsilon = EPSILON)).isFalse()
}
@Test
fun isZero_negativeEpsilon_false() {
assertThat((-EPSILON).isZero(epsilon = EPSILON)).isFalse()
}
@Test
fun isZero_positive_false() {
assertThat(1f.isZero(epsilon = EPSILON)).isFalse()
}
@Test
fun isZero_negative_false() {
assertThat((-1f).isZero(epsilon = EPSILON)).isFalse()
}
companion object {
private const val EPSILON = 0.0001f
}
}

View File

@@ -1,127 +0,0 @@
/*
* 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.multishade.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class MultiShadeViewModelTest : SysuiTestCase() {
private lateinit var testScope: TestScope
private lateinit var inputProxy: MultiShadeInputProxy
@Before
fun setUp() {
testScope = TestScope()
inputProxy = MultiShadeInputProxy()
}
@Test
fun scrim_whenDualShadeCollapsed() =
testScope.runTest {
val alpha = 0.5f
overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
overrideResource(R.bool.dual_shade_enabled, true)
val underTest = create()
val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
assertThat(scrimAlpha).isZero()
assertThat(isScrimEnabled).isFalse()
}
@Test
fun scrim_whenDualShadeExpanded() =
testScope.runTest {
val alpha = 0.5f
overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
overrideResource(R.bool.dual_shade_enabled, true)
val underTest = create()
val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
assertThat(scrimAlpha).isZero()
assertThat(isScrimEnabled).isFalse()
underTest.leftShade.onExpansionChanged(0.5f)
assertThat(scrimAlpha).isEqualTo(alpha * 0.5f)
assertThat(isScrimEnabled).isTrue()
underTest.rightShade.onExpansionChanged(1f)
assertThat(scrimAlpha).isEqualTo(alpha * 1f)
assertThat(isScrimEnabled).isTrue()
}
@Test
fun scrim_whenSingleShadeCollapsed() =
testScope.runTest {
val alpha = 0.5f
overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
overrideResource(R.bool.dual_shade_enabled, false)
val underTest = create()
val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
assertThat(scrimAlpha).isZero()
assertThat(isScrimEnabled).isFalse()
}
@Test
fun scrim_whenSingleShadeExpanded() =
testScope.runTest {
val alpha = 0.5f
overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
overrideResource(R.bool.dual_shade_enabled, false)
val underTest = create()
val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
underTest.singleShade.onExpansionChanged(0.95f)
assertThat(scrimAlpha).isZero()
assertThat(isScrimEnabled).isFalse()
}
private fun create(): MultiShadeViewModel {
return MultiShadeViewModel(
viewModelScope = testScope.backgroundScope,
interactor =
MultiShadeInteractorTest.create(
testScope = testScope,
context = context,
inputProxy = inputProxy,
),
)
}
}

View File

@@ -1,226 +0,0 @@
/*
* 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.multishade.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
import com.android.systemui.multishade.shared.model.ProxiedInputModel
import com.android.systemui.multishade.shared.model.ShadeId
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class ShadeViewModelTest : SysuiTestCase() {
private lateinit var testScope: TestScope
private lateinit var inputProxy: MultiShadeInputProxy
private var interactor: MultiShadeInteractor? = null
@Before
fun setUp() {
testScope = TestScope()
inputProxy = MultiShadeInputProxy()
}
@Test
fun isVisible_dualShadeConfig() =
testScope.runTest {
overrideResource(R.bool.dual_shade_enabled, true)
val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
assertThat(isLeftShadeVisible).isTrue()
assertThat(isRightShadeVisible).isTrue()
assertThat(isSingleShadeVisible).isFalse()
}
@Test
fun isVisible_singleShadeConfig() =
testScope.runTest {
overrideResource(R.bool.dual_shade_enabled, false)
val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
assertThat(isLeftShadeVisible).isFalse()
assertThat(isRightShadeVisible).isFalse()
assertThat(isSingleShadeVisible).isTrue()
}
@Test
fun isSwipingEnabled() =
testScope.runTest {
val underTest = create(ShadeId.LEFT)
val isSwipingEnabled: Boolean? by collectLastValue(underTest.isSwipingEnabled)
assertWithMessage("isSwipingEnabled should start as true!")
.that(isSwipingEnabled)
.isTrue()
// Need to collect proxied input so the flows become hot as the gesture cancelation code
// logic sits in side the proxiedInput flow for each shade.
collectLastValue(underTest.proxiedInput)
collectLastValue(create(ShadeId.RIGHT).proxiedInput)
// Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
// the
// same shade.
inputProxy.onProxiedInput(
ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
)
assertThat(isSwipingEnabled).isFalse()
// Registering the end of the proxied interaction re-allows it.
inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
assertThat(isSwipingEnabled).isTrue()
// Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
// disallowing non-proxied input on the LEFT shade.
inputProxy.onProxiedInput(
ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
)
assertThat(isSwipingEnabled).isFalse()
// Registering the end of the interaction on the RIGHT shade re-allows it.
inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
assertThat(isSwipingEnabled).isTrue()
}
@Test
fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
testScope.runTest {
val leftShade = create(ShadeId.LEFT)
val rightShade = create(ShadeId.RIGHT)
val isLeftShadeForceCollapsed: Boolean? by collectLastValue(leftShade.isForceCollapsed)
val isRightShadeForceCollapsed: Boolean? by
collectLastValue(rightShade.isForceCollapsed)
val isSingleShadeForceCollapsed: Boolean? by
collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
assertWithMessage("isForceCollapsed should start as false!")
.that(isLeftShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isRightShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isSingleShadeForceCollapsed)
.isFalse()
// Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
// shade.
rightShade.onDragStarted()
assertThat(isLeftShadeForceCollapsed).isTrue()
assertThat(isRightShadeForceCollapsed).isFalse()
assertThat(isSingleShadeForceCollapsed).isFalse()
// Registering the end of the interaction on the RIGHT shade re-allows it.
rightShade.onDragEnded()
assertThat(isLeftShadeForceCollapsed).isFalse()
assertThat(isRightShadeForceCollapsed).isFalse()
assertThat(isSingleShadeForceCollapsed).isFalse()
// Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
// shade.
leftShade.onDragStarted()
assertThat(isLeftShadeForceCollapsed).isFalse()
assertThat(isRightShadeForceCollapsed).isTrue()
assertThat(isSingleShadeForceCollapsed).isFalse()
// Registering the end of the interaction on the LEFT shade re-allows it.
leftShade.onDragEnded()
assertThat(isLeftShadeForceCollapsed).isFalse()
assertThat(isRightShadeForceCollapsed).isFalse()
assertThat(isSingleShadeForceCollapsed).isFalse()
}
@Test
fun onTapOutside_collapsesAll() =
testScope.runTest {
val isLeftShadeForceCollapsed: Boolean? by
collectLastValue(create(ShadeId.LEFT).isForceCollapsed)
val isRightShadeForceCollapsed: Boolean? by
collectLastValue(create(ShadeId.RIGHT).isForceCollapsed)
val isSingleShadeForceCollapsed: Boolean? by
collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
assertWithMessage("isForceCollapsed should start as false!")
.that(isLeftShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isRightShadeForceCollapsed)
.isFalse()
assertWithMessage("isForceCollapsed should start as false!")
.that(isSingleShadeForceCollapsed)
.isFalse()
inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
assertThat(isLeftShadeForceCollapsed).isTrue()
assertThat(isRightShadeForceCollapsed).isTrue()
assertThat(isSingleShadeForceCollapsed).isTrue()
}
@Test
fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
testScope.runTest {
val underTest = create(ShadeId.RIGHT)
val proxiedInput: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
underTest.onDragStarted()
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
assertThat(proxiedInput).isNull()
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
assertThat(proxiedInput).isNull()
underTest.onDragEnded()
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
assertThat(proxiedInput).isNotNull()
}
private fun create(
shadeId: ShadeId,
): ShadeViewModel {
return ShadeViewModel(
viewModelScope = testScope.backgroundScope,
shadeId = shadeId,
interactor = interactor
?: MultiShadeInteractorTest.create(
testScope = testScope,
context = context,
inputProxy = inputProxy,
)
.also { interactor = it },
)
}
}

View File

@@ -108,7 +108,6 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
import com.android.systemui.model.SysUiState;
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.ActivityStarter;
@@ -299,7 +298,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
@Mock protected GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
@Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@Mock protected MultiShadeInteractor mMultiShadeInteractor;
@Mock protected KeyguardLongPressViewModel mKeyuardLongPressViewModel;
@Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor;
@Mock protected MotionEvent mDownMotionEvent;
@@ -615,7 +613,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mLockscreenToOccludedTransitionViewModel,
mMainDispatcher,
mKeyguardTransitionInteractor,
() -> mMultiShadeInteractor,
mDumpManager,
mKeyuardLongPressViewModel,
mKeyguardInteractor,

View File

@@ -34,21 +34,15 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.log.BouncerLogger
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.data.repository.MultiShadeRepository
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -67,6 +61,7 @@ import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
@@ -80,9 +75,8 @@ import org.mockito.Mockito.anyFloat
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.Optional
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -117,8 +111,9 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
@Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
@Mock private lateinit var unfoldTransitionProgressProvider:
Optional<UnfoldTransitionProgressProvider>
@Mock
private lateinit var unfoldTransitionProgressProvider:
Optional<UnfoldTransitionProgressProvider>
@Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@Mock
lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
@@ -146,23 +141,11 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
val featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
val inputProxy = MultiShadeInputProxy()
testScope = TestScope()
val multiShadeInteractor =
MultiShadeInteractor(
applicationScope = testScope.backgroundScope,
repository =
MultiShadeRepository(
applicationContext = context,
inputProxy = inputProxy,
),
inputProxy = inputProxy,
)
underTest =
NotificationShadeWindowViewController(
lockscreenShadeTransitionController,
@@ -193,25 +176,14 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
{ multiShadeInteractor },
FakeSystemClock(),
{
MultiShadeMotionEventInteractor(
applicationContext = context,
applicationScope = testScope.backgroundScope,
multiShadeInteractor = multiShadeInteractor,
featureFlags = featureFlags,
keyguardTransitionInteractor =
KeyguardTransitionInteractorFactory.create(
scope = TestScope().backgroundScope,
).keyguardTransitionInteractor,
falsingManager = FalsingManagerFake(),
shadeController = shadeController,
)
},
BouncerMessageInteractor(FakeBouncerMessageRepository(),
mock(BouncerMessageFactory::class.java),
FakeUserRepository(), CountDownTimerUtil(), featureFlags),
BouncerMessageInteractor(
FakeBouncerMessageRepository(),
mock(BouncerMessageFactory::class.java),
FakeUserRepository(),
CountDownTimerUtil(),
featureFlags
),
BouncerLogger(logcatLogBuffer("BouncerLog"))
)
underTest.setupExpandedStatusBar()

View File

@@ -34,20 +34,14 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.log.BouncerLogger
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.data.repository.MultiShadeRepository
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
import com.android.systemui.statusbar.DragDownHelper
@@ -70,7 +64,6 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -85,7 +78,6 @@ import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -160,22 +152,10 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
val featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
val inputProxy = MultiShadeInputProxy()
testScope = TestScope()
val multiShadeInteractor =
MultiShadeInteractor(
applicationScope = testScope.backgroundScope,
repository =
MultiShadeRepository(
applicationContext = context,
inputProxy = inputProxy,
),
inputProxy = inputProxy,
)
controller =
NotificationShadeWindowViewController(
lockscreenShadeTransitionController,
@@ -206,23 +186,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
{ multiShadeInteractor },
FakeSystemClock(),
{
MultiShadeMotionEventInteractor(
applicationContext = context,
applicationScope = testScope.backgroundScope,
multiShadeInteractor = multiShadeInteractor,
featureFlags = featureFlags,
keyguardTransitionInteractor =
KeyguardTransitionInteractorFactory.create(
scope = TestScope().backgroundScope,
)
.keyguardTransitionInteractor,
falsingManager = FalsingManagerFake(),
shadeController = shadeController,
)
},
BouncerMessageInteractor(
FakeBouncerMessageRepository(),
Mockito.mock(BouncerMessageFactory::class.java),