Prevent alternateBouncer from consuming touches in new UdfpsOverlay

UdfpsController now passes UdfpsOverlayParams to
AlternateBouncerInteractor to allow the
NotificationShadeWIndowViewController to check if a touch is within the
UdfpsOverlay bounds before intercepting the touch.

UdfpsController sends the touch back if it's not being used for
authentication.

Bug: 267487141
Bug: 269788322
Test: atest SystemUITests:com.android.systemui.biometrics
Change-Id: Ie6f253365210bb5262008a1dad154bc8212f4500
This commit is contained in:
Austin Delgado
2023-02-02 10:18:40 -08:00
parent 04884f2145
commit c95ced466b
11 changed files with 276 additions and 17 deletions

View File

@@ -51,6 +51,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.android.settingslib.udfps.UdfpsOverlayParams;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.biometrics.AuthController;
@@ -778,7 +779,7 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
}
@Override
public void onUdfpsLocationChanged() {
public void onUdfpsLocationChanged(UdfpsOverlayParams udfpsOverlayParams) {
updateUdfpsConfig();
}
};

View File

@@ -148,6 +148,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
@NonNull private final WindowManager mWindowManager;
@NonNull private final DisplayManager mDisplayManager;
@Nullable private UdfpsController mUdfpsController;
@Nullable private UdfpsOverlayParams mUdfpsOverlayParams;
@Nullable private IUdfpsRefreshRateRequestCallback mUdfpsRefreshRateRequestCallback;
@Nullable private SideFpsController mSideFpsController;
@Nullable private UdfpsLogger mUdfpsLogger;
@@ -806,6 +807,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
final FingerprintSensorPropertiesInternal udfpsProp = mUdfpsProps.get(0);
final Rect previousUdfpsBounds = mUdfpsBounds;
final UdfpsOverlayParams previousUdfpsOverlayParams = mUdfpsOverlayParams;
mUdfpsBounds = udfpsProp.getLocation().getRect();
mUdfpsBounds.scale(mScaleFactor);
@@ -815,7 +818,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
mCachedDisplayInfo.getNaturalWidth(), /* right */
mCachedDisplayInfo.getNaturalHeight() /* botom */);
final UdfpsOverlayParams overlayParams = new UdfpsOverlayParams(
mUdfpsOverlayParams = new UdfpsOverlayParams(
mUdfpsBounds,
overlayBounds,
mCachedDisplayInfo.getNaturalWidth(),
@@ -823,10 +826,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
mScaleFactor,
mCachedDisplayInfo.rotation);
mUdfpsController.updateOverlayParams(udfpsProp, overlayParams);
if (!Objects.equals(previousUdfpsBounds, mUdfpsBounds)) {
mUdfpsController.updateOverlayParams(udfpsProp, mUdfpsOverlayParams);
if (!Objects.equals(previousUdfpsBounds, mUdfpsBounds) || !Objects.equals(
previousUdfpsOverlayParams, mUdfpsOverlayParams)) {
for (Callback cb : mCallbacks) {
cb.onUdfpsLocationChanged();
cb.onUdfpsLocationChanged(mUdfpsOverlayParams);
}
}
}
@@ -1336,7 +1340,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
* On devices with UDFPS, this is always called alongside
* {@link #onFingerprintLocationChanged}.
*/
default void onUdfpsLocationChanged() {}
default void onUdfpsLocationChanged(UdfpsOverlayParams udfpsOverlayParams) {}
/**
* Called when the location of the face unlock sensor (typically the front facing camera)

View File

@@ -28,6 +28,7 @@ import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.keyguard.logging.KeyguardLogger
import com.android.settingslib.Utils
import com.android.settingslib.udfps.UdfpsOverlayParams
import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.flags.FeatureFlags
@@ -326,7 +327,7 @@ class AuthRippleController @Inject constructor(
updateUdfpsDependentParams()
}
override fun onUdfpsLocationChanged() {
override fun onUdfpsLocationChanged(udfpsOverlayParams: UdfpsOverlayParams) {
updateUdfpsDependentParams()
}
}

View File

@@ -100,6 +100,8 @@ import com.android.systemui.util.concurrency.Execution;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.time.SystemClock;
import kotlin.Unit;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
@@ -110,8 +112,6 @@ import java.util.concurrent.Executor;
import javax.inject.Inject;
import javax.inject.Provider;
import kotlin.Unit;
/**
* Shows and hides the under-display fingerprint sensor (UDFPS) overlay, handles UDFPS touch events,
* and toggles the UDFPS display mode.
@@ -598,14 +598,20 @@ public class UdfpsController implements DozeReceiver, Dumpable {
mFalsingManager.isFalseTouch(UDFPS_AUTHENTICATION);
break;
case UNCHANGED:
if (!isWithinSensorArea(mOverlay.getOverlayView(), event.getX(), event.getY(),
true) && mActivePointerId == MotionEvent.INVALID_POINTER_ID
&& event.getActionMasked() == MotionEvent.ACTION_DOWN
&& mAlternateBouncerInteractor.isVisibleState()) {
// No pointer on sensor, forward to keyguard if alternateBouncer is visible
mKeyguardViewManager.onTouch(event);
}
default:
break;
}
logBiometricTouch(processedTouch.getEvent(), data);
// We should only consume touches that are within the sensor. By returning "false" for
// touches outside of the sensor, we let other UI components consume these events and act on
// them appropriately.
return processedTouch.getTouchData().isWithinSensor(mOverlayParams.getNativeSensorBounds());
}

View File

@@ -0,0 +1,71 @@
/*
* 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.biometrics.domain.interactor
import android.view.MotionEvent
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.settingslib.udfps.UdfpsOverlayParams
import com.android.systemui.biometrics.AuthController
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
/** Encapsulates business logic for interacting with the UDFPS overlay. */
@SysUISingleton
class UdfpsOverlayInteractor
@Inject
constructor(private val authController: AuthController, @Application scope: CoroutineScope) {
/** Whether a touch should be intercepted or allowed to pass to the UdfpsOverlay */
fun canInterceptTouchInUdfpsBounds(ev: MotionEvent): Boolean {
val isUdfpsEnrolled = authController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())
val isWithinUdfpsOverlayBounds =
udfpsOverlayParams.value.overlayBounds.contains(ev.rawX.toInt(), ev.rawY.toInt())
return !isUdfpsEnrolled || !isWithinUdfpsOverlayBounds
}
/** Returns the current udfpsOverlayParams */
val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> =
ConflatedCallbackFlow.conflatedCallbackFlow {
val callback =
object : AuthController.Callback {
override fun onUdfpsLocationChanged(
udfpsOverlayParams: UdfpsOverlayParams
) {
trySendWithFailureLogging(
udfpsOverlayParams,
TAG,
"update udfpsOverlayParams"
)
}
}
authController.addCallback(callback)
awaitClose { authController.removeCallback(callback) }
}
.stateIn(scope, started = SharingStarted.Eagerly, initialValue = UdfpsOverlayParams())
companion object {
private const val TAG = "UdfpsOverlayInteractor"
}
}

View File

@@ -36,6 +36,7 @@ import com.android.keyguard.AuthKeyguardMessageArea;
import com.android.keyguard.LockIconViewController;
import com.android.keyguard.dagger.KeyguardBouncerComponent;
import com.android.systemui.R;
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dock.DockManager;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
@@ -86,7 +87,7 @@ public class NotificationShadeWindowViewController {
private final PulsingGestureListener mPulsingGestureListener;
private final NotificationInsetsController mNotificationInsetsController;
private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private final UdfpsOverlayInteractor mUdfpsOverlayInteractor;
private GestureDetector mPulsingWakeupGestureHandler;
private View mBrightnessMirror;
private boolean mTouchActive;
@@ -134,6 +135,7 @@ public class NotificationShadeWindowViewController {
KeyguardBouncerViewModel keyguardBouncerViewModel,
KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
AlternateBouncerInteractor alternateBouncerInteractor,
UdfpsOverlayInteractor udfpsOverlayInteractor,
KeyguardTransitionInteractor keyguardTransitionInteractor,
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel
) {
@@ -156,6 +158,7 @@ public class NotificationShadeWindowViewController {
mPulsingGestureListener = pulsingGestureListener;
mNotificationInsetsController = notificationInsetsController;
mAlternateBouncerInteractor = alternateBouncerInteractor;
mUdfpsOverlayInteractor = udfpsOverlayInteractor;
// This view is not part of the newly inflated expanded status bar.
mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -240,7 +243,6 @@ public class NotificationShadeWindowViewController {
mFalsingCollector.onTouchEvent(ev);
mPulsingWakeupGestureHandler.onTouchEvent(ev);
mStatusBarKeyguardViewManager.onTouch(ev);
if (mBrightnessMirror != null
&& mBrightnessMirror.getVisibility() == View.VISIBLE) {
// Disallow new pointers while the brightness mirror is visible. This is so that
@@ -316,8 +318,8 @@ public class NotificationShadeWindowViewController {
}
if (mAlternateBouncerInteractor.isVisibleState()) {
// capture all touches if the alt auth bouncer is showing
return true;
// If using UDFPS, don't intercept touches that are within its overlay bounds
return mUdfpsOverlayInteractor.canInterceptTouchInUdfpsBounds(ev);
}
if (mLockIconViewController.onInterceptTouchEvent(ev)) {
@@ -355,6 +357,7 @@ public class NotificationShadeWindowViewController {
if (mAlternateBouncerInteractor.isVisibleState()) {
// eat the touch
mStatusBarKeyguardViewManager.onTouch(ev);
handled = true;
}

View File

@@ -36,6 +36,7 @@ import android.util.Pair;
import androidx.test.filters.SmallTest;
import com.android.settingslib.udfps.UdfpsOverlayParams;
import com.android.systemui.doze.util.BurnInHelperKt;
import org.junit.Test;
@@ -107,7 +108,7 @@ public class LockIconViewControllerTest extends LockIconViewControllerBaseTest {
Pair<Float, Point> udfps = setupUdfps();
// WHEN udfps location changes
mAuthControllerCallback.onUdfpsLocationChanged();
mAuthControllerCallback.onUdfpsLocationChanged(new UdfpsOverlayParams());
mDelayableExecutor.runAllReady();
// THEN lock icon view location is updated with the same coordinates as auth controller vals

View File

@@ -1223,6 +1223,44 @@ public class UdfpsControllerTest extends SysuiTestCase {
anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean());
}
@Test
public void onTouch_WithNewTouchDetection_forwardToKeyguard() throws RemoteException {
final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L,
0L);
final TouchProcessorResult processorResultDown = new TouchProcessorResult.ProcessedTouch(
InteractionEvent.UNCHANGED, -1 /* pointerOnSensorId */, touchData);
// Enable new touch detection.
when(mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)).thenReturn(true);
// Configure UdfpsController to use FingerprintManager as opposed to AlternateTouchProvider.
initUdfpsController(mOpticalProps, false /* hasAlternateTouchProvider */);
// Configure UdfpsView to accept the ACTION_DOWN event
when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(false);
// GIVEN that the overlay is showing and a11y touch exploration NOT enabled
when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false);
mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
mFgExecutor.runAllReady();
verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
// WHEN ACTION_DOWN is received
when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn(
processorResultDown);
MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
mBiometricExecutor.runAllReady();
// THEN the touch is forwarded to Keyguard
verify(mStatusBarKeyguardViewManager).onTouch(downEvent);
downEvent.recycle();
}
@Test
public void onAodInterrupt_onAcquiredGood_fingerNoLongerDown() throws RemoteException {
// GIVEN UDFPS overlay is showing

View File

@@ -0,0 +1,116 @@
/*
* 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.biometrics.domain.interactor
import android.graphics.Rect
import android.test.suitebuilder.annotation.SmallTest
import android.view.MotionEvent
import android.view.Surface
import com.android.settingslib.udfps.UdfpsOverlayParams
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@SmallTest
@RunWith(JUnit4::class)
class UdfpsOverlayInteractorTest : SysuiTestCase() {
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
private lateinit var testScope: TestScope
@Mock private lateinit var authController: AuthController
@Captor private lateinit var authControllerCallback: ArgumentCaptor<AuthController.Callback>
@Mock private lateinit var udfpsOverlayParams: UdfpsOverlayParams
@Mock private lateinit var overlayBounds: Rect
private lateinit var underTest: UdfpsOverlayInteractor
@Before
fun setUp() {
testScope = TestScope(StandardTestDispatcher())
}
@Test
fun testShouldInterceptTouch() =
testScope.runTest {
createUdpfsOverlayInteractor()
// When fingerprint enrolled and touch is within bounds
verify(authController).addCallback(authControllerCallback.capture())
authControllerCallback.value.onUdfpsLocationChanged(udfpsOverlayParams)
whenever(authController.isUdfpsEnrolled(anyInt())).thenReturn(true)
whenever(udfpsOverlayParams.overlayBounds).thenReturn(overlayBounds)
whenever(overlayBounds.contains(downEv.x.toInt(), downEv.y.toInt())).thenReturn(true)
runCurrent()
// Then touch should not be intercepted
val canInterceptTrue = underTest.canInterceptTouchInUdfpsBounds(downEv)
assertThat(canInterceptTrue).isFalse()
// When touch is outside of bounds
whenever(overlayBounds.contains(downEv.x.toInt(), downEv.y.toInt())).thenReturn(false)
// Then touch should be intercepted
val canInterceptFalse = underTest.canInterceptTouchInUdfpsBounds(downEv)
assertThat(canInterceptFalse).isTrue()
}
@Test
fun testUdfpsOverlayParamsChange() =
testScope.runTest {
createUdpfsOverlayInteractor()
val udfpsOverlayParams = collectLastValue(underTest.udfpsOverlayParams)
runCurrent()
verify(authController).addCallback(authControllerCallback.capture())
// When udfpsLocationChanges in authcontroller
authControllerCallback.value.onUdfpsLocationChanged(firstParams)
// Then the value in the interactor should be updated
assertThat(udfpsOverlayParams()).isEqualTo(firstParams)
}
private fun createUdpfsOverlayInteractor() {
underTest = UdfpsOverlayInteractor(authController, testScope.backgroundScope)
testScope.runCurrent()
}
}
private val firstParams =
UdfpsOverlayParams(Rect(0, 0, 10, 10), Rect(0, 0, 10, 10), 1, 1, 1f, Surface.ROTATION_0)
private val downEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)

View File

@@ -26,6 +26,7 @@ import com.android.keyguard.LockIconViewController
import com.android.keyguard.dagger.KeyguardBouncerComponent
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.dock.DockManager
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
@@ -86,6 +87,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
@Mock private lateinit var pulsingGestureListener: PulsingGestureListener
@Mock private lateinit var notificationInsetsController: NotificationInsetsController
@Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
@Mock private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor
@Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
@@ -133,6 +135,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
keyguardBouncerViewModel,
keyguardBouncerComponentFactory,
alternateBouncerInteractor,
udfpsOverlayInteractor,
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
)
@@ -264,6 +267,17 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
assertThat(returnVal).isTrue()
}
@Test
fun shouldInterceptTouchEvent_downEventAlternateBouncer_ignoreIfInUdfpsOverlay() {
// Down event within udfpsOverlay bounds while alternateBouncer is showing
whenever(udfpsOverlayInteractor.canInterceptTouchInUdfpsBounds(downEv)).thenReturn(false)
whenever(alternateBouncerInteractor.isVisibleState()).thenReturn(true)
// Then touch should not be intercepted
val shouldIntercept = interactionEventHandler.shouldInterceptTouchEvent(downEv)
assertThat(shouldIntercept).isFalse()
}
@Test
fun testGetBouncerContainer() {
Mockito.clearInvocations(view)

View File

@@ -40,6 +40,7 @@ import com.android.keyguard.LockIconViewController;
import com.android.keyguard.dagger.KeyguardBouncerComponent;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.dock.DockManager;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
@@ -101,6 +102,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase {
@Mock private KeyguardSecurityContainerController mKeyguardSecurityContainerController;
@Mock private NotificationInsetsController mNotificationInsetsController;
@Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
@Mock private UdfpsOverlayInteractor mUdfpsOverlayInteractor;
@Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
@@ -152,6 +154,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase {
mKeyguardBouncerViewModel,
mKeyguardBouncerComponentFactory,
mAlternateBouncerInteractor,
mUdfpsOverlayInteractor,
mKeyguardTransitionInteractor,
mPrimaryBouncerToGoneTransitionViewModel
);
@@ -177,6 +180,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase {
// WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept
when(mStatusBarStateController.isDozing()).thenReturn(false);
when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
when(mUdfpsOverlayInteractor.canInterceptTouchInUdfpsBounds(any())).thenReturn(true);
when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
// THEN we should intercept touch