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(), ) }, )