Merge changes Icb761be8,I94130e5e into udc-dev
* changes: Removes unused getBouncerContainer method. [multi-shade] Lock screen touch integration.
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,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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user