Merge changes I81118cb2,Iab6a7568,I9f7ff736 into udc-dev

* changes:
  [multi-shade] Bouncer touch integration.
  KeyguardTransitionInteractor#transitionValue.
  Adds Flow extension for collecting values into a list.
This commit is contained in:
Ale Nijamkin
2023-03-24 21:46:02 +00:00
committed by Android (Google) Code Review
8 changed files with 479 additions and 164 deletions

View File

@@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.merge
class KeyguardTransitionInteractor
@Inject
constructor(
repository: KeyguardTransitionRepository,
private val repository: KeyguardTransitionRepository,
) {
/** (any)->GONE transition information */
val anyStateToGoneTransition: Flow<TransitionStep> =
@@ -62,10 +62,6 @@ constructor(
/** LOCKSCREEN->AOD transition information. */
val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
/** LOCKSCREEN->PRIMARY_BOUNCER transition information. */
val mLockscreenToPrimaryBouncerTransition: Flow<TransitionStep> =
repository.transition(LOCKSCREEN, PRIMARY_BOUNCER)
/** LOCKSCREEN->DREAMING transition information. */
val lockscreenToDreamingTransition: Flow<TransitionStep> =
repository.transition(LOCKSCREEN, DREAMING)
@@ -92,19 +88,39 @@ constructor(
lockscreenToAodTransition,
)
/* The last [TransitionStep] with a [TransitionState] of STARTED */
/** The last [TransitionStep] with a [TransitionState] of STARTED */
val startedKeyguardTransitionStep: Flow<TransitionStep> =
repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED }
/* The last [TransitionStep] with a [TransitionState] of CANCELED */
/** The last [TransitionStep] with a [TransitionState] of CANCELED */
val canceledKeyguardTransitionStep: Flow<TransitionStep> =
repository.transitions.filter { step -> step.transitionState == TransitionState.CANCELED }
/* The last [TransitionStep] with a [TransitionState] of FINISHED */
/** The last [TransitionStep] with a [TransitionState] of FINISHED */
val finishedKeyguardTransitionStep: Flow<TransitionStep> =
repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
/* The last completed [KeyguardState] transition */
/** The last completed [KeyguardState] transition */
val finishedKeyguardState: Flow<KeyguardState> =
finishedKeyguardTransitionStep.map { step -> step.to }
/**
* The amount of transition into or out of the given [KeyguardState].
*
* The value will be `0` (or close to `0`, due to float point arithmetic) if not in this step or
* `1` when fully in the given state.
*/
fun transitionValue(
state: KeyguardState,
): Flow<Float> {
return repository.transitions
.filter { it.from == state || it.to == state }
.map {
if (it.from == state) {
1 - it.value
} else {
it.value
}
}
}
}

View File

@@ -19,13 +19,20 @@ 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.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
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 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
/**
@@ -39,15 +46,27 @@ class MultiShadeMotionEventInteractor
constructor(
@Application private val applicationContext: Context,
@Application private val applicationScope: CoroutineScope,
private val interactor: MultiShadeInteractor,
private val multiShadeInteractor: MultiShadeInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val bouncerInteractor: PrimaryBouncerInteractor,
private val falsingManager: FalsingManager,
) {
private val isAnyShadeExpanded: StateFlow<Boolean> =
interactor.isAnyShadeExpanded.stateIn(
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
@@ -65,6 +84,10 @@ constructor(
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.
@@ -75,7 +98,7 @@ constructor(
currentY = event.y,
pointerId = event.getPointerId(0),
isDraggingHorizontally = false,
isDraggingVertically = false,
draggedVertically = Dragged.NONE,
)
false
@@ -85,16 +108,25 @@ constructor(
val pointerIndex = event.findPointerIndex(it.pointerId)
val currentX = event.getX(pointerIndex)
val currentY = event.getY(pointerIndex)
if (!it.isDraggingHorizontally && !it.isDraggingVertically) {
val xDistanceTravelled = abs(currentX - it.initialX)
val yDistanceTravelled = abs(currentY - it.initialY)
if (!it.isDraggingHorizontally && it.draggedVertically == Dragged.NONE) {
val xDistanceTravelled = currentX - it.initialX
val yDistanceTravelled = currentY - it.initialY
val touchSlop = ViewConfiguration.get(applicationContext).scaledTouchSlop
interactionState =
when {
yDistanceTravelled > touchSlop ->
it.copy(isDraggingVertically = true)
xDistanceTravelled > touchSlop ->
it.copy(isDraggingHorizontally = true)
abs(yDistanceTravelled) > touchSlop ->
it.copy(
draggedVertically =
if (yDistanceTravelled > 0) {
Dragged.SHADE
} else {
Dragged.BOUNCER
}
)
abs(xDistanceTravelled) > touchSlop ->
it.copy(
isDraggingHorizontally = true,
)
else -> interactionState
}
}
@@ -124,7 +156,7 @@ constructor(
return when (event.actionMasked) {
MotionEvent.ACTION_MOVE -> {
interactionState?.let {
if (it.isDraggingVertically) {
if (it.draggedVertically != Dragged.NONE) {
val pointerIndex = event.findPointerIndex(it.pointerId)
val previousY = it.currentY
val currentY = event.getY(pointerIndex)
@@ -133,32 +165,48 @@ constructor(
currentY = currentY,
)
val yDragAmountPx = currentY - previousY
if (yDragAmountPx != 0f) {
interactor.sendProxiedInput(
ProxiedInputModel.OnDrag(
xFraction = event.x / viewWidthPx,
yDragAmountPx = yDragAmountPx,
)
)
when (it.draggedVertically) {
Dragged.SHADE -> {
val yDragAmountPx = currentY - previousY
if (yDragAmountPx != 0f) {
multiShadeInteractor.sendProxiedInput(
ProxiedInputModel.OnDrag(
xFraction = event.x / viewWidthPx,
yDragAmountPx = yDragAmountPx,
)
)
}
true
}
Dragged.BOUNCER -> {
bouncerInteractor.show(isScrimmed = true)
false
}
else -> false
}
} else {
false
}
}
true
?: false
}
MotionEvent.ACTION_UP -> {
if (interactionState.isDraggingVertically()) {
if (interactionState?.draggedVertically == Dragged.SHADE) {
// We finished dragging. Record that so the multi-shade framework can issue a
// fling, if the velocity reached in the drag was high enough, for example.
interactor.sendProxiedInput(ProxiedInputModel.OnDragEnd)
multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragEnd)
if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
multiShadeInteractor.collapseAll()
}
}
interactionState = null
true
}
MotionEvent.ACTION_CANCEL -> {
if (interactionState.isDraggingVertically()) {
if (interactionState?.draggedVertically == Dragged.SHADE) {
// 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
@@ -166,7 +214,15 @@ constructor(
// 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.
interactor.sendProxiedInput(ProxiedInputModel.OnDragCancel)
multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragCancel)
if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
multiShadeInteractor.collapseAll()
}
} else if (interactionState?.draggedVertically == Dragged.BOUNCER) {
if (falsingManager.isFalseTouch(Classifier.BOUNCER_UNLOCK)) {
bouncerInteractor.hide()
}
}
interactionState = null
@@ -181,11 +237,23 @@ constructor(
val initialY: Float,
val currentY: Float,
val pointerId: Int,
/** Whether the current gesture is dragging horizontally. */
val isDraggingHorizontally: Boolean,
val isDraggingVertically: Boolean,
/** The UI component that is being dragged vertically, if any. */
val draggedVertically: Dragged,
)
/** Enumerates the UI components that can be dragged by the user. */
private enum class Dragged {
/** The bouncer is being dragged by the user. */
BOUNCER,
/** A shade is being dragged by the user. */
SHADE,
/** No UI component is being dragged by the user. */
NONE,
}
private fun InteractionState?.isDraggingVertically(): Boolean {
return this?.isDraggingVertically == true
return this?.draggedVertically != Dragged.NONE
}
}

View File

@@ -234,6 +234,8 @@ import com.android.systemui.util.Utils;
import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.animation.FlingAnimationUtils;
import kotlin.Unit;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
@@ -244,7 +246,6 @@ import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Provider;
import kotlin.Unit;
import kotlinx.coroutines.CoroutineDispatcher;
@CentralSurfacesComponent.CentralSurfacesScope
@@ -388,6 +389,7 @@ public final class NotificationPanelViewController implements Dumpable {
private KeyguardBottomAreaView mKeyguardBottomArea;
private boolean mExpanding;
private boolean mSplitShadeEnabled;
private final boolean mMultiShadeEnabled;
/** The bottom padding reserved for elements of the keyguard measuring notifications. */
private float mKeyguardNotificationBottomPadding;
/**
@@ -852,6 +854,7 @@ public final class NotificationPanelViewController implements Dumpable {
mFeatureFlags = featureFlags;
mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE);
mTrackpadGestureBack = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_FEATURES);
mMultiShadeEnabled = mFeatureFlags.isEnabled(Flags.DUAL_SHADE);
mFalsingCollector = falsingCollector;
mPowerManager = powerManager;
mWakeUpCoordinator = coordinator;
@@ -4021,8 +4024,11 @@ public final class NotificationPanelViewController implements Dumpable {
* {@link #updateVisibility()}? That would allow us to make this method private.
*/
public void updatePanelExpansionAndVisibility() {
mShadeExpansionStateManager.onPanelExpansionChanged(
mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
if (!mMultiShadeEnabled) {
mShadeExpansionStateManager.onPanelExpansionChanged(
mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
}
updateVisibility();
}

View File

@@ -20,9 +20,10 @@ package com.android.systemui.keyguard.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
@@ -30,9 +31,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -53,138 +52,151 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
}
@Test
fun `transition collectors receives only appropriate events`() =
runTest(UnconfinedTestDispatcher()) {
var lockscreenToAodSteps = mutableListOf<TransitionStep>()
val job1 =
underTest.lockscreenToAodTransition
.onEach { lockscreenToAodSteps.add(it) }
.launchIn(this)
fun `transition collectors receives only appropriate events`() = runTest {
val lockscreenToAodSteps by collectValues(underTest.lockscreenToAodTransition)
val aodToLockscreenSteps by collectValues(underTest.aodToLockscreenTransition)
var aodToLockscreenSteps = mutableListOf<TransitionStep>()
val job2 =
underTest.aodToLockscreenTransition
.onEach { aodToLockscreenSteps.add(it) }
.launchIn(this)
val steps = mutableListOf<TransitionStep>()
steps.add(TransitionStep(AOD, GONE, 0f, STARTED))
steps.add(TransitionStep(AOD, GONE, 1f, FINISHED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.1f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.2f, RUNNING))
val steps = mutableListOf<TransitionStep>()
steps.add(TransitionStep(AOD, GONE, 0f, STARTED))
steps.add(TransitionStep(AOD, GONE, 1f, FINISHED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.1f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.2f, RUNNING))
steps.forEach { repository.sendTransitionStep(it) }
assertThat(aodToLockscreenSteps).isEqualTo(steps.subList(2, 5))
assertThat(lockscreenToAodSteps).isEqualTo(steps.subList(5, 8))
job1.cancel()
job2.cancel()
steps.forEach {
repository.sendTransitionStep(it)
runCurrent()
}
assertThat(aodToLockscreenSteps).isEqualTo(steps.subList(2, 5))
assertThat(lockscreenToAodSteps).isEqualTo(steps.subList(5, 8))
}
@Test
fun dozeAmountTransitionTest() =
runTest(UnconfinedTestDispatcher()) {
var dozeAmountSteps = mutableListOf<TransitionStep>()
val job = underTest.dozeAmountTransition.onEach { dozeAmountSteps.add(it) }.launchIn(this)
fun dozeAmountTransitionTest() = runTest {
val dozeAmountSteps by collectValues(underTest.dozeAmountTransition)
val steps = mutableListOf<TransitionStep>()
val steps = mutableListOf<TransitionStep>()
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.8f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.8f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.forEach { repository.sendTransitionStep(it) }
steps.forEach {
repository.sendTransitionStep(it)
runCurrent()
}
assertThat(dozeAmountSteps.subList(0, 3))
.isEqualTo(
listOf(
steps[0].copy(value = 1f - steps[0].value),
steps[1].copy(value = 1f - steps[1].value),
steps[2].copy(value = 1f - steps[2].value),
)
assertThat(dozeAmountSteps.subList(0, 3))
.isEqualTo(
listOf(
steps[0].copy(value = 1f - steps[0].value),
steps[1].copy(value = 1f - steps[1].value),
steps[2].copy(value = 1f - steps[2].value),
)
assertThat(dozeAmountSteps.subList(3, 7)).isEqualTo(steps.subList(3, 7))
job.cancel()
}
)
assertThat(dozeAmountSteps.subList(3, 7)).isEqualTo(steps.subList(3, 7))
}
@Test
fun keyguardStateTests() =
runTest(UnconfinedTestDispatcher()) {
var finishedSteps = mutableListOf<KeyguardState>()
val job = underTest.finishedKeyguardState.onEach { finishedSteps.add(it) }.launchIn(this)
fun keyguardStateTests() = runTest {
val finishedSteps by collectValues(underTest.finishedKeyguardState)
val steps = mutableListOf<TransitionStep>()
val steps = mutableListOf<TransitionStep>()
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
steps.forEach { repository.sendTransitionStep(it) }
assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, AOD))
job.cancel()
steps.forEach {
repository.sendTransitionStep(it)
runCurrent()
}
assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, AOD))
}
@Test
fun finishedKeyguardTransitionStepTests() =
runTest(UnconfinedTestDispatcher()) {
var finishedSteps = mutableListOf<TransitionStep>()
val job =
underTest.finishedKeyguardTransitionStep.onEach { finishedSteps.add(it) }.launchIn(this)
fun finishedKeyguardTransitionStepTests() = runTest {
val finishedSteps by collectValues(underTest.finishedKeyguardTransitionStep)
val steps = mutableListOf<TransitionStep>()
val steps = mutableListOf<TransitionStep>()
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
steps.forEach { repository.sendTransitionStep(it) }
assertThat(finishedSteps).isEqualTo(listOf(steps[2], steps[5]))
job.cancel()
steps.forEach {
repository.sendTransitionStep(it)
runCurrent()
}
assertThat(finishedSteps).isEqualTo(listOf(steps[2], steps[5]))
}
@Test
fun startedKeyguardTransitionStepTests() =
runTest(UnconfinedTestDispatcher()) {
var startedSteps = mutableListOf<TransitionStep>()
val job =
underTest.startedKeyguardTransitionStep.onEach { startedSteps.add(it) }.launchIn(this)
fun startedKeyguardTransitionStepTests() = runTest {
val startedSteps by collectValues(underTest.startedKeyguardTransitionStep)
val steps = mutableListOf<TransitionStep>()
val steps = mutableListOf<TransitionStep>()
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
steps.forEach { repository.sendTransitionStep(it) }
assertThat(startedSteps).isEqualTo(listOf(steps[0], steps[3], steps[6]))
job.cancel()
steps.forEach {
repository.sendTransitionStep(it)
runCurrent()
}
assertThat(startedSteps).isEqualTo(listOf(steps[0], steps[3], steps[6]))
}
@Test
fun transitionValue() = runTest {
val startedSteps by collectValues(underTest.transitionValue(state = DOZING))
val toSteps =
listOf(
TransitionStep(AOD, DOZING, 0f, STARTED),
TransitionStep(AOD, DOZING, 0.5f, RUNNING),
TransitionStep(AOD, DOZING, 1f, FINISHED),
)
toSteps.forEach {
repository.sendTransitionStep(it)
runCurrent()
}
val fromSteps =
listOf(
TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING),
TransitionStep(DOZING, LOCKSCREEN, 1f, FINISHED),
)
fromSteps.forEach {
repository.sendTransitionStep(it)
runCurrent()
}
assertThat(startedSteps).isEqualTo(listOf(0f, 0.5f, 1f, 1f, 0.5f, 0f))
}
}

View File

@@ -20,7 +20,14 @@ 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.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
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
@@ -36,12 +43,18 @@ 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.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
@Mock private lateinit var bouncerInteractor: PrimaryBouncerInteractor
private lateinit var underTest: MultiShadeMotionEventInteractor
private lateinit var testScope: TestScope
@@ -49,9 +62,13 @@ class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
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
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
testScope = TestScope()
motionEvents = mutableSetOf()
@@ -67,11 +84,19 @@ class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
repository = repository,
inputProxy = inputProxy,
)
keyguardTransitionRepository = FakeKeyguardTransitionRepository()
falsingManager = FalsingManagerFake()
underTest =
MultiShadeMotionEventInteractor(
applicationContext = context,
applicationScope = testScope.backgroundScope,
interactor = interactor,
multiShadeInteractor = interactor,
keyguardTransitionInteractor =
KeyguardTransitionInteractor(
repository = keyguardTransitionRepository,
),
bouncerInteractor = bouncerInteractor,
falsingManager = falsingManager,
)
}
@@ -201,6 +226,60 @@ class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
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 {
@@ -233,7 +312,7 @@ class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
}
@Test
fun dragAboveTouchSlopAndUp() =
fun dragShadeAboveTouchSlopAndUp() =
testScope.runTest {
val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
@@ -277,7 +356,7 @@ class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
}
@Test
fun dragAboveTouchSlopAndCancel() =
fun dragShadeAboveTouchSlopAndCancel() =
testScope.runTest {
val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
@@ -320,6 +399,87 @@ class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
assertThat(singleShadeProxiedInput).isNull()
}
@Test
fun dragBouncerAboveTouchSlopAndUp_showsBouncer() =
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)).isTrue()
underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
verify(bouncerInteractor).show(isScrimmed = true)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
val upEvent = motionEvent(MotionEvent.ACTION_UP)
assertThat(underTest.shouldIntercept(upEvent)).isTrue()
underTest.onTouchEvent(upEvent, viewWidthPx = 1000)
verify(bouncerInteractor, never()).hide()
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
}
@Test
fun dragBouncerAboveTouchSlopAndCancel_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)).isTrue()
underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
verify(bouncerInteractor).show(isScrimmed = true)
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
falsingManager.setIsFalseTouch(true)
val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL)
assertThat(underTest.shouldIntercept(cancelEvent)).isTrue()
underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000)
verify(bouncerInteractor).hide()
assertThat(leftShadeProxiedInput).isNull()
assertThat(rightShadeProxiedInput).isNull()
assertThat(singleShadeProxiedInput).isNull()
}
private fun TestScope.motionEvent(
action: Int,
downTime: Long = currentTime,

View File

@@ -27,10 +27,12 @@ import com.android.keyguard.dagger.KeyguardBouncerComponent
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
@@ -67,8 +69,8 @@ import org.mockito.Mockito.anyFloat
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -170,7 +172,13 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
MultiShadeMotionEventInteractor(
applicationContext = context,
applicationScope = testScope.backgroundScope,
interactor = multiShadeInteractor,
multiShadeInteractor = multiShadeInteractor,
keyguardTransitionInteractor =
KeyguardTransitionInteractor(
repository = FakeKeyguardTransitionRepository(),
),
bouncerInteractor = com.android.systemui.util.mockito.mock(),
falsingManager = FalsingManagerFake(),
)
},
)

View File

@@ -27,10 +27,12 @@ import com.android.keyguard.dagger.KeyguardBouncerComponent
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
@@ -182,7 +184,13 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
MultiShadeMotionEventInteractor(
applicationContext = context,
applicationScope = testScope.backgroundScope,
interactor = multiShadeInteractor,
multiShadeInteractor = multiShadeInteractor,
keyguardTransitionInteractor =
KeyguardTransitionInteractor(
repository = FakeKeyguardTransitionRepository(),
),
bouncerInteractor = mock(),
falsingManager = FalsingManagerFake(),
)
},
)

View File

@@ -14,6 +14,8 @@
* limitations under the License.
*/
@file:Suppress("OPT_IN_USAGE")
package com.android.systemui.coroutines
import kotlin.coroutines.CoroutineContext
@@ -43,20 +45,55 @@ fun <T> TestScope.collectLastValue(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
): FlowValue<T?> {
var lastValue: T? = null
backgroundScope.launch(context, start) { flow.collect { lastValue = it } }
return FlowValueImpl {
val values by
collectValues(
flow = flow,
context = context,
start = start,
)
return FlowValueImpl { values.lastOrNull() }
}
/**
* Collect [flow] in a new [Job] and return a getter for the collection of values collected.
*
* ```
* fun myTest() = runTest {
* // ...
* val values by collectValues(underTest.flow)
* assertThat(values).isEqualTo(listOf(expected1, expected2, ...))
* }
* ```
*/
fun <T> TestScope.collectValues(
flow: Flow<T>,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
): FlowValues<T> {
val values = mutableListOf<T>()
backgroundScope.launch(context, start) { flow.collect(values::add) }
return FlowValuesImpl {
runCurrent()
lastValue
values.toList()
}
}
/** @see collectLastValue */
interface FlowValue<T> : ReadOnlyProperty<Any?, T?> {
operator fun invoke(): T?
interface FlowValue<T> : ReadOnlyProperty<Any?, T> {
operator fun invoke(): T
}
private class FlowValueImpl<T>(private val block: () -> T?) : FlowValue<T> {
override operator fun invoke(): T? = block()
override fun getValue(thisRef: Any?, property: KProperty<*>): T? = invoke()
/** @see collectValues */
interface FlowValues<T> : ReadOnlyProperty<Any?, List<T>> {
operator fun invoke(): List<T>
}
private class FlowValueImpl<T>(private val block: () -> T) : FlowValue<T> {
override operator fun invoke(): T = block()
override fun getValue(thisRef: Any?, property: KProperty<*>): T = invoke()
}
private class FlowValuesImpl<T>(private val block: () -> List<T>) : FlowValues<T> {
override operator fun invoke(): List<T> = block()
override fun getValue(thisRef: Any?, property: KProperty<*>): List<T> = invoke()
}