From 1263ecaf89492592b59ed086835172662ee32121 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Fri, 17 Mar 2023 14:01:38 -0700 Subject: [PATCH 1/2] [multi-shade] Lock screen touch integration. Integrates the multi-shade framework into the existing touch routing system in NotificationShadeWindowView such that, on the lock screen, swiping down shows the dual shades and tapping outside collapses them. The approach taken was to implement a second interactor, MultiShadeMotionEventInteractor and integrating it where the DragDownHelper is currently integrated. The next steps are: a. Refactor the bouncer a bit so it can receive its expansion from multi-shade, not just from the current shade expansion b. Drive the expansion of the bouncer by dragging up when the shades are collapsed, while on the lock screen c. Figure out why clicking on the user switcher chip in the status bar doesn't work in dual shade (there likely is some interference between the right-hand side shade and the chip) Bug: 274159734 Test: included new unit tests for MultiShadeMotionEventInteractor Test: updated existing unit tests for MultiShadeInteractor Test: manually verified the following interactions on the lock screen: 1. Dragging down anywhere reveals the correct shade 2. As the shades are revealed, the scrim fades in (there was a bug in this before) 3. When shade A is expanded, touching anywhere in the area of shade B collapses shade A 4. Clicking outside the shade collapses it 5. When any shade is expanded, it's not possible to touch things behind the scrim 6. When both shades are collapsed, touch passes correctly to the existing UI elements like the notifications Change-Id: I94130e5e8dbb3b2a398452651855a47f231c2764 --- .../multishade/ui/composable/MultiShade.kt | 3 +- .../domain/interactor/MultiShadeInteractor.kt | 5 + .../MultiShadeMotionEventInteractor.kt | 191 ++++++++++ .../systemui/multishade/shared/math/Math.kt | 27 ++ .../ui/viewmodel/MultiShadeViewModel.kt | 6 +- ...NotificationShadeWindowViewController.java | 40 ++- .../interactor/MultiShadeInteractorTest.kt | 22 ++ .../MultiShadeMotionEventInteractorTest.kt | 334 ++++++++++++++++++ .../multishade/shared/math/MathTest.kt | 68 ++++ ...tificationShadeWindowViewControllerTest.kt | 28 +- .../shade/NotificationShadeWindowViewTest.kt | 24 +- 11 files changed, 712 insertions(+), 36 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt index b9e38cf3cc60e..99fe26ce1f3bb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt @@ -53,6 +53,7 @@ fun MultiShade( modifier: Modifier = Modifier, ) { val isScrimEnabled: Boolean by viewModel.isScrimEnabled.collectAsState() + val scrimAlpha: Float by viewModel.scrimAlpha.collectAsState() // TODO(b/273298030): find a different way to get the height constraint from its parent. BoxWithConstraints(modifier = modifier) { @@ -61,7 +62,7 @@ fun MultiShade( Scrim( modifier = Modifier.fillMaxSize(), remoteTouch = viewModel::onScrimTouched, - alpha = { viewModel.scrimAlpha.value }, + alpha = { scrimAlpha }, isScrimEnabled = isScrimEnabled, ) Shade( diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt index b9f6d83d8406f..ebb8639b8922a 100644 --- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt @@ -23,6 +23,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.multishade.data.model.MultiShadeInteractionModel import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy import com.android.systemui.multishade.data.repository.MultiShadeRepository +import com.android.systemui.multishade.shared.math.isZero import com.android.systemui.multishade.shared.model.ProxiedInputModel import com.android.systemui.multishade.shared.model.ShadeConfig import com.android.systemui.multishade.shared.model.ShadeId @@ -63,6 +64,10 @@ constructor( } } + /** Whether any shade is expanded, even a little bit. */ + val isAnyShadeExpanded: Flow = + maxShadeExpansion.map { maxExpansion -> !maxExpansion.isZero() }.distinctUntilChanged() + /** * A _processed_ version of the proxied input flow. * 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 new file mode 100644 index 0000000000000..ff7c9015eef45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.multishade.domain.interactor + +import android.content.Context +import android.view.MotionEvent +import android.view.ViewConfiguration +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.multishade.shared.model.ProxiedInputModel +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.stateIn + +/** + * Encapsulates business logic to handle [MotionEvent]-based user input. + * + * This class is meant purely for the legacy `View`-based system to be able to pass `MotionEvent`s + * into the newer multi-shade framework for processing. + */ +class MultiShadeMotionEventInteractor +@Inject +constructor( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + private val interactor: MultiShadeInteractor, +) { + + private val isAnyShadeExpanded: StateFlow = + interactor.isAnyShadeExpanded.stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + private var interactionState: InteractionState? = null + + /** + * Returns `true` if the given [MotionEvent] and the rest of events in this gesture should be + * passed to this interactor's [onTouchEvent] method. + * + * Note: the caller should continue to pass [MotionEvent] instances into this method, even if it + * returns `false` as the gesture may be intercepted mid-stream. + */ + fun shouldIntercept(event: MotionEvent): Boolean { + if (isAnyShadeExpanded.value) { + // If any shade is expanded, we assume that touch handling outside the shades is handled + // by the scrim that appears behind the shades. No need to intercept anything here. + return false + } + + return when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + // Record where the pointer was placed and which pointer it was. + interactionState = + InteractionState( + initialX = event.x, + initialY = event.y, + currentY = event.y, + pointerId = event.getPointerId(0), + isDraggingHorizontally = false, + isDraggingVertically = false, + ) + + false + } + MotionEvent.ACTION_MOVE -> { + interactionState?.let { + 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) + val touchSlop = ViewConfiguration.get(applicationContext).scaledTouchSlop + interactionState = + when { + yDistanceTravelled > touchSlop -> + it.copy(isDraggingVertically = true) + xDistanceTravelled > touchSlop -> + it.copy(isDraggingHorizontally = true) + else -> interactionState + } + } + } + + // We want to intercept the rest of the gesture if we're dragging. + interactionState.isDraggingVertically() + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> + // Make sure that we intercept the up or cancel if we're dragging, to handle drag + // end and cancel. + interactionState.isDraggingVertically() + else -> false + } + } + + /** + * Notifies that a [MotionEvent] in a series of events of a gesture that was intercepted due to + * the result of [shouldIntercept] has been received. + * + * @param event The [MotionEvent] to handle. + * @param viewWidthPx The width of the view, in pixels. + * @return `true` if the event was consumed, `false` otherwise. + */ + fun onTouchEvent(event: MotionEvent, viewWidthPx: Int): Boolean { + return when (event.actionMasked) { + MotionEvent.ACTION_MOVE -> { + interactionState?.let { + if (it.isDraggingVertically) { + val pointerIndex = event.findPointerIndex(it.pointerId) + val previousY = it.currentY + val currentY = event.getY(pointerIndex) + interactionState = + it.copy( + currentY = currentY, + ) + + val yDragAmountPx = currentY - previousY + if (yDragAmountPx != 0f) { + interactor.sendProxiedInput( + ProxiedInputModel.OnDrag( + xFraction = event.x / viewWidthPx, + yDragAmountPx = yDragAmountPx, + ) + ) + } + } + } + + true + } + MotionEvent.ACTION_UP -> { + if (interactionState.isDraggingVertically()) { + // 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) + } + + interactionState = null + true + } + MotionEvent.ACTION_CANCEL -> { + if (interactionState.isDraggingVertically()) { + // Our drag gesture was canceled by the system. This happens primarily in one of + // two occasions: (a) the parent view has decided to intercept the gesture + // itself and/or route it to a different child view or (b) the pointer has + // traveled beyond the bounds of our view and/or the touch display. Either way, + // we pass the cancellation event to the multi-shade framework to record it. + // Doing that allows the multi-shade framework to know that the gesture ended to + // allow new gestures to be accepted. + interactor.sendProxiedInput(ProxiedInputModel.OnDragCancel) + } + + interactionState = null + true + } + else -> false + } + } + + private data class InteractionState( + val initialX: Float, + val initialY: Float, + val currentY: Float, + val pointerId: Int, + val isDraggingHorizontally: Boolean, + val isDraggingVertically: Boolean, + ) + + private fun InteractionState?.isDraggingVertically(): Boolean { + return this?.isDraggingVertically == true + } +} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt new file mode 100644 index 0000000000000..c2eaf72a841a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.multishade.shared.math + +import androidx.annotation.VisibleForTesting +import kotlin.math.abs + +/** Returns `true` if this [Float] is within [epsilon] of `0`. */ +fun Float.isZero(epsilon: Float = EPSILON): Boolean { + return abs(this) < epsilon +} + +@VisibleForTesting private const val EPSILON = 0.0001f diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt index ce6ab977dea26..ed92c5469d236 100644 --- a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -87,10 +86,7 @@ class MultiShadeViewModel( when (shadeConfig) { // In the dual shade configuration, the scrim is enabled when the expansion is // greater than zero on any one of the shades. - is ShadeConfig.DualShadeConfig -> - interactor.maxShadeExpansion - .map { expansion -> expansion > 0 } - .distinctUntilChanged() + is ShadeConfig.DualShadeConfig -> interactor.isAnyShadeExpanded // No scrim in the single shade configuration. is ShadeConfig.SingleShadeConfig -> flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 5f6f158277d72..788908fd71b91 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -32,6 +32,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.AuthKeyguardMessageArea; import com.android.keyguard.LockIconViewController; @@ -50,6 +52,7 @@ import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor; +import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor; import com.android.systemui.multishade.ui.view.MultiShadeView; import com.android.systemui.statusbar.DragDownHelper; import com.android.systemui.statusbar.LockscreenShadeTransitionController; @@ -118,6 +121,7 @@ public class NotificationShadeWindowViewController { step.getTransitionState() == TransitionState.RUNNING; }; private final SystemClock mClock; + private final @Nullable MultiShadeMotionEventInteractor mMultiShadeMotionEventInteractor; @Inject public NotificationShadeWindowViewController( @@ -145,7 +149,8 @@ public class NotificationShadeWindowViewController { PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel, FeatureFlags featureFlags, Provider multiShadeInteractorProvider, - SystemClock clock) { + SystemClock clock, + Provider multiShadeMotionEventInteractorProvider) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; mStatusBarStateController = statusBarStateController; @@ -180,11 +185,14 @@ public class NotificationShadeWindowViewController { mClock = clock; if (ComposeFacade.INSTANCE.isComposeAvailable() && featureFlags.isEnabled(Flags.DUAL_SHADE)) { + mMultiShadeMotionEventInteractor = multiShadeMotionEventInteractorProvider.get(); final ViewStub multiShadeViewStub = mView.findViewById(R.id.multi_shade_stub); if (multiShadeViewStub != null) { final MultiShadeView multiShadeView = (MultiShadeView) multiShadeViewStub.inflate(); multiShadeView.init(multiShadeInteractorProvider.get(), clock); } + } else { + mMultiShadeMotionEventInteractor = null; } } @@ -349,16 +357,17 @@ public class NotificationShadeWindowViewController { return true; } - boolean intercept = false; - if (mNotificationPanelViewController.isFullyExpanded() + if (mMultiShadeMotionEventInteractor != null) { + // This interactor is not null only if the dual shade feature is enabled. + return mMultiShadeMotionEventInteractor.shouldIntercept(ev); + } else if (mNotificationPanelViewController.isFullyExpanded() && mDragDownHelper.isDragDownEnabled() && !mService.isBouncerShowing() && !mStatusBarStateController.isDozing()) { - intercept = mDragDownHelper.onInterceptTouchEvent(ev); + return mDragDownHelper.onInterceptTouchEvent(ev); + } else { + return false; } - - return intercept; - } @Override @@ -381,13 +390,20 @@ public class NotificationShadeWindowViewController { return true; } - if ((mDragDownHelper.isDragDownEnabled() && !handled) - || mDragDownHelper.isDraggingDown()) { - // we still want to finish our drag down gesture when locking the screen - handled = mDragDownHelper.onTouchEvent(ev); + if (handled) { + return true; } - return handled; + if (mMultiShadeMotionEventInteractor != null) { + // This interactor is not null only if the dual shade feature is enabled. + return mMultiShadeMotionEventInteractor.onTouchEvent(ev, mView.getWidth()); + } else if (mDragDownHelper.isDragDownEnabled() + || mDragDownHelper.isDraggingDown()) { + // we still want to finish our drag down gesture when locking the screen + return mDragDownHelper.onTouchEvent(ev); + } else { + return false; + } } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt index 415e68f6013df..bcc99bc8dd0ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt @@ -72,6 +72,28 @@ class MultiShadeInteractorTest : SysuiTestCase() { assertThat(maxShadeExpansion).isEqualTo(0f) } + @Test + fun isAnyShadeExpanded() = + testScope.runTest { + val underTest = create() + val isAnyShadeExpanded: Boolean? by collectLastValue(underTest.isAnyShadeExpanded) + assertWithMessage("isAnyShadeExpanded must start with false!") + .that(isAnyShadeExpanded) + .isFalse() + + underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f) + assertThat(isAnyShadeExpanded).isTrue() + + underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f) + assertThat(isAnyShadeExpanded).isTrue() + + underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f) + assertThat(isAnyShadeExpanded).isTrue() + + underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f) + assertThat(isAnyShadeExpanded).isFalse() + } + @Test fun isVisible_dualShadeConfig() = testScope.runTest { 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 new file mode 100644 index 0000000000000..f807146cdf121 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.multishade.domain.interactor + +import android.view.MotionEvent +import android.view.ViewConfiguration +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy +import com.android.systemui.multishade.data.repository.MultiShadeRepository +import com.android.systemui.multishade.shared.model.ProxiedInputModel +import com.android.systemui.multishade.shared.model.ShadeId +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class MultiShadeMotionEventInteractorTest : SysuiTestCase() { + + private lateinit var underTest: MultiShadeMotionEventInteractor + + private lateinit var testScope: TestScope + private lateinit var motionEvents: MutableSet + private lateinit var repository: MultiShadeRepository + private lateinit var interactor: MultiShadeInteractor + private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop + + @Before + fun setUp() { + testScope = TestScope() + motionEvents = mutableSetOf() + + val inputProxy = MultiShadeInputProxy() + repository = + MultiShadeRepository( + applicationContext = context, + inputProxy = inputProxy, + ) + interactor = + MultiShadeInteractor( + applicationScope = testScope.backgroundScope, + repository = repository, + inputProxy = inputProxy, + ) + underTest = + MultiShadeMotionEventInteractor( + applicationContext = context, + applicationScope = testScope.backgroundScope, + interactor = interactor, + ) + } + + @After + fun tearDown() { + motionEvents.forEach { motionEvent -> motionEvent.recycle() } + } + + @Test + fun shouldIntercept_initialDown_returnsFalse() = + testScope.runTest { + assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))).isFalse() + } + + @Test + fun shouldIntercept_moveBelowTouchSlop_returnsFalse() = + testScope.runTest { + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) + + assertThat( + underTest.shouldIntercept( + motionEvent( + MotionEvent.ACTION_MOVE, + y = touchSlop - 1f, + ) + ) + ) + .isFalse() + } + + @Test + fun shouldIntercept_moveAboveTouchSlop_returnsTrue() = + testScope.runTest { + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) + + assertThat( + underTest.shouldIntercept( + motionEvent( + MotionEvent.ACTION_MOVE, + y = touchSlop + 1f, + ) + ) + ) + .isTrue() + } + + @Test + fun shouldIntercept_moveAboveTouchSlop_butHorizontalFirst_returnsFalse() = + testScope.runTest { + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) + + assertThat( + underTest.shouldIntercept( + motionEvent( + MotionEvent.ACTION_MOVE, + x = touchSlop + 1f, + ) + ) + ) + .isFalse() + assertThat( + underTest.shouldIntercept( + motionEvent( + MotionEvent.ACTION_MOVE, + y = touchSlop + 1f, + ) + ) + ) + .isFalse() + } + + @Test + fun shouldIntercept_up_afterMovedAboveTouchSlop_returnsTrue() = + testScope.runTest { + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f)) + + assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isTrue() + } + + @Test + fun shouldIntercept_cancel_afterMovedAboveTouchSlop_returnsTrue() = + testScope.runTest { + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f)) + + assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isTrue() + } + + @Test + fun shouldIntercept_moveAboveTouchSlopAndUp_butShadeExpanded_returnsFalse() = + testScope.runTest { + repository.setExpansion(ShadeId.LEFT, 0.1f) + runCurrent() + + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) + + assertThat( + underTest.shouldIntercept( + motionEvent( + MotionEvent.ACTION_MOVE, + y = touchSlop + 1f, + ) + ) + ) + .isFalse() + assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse() + } + + @Test + fun shouldIntercept_moveAboveTouchSlopAndCancel_butShadeExpanded_returnsFalse() = + testScope.runTest { + repository.setExpansion(ShadeId.LEFT, 0.1f) + runCurrent() + + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) + + assertThat( + underTest.shouldIntercept( + motionEvent( + MotionEvent.ACTION_MOVE, + y = touchSlop + 1f, + ) + ) + ) + .isFalse() + assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse() + } + + @Test + fun tap_doesNotSendProxiedInput() = + testScope.runTest { + val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) + val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) + val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) + + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP)) + + assertThat(leftShadeProxiedInput).isNull() + assertThat(rightShadeProxiedInput).isNull() + assertThat(singleShadeProxiedInput).isNull() + } + + @Test + fun dragBelowTouchSlop_doesNotSendProxiedInput() = + testScope.runTest { + val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) + val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) + val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) + + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop - 1f)) + underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP)) + + assertThat(leftShadeProxiedInput).isNull() + assertThat(rightShadeProxiedInput).isNull() + assertThat(singleShadeProxiedInput).isNull() + } + + @Test + fun dragAboveTouchSlopAndUp() = + testScope.runTest { + val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) + val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) + val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) + + underTest.shouldIntercept( + motionEvent( + MotionEvent.ACTION_DOWN, + x = 100f, // left shade + ) + ) + assertThat(leftShadeProxiedInput).isNull() + assertThat(rightShadeProxiedInput).isNull() + assertThat(singleShadeProxiedInput).isNull() + + val yDragAmountPx = touchSlop + 1f + val moveEvent = + motionEvent( + MotionEvent.ACTION_MOVE, + x = 100f, // left shade + y = yDragAmountPx, + ) + assertThat(underTest.shouldIntercept(moveEvent)).isTrue() + underTest.onTouchEvent(moveEvent, viewWidthPx = 1000) + assertThat(leftShadeProxiedInput) + .isEqualTo( + ProxiedInputModel.OnDrag( + xFraction = 0.1f, + yDragAmountPx = yDragAmountPx, + ) + ) + assertThat(rightShadeProxiedInput).isNull() + assertThat(singleShadeProxiedInput).isNull() + + val upEvent = motionEvent(MotionEvent.ACTION_UP) + assertThat(underTest.shouldIntercept(upEvent)).isTrue() + underTest.onTouchEvent(upEvent, viewWidthPx = 1000) + assertThat(leftShadeProxiedInput).isNull() + assertThat(rightShadeProxiedInput).isNull() + assertThat(singleShadeProxiedInput).isNull() + } + + @Test + fun dragAboveTouchSlopAndCancel() = + testScope.runTest { + val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) + val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) + val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) + + underTest.shouldIntercept( + motionEvent( + MotionEvent.ACTION_DOWN, + x = 900f, // right shade + ) + ) + assertThat(leftShadeProxiedInput).isNull() + assertThat(rightShadeProxiedInput).isNull() + assertThat(singleShadeProxiedInput).isNull() + + val yDragAmountPx = touchSlop + 1f + val moveEvent = + motionEvent( + MotionEvent.ACTION_MOVE, + x = 900f, // right shade + y = yDragAmountPx, + ) + assertThat(underTest.shouldIntercept(moveEvent)).isTrue() + underTest.onTouchEvent(moveEvent, viewWidthPx = 1000) + assertThat(leftShadeProxiedInput).isNull() + assertThat(rightShadeProxiedInput) + .isEqualTo( + ProxiedInputModel.OnDrag( + xFraction = 0.9f, + yDragAmountPx = yDragAmountPx, + ) + ) + assertThat(singleShadeProxiedInput).isNull() + + val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL) + assertThat(underTest.shouldIntercept(cancelEvent)).isTrue() + underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000) + assertThat(leftShadeProxiedInput).isNull() + assertThat(rightShadeProxiedInput).isNull() + assertThat(singleShadeProxiedInput).isNull() + } + + private fun TestScope.motionEvent( + action: Int, + downTime: Long = currentTime, + eventTime: Long = currentTime, + x: Float = 0f, + y: Float = 0f, + ): MotionEvent { + val motionEvent = MotionEvent.obtain(downTime, eventTime, action, x, y, 0) + motionEvents.add(motionEvent) + return motionEvent + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt new file mode 100644 index 0000000000000..8935309829267 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.multishade.shared.math + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class MathTest : SysuiTestCase() { + + @Test + fun isZero_zero_true() { + assertThat(0f.isZero(epsilon = EPSILON)).isTrue() + } + + @Test + fun isZero_belowPositiveEpsilon_true() { + assertThat((EPSILON * 0.999999f).isZero(epsilon = EPSILON)).isTrue() + } + + @Test + fun isZero_aboveNegativeEpsilon_true() { + assertThat((EPSILON * -0.999999f).isZero(epsilon = EPSILON)).isTrue() + } + + @Test + fun isZero_positiveEpsilon_false() { + assertThat(EPSILON.isZero(epsilon = EPSILON)).isFalse() + } + + @Test + fun isZero_negativeEpsilon_false() { + assertThat((-EPSILON).isZero(epsilon = EPSILON)).isFalse() + } + + @Test + fun isZero_positive_false() { + assertThat(1f.isZero(epsilon = EPSILON)).isFalse() + } + + @Test + fun isZero_negative_false() { + assertThat((-1f).isZero(epsilon = EPSILON)).isFalse() + } + + companion object { + private const val EPSILON = 0.0001f + } +} 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 629208e130afb..f3dc7b56d6e84 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransition import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy import com.android.systemui.multishade.data.repository.MultiShadeRepository import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor +import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.NotificationInsetsController @@ -67,8 +68,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 @@ -129,6 +130,16 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { val inputProxy = MultiShadeInputProxy() testScope = TestScope() + val multiShadeInteractor = + MultiShadeInteractor( + applicationScope = testScope.backgroundScope, + repository = + MultiShadeRepository( + applicationContext = context, + inputProxy = inputProxy, + ), + inputProxy = inputProxy, + ) underTest = NotificationShadeWindowViewController( lockscreenShadeTransitionController, @@ -154,18 +165,15 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { keyguardTransitionInteractor, primaryBouncerToGoneTransitionViewModel, featureFlags, + { multiShadeInteractor }, + FakeSystemClock(), { - MultiShadeInteractor( + MultiShadeMotionEventInteractor( + applicationContext = context, applicationScope = testScope.backgroundScope, - repository = - MultiShadeRepository( - applicationContext = context, - inputProxy = inputProxy, - ), - inputProxy = inputProxy, + interactor = multiShadeInteractor, ) }, - FakeSystemClock(), ) underTest.setupExpandedStatusBar() @@ -308,7 +316,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { fun shouldInterceptTouchEvent_statusBarKeyguardViewManagerShouldIntercept() { // down event should be intercepted by keyguardViewManager whenever(statusBarKeyguardViewManager.shouldInterceptTouchEvent(DOWN_EVENT)) - .thenReturn(true) + .thenReturn(true) // Then touch should not be intercepted val shouldIntercept = interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT) 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 b4b5ec1262346..b40181e24e6d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -37,6 +37,7 @@ import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransition import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy import com.android.systemui.multishade.data.repository.MultiShadeRepository import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor +import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.statusbar.DragDownHelper import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -140,6 +141,16 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { featureFlags.set(Flags.DUAL_SHADE, false) val inputProxy = MultiShadeInputProxy() testScope = TestScope() + val multiShadeInteractor = + MultiShadeInteractor( + applicationScope = testScope.backgroundScope, + repository = + MultiShadeRepository( + applicationContext = context, + inputProxy = inputProxy, + ), + inputProxy = inputProxy, + ) controller = NotificationShadeWindowViewController( lockscreenShadeTransitionController, @@ -165,18 +176,15 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { keyguardTransitionInteractor, primaryBouncerToGoneTransitionViewModel, featureFlags, + { multiShadeInteractor }, + FakeSystemClock(), { - MultiShadeInteractor( + MultiShadeMotionEventInteractor( + applicationContext = context, applicationScope = testScope.backgroundScope, - repository = - MultiShadeRepository( - applicationContext = context, - inputProxy = inputProxy, - ), - inputProxy = inputProxy, + interactor = multiShadeInteractor, ) }, - FakeSystemClock(), ) controller.setupExpandedStatusBar() From 45028616b78de9c1cee204805fbc1493ebc69c29 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Sun, 19 Mar 2023 09:37:12 -0700 Subject: [PATCH 2/2] Removes unused getBouncerContainer method. Bug: 274159734 Test: code still builds. Change-Id: Icb761be844f1b14736984dcab8815ea1aff8f0ff --- .../shade/NotificationShadeWindowViewController.java | 7 ------- .../systemui/statusbar/phone/CentralSurfaces.java | 2 -- .../systemui/statusbar/phone/CentralSurfacesImpl.java | 5 ----- .../statusbar/phone/StatusBarKeyguardViewManager.java | 1 - .../NotificationShadeWindowViewControllerTest.kt | 11 +---------- .../phone/StatusBarKeyguardViewManagerTest.java | 1 - 6 files changed, 1 insertion(+), 26 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 788908fd71b91..0318fa570a787 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -196,13 +196,6 @@ public class NotificationShadeWindowViewController { } } - /** - * @return Location where to place the KeyguardBouncer - */ - public ViewGroup getBouncerContainer() { - return mView.findViewById(R.id.keyguard_bouncer_container); - } - /** * @return Location where to place the KeyguardMessageArea */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 55fa47951fe1a..7f8c1351aa7a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -217,8 +217,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn NotificationPanelViewController getNotificationPanelViewController(); - ViewGroup getBouncerContainer(); - /** Get the Keyguard Message Area that displays auth messages. */ AuthKeyguardMessageArea getKeyguardMessageArea(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 5e3c1c59666d8..2f404873dc7a4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1721,11 +1721,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return mNotificationPanelViewController; } - @Override - public ViewGroup getBouncerContainer() { - return mNotificationShadeWindowViewController.getBouncerContainer(); - } - @Override public AuthKeyguardMessageArea getKeyguardMessageArea() { return mNotificationShadeWindowViewController.getKeyguardMessageArea(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 06d0758c90eb4..f06b5db845885 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -381,7 +381,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mCentralSurfaces = centralSurfaces; mBiometricUnlockController = biometricUnlockController; - ViewGroup container = mCentralSurfaces.getBouncerContainer(); mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mExpansionCallback); mNotificationPanelViewController = notificationPanelViewController; if (shadeExpansionStateManager != null) { 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 f3dc7b56d6e84..5f34b2f0f87ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -63,13 +63,12 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock -import org.mockito.Mockito import org.mockito.Mockito.anyFloat import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +import org.mockito.Mockito.`when` as whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -323,14 +322,6 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { assertThat(shouldIntercept).isTrue() } - @Test - fun testGetBouncerContainer() = - testScope.runTest { - Mockito.clearInvocations(view) - underTest.bouncerContainer - verify(view).findViewById(R.id.keyguard_bouncer_container) - } - @Test fun testGetKeyguardMessageArea() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index d9546877a861d..14aee4e13a8f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -154,7 +154,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); - when(mCentralSurfaces.getBouncerContainer()).thenReturn(mContainer); when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea); when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class))) .thenReturn(mKeyguardMessageAreaController);