From 0628dfba68283b01d136c1069bb57437714579e3 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 23 Mar 2023 14:30:16 -0700 Subject: [PATCH 1/3] Adds Flow extension for collecting values into a list. Usage is just: val values by collectValues(underTest.flow) assertThat(values).isEqualTo(listOf(expected1, expected2, ...)) Test: N/A Bug: 274159734 Flag: N/A Change-Id: I9f7ff736f9b7795c4bea91355bb2a42d00b6dfb5 --- .../com/android/systemui/coroutines/Flow.kt | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt index c2947b42f56d5..e1ba074ac860d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt @@ -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 TestScope.collectLastValue( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, ): FlowValue { - 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 TestScope.collectValues( + flow: Flow, + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, +): FlowValues { + val values = mutableListOf() + backgroundScope.launch(context, start) { flow.collect(values::add) } + return FlowValuesImpl { runCurrent() - lastValue + values.toList() } } /** @see collectLastValue */ -interface FlowValue : ReadOnlyProperty { - operator fun invoke(): T? +interface FlowValue : ReadOnlyProperty { + operator fun invoke(): T } -private class FlowValueImpl(private val block: () -> T?) : FlowValue { - override operator fun invoke(): T? = block() - override fun getValue(thisRef: Any?, property: KProperty<*>): T? = invoke() +/** @see collectValues */ +interface FlowValues : ReadOnlyProperty> { + operator fun invoke(): List +} + +private class FlowValueImpl(private val block: () -> T) : FlowValue { + override operator fun invoke(): T = block() + override fun getValue(thisRef: Any?, property: KProperty<*>): T = invoke() +} + +private class FlowValuesImpl(private val block: () -> List) : FlowValues { + override operator fun invoke(): List = block() + override fun getValue(thisRef: Any?, property: KProperty<*>): List = invoke() } From 1197d0e142baf0127011ec342c23145571871e34 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 23 Mar 2023 14:32:36 -0700 Subject: [PATCH 2/3] KeyguardTransitionInteractor#transitionValue. This is a function that returns a flow of all transitions in or out of the given KeyguardState. Should be useful when a consumer cares about a specific state but doesn't care about which other state we're move in from or moving out to. Bug: 274159734 Flag: N/A Test: unit test included Test: actively calling this from the next CL Change-Id: Iab6a756892a388e306562eae1a18a17e32112417 --- .../KeyguardTransitionInteractor.kt | 34 ++- .../KeyguardTransitionInteractorTest.kt | 232 +++++++++--------- 2 files changed, 147 insertions(+), 119 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 3c0ec350c5c5b..aabd212c1bd37 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -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 = @@ -62,10 +62,6 @@ constructor( /** LOCKSCREEN->AOD transition information. */ val lockscreenToAodTransition: Flow = repository.transition(LOCKSCREEN, AOD) - /** LOCKSCREEN->PRIMARY_BOUNCER transition information. */ - val mLockscreenToPrimaryBouncerTransition: Flow = - repository.transition(LOCKSCREEN, PRIMARY_BOUNCER) - /** LOCKSCREEN->DREAMING transition information. */ val lockscreenToDreamingTransition: Flow = 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 = 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 = 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 = repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED } - /* The last completed [KeyguardState] transition */ + /** The last completed [KeyguardState] transition */ val finishedKeyguardState: Flow = 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 { + return repository.transitions + .filter { it.from == state || it.to == state } + .map { + if (it.from == state) { + 1 - it.value + } else { + it.value + } + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 3d13d80926516..276b3e39180b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -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() - 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() - val job2 = - underTest.aodToLockscreenTransition - .onEach { aodToLockscreenSteps.add(it) } - .launchIn(this) + val steps = mutableListOf() + 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() - 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() - val job = underTest.dozeAmountTransition.onEach { dozeAmountSteps.add(it) }.launchIn(this) + fun dozeAmountTransitionTest() = runTest { + val dozeAmountSteps by collectValues(underTest.dozeAmountTransition) - val steps = mutableListOf() + val steps = mutableListOf() - 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() - val job = underTest.finishedKeyguardState.onEach { finishedSteps.add(it) }.launchIn(this) + fun keyguardStateTests() = runTest { + val finishedSteps by collectValues(underTest.finishedKeyguardState) - val steps = mutableListOf() + val steps = mutableListOf() - 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() - val job = - underTest.finishedKeyguardTransitionStep.onEach { finishedSteps.add(it) }.launchIn(this) + fun finishedKeyguardTransitionStepTests() = runTest { + val finishedSteps by collectValues(underTest.finishedKeyguardTransitionStep) - val steps = mutableListOf() + val steps = mutableListOf() - 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() - val job = - underTest.startedKeyguardTransitionStep.onEach { startedSteps.add(it) }.launchIn(this) + fun startedKeyguardTransitionStepTests() = runTest { + val startedSteps by collectValues(underTest.startedKeyguardTransitionStep) - val steps = mutableListOf() + val steps = mutableListOf() - 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)) + } } From 58cd612694eff84cb86fc161e9afa7afa78f9d8e Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 23 Mar 2023 14:43:58 -0700 Subject: [PATCH 3/3] [multi-shade] Bouncer touch integration. Connects multi-shade's touch interaction to the bouncer. On the lock screen such that dragging up when shades are collapsed brings up the bouncer. https://drive.google.com/file/d/1AiRelfhAg2QFY-EoHvHtG8V6KIzcST_g/view?usp=sharing Bug: 274159734 Flag: DUAL_SHADE Test: unit tests expanded for MultiShadeEventInteractor Test: manually verified that: 1. dragging up when shades are collapsed brings up the bouncer. 2. back gesture hides the bouncer, allowing a drag down to show the dual shades again. 3. unlocking the bouncer works as expected. Change-Id: I81118cb2ccba3bb2887def2568403198e7852c97 --- .../MultiShadeMotionEventInteractor.kt | 122 ++++++++++--- .../NotificationPanelViewController.java | 12 +- .../MultiShadeMotionEventInteractorTest.kt | 166 +++++++++++++++++- ...tificationShadeWindowViewControllerTest.kt | 12 +- .../shade/NotificationShadeWindowViewTest.kt | 10 +- 5 files changed, 286 insertions(+), 36 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt index ff7c9015eef45..33b3087678e13 100644 --- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt @@ -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 = - interactor.isAnyShadeExpanded.stateIn( + multiShadeInteractor.isAnyShadeExpanded.stateIn( scope = applicationScope, started = SharingStarted.Eagerly, initialValue = false, ) + private val isBouncerShowing: StateFlow = + 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 } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index fbcc9ff583b32..9bfa1b32d3559 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -233,6 +233,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; @@ -243,7 +245,6 @@ import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Provider; -import kotlin.Unit; import kotlinx.coroutines.CoroutineDispatcher; @CentralSurfacesComponent.CentralSurfacesScope @@ -387,6 +388,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; /** @@ -851,6 +853,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; @@ -4018,8 +4021,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(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt index f807146cdf121..c6db565d79545 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt @@ -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, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 5f34b2f0f87ff..006a7c7355507 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -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(), ) }, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index b40181e24e6d1..1a52067e83061 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -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(), ) }, )