Merge changes Icb761be8,I94130e5e into udc-dev

* changes:
  Removes unused getBouncerContainer method.
  [multi-shade] Lock screen touch integration.
This commit is contained in:
Ale Nijamkin
2023-03-20 17:21:34 +00:00
committed by Android (Google) Code Review
15 changed files with 711 additions and 60 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,21 +185,17 @@ 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;
}
}
/**
* @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
*/
@@ -349,16 +350,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 +383,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

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

View File

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

View File

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

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
@@ -62,7 +63,6 @@ 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
@@ -129,6 +129,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 +164,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,21 +315,13 @@ 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)
assertThat(shouldIntercept).isTrue()
}
@Test
fun testGetBouncerContainer() =
testScope.runTest {
Mockito.clearInvocations(view)
underTest.bouncerContainer
verify(view).findViewById<ViewGroup>(R.id.keyguard_bouncer_container)
}
@Test
fun testGetKeyguardMessageArea() =
testScope.runTest {

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

View File

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