Merge "[multishade] Deletes unused multi-shade code." into udc-qpr-dev
This commit is contained in:
@@ -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
|
|
||||||
}
|
|
||||||
@@ -21,13 +21,11 @@ import android.content.Context
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
|
|
||||||
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||||
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
|
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
|
||||||
import com.android.systemui.scene.shared.model.Scene
|
import com.android.systemui.scene.shared.model.Scene
|
||||||
import com.android.systemui.scene.shared.model.SceneKey
|
import com.android.systemui.scene.shared.model.SceneKey
|
||||||
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
|
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
|
||||||
import com.android.systemui.util.time.SystemClock
|
|
||||||
|
|
||||||
/** The Compose facade, when Compose is *not* available. */
|
/** The Compose facade, when Compose is *not* available. */
|
||||||
object ComposeFacade : BaseComposeFacade {
|
object ComposeFacade : BaseComposeFacade {
|
||||||
@@ -53,14 +51,6 @@ object ComposeFacade : BaseComposeFacade {
|
|||||||
throwComposeUnavailableError()
|
throwComposeUnavailableError()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createMultiShadeView(
|
|
||||||
context: Context,
|
|
||||||
viewModel: MultiShadeViewModel,
|
|
||||||
clock: SystemClock,
|
|
||||||
): View {
|
|
||||||
throwComposeUnavailableError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createSceneContainerView(
|
override fun createSceneContainerView(
|
||||||
context: Context,
|
context: Context,
|
||||||
viewModel: SceneContainerViewModel,
|
viewModel: SceneContainerViewModel,
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.android.compose.theme.PlatformTheme
|
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.compose.PeopleScreen
|
||||||
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||||
import com.android.systemui.qs.footer.ui.compose.FooterActions
|
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.ComposableScene
|
||||||
import com.android.systemui.scene.ui.composable.SceneContainer
|
import com.android.systemui.scene.ui.composable.SceneContainer
|
||||||
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
|
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
|
||||||
import com.android.systemui.util.time.SystemClock
|
|
||||||
|
|
||||||
/** The Compose facade, when Compose is available. */
|
/** The Compose facade, when Compose is available. */
|
||||||
object ComposeFacade : BaseComposeFacade {
|
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(
|
override fun createSceneContainerView(
|
||||||
context: Context,
|
context: Context,
|
||||||
viewModel: SceneContainerViewModel,
|
viewModel: SceneContainerViewModel,
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -21,13 +21,11 @@ import android.content.Context
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
|
|
||||||
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||||
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
|
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
|
||||||
import com.android.systemui.scene.shared.model.Scene
|
import com.android.systemui.scene.shared.model.Scene
|
||||||
import com.android.systemui.scene.shared.model.SceneKey
|
import com.android.systemui.scene.shared.model.SceneKey
|
||||||
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
|
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.
|
* A facade to interact with Compose, when it is available.
|
||||||
@@ -63,13 +61,6 @@ interface BaseComposeFacade {
|
|||||||
qsVisibilityLifecycleOwner: LifecycleOwner,
|
qsVisibilityLifecycleOwner: LifecycleOwner,
|
||||||
): View
|
): 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. */
|
/** Create a [View] to represent [viewModel] on screen. */
|
||||||
fun createSceneContainerView(
|
fun createSceneContainerView(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ object Flags {
|
|||||||
// TODO(b/278873737): Tracking Bug
|
// TODO(b/278873737): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val LOAD_NOTIFICATIONS_BEFORE_THE_USER_SWITCH_IS_COMPLETE =
|
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
|
// TODO(b/277338665): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -92,11 +92,7 @@ object Flags {
|
|||||||
// TODO(b/288326013): Tracking Bug
|
// TODO(b/288326013): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION =
|
val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION =
|
||||||
unreleasedFlag(
|
unreleasedFlag(288326013, "notification_async_hybrid_view_inflation", teamfood = false)
|
||||||
288326013,
|
|
||||||
"notification_async_hybrid_view_inflation",
|
|
||||||
teamfood = false
|
|
||||||
)
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val ANIMATED_NOTIFICATION_SHADE_INSETS =
|
val ANIMATED_NOTIFICATION_SHADE_INSETS =
|
||||||
@@ -104,18 +100,17 @@ object Flags {
|
|||||||
|
|
||||||
// TODO(b/268005230): Tracking Bug
|
// TODO(b/268005230): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val SENSITIVE_REVEAL_ANIM =
|
val SENSITIVE_REVEAL_ANIM = unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
|
||||||
unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
|
|
||||||
|
|
||||||
// TODO(b/280783617): Tracking Bug
|
// TODO(b/280783617): Tracking Bug
|
||||||
@Keep
|
@Keep
|
||||||
@JvmField
|
@JvmField
|
||||||
val BUILDER_EXTRAS_OVERRIDE =
|
val BUILDER_EXTRAS_OVERRIDE =
|
||||||
sysPropBooleanFlag(
|
sysPropBooleanFlag(
|
||||||
128,
|
128,
|
||||||
"persist.sysui.notification.builder_extras_override",
|
"persist.sysui.notification.builder_extras_override",
|
||||||
default = false
|
default = false
|
||||||
)
|
)
|
||||||
|
|
||||||
// 200 - keyguard/lockscreen
|
// 200 - keyguard/lockscreen
|
||||||
// ** Flag retired **
|
// ** Flag retired **
|
||||||
@@ -133,16 +128,17 @@ object Flags {
|
|||||||
|
|
||||||
// TODO(b/254512676): Tracking Bug
|
// TODO(b/254512676): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val LOCKSCREEN_CUSTOM_CLOCKS = resourceBooleanFlag(
|
val LOCKSCREEN_CUSTOM_CLOCKS =
|
||||||
207,
|
resourceBooleanFlag(
|
||||||
R.bool.config_enableLockScreenCustomClocks,
|
207,
|
||||||
"lockscreen_custom_clocks"
|
R.bool.config_enableLockScreenCustomClocks,
|
||||||
)
|
"lockscreen_custom_clocks"
|
||||||
|
)
|
||||||
|
|
||||||
// TODO(b/275694445): Tracking Bug
|
// TODO(b/275694445): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING = releasedFlag(208,
|
val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING =
|
||||||
"lockscreen_without_secure_lock_when_dreaming")
|
releasedFlag(208, "lockscreen_without_secure_lock_when_dreaming")
|
||||||
|
|
||||||
// TODO(b/286092087): Tracking Bug
|
// TODO(b/286092087): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -156,8 +152,7 @@ object Flags {
|
|||||||
* Whether the clock on a wide lock screen should use the new "stepping" animation for moving
|
* Whether the clock on a wide lock screen should use the new "stepping" animation for moving
|
||||||
* the digits when the clock moves.
|
* the digits when the clock moves.
|
||||||
*/
|
*/
|
||||||
@JvmField
|
@JvmField val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
|
||||||
val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
|
* 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")
|
@JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
|
||||||
|
|
||||||
// TODO(b/262780002): Tracking Bug
|
// TODO(b/262780002): Tracking Bug
|
||||||
@JvmField
|
@JvmField val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
|
||||||
val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
|
|
||||||
|
|
||||||
// flag for controlling auto pin confirmation and material u shapes in bouncer
|
// flag for controlling auto pin confirmation and material u shapes in bouncer
|
||||||
@JvmField
|
@JvmField
|
||||||
val AUTO_PIN_CONFIRMATION =
|
val AUTO_PIN_CONFIRMATION = releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
|
||||||
releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
|
|
||||||
|
|
||||||
// TODO(b/262859270): Tracking Bug
|
// TODO(b/262859270): Tracking Bug
|
||||||
@JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded")
|
@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. */
|
/** Whether the long-press gesture to open wallpaper picker is enabled. */
|
||||||
// TODO(b/266242192): Tracking Bug
|
// TODO(b/266242192): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val LOCK_SCREEN_LONG_PRESS_ENABLED =
|
val LOCK_SCREEN_LONG_PRESS_ENABLED = releasedFlag(228, "lock_screen_long_press_enabled")
|
||||||
releasedFlag(
|
|
||||||
228,
|
|
||||||
"lock_screen_long_press_enabled"
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Enables UI updates for AI wallpapers in the wallpaper picker. */
|
/** Enables UI updates for AI wallpapers in the wallpaper picker. */
|
||||||
// TODO(b/267722622): Tracking Bug
|
// TODO(b/267722622): Tracking Bug
|
||||||
@JvmField
|
@JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag(229, "wallpaper_picker_ui_for_aiwp")
|
||||||
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. */
|
/** Whether to use a new data source for intents to run on keyguard dismissal. */
|
||||||
// TODO(b/275069969): Tracking bug.
|
// TODO(b/275069969): Tracking bug.
|
||||||
@@ -239,27 +223,21 @@ object Flags {
|
|||||||
|
|
||||||
/** Provide new auth messages on the bouncer. */
|
/** Provide new auth messages on the bouncer. */
|
||||||
// TODO(b/277961132): Tracking bug.
|
// TODO(b/277961132): Tracking bug.
|
||||||
@JvmField
|
@JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag(234, "revamped_bouncer_messages")
|
||||||
val REVAMPED_BOUNCER_MESSAGES =
|
|
||||||
unreleasedFlag(234, "revamped_bouncer_messages")
|
|
||||||
|
|
||||||
/** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
|
/** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
|
||||||
// TODO(b/279794160): Tracking bug.
|
// TODO(b/279794160): Tracking bug.
|
||||||
@JvmField
|
@JvmField val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
|
||||||
val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
|
|
||||||
|
|
||||||
|
|
||||||
/** Keyguard Migration */
|
/** Keyguard Migration */
|
||||||
|
|
||||||
/** Migrate the indication area to the new keyguard root view. */
|
/** Migrate the indication area to the new keyguard root view. */
|
||||||
// TODO(b/280067944): Tracking bug.
|
// TODO(b/280067944): Tracking bug.
|
||||||
@JvmField
|
@JvmField val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area")
|
||||||
val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate the bottom area to the new keyguard root view.
|
* Migrate the bottom area to the new keyguard root view. Because there is no such thing as a
|
||||||
* Because there is no such thing as a "bottom area" after this, this also breaks it up into
|
* "bottom area" after this, this also breaks it up into many smaller, modular pieces.
|
||||||
* many smaller, modular pieces.
|
|
||||||
*/
|
*/
|
||||||
// TODO(b/290652751): Tracking bug.
|
// TODO(b/290652751): Tracking bug.
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -268,36 +246,29 @@ object Flags {
|
|||||||
|
|
||||||
/** Whether to listen for fingerprint authentication over keyguard occluding activities. */
|
/** Whether to listen for fingerprint authentication over keyguard occluding activities. */
|
||||||
// TODO(b/283260512): Tracking bug.
|
// TODO(b/283260512): Tracking bug.
|
||||||
@JvmField
|
@JvmField val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
|
||||||
val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
|
|
||||||
|
|
||||||
/** Flag meant to guard the talkback fix for the KeyguardIndicationTextView */
|
/** Flag meant to guard the talkback fix for the KeyguardIndicationTextView */
|
||||||
// TODO(b/286563884): Tracking bug
|
// TODO(b/286563884): Tracking bug
|
||||||
@JvmField
|
@JvmField val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
|
||||||
val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
|
|
||||||
|
|
||||||
// TODO(b/287268101): Tracking bug.
|
// TODO(b/287268101): Tracking bug.
|
||||||
@JvmField
|
@JvmField val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
|
||||||
val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
|
|
||||||
|
|
||||||
/** Migrate the lock icon view to the new keyguard root view. */
|
/** Migrate the lock icon view to the new keyguard root view. */
|
||||||
// TODO(b/286552209): Tracking bug.
|
// TODO(b/286552209): Tracking bug.
|
||||||
@JvmField
|
@JvmField val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
|
||||||
val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
|
|
||||||
|
|
||||||
// TODO(b/288276738): Tracking bug.
|
// TODO(b/288276738): Tracking bug.
|
||||||
@JvmField
|
@JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
|
||||||
val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
|
|
||||||
|
|
||||||
/** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */
|
/** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */
|
||||||
// TODO(b/288074305): Tracking bug.
|
// TODO(b/288074305): Tracking bug.
|
||||||
@JvmField
|
@JvmField val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
|
||||||
val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
|
|
||||||
|
|
||||||
/** Migrate the status view from the notification panel to keyguard root view. */
|
/** Migrate the status view from the notification panel to keyguard root view. */
|
||||||
// TODO(b/291767565): Tracking bug.
|
// TODO(b/291767565): Tracking bug.
|
||||||
@JvmField
|
@JvmField val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view")
|
||||||
val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view")
|
|
||||||
|
|
||||||
// 300 - power menu
|
// 300 - power menu
|
||||||
// TODO(b/254512600): Tracking Bug
|
// TODO(b/254512600): Tracking Bug
|
||||||
@@ -316,8 +287,7 @@ object Flags {
|
|||||||
|
|
||||||
// TODO(b/270223352): Tracking Bug
|
// TODO(b/270223352): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val HIDE_SMARTSPACE_ON_DREAM_OVERLAY =
|
val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = releasedFlag(404, "hide_smartspace_on_dream_overlay")
|
||||||
releasedFlag(404, "hide_smartspace_on_dream_overlay")
|
|
||||||
|
|
||||||
// TODO(b/271460958): Tracking Bug
|
// TODO(b/271460958): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -357,8 +327,7 @@ object Flags {
|
|||||||
|
|
||||||
/** Enables Font Scaling Quick Settings tile */
|
/** Enables Font Scaling Quick Settings tile */
|
||||||
// TODO(b/269341316): Tracking Bug
|
// TODO(b/269341316): Tracking Bug
|
||||||
@JvmField
|
@JvmField val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
|
||||||
val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
|
|
||||||
|
|
||||||
/** Enables new QS Edit Mode visual refresh */
|
/** Enables new QS Edit Mode visual refresh */
|
||||||
// TODO(b/269787742): Tracking Bug
|
// TODO(b/269787742): Tracking Bug
|
||||||
@@ -367,13 +336,11 @@ object Flags {
|
|||||||
|
|
||||||
// 600- status bar
|
// 600- status bar
|
||||||
|
|
||||||
|
|
||||||
// TODO(b/265892345): Tracking Bug
|
// TODO(b/265892345): Tracking Bug
|
||||||
val PLUG_IN_STATUS_BAR_CHIP = releasedFlag(265892345, "plug_in_status_bar_chip")
|
val PLUG_IN_STATUS_BAR_CHIP = releasedFlag(265892345, "plug_in_status_bar_chip")
|
||||||
|
|
||||||
// TODO(b/280426085): Tracking Bug
|
// TODO(b/280426085): Tracking Bug
|
||||||
@JvmField
|
@JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
|
||||||
val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
|
|
||||||
|
|
||||||
// 700 - dialer/calls
|
// 700 - dialer/calls
|
||||||
// TODO(b/254512734): Tracking Bug
|
// TODO(b/254512734): Tracking Bug
|
||||||
@@ -441,8 +408,8 @@ object Flags {
|
|||||||
|
|
||||||
// TODO(b/273509374): Tracking Bug
|
// TODO(b/273509374): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS = releasedFlag(1006,
|
val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS =
|
||||||
"always_show_home_controls_on_dreams")
|
releasedFlag(1006, "always_show_home_controls_on_dreams")
|
||||||
|
|
||||||
// 1100 - windowing
|
// 1100 - windowing
|
||||||
@Keep
|
@Keep
|
||||||
@@ -490,11 +457,7 @@ object Flags {
|
|||||||
@Keep
|
@Keep
|
||||||
@JvmField
|
@JvmField
|
||||||
val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
|
val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
|
||||||
sysPropBooleanFlag(
|
sysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", default = true)
|
||||||
1110,
|
|
||||||
"persist.wm.debug.enable_pip_keep_clear_algorithm",
|
|
||||||
default = true
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO(b/256873975): Tracking Bug
|
// TODO(b/256873975): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -532,7 +495,8 @@ object Flags {
|
|||||||
|
|
||||||
// TODO(b/273443374): Tracking Bug
|
// TODO(b/273443374): Tracking Bug
|
||||||
@Keep
|
@Keep
|
||||||
@JvmField val LOCKSCREEN_LIVE_WALLPAPER =
|
@JvmField
|
||||||
|
val LOCKSCREEN_LIVE_WALLPAPER =
|
||||||
sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = true)
|
sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = true)
|
||||||
|
|
||||||
// TODO(b/281648899): Tracking bug
|
// TODO(b/281648899): Tracking bug
|
||||||
@@ -547,7 +511,6 @@ object Flags {
|
|||||||
val ENABLE_PIP2_IMPLEMENTATION =
|
val ENABLE_PIP2_IMPLEMENTATION =
|
||||||
sysPropBooleanFlag(1119, "persist.wm.debug.enable_pip2_implementation", default = false)
|
sysPropBooleanFlag(1119, "persist.wm.debug.enable_pip2_implementation", default = false)
|
||||||
|
|
||||||
|
|
||||||
// 1200 - predictive back
|
// 1200 - predictive back
|
||||||
@Keep
|
@Keep
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -573,8 +536,7 @@ object Flags {
|
|||||||
unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true)
|
unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true)
|
||||||
|
|
||||||
// TODO(b/270987164): Tracking Bug
|
// TODO(b/270987164): Tracking Bug
|
||||||
@JvmField
|
@JvmField val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
|
||||||
val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
|
|
||||||
|
|
||||||
// TODO(b/263826204): Tracking Bug
|
// TODO(b/263826204): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -597,8 +559,7 @@ object Flags {
|
|||||||
unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
|
unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
|
||||||
|
|
||||||
// TODO(b/273800936): Tracking Bug
|
// TODO(b/273800936): Tracking Bug
|
||||||
@JvmField
|
@JvmField val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
|
||||||
val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
|
|
||||||
|
|
||||||
// 1300 - screenshots
|
// 1300 - screenshots
|
||||||
// TODO(b/264916608): Tracking Bug
|
// TODO(b/264916608): Tracking Bug
|
||||||
@@ -623,16 +584,12 @@ object Flags {
|
|||||||
// 1700 - clipboard
|
// 1700 - clipboard
|
||||||
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
|
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
|
||||||
// TODO(b/278714186) Tracking Bug
|
// TODO(b/278714186) Tracking Bug
|
||||||
@JvmField val CLIPBOARD_IMAGE_TIMEOUT =
|
@JvmField
|
||||||
unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
|
val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
|
||||||
// TODO(b/279405451): Tracking Bug
|
// TODO(b/279405451): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val CLIPBOARD_SHARED_TRANSITIONS = unreleasedFlag(1703, "clipboard_shared_transitions")
|
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
|
// TODO(b/283300105): Tracking Bug
|
||||||
@JvmField val SCENE_CONTAINER = unreleasedFlag(1802, "scene_container")
|
@JvmField val SCENE_CONTAINER = unreleasedFlag(1802, "scene_container")
|
||||||
|
|
||||||
@@ -642,19 +599,15 @@ object Flags {
|
|||||||
// 2000 - device controls
|
// 2000 - device controls
|
||||||
@Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels")
|
@Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels")
|
||||||
|
|
||||||
@JvmField
|
@JvmField val APP_PANELS_ALL_APPS_ALLOWED = releasedFlag(2001, "app_panels_all_apps_allowed")
|
||||||
val APP_PANELS_ALL_APPS_ALLOWED =
|
|
||||||
releasedFlag(2001, "app_panels_all_apps_allowed")
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CONTROLS_MANAGEMENT_NEW_FLOWS =
|
val CONTROLS_MANAGEMENT_NEW_FLOWS = releasedFlag(2002, "controls_management_new_flows")
|
||||||
releasedFlag(2002, "controls_management_new_flows")
|
|
||||||
|
|
||||||
// Enables removing app from Home control panel as a part of a new flow
|
// Enables removing app from Home control panel as a part of a new flow
|
||||||
// TODO(b/269132640): Tracking Bug
|
// TODO(b/269132640): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val APP_PANELS_REMOVE_APPS_ALLOWED =
|
val APP_PANELS_REMOVE_APPS_ALLOWED = releasedFlag(2003, "app_panels_remove_apps_allowed")
|
||||||
releasedFlag(2003, "app_panels_remove_apps_allowed")
|
|
||||||
|
|
||||||
// 2200 - biometrics (udfps, sfps, BiometricPrompt, etc.)
|
// 2200 - biometrics (udfps, sfps, BiometricPrompt, etc.)
|
||||||
// TODO(b/259264861): Tracking Bug
|
// TODO(b/259264861): Tracking Bug
|
||||||
@@ -665,11 +618,9 @@ object Flags {
|
|||||||
|
|
||||||
// 2300 - stylus
|
// 2300 - stylus
|
||||||
@JvmField val TRACK_STYLUS_EVER_USED = releasedFlag(2300, "track_stylus_ever_used")
|
@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
|
@JvmField
|
||||||
val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui")
|
val ENABLE_USI_BATTERY_NOTIFICATIONS = releasedFlag(2302, "enable_usi_battery_notifications")
|
||||||
@JvmField
|
|
||||||
val ENABLE_USI_BATTERY_NOTIFICATIONS =
|
|
||||||
releasedFlag(2302, "enable_usi_battery_notifications")
|
|
||||||
@JvmField val ENABLE_STYLUS_EDUCATION = releasedFlag(2303, "enable_stylus_education")
|
@JvmField val ENABLE_STYLUS_EDUCATION = releasedFlag(2303, "enable_stylus_education")
|
||||||
|
|
||||||
// 2400 - performance tools and debugging info
|
// 2400 - performance tools and debugging info
|
||||||
@@ -681,11 +632,10 @@ object Flags {
|
|||||||
// TODO(b/283071711): Tracking bug
|
// TODO(b/283071711): Tracking bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK =
|
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
|
// TODO:(b/283203305): Tracking bug
|
||||||
@JvmField
|
@JvmField val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
|
||||||
val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
|
|
||||||
|
|
||||||
// 2700 - unfold transitions
|
// 2700 - unfold transitions
|
||||||
// TODO(b/265764985): Tracking Bug
|
// TODO(b/265764985): Tracking Bug
|
||||||
@@ -708,27 +658,21 @@ object Flags {
|
|||||||
@JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = releasedFlag(2600, "shortcut_list_search_layout")
|
@JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = releasedFlag(2600, "shortcut_list_search_layout")
|
||||||
|
|
||||||
// TODO(b/259428678): Tracking Bug
|
// TODO(b/259428678): Tracking Bug
|
||||||
@JvmField
|
@JvmField val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
|
||||||
val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
|
|
||||||
|
|
||||||
// TODO(b/277192623): Tracking Bug
|
// TODO(b/277192623): Tracking Bug
|
||||||
@JvmField
|
@JvmField val KEYBOARD_EDUCATION = unreleasedFlag(2603, "keyboard_education", teamfood = false)
|
||||||
val KEYBOARD_EDUCATION =
|
|
||||||
unreleasedFlag(2603, "keyboard_education", teamfood = false)
|
|
||||||
|
|
||||||
// TODO(b/277201412): Tracking Bug
|
// TODO(b/277201412): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION =
|
val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION = releasedFlag(2805, "split_shade_subpixel_optimization")
|
||||||
releasedFlag(2805, "split_shade_subpixel_optimization")
|
|
||||||
|
|
||||||
// TODO(b/288868056): Tracking Bug
|
// TODO(b/288868056): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val PARTIAL_SCREEN_SHARING_TASK_SWITCHER =
|
val PARTIAL_SCREEN_SHARING_TASK_SWITCHER = unreleasedFlag(288868056, "pss_task_switcher")
|
||||||
unreleasedFlag(288868056, "pss_task_switcher")
|
|
||||||
|
|
||||||
// TODO(b/278761837): Tracking Bug
|
// TODO(b/278761837): Tracking Bug
|
||||||
@JvmField
|
@JvmField val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
|
||||||
val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
|
|
||||||
|
|
||||||
// 2900 - Zero Jank fixes. Naming convention is: zj_<bug number>_<cuj name>
|
// 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")
|
unreleasedFlag(3000, name = "enable_lockscreen_wallpaper_dream")
|
||||||
|
|
||||||
// TODO(b/283084712): Tracking Bug
|
// TODO(b/283084712): Tracking Bug
|
||||||
@JvmField
|
@JvmField val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
|
||||||
val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
|
|
||||||
|
|
||||||
// TODO(b/283447257): Tracking bug
|
// TODO(b/283447257): Tracking bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val BIGPICTURE_NOTIFICATION_LAZY_LOADING =
|
val BIGPICTURE_NOTIFICATION_LAZY_LOADING =
|
||||||
unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
|
unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
|
||||||
|
|
||||||
// TODO(b/283740863): Tracking Bug
|
// TODO(b/283740863): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val ENABLE_NEW_PRIVACY_DIALOG =
|
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
|
// TODO(b/289573946): Tracking Bug
|
||||||
@JvmField
|
@JvmField val PRECOMPUTED_TEXT = unreleasedFlag(289573946, "precomputed_text")
|
||||||
val PRECOMPUTED_TEXT =
|
|
||||||
unreleasedFlag(289573946, "precomputed_text")
|
|
||||||
|
|
||||||
// 2900 - CentralSurfaces-related flags
|
// 2900 - CentralSurfaces-related flags
|
||||||
|
|
||||||
@@ -773,6 +714,5 @@ object Flags {
|
|||||||
|
|
||||||
// TODO(b/290213663): Tracking Bug
|
// TODO(b/290213663): Tracking Bug
|
||||||
@JvmField
|
@JvmField
|
||||||
val ONE_WAY_HAPTICS_API_MIGRATION =
|
val ONE_WAY_HAPTICS_API_MIGRATION = unreleasedFlag(3100, "oneway_haptics_api_migration")
|
||||||
unreleasedFlag(3100, "oneway_haptics_api_migration")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.KeyguardMediaController;
|
||||||
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
|
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
|
||||||
import com.android.systemui.model.SysUiState;
|
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.NavigationBarController;
|
||||||
import com.android.systemui.navigationbar.NavigationBarView;
|
import com.android.systemui.navigationbar.NavigationBarView;
|
||||||
import com.android.systemui.navigationbar.NavigationModeController;
|
import com.android.systemui.navigationbar.NavigationModeController;
|
||||||
@@ -365,7 +364,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
|
|||||||
private KeyguardBottomAreaView mKeyguardBottomArea;
|
private KeyguardBottomAreaView mKeyguardBottomArea;
|
||||||
private boolean mExpanding;
|
private boolean mExpanding;
|
||||||
private boolean mSplitShadeEnabled;
|
private boolean mSplitShadeEnabled;
|
||||||
private boolean mDualShadeEnabled;
|
|
||||||
/** The bottom padding reserved for elements of the keyguard measuring notifications. */
|
/** The bottom padding reserved for elements of the keyguard measuring notifications. */
|
||||||
private float mKeyguardNotificationBottomPadding;
|
private float mKeyguardNotificationBottomPadding;
|
||||||
/**
|
/**
|
||||||
@@ -599,7 +597,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
|
|||||||
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
|
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
|
||||||
private final KeyguardInteractor mKeyguardInteractor;
|
private final KeyguardInteractor mKeyguardInteractor;
|
||||||
private final KeyguardViewConfigurator mKeyguardViewConfigurator;
|
private final KeyguardViewConfigurator mKeyguardViewConfigurator;
|
||||||
private final @Nullable MultiShadeInteractor mMultiShadeInteractor;
|
|
||||||
private final CoroutineDispatcher mMainDispatcher;
|
private final CoroutineDispatcher mMainDispatcher;
|
||||||
private boolean mIsAnyMultiShadeExpanded;
|
private boolean mIsAnyMultiShadeExpanded;
|
||||||
private boolean mIsOcclusionTransitionRunning = false;
|
private boolean mIsOcclusionTransitionRunning = false;
|
||||||
@@ -735,7 +732,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
|
|||||||
LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
|
LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
|
||||||
@Main CoroutineDispatcher mainDispatcher,
|
@Main CoroutineDispatcher mainDispatcher,
|
||||||
KeyguardTransitionInteractor keyguardTransitionInteractor,
|
KeyguardTransitionInteractor keyguardTransitionInteractor,
|
||||||
Provider<MultiShadeInteractor> multiShadeInteractorProvider,
|
|
||||||
DumpManager dumpManager,
|
DumpManager dumpManager,
|
||||||
KeyguardLongPressViewModel keyguardLongPressViewModel,
|
KeyguardLongPressViewModel keyguardLongPressViewModel,
|
||||||
KeyguardInteractor keyguardInteractor,
|
KeyguardInteractor keyguardInteractor,
|
||||||
@@ -839,8 +835,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
|
|||||||
mFeatureFlags = featureFlags;
|
mFeatureFlags = featureFlags;
|
||||||
mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE);
|
mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE);
|
||||||
mTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_FEATURES);
|
mTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_FEATURES);
|
||||||
mDualShadeEnabled = mFeatureFlags.isEnabled(Flags.DUAL_SHADE);
|
|
||||||
mMultiShadeInteractor = mDualShadeEnabled ? multiShadeInteractorProvider.get() : null;
|
|
||||||
mFalsingCollector = falsingCollector;
|
mFalsingCollector = falsingCollector;
|
||||||
mPowerManager = powerManager;
|
mPowerManager = powerManager;
|
||||||
mWakeUpCoordinator = coordinator;
|
mWakeUpCoordinator = coordinator;
|
||||||
@@ -1079,11 +1073,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
|
|||||||
mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
|
mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
|
||||||
controller.setup(mNotificationContainerParent));
|
controller.setup(mNotificationContainerParent));
|
||||||
|
|
||||||
if (mDualShadeEnabled) {
|
|
||||||
collectFlow(mView, mMultiShadeInteractor.isAnyShadeExpanded(),
|
|
||||||
mMultiShadeExpansionConsumer, mMainDispatcher);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dreaming->Lockscreen
|
// Dreaming->Lockscreen
|
||||||
collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
|
collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
|
||||||
mDreamingToLockscreenTransition, mMainDispatcher);
|
mDreamingToLockscreenTransition, mMainDispatcher);
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ import android.view.KeyEvent;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewStub;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
import com.android.keyguard.AuthKeyguardMessageArea;
|
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.binder.KeyguardBouncerViewBinder;
|
||||||
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
|
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
|
||||||
import com.android.systemui.classifier.FalsingCollector;
|
import com.android.systemui.classifier.FalsingCollector;
|
||||||
import com.android.systemui.compose.ComposeFacade;
|
|
||||||
import com.android.systemui.dagger.SysUISingleton;
|
import com.android.systemui.dagger.SysUISingleton;
|
||||||
import com.android.systemui.dock.DockManager;
|
import com.android.systemui.dock.DockManager;
|
||||||
import com.android.systemui.flags.FeatureFlags;
|
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.shared.model.TransitionStep;
|
||||||
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
|
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
|
||||||
import com.android.systemui.log.BouncerLogger;
|
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.power.domain.interactor.PowerInteractor;
|
||||||
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
|
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
|
||||||
import com.android.systemui.statusbar.DragDownHelper;
|
import com.android.systemui.statusbar.DragDownHelper;
|
||||||
@@ -83,7 +76,6 @@ import java.util.Optional;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Provider;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller for {@link NotificationShadeWindowView}.
|
* Controller for {@link NotificationShadeWindowView}.
|
||||||
@@ -135,7 +127,6 @@ public class NotificationShadeWindowViewController {
|
|||||||
step.getTransitionState() == TransitionState.RUNNING;
|
step.getTransitionState() == TransitionState.RUNNING;
|
||||||
};
|
};
|
||||||
private final SystemClock mClock;
|
private final SystemClock mClock;
|
||||||
private final @Nullable MultiShadeMotionEventInteractor mMultiShadeMotionEventInteractor;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public NotificationShadeWindowViewController(
|
public NotificationShadeWindowViewController(
|
||||||
@@ -167,9 +158,7 @@ public class NotificationShadeWindowViewController {
|
|||||||
KeyguardTransitionInteractor keyguardTransitionInteractor,
|
KeyguardTransitionInteractor keyguardTransitionInteractor,
|
||||||
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
|
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
|
||||||
FeatureFlags featureFlags,
|
FeatureFlags featureFlags,
|
||||||
Provider<MultiShadeInteractor> multiShadeInteractorProvider,
|
|
||||||
SystemClock clock,
|
SystemClock clock,
|
||||||
Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider,
|
|
||||||
BouncerMessageInteractor bouncerMessageInteractor,
|
BouncerMessageInteractor bouncerMessageInteractor,
|
||||||
BouncerLogger bouncerLogger) {
|
BouncerLogger bouncerLogger) {
|
||||||
mLockscreenShadeTransitionController = transitionController;
|
mLockscreenShadeTransitionController = transitionController;
|
||||||
@@ -219,17 +208,6 @@ public class NotificationShadeWindowViewController {
|
|||||||
progressProvider -> progressProvider.addCallback(
|
progressProvider -> progressProvider.addCallback(
|
||||||
mDisableSubpixelTextTransitionListener));
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mMultiShadeMotionEventInteractor != null) {
|
if (mNotificationPanelViewController.isFullyExpanded()
|
||||||
// This interactor is not null only if the dual shade feature is enabled.
|
|
||||||
return mMultiShadeMotionEventInteractor.shouldIntercept(ev);
|
|
||||||
} else if (mNotificationPanelViewController.isFullyExpanded()
|
|
||||||
&& mDragDownHelper.isDragDownEnabled()
|
&& mDragDownHelper.isDragDownEnabled()
|
||||||
&& !mService.isBouncerShowing()
|
&& !mService.isBouncerShowing()
|
||||||
&& !mStatusBarStateController.isDozing()) {
|
&& !mStatusBarStateController.isDozing()) {
|
||||||
@@ -428,10 +403,7 @@ public class NotificationShadeWindowViewController {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mMultiShadeMotionEventInteractor != null) {
|
if (mDragDownHelper.isDragDownEnabled()
|
||||||
// This interactor is not null only if the dual shade feature is enabled.
|
|
||||||
return mMultiShadeMotionEventInteractor.onTouchEvent(ev, mView.getWidth());
|
|
||||||
} else if (mDragDownHelper.isDragDownEnabled()
|
|
||||||
|| mDragDownHelper.isDraggingDown()) {
|
|| mDragDownHelper.isDraggingDown()) {
|
||||||
// we still want to finish our drag down gesture when locking the screen
|
// we still want to finish our drag down gesture when locking the screen
|
||||||
return mDragDownHelper.onTouchEvent(ev) || handled;
|
return mDragDownHelper.onTouchEvent(ev) || handled;
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.KeyguardMediaController;
|
||||||
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
|
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
|
||||||
import com.android.systemui.model.SysUiState;
|
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.NavigationBarController;
|
||||||
import com.android.systemui.navigationbar.NavigationModeController;
|
import com.android.systemui.navigationbar.NavigationModeController;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
@@ -299,7 +298,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
|
|||||||
@Mock protected GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
|
@Mock protected GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
|
||||||
|
|
||||||
@Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor;
|
@Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor;
|
||||||
@Mock protected MultiShadeInteractor mMultiShadeInteractor;
|
|
||||||
@Mock protected KeyguardLongPressViewModel mKeyuardLongPressViewModel;
|
@Mock protected KeyguardLongPressViewModel mKeyuardLongPressViewModel;
|
||||||
@Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor;
|
@Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor;
|
||||||
@Mock protected MotionEvent mDownMotionEvent;
|
@Mock protected MotionEvent mDownMotionEvent;
|
||||||
@@ -615,7 +613,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
|
|||||||
mLockscreenToOccludedTransitionViewModel,
|
mLockscreenToOccludedTransitionViewModel,
|
||||||
mMainDispatcher,
|
mMainDispatcher,
|
||||||
mKeyguardTransitionInteractor,
|
mKeyguardTransitionInteractor,
|
||||||
() -> mMultiShadeInteractor,
|
|
||||||
mDumpManager,
|
mDumpManager,
|
||||||
mKeyuardLongPressViewModel,
|
mKeyuardLongPressViewModel,
|
||||||
mKeyguardInteractor,
|
mKeyguardInteractor,
|
||||||
|
|||||||
@@ -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.domain.interactor.CountDownTimerUtil
|
||||||
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
|
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
|
||||||
import com.android.systemui.classifier.FalsingCollectorFake
|
import com.android.systemui.classifier.FalsingCollectorFake
|
||||||
import com.android.systemui.classifier.FalsingManagerFake
|
|
||||||
import com.android.systemui.dock.DockManager
|
import com.android.systemui.dock.DockManager
|
||||||
import com.android.systemui.dump.logcatLogBuffer
|
import com.android.systemui.dump.logcatLogBuffer
|
||||||
import com.android.systemui.flags.FakeFeatureFlags
|
import com.android.systemui.flags.FakeFeatureFlags
|
||||||
import com.android.systemui.flags.Flags
|
import com.android.systemui.flags.Flags
|
||||||
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
|
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
|
||||||
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
|
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.shared.model.TransitionStep
|
||||||
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
|
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
|
||||||
import com.android.systemui.log.BouncerLogger
|
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.power.domain.interactor.PowerInteractor
|
||||||
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
|
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
|
||||||
import com.android.systemui.statusbar.LockscreenShadeTransitionController
|
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.mockito.any
|
||||||
import com.android.systemui.util.time.FakeSystemClock
|
import com.android.systemui.util.time.FakeSystemClock
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import java.util.Optional
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
@@ -80,9 +75,8 @@ import org.mockito.Mockito.anyFloat
|
|||||||
import org.mockito.Mockito.mock
|
import org.mockito.Mockito.mock
|
||||||
import org.mockito.Mockito.never
|
import org.mockito.Mockito.never
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.MockitoAnnotations
|
|
||||||
import java.util.Optional
|
|
||||||
import org.mockito.Mockito.`when` as whenever
|
import org.mockito.Mockito.`when` as whenever
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@@ -117,8 +111,9 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
|
|||||||
@Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
|
@Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
|
||||||
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
|
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
|
||||||
@Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
|
@Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
|
||||||
@Mock private lateinit var unfoldTransitionProgressProvider:
|
@Mock
|
||||||
Optional<UnfoldTransitionProgressProvider>
|
private lateinit var unfoldTransitionProgressProvider:
|
||||||
|
Optional<UnfoldTransitionProgressProvider>
|
||||||
@Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
|
@Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
|
||||||
@Mock
|
@Mock
|
||||||
lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
|
lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
|
||||||
@@ -146,23 +141,11 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
|
|||||||
val featureFlags = FakeFeatureFlags()
|
val featureFlags = FakeFeatureFlags()
|
||||||
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
|
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
|
||||||
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
|
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
|
||||||
featureFlags.set(Flags.DUAL_SHADE, false)
|
|
||||||
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
|
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
|
||||||
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
|
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
|
||||||
featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
|
featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
|
||||||
|
|
||||||
val inputProxy = MultiShadeInputProxy()
|
|
||||||
testScope = TestScope()
|
testScope = TestScope()
|
||||||
val multiShadeInteractor =
|
|
||||||
MultiShadeInteractor(
|
|
||||||
applicationScope = testScope.backgroundScope,
|
|
||||||
repository =
|
|
||||||
MultiShadeRepository(
|
|
||||||
applicationContext = context,
|
|
||||||
inputProxy = inputProxy,
|
|
||||||
),
|
|
||||||
inputProxy = inputProxy,
|
|
||||||
)
|
|
||||||
underTest =
|
underTest =
|
||||||
NotificationShadeWindowViewController(
|
NotificationShadeWindowViewController(
|
||||||
lockscreenShadeTransitionController,
|
lockscreenShadeTransitionController,
|
||||||
@@ -193,25 +176,14 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
|
|||||||
keyguardTransitionInteractor,
|
keyguardTransitionInteractor,
|
||||||
primaryBouncerToGoneTransitionViewModel,
|
primaryBouncerToGoneTransitionViewModel,
|
||||||
featureFlags,
|
featureFlags,
|
||||||
{ multiShadeInteractor },
|
|
||||||
FakeSystemClock(),
|
FakeSystemClock(),
|
||||||
{
|
BouncerMessageInteractor(
|
||||||
MultiShadeMotionEventInteractor(
|
FakeBouncerMessageRepository(),
|
||||||
applicationContext = context,
|
mock(BouncerMessageFactory::class.java),
|
||||||
applicationScope = testScope.backgroundScope,
|
FakeUserRepository(),
|
||||||
multiShadeInteractor = multiShadeInteractor,
|
CountDownTimerUtil(),
|
||||||
featureFlags = featureFlags,
|
featureFlags
|
||||||
keyguardTransitionInteractor =
|
),
|
||||||
KeyguardTransitionInteractorFactory.create(
|
|
||||||
scope = TestScope().backgroundScope,
|
|
||||||
).keyguardTransitionInteractor,
|
|
||||||
falsingManager = FalsingManagerFake(),
|
|
||||||
shadeController = shadeController,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
BouncerMessageInteractor(FakeBouncerMessageRepository(),
|
|
||||||
mock(BouncerMessageFactory::class.java),
|
|
||||||
FakeUserRepository(), CountDownTimerUtil(), featureFlags),
|
|
||||||
BouncerLogger(logcatLogBuffer("BouncerLog"))
|
BouncerLogger(logcatLogBuffer("BouncerLog"))
|
||||||
)
|
)
|
||||||
underTest.setupExpandedStatusBar()
|
underTest.setupExpandedStatusBar()
|
||||||
|
|||||||
@@ -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.domain.interactor.CountDownTimerUtil
|
||||||
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
|
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
|
||||||
import com.android.systemui.classifier.FalsingCollectorFake
|
import com.android.systemui.classifier.FalsingCollectorFake
|
||||||
import com.android.systemui.classifier.FalsingManagerFake
|
|
||||||
import com.android.systemui.dock.DockManager
|
import com.android.systemui.dock.DockManager
|
||||||
import com.android.systemui.dump.logcatLogBuffer
|
import com.android.systemui.dump.logcatLogBuffer
|
||||||
import com.android.systemui.flags.FakeFeatureFlags
|
import com.android.systemui.flags.FakeFeatureFlags
|
||||||
import com.android.systemui.flags.Flags
|
import com.android.systemui.flags.Flags
|
||||||
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
|
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
|
||||||
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
|
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.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
|
||||||
import com.android.systemui.log.BouncerLogger
|
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.power.domain.interactor.PowerInteractor
|
||||||
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
|
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
|
||||||
import com.android.systemui.statusbar.DragDownHelper
|
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.android.systemui.util.time.FakeSystemClock
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -85,7 +78,6 @@ import org.mockito.Mockito.spy
|
|||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
@RunWith(AndroidTestingRunner::class)
|
@RunWith(AndroidTestingRunner::class)
|
||||||
@RunWithLooper(setAsMainLooper = true)
|
@RunWithLooper(setAsMainLooper = true)
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@@ -160,22 +152,10 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
|
|||||||
val featureFlags = FakeFeatureFlags()
|
val featureFlags = FakeFeatureFlags()
|
||||||
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
|
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
|
||||||
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
|
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
|
||||||
featureFlags.set(Flags.DUAL_SHADE, false)
|
|
||||||
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
|
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
|
||||||
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
|
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
|
||||||
featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
|
featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
|
||||||
val inputProxy = MultiShadeInputProxy()
|
|
||||||
testScope = TestScope()
|
testScope = TestScope()
|
||||||
val multiShadeInteractor =
|
|
||||||
MultiShadeInteractor(
|
|
||||||
applicationScope = testScope.backgroundScope,
|
|
||||||
repository =
|
|
||||||
MultiShadeRepository(
|
|
||||||
applicationContext = context,
|
|
||||||
inputProxy = inputProxy,
|
|
||||||
),
|
|
||||||
inputProxy = inputProxy,
|
|
||||||
)
|
|
||||||
controller =
|
controller =
|
||||||
NotificationShadeWindowViewController(
|
NotificationShadeWindowViewController(
|
||||||
lockscreenShadeTransitionController,
|
lockscreenShadeTransitionController,
|
||||||
@@ -206,23 +186,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
|
|||||||
keyguardTransitionInteractor,
|
keyguardTransitionInteractor,
|
||||||
primaryBouncerToGoneTransitionViewModel,
|
primaryBouncerToGoneTransitionViewModel,
|
||||||
featureFlags,
|
featureFlags,
|
||||||
{ multiShadeInteractor },
|
|
||||||
FakeSystemClock(),
|
FakeSystemClock(),
|
||||||
{
|
|
||||||
MultiShadeMotionEventInteractor(
|
|
||||||
applicationContext = context,
|
|
||||||
applicationScope = testScope.backgroundScope,
|
|
||||||
multiShadeInteractor = multiShadeInteractor,
|
|
||||||
featureFlags = featureFlags,
|
|
||||||
keyguardTransitionInteractor =
|
|
||||||
KeyguardTransitionInteractorFactory.create(
|
|
||||||
scope = TestScope().backgroundScope,
|
|
||||||
)
|
|
||||||
.keyguardTransitionInteractor,
|
|
||||||
falsingManager = FalsingManagerFake(),
|
|
||||||
shadeController = shadeController,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
BouncerMessageInteractor(
|
BouncerMessageInteractor(
|
||||||
FakeBouncerMessageRepository(),
|
FakeBouncerMessageRepository(),
|
||||||
Mockito.mock(BouncerMessageFactory::class.java),
|
Mockito.mock(BouncerMessageFactory::class.java),
|
||||||
|
|||||||
Reference in New Issue
Block a user