[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
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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<Boolean> =
|
||||
maxShadeExpansion.map { maxExpansion -> !maxExpansion.isZero() }.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* A _processed_ version of the proxied input flow.
|
||||
*
|
||||
|
||||
@@ -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<Boolean> =
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<MultiShadeInteractor> multiShadeInteractorProvider,
|
||||
SystemClock clock) {
|
||||
SystemClock clock,
|
||||
Provider<MultiShadeMotionEventInteractor> 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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<MotionEvent>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user