[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:
Alejandro Nijamkin
2023-03-17 14:01:38 -07:00
parent 4ea5e12c25
commit 1263ecaf89
11 changed files with 712 additions and 36 deletions

View File

@@ -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(

View File

@@ -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.
*

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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()