/* * Copyright (C) 2020 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; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static com.android.internal.util.FunctionalUtils.ThrowingConsumer; import static com.android.systemui.classifier.Classifier.UDFPS_AUTHENTICATION; import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Rect; import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricOverlayConstants; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.SensorProperties; import android.hardware.display.DisplayManager; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IUdfpsOverlayController; import android.hardware.fingerprint.IUdfpsOverlayControllerCallback; import android.hardware.input.InputManager; import android.os.Handler; import android.os.PowerManager; import android.os.RemoteException; import android.os.VibrationAttributes; import android.testing.TestableLooper.RunWithLooper; import android.util.Pair; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.settingslib.udfps.UdfpsOverlayParams; import com.android.settingslib.udfps.UdfpsUtils; import com.android.systemui.R; import com.android.systemui.RoboPilotTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.biometrics.udfps.InteractionEvent; import com.android.systemui.biometrics.udfps.NormalizedTouchData; import com.android.systemui.biometrics.udfps.SinglePointerTouchProcessor; import com.android.systemui.biometrics.udfps.TouchProcessorResult; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.Execution; import com.android.systemui.util.concurrency.FakeExecution; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.SystemClock; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; import java.util.Optional; import javax.inject.Provider; @SmallTest @RoboPilotTest @RunWith(AndroidJUnit4.class) @RunWithLooper(setAsMainLooper = true) public class UdfpsControllerTest extends SysuiTestCase { private static final long TEST_REQUEST_ID = 70; @Rule public MockitoRule rule = MockitoJUnit.rule(); // Unit under test private UdfpsController mUdfpsController; // Dependencies private FakeExecutor mBiometricExecutor; @Mock private LayoutInflater mLayoutInflater; @Mock private FingerprintManager mFingerprintManager; @Mock private WindowManager mWindowManager; @Mock private StatusBarStateController mStatusBarStateController; @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @Mock private DumpManager mDumpManager; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private IUdfpsOverlayControllerCallback mUdfpsOverlayControllerCallback; @Mock private FalsingManager mFalsingManager; @Mock private PowerManager mPowerManager; @Mock private AccessibilityManager mAccessibilityManager; @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController; @Mock private ScreenLifecycle mScreenLifecycle; @Mock private VibratorHelper mVibrator; @Mock private UdfpsHapticsSimulator mUdfpsHapticsSimulator; @Mock private UdfpsShell mUdfpsShell; @Mock private KeyguardStateController mKeyguardStateController; @Mock private DisplayManager mDisplayManager; @Mock private Handler mHandler; @Mock private ConfigurationController mConfigurationController; @Mock private SystemClock mSystemClock; @Mock private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; @Mock private LatencyTracker mLatencyTracker; private FakeExecutor mFgExecutor; @Mock private UdfpsDisplayMode mUdfpsDisplayMode; @Mock private FeatureFlags mFeatureFlags; // Stuff for configuring mocks @Mock private UdfpsView mUdfpsView; @Mock private UdfpsBpView mBpView; @Mock private UdfpsFpmEmptyView mFpmEmptyView; @Mock private UdfpsKeyguardViewLegacy mKeyguardView; private final UdfpsAnimationViewController mUdfpsKeyguardViewController = mock(UdfpsKeyguardViewControllerLegacy.class); @Mock private UdfpsAnimationViewController mUdfpsAnimationViewController; @Mock private SystemUIDialogManager mSystemUIDialogManager; @Mock private ActivityLaunchAnimator mActivityLaunchAnimator; @Mock private AlternateUdfpsTouchProvider mAlternateTouchProvider; @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; @Mock private SinglePointerTouchProcessor mSinglePointerTouchProcessor; @Mock private SessionTracker mSessionTracker; @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private SecureSettings mSecureSettings; @Mock private UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate; @Mock private Provider mUdfpsKeyguardViewModels; // Capture listeners so that they can be used to send events @Captor private ArgumentCaptor mOverlayCaptor; private IUdfpsOverlayController mOverlayController; @Captor private ArgumentCaptor mTouchListenerCaptor; @Captor private ArgumentCaptor mHoverListenerCaptor; @Captor private ArgumentCaptor mOnDisplayConfiguredCaptor; @Captor private ArgumentCaptor mScreenObserverCaptor; @Captor private ArgumentCaptor mUdfpsOverlayControllerCaptor; private ScreenLifecycle.Observer mScreenObserver; private FingerprintSensorPropertiesInternal mOpticalProps; private FingerprintSensorPropertiesInternal mUltrasonicProps; private UdfpsUtils mUdfpsUtils; @Mock private InputManager mInputManager; @Mock private ViewRootImpl mViewRootImpl; @Before public void setUp() { Execution execution = new FakeExecution(); mUdfpsUtils = new UdfpsUtils(); when(mLayoutInflater.inflate(R.layout.udfps_view, null, false)) .thenReturn(mUdfpsView); when(mLayoutInflater.inflate(R.layout.udfps_keyguard_view_legacy, null)) .thenReturn(mKeyguardView); // for showOverlay REASON_AUTH_FPM_KEYGUARD when(mLayoutInflater.inflate(R.layout.udfps_bp_view, null)) .thenReturn(mBpView); when(mLayoutInflater.inflate(R.layout.udfps_fpm_empty_view, null)) .thenReturn(mFpmEmptyView); when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true); when(mSessionTracker.getSessionId(anyInt())).thenReturn( (new InstanceIdSequence(1 << 20)).newInstanceId()); when(mUdfpsView.getViewRootImpl()).thenReturn(mViewRootImpl); when(mUdfpsView.getAnimationViewController()).thenReturn(mUdfpsAnimationViewController); final List componentInfo = new ArrayList<>(); componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, "00000001" /* serialNumber */, "" /* softwareVersion */)); componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, "vendor/version/revision" /* softwareVersion */)); mOpticalProps = new FingerprintSensorPropertiesInternal(1 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, componentInfo, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, true /* resetLockoutRequiresHardwareAuthToken */); mUltrasonicProps = new FingerprintSensorPropertiesInternal(2 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, componentInfo, FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC, true /* resetLockoutRequiresHardwareAuthToken */); mFgExecutor = new FakeExecutor(new FakeSystemClock()); // Create a fake background executor. mBiometricExecutor = new FakeExecutor(new FakeSystemClock()); initUdfpsController(true /* hasAlternateTouchProvider */); } private void initUdfpsController(boolean hasAlternateTouchProvider) { initUdfpsController(mOpticalProps, hasAlternateTouchProvider); } private void initUdfpsController(FingerprintSensorPropertiesInternal sensorProps, boolean hasAlternateTouchProvider) { reset(mFingerprintManager); reset(mScreenLifecycle); final Optional> alternateTouchProvider = hasAlternateTouchProvider ? Optional.of( (Provider) () -> mAlternateTouchProvider) : Optional.empty(); mUdfpsController = new UdfpsController(mContext, new FakeExecution(), mLayoutInflater, mFingerprintManager, mWindowManager, mStatusBarStateController, mFgExecutor, new ShadeExpansionStateManager(), mStatusBarKeyguardViewManager, mDumpManager, mKeyguardUpdateMonitor, mFeatureFlags, mFalsingManager, mPowerManager, mAccessibilityManager, mLockscreenShadeTransitionController, mScreenLifecycle, mVibrator, mUdfpsHapticsSimulator, mUdfpsShell, mKeyguardStateController, mDisplayManager, mHandler, mConfigurationController, mSystemClock, mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker, mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor, mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, mSessionTracker, mAlternateBouncerInteractor, mSecureSettings, mInputManager, mUdfpsUtils, mock(KeyguardFaceAuthInteractor.class), mUdfpsKeyguardAccessibilityDelegate, mUdfpsKeyguardViewModels); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); mScreenObserver = mScreenObserverCaptor.getValue(); mUdfpsController.updateOverlayParams(sensorProps, new UdfpsOverlayParams()); mUdfpsController.setUdfpsDisplayMode(mUdfpsDisplayMode); } @Test public void dozeTimeTick() throws RemoteException { mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); mUdfpsController.dozeTimeTick(); verify(mUdfpsView).dozeTimeTick(); } @Test public void onActionDownTouch_whenCanDismissLockScreen_entersDevice() throws RemoteException { // GIVEN can dismiss lock screen and the current animation is an UdfpsKeyguardViewController when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); when(mUdfpsView.getAnimationViewController()).thenReturn(mUdfpsKeyguardViewController); // GIVEN that the overlay is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // WHEN ACTION_DOWN is received verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); // THEN notify keyguard authenticate to dismiss the keyguard verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean()); } @Test public void onActionMoveTouch_whenCanDismissLockScreen_entersDevice() throws RemoteException { onActionMoveTouch_whenCanDismissLockScreen_entersDevice(false /* stale */); } @Test public void onActionMoveTouch_whenCanDismissLockScreen_entersDevice_ignoreStale() throws RemoteException { onActionMoveTouch_whenCanDismissLockScreen_entersDevice(true /* stale */); } public void onActionMoveTouch_whenCanDismissLockScreen_entersDevice(boolean stale) throws RemoteException { // GIVEN can dismiss lock screen and the current animation is an UdfpsKeyguardViewController when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); when(mUdfpsView.getAnimationViewController()).thenReturn(mUdfpsKeyguardViewController); // GIVEN that the overlay is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // WHEN ACTION_MOVE is received verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); if (stale) { mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId); mFgExecutor.runAllReady(); } mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); mBiometricExecutor.runAllReady(); moveEvent.recycle(); // THEN notify keyguard authenticate to dismiss the keyguard verify(mStatusBarKeyguardViewManager, stale ? never() : times(1)) .notifyKeyguardAuthenticated(anyBoolean()); } @Test public void onMultipleTouch_whenCanDismissLockScreen_entersDeviceOnce() throws RemoteException { // GIVEN can dismiss lock screen and the current animation is an UdfpsKeyguardViewController when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); when(mUdfpsView.getAnimationViewController()).thenReturn(mUdfpsKeyguardViewController); // GIVEN that the overlay is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // WHEN multiple touches are received verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); mBiometricExecutor.runAllReady(); moveEvent.recycle(); // THEN notify keyguard authenticate to dismiss the keyguard verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean()); } @Test public void hideUdfpsOverlay_resetsAltAuthBouncerWhenShowing() throws RemoteException { // GIVEN overlay was showing and the udfps bouncer is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); // WHEN the overlay is hidden mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId); mFgExecutor.runAllReady(); // THEN the udfps bouncer is reset verify(mStatusBarKeyguardViewManager).hideAlternateBouncer(eq(true)); } @Test public void showUdfpsOverlay_callsListener() throws RemoteException { mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mFingerprintManager).onUdfpsUiEvent(FingerprintManager.UDFPS_UI_OVERLAY_SHOWN, TEST_REQUEST_ID, mOpticalProps.sensorId); } @Test public void testSubscribesToOrientationChangesWhenShowingOverlay() throws Exception { mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mDisplayManager).registerDisplayListener(any(), eq(mHandler), anyLong()); mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId); mFgExecutor.runAllReady(); verify(mDisplayManager).unregisterDisplayListener(any()); } @Test public void updateOverlayParams_recreatesOverlay_ifParamsChanged() throws Exception { final Rect[] sensorBounds = new Rect[]{new Rect(10, 10, 20, 20), new Rect(5, 5, 25, 25)}; final int[] displayWidth = new int[]{1080, 1440}; final int[] displayHeight = new int[]{1920, 2560}; final float[] scaleFactor = new float[]{1f, displayHeight[1] / (float) displayHeight[0]}; final int[] rotation = new int[]{Surface.ROTATION_0, Surface.ROTATION_90}; final UdfpsOverlayParams oldParams = new UdfpsOverlayParams(sensorBounds[0], sensorBounds[0], displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]); for (int i1 = 0; i1 <= 1; ++i1) { for (int i2 = 0; i2 <= 1; ++i2) { for (int i3 = 0; i3 <= 1; ++i3) { for (int i4 = 0; i4 <= 1; ++i4) { for (int i5 = 0; i5 <= 1; ++i5) { final UdfpsOverlayParams newParams = new UdfpsOverlayParams( sensorBounds[i1], sensorBounds[i1], displayWidth[i2], displayHeight[i3], scaleFactor[i4], rotation[i5]); if (newParams.equals(oldParams)) { continue; } // Initialize the overlay with old parameters. mUdfpsController.updateOverlayParams(mOpticalProps, oldParams); // Show the overlay. reset(mWindowManager); mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mWindowManager).addView(any(), any()); // Update overlay parameters. reset(mWindowManager); mUdfpsController.updateOverlayParams(mOpticalProps, newParams); mFgExecutor.runAllReady(); // Ensure the overlay was recreated. verify(mWindowManager).removeView(any()); verify(mWindowManager).addView(any(), any()); } } } } } } @Test public void updateOverlayParams_doesNothing_ifParamsDidntChange() throws Exception { final Rect sensorBounds = new Rect(10, 10, 20, 20); final int displayWidth = 1080; final int displayHeight = 1920; final float scaleFactor = 1f; final int rotation = Surface.ROTATION_0; // Initialize the overlay. mUdfpsController.updateOverlayParams(mOpticalProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, rotation)); // Show the overlay. mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mWindowManager).addView(any(), any()); // Update overlay with the same parameters. mUdfpsController.updateOverlayParams(mOpticalProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, rotation)); mFgExecutor.runAllReady(); // Ensure the overlay was not recreated. verify(mWindowManager, never()).removeView(any()); } private static MotionEvent obtainMotionEvent(int action, float x, float y, float minor, float major) { MotionEvent.PointerProperties pp = new MotionEvent.PointerProperties(); pp.id = 1; MotionEvent.PointerCoords pc = new MotionEvent.PointerCoords(); pc.x = x; pc.y = y; pc.touchMinor = minor; pc.touchMajor = major; return MotionEvent.obtain(0, 0, action, 1, new MotionEvent.PointerProperties[]{pp}, new MotionEvent.PointerCoords[]{pc}, 0, 0, 1f, 1f, 0, 0, 0, 0); } private static class TestParams { public final FingerprintSensorPropertiesInternal sensorProps; public final boolean hasAlternateTouchProvider; TestParams(FingerprintSensorPropertiesInternal sensorProps, boolean hasAlternateTouchProvider) { this.sensorProps = sensorProps; this.hasAlternateTouchProvider = hasAlternateTouchProvider; } } private void runWithAllParams(ThrowingConsumer testParamsConsumer) { for (FingerprintSensorPropertiesInternal sensorProps : List.of(mOpticalProps, mUltrasonicProps)) { for (boolean hasAlternateTouchProvider : new boolean[]{false, true}) { initUdfpsController(sensorProps, hasAlternateTouchProvider); testParamsConsumer.accept(new TestParams(sensorProps, hasAlternateTouchProvider)); } } } @Test public void onTouch_propagatesTouchInNativeOrientationAndResolution() { runWithAllParams( this::onTouch_propagatesTouchInNativeOrientationAndResolutionParameterized); } private void onTouch_propagatesTouchInNativeOrientationAndResolutionParameterized( TestParams testParams) throws RemoteException { reset(mUdfpsView); final Rect sensorBounds = new Rect(1000, 1900, 1080, 1920); // Bottom right corner. final int displayWidth = 1080; final int displayHeight = 1920; final float scaleFactor = 0.75f; // This means the native resolution is 1440x2560. final float touchMinor = 10f; final float touchMajor = 20f; // Expecting a touch at the very bottom right corner in native orientation and resolution. final int expectedX = (int) (displayWidth / scaleFactor); final int expectedY = (int) (displayHeight / scaleFactor); final float expectedMinor = touchMinor / scaleFactor; final float expectedMajor = touchMajor / scaleFactor; // Configure UdfpsView to accept the ACTION_DOWN event when(mUdfpsView.isDisplayConfigured()).thenReturn(false); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // Show the overlay. mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); // Test ROTATION_0 mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, Surface.ROTATION_0)); MotionEvent event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); event = obtainMotionEvent(ACTION_MOVE, displayWidth, displayHeight, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); if (testParams.hasAlternateTouchProvider) { verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), eq(expectedY), eq(expectedMinor), eq(expectedMajor)); } else { verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY), eq(expectedMinor), eq(expectedMajor)); } // Test ROTATION_90 reset(mAlternateTouchProvider); reset(mFingerprintManager); mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, Surface.ROTATION_90)); event = obtainMotionEvent(ACTION_DOWN, displayHeight, 0, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); event = obtainMotionEvent(ACTION_MOVE, displayHeight, 0, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); if (testParams.hasAlternateTouchProvider) { verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), eq(expectedY), eq(expectedMinor), eq(expectedMajor)); } else { verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY), eq(expectedMinor), eq(expectedMajor)); } // Test ROTATION_270 reset(mAlternateTouchProvider); reset(mFingerprintManager); mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, Surface.ROTATION_270)); event = obtainMotionEvent(ACTION_DOWN, 0, displayWidth, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); event = obtainMotionEvent(ACTION_MOVE, 0, displayWidth, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); if (testParams.hasAlternateTouchProvider) { verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), eq(expectedY), eq(expectedMinor), eq(expectedMajor)); } else { verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY), eq(expectedMinor), eq(expectedMajor)); } // Test ROTATION_180 reset(mAlternateTouchProvider); reset(mFingerprintManager); mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, Surface.ROTATION_180)); // ROTATION_180 is not supported. It should be treated like ROTATION_0. event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); event = obtainMotionEvent(ACTION_MOVE, displayWidth, displayHeight, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); if (testParams.hasAlternateTouchProvider) { verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), eq(expectedY), eq(expectedMinor), eq(expectedMajor)); } else { verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY), eq(expectedMinor), eq(expectedMajor)); } } @Test public void fingerDown() { runWithAllParams(this::fingerDownParameterized); } private void fingerDownParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView, mAlternateTouchProvider, mFingerprintManager, mLatencyTracker, mKeyguardUpdateMonitor); // Configure UdfpsView to accept the ACTION_DOWN event when(mUdfpsView.isDisplayConfigured()).thenReturn(false); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true); // GIVEN that the overlay is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); // WHEN ACTION_DOWN is received MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); mBiometricExecutor.runAllReady(); moveEvent.recycle(); mFgExecutor.runAllReady(); // THEN the touch provider is notified about onPointerDown. if (testParams.hasAlternateTouchProvider) { verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(0), eq(0), eq(0f), eq(0f)); verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), anyInt(), anyFloat(), anyFloat()); verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID)); } else { verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), eq(testParams.sensorProps.sensorId), eq(0), eq(0), eq(0f), eq(0f)); verify(mAlternateTouchProvider, never()).onPointerDown(anyInt(), anyInt(), anyInt(), anyFloat(), anyFloat()); } // AND display configuration begins if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { verify(mLatencyTracker).onActionStart(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); verify(mUdfpsView).configureDisplay(mOnDisplayConfiguredCaptor.capture()); } else { verify(mLatencyTracker, never()).onActionStart( eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); verify(mUdfpsView, never()).configureDisplay(any()); } verify(mLatencyTracker, never()).onActionEnd(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // AND onDisplayConfigured notifies FingerprintManager about onUiReady mOnDisplayConfiguredCaptor.getValue().run(); mBiometricExecutor.runAllReady(); if (testParams.hasAlternateTouchProvider) { InOrder inOrder = inOrder(mAlternateTouchProvider, mLatencyTracker); inOrder.verify(mAlternateTouchProvider).onUiReady(); inOrder.verify(mLatencyTracker).onActionEnd( eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); verify(mFingerprintManager, never()).onUdfpsUiEvent( eq(FingerprintManager.UDFPS_UI_READY), anyLong(), anyInt()); } else { InOrder inOrder = inOrder(mFingerprintManager, mLatencyTracker); inOrder.verify(mFingerprintManager).onUdfpsUiEvent( eq(FingerprintManager.UDFPS_UI_READY), eq(TEST_REQUEST_ID), eq(testParams.sensorProps.sensorId)); inOrder.verify(mLatencyTracker).onActionEnd( eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); verify(mAlternateTouchProvider, never()).onUiReady(); } } else { verify(mFingerprintManager, never()).onUdfpsUiEvent( eq(FingerprintManager.UDFPS_UI_READY), anyLong(), anyInt()); verify(mAlternateTouchProvider, never()).onUiReady(); verify(mLatencyTracker, never()).onActionEnd( eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); } } @Test public void aodInterrupt() { runWithAllParams(this::aodInterruptParameterized); } private void aodInterruptParameterized(TestParams testParams) throws RemoteException { mUdfpsController.cancelAodSendFingerUpAction(); reset(mUdfpsView, mAlternateTouchProvider, mFingerprintManager, mKeyguardUpdateMonitor); when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true); // GIVEN that the overlay is showing and screen is on and fp is running mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); // WHEN fingerprint is requested because of AOD interrupt mUdfpsController.onAodInterrupt(0, 0, 2f, 3f); mFgExecutor.runAllReady(); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // THEN display configuration begins // AND onDisplayConfigured notifies FingerprintManager about onUiReady verify(mUdfpsView).configureDisplay(mOnDisplayConfiguredCaptor.capture()); mOnDisplayConfiguredCaptor.getValue().run(); } else { verify(mUdfpsView, never()).configureDisplay(mOnDisplayConfiguredCaptor.capture()); } mBiometricExecutor.runAllReady(); if (testParams.hasAlternateTouchProvider) { verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(0), eq(0), eq(3f) /* minor */, eq(2f) /* major */); verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), anyInt(), anyFloat(), anyFloat()); verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID)); } else { verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), eq(testParams.sensorProps.sensorId), eq(0), eq(0), eq(3f) /* minor */, eq(2f) /* major */); verify(mAlternateTouchProvider, never()).onPointerDown(anyLong(), anyInt(), anyInt(), anyFloat(), anyFloat()); } } @Test public void tryAodSendFingerUp_displayConfigurationChanges() { runWithAllParams(this::cancelAodInterruptParameterized); } private void cancelAodInterruptParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); // GIVEN AOD interrupt mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { when(mUdfpsView.isDisplayConfigured()).thenReturn(true); // WHEN it is cancelled mUdfpsController.tryAodSendFingerUp(); // THEN the display is unconfigured verify(mUdfpsView).unconfigureDisplay(); } else { when(mUdfpsView.isDisplayConfigured()).thenReturn(false); // WHEN it is cancelled mUdfpsController.tryAodSendFingerUp(); // THEN the display configuration is unchanged. verify(mUdfpsView, never()).unconfigureDisplay(); } } @Test public void onFingerUp_displayConfigurationChange() { runWithAllParams(this::onFingerUp_displayConfigurationParameterized); } private void onFingerUp_displayConfigurationParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); // GIVEN AOD interrupt mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { when(mUdfpsView.isDisplayConfigured()).thenReturn(true); // WHEN up-action received verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); MotionEvent upEvent = MotionEvent.obtain(0, 0, ACTION_UP, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent); mBiometricExecutor.runAllReady(); upEvent.recycle(); mFgExecutor.runAllReady(); // THEN the display is unconfigured verify(mUdfpsView).unconfigureDisplay(); } else { when(mUdfpsView.isDisplayConfigured()).thenReturn(false); // WHEN up-action received verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); MotionEvent upEvent = MotionEvent.obtain(0, 0, ACTION_UP, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent); mBiometricExecutor.runAllReady(); upEvent.recycle(); mFgExecutor.runAllReady(); // THEN the display configuration is unchanged. verify(mUdfpsView, never()).unconfigureDisplay(); } } @Test public void onAcquiredGood_displayConfigurationChange() { runWithAllParams(this::onAcquiredGood_displayConfigurationParameterized); } private void onAcquiredGood_displayConfigurationParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); // GIVEN overlay is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { when(mUdfpsView.isDisplayConfigured()).thenReturn(true); // WHEN ACQUIRED_GOOD received mOverlayController.onAcquired(testParams.sensorProps.sensorId, BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD); mFgExecutor.runAllReady(); // THEN the display is unconfigured verify(mUdfpsView).unconfigureDisplay(); } else { when(mUdfpsView.isDisplayConfigured()).thenReturn(false); // WHEN ACQUIRED_GOOD received mOverlayController.onAcquired(testParams.sensorProps.sensorId, BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD); mFgExecutor.runAllReady(); // THEN the display configuration is unchanged. verify(mUdfpsView, never()).unconfigureDisplay(); } } @Test public void aodInterruptTimeout() { runWithAllParams(this::aodInterruptTimeoutParameterized); } private void aodInterruptTimeoutParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); // GIVEN AOD interrupt mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); mFgExecutor.runAllReady(); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { when(mUdfpsView.isDisplayConfigured()).thenReturn(true); } else { when(mUdfpsView.isDisplayConfigured()).thenReturn(false); } // WHEN it times out mFgExecutor.advanceClockToNext(); mFgExecutor.runAllReady(); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // THEN the display is unconfigured. verify(mUdfpsView).unconfigureDisplay(); } else { // THEN the display configuration is unchanged. verify(mUdfpsView, never()).unconfigureDisplay(); } } @Test public void aodInterruptCancelTimeoutActionOnFingerUp() { runWithAllParams(this::aodInterruptCancelTimeoutActionOnFingerUpParameterized); } private void aodInterruptCancelTimeoutActionOnFingerUpParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN AOD interrupt mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); mFgExecutor.runAllReady(); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // Configure UdfpsView to accept the ACTION_UP event when(mUdfpsView.isDisplayConfigured()).thenReturn(true); } else { when(mUdfpsView.isDisplayConfigured()).thenReturn(false); } // WHEN ACTION_UP is received verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); MotionEvent upEvent = MotionEvent.obtain(0, 0, ACTION_UP, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent); mBiometricExecutor.runAllReady(); upEvent.recycle(); // Configure UdfpsView to accept the ACTION_DOWN event when(mUdfpsView.isDisplayConfigured()).thenReturn(false); // WHEN ACTION_DOWN is received MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); // WHEN ACTION_MOVE is received MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); mBiometricExecutor.runAllReady(); moveEvent.recycle(); mFgExecutor.runAllReady(); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // Configure UdfpsView to accept the finger up event when(mUdfpsView.isDisplayConfigured()).thenReturn(true); } else { when(mUdfpsView.isDisplayConfigured()).thenReturn(false); } // WHEN it times out mFgExecutor.advanceClockToNext(); mFgExecutor.runAllReady(); if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // THEN the display should be unconfigured once. If the timeout action is not // cancelled, the display would be unconfigured twice which would cause two // FP attempts. verify(mUdfpsView).unconfigureDisplay(); } else { verify(mUdfpsView, never()).unconfigureDisplay(); } } @Test public void aodInterruptScreenOff() { runWithAllParams(this::aodInterruptScreenOffParameterized); } private void aodInterruptScreenOffParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); // GIVEN screen off mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOff(); mFgExecutor.runAllReady(); // WHEN aod interrupt is received mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); // THEN display doesn't get configured because it's off verify(mUdfpsView, never()).configureDisplay(any()); } @Test public void aodInterrupt_fingerprintNotRunning() { runWithAllParams(this::aodInterrupt_fingerprintNotRunningParameterized); } private void aodInterrupt_fingerprintNotRunningParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); // GIVEN showing overlay mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); // WHEN aod interrupt is received when the fingerprint service isn't running when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(false); mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); // THEN display doesn't get configured because it's off verify(mUdfpsView, never()).configureDisplay(any()); } @Test public void playHapticOnTouchUdfpsArea_a11yTouchExplorationEnabled() throws RemoteException { // Configure UdfpsView to accept the ACTION_DOWN event when(mUdfpsView.isDisplayConfigured()).thenReturn(false); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN that the overlay is showing and a11y touch exploration enabled when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true); mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // WHEN ACTION_HOVER is received verify(mUdfpsView).setOnHoverListener(mHoverListenerCaptor.capture()); MotionEvent enterEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0, 0, 0); mHoverListenerCaptor.getValue().onHover(mUdfpsView, enterEvent); enterEvent.recycle(); MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_MOVE, 0, 0, 0); mHoverListenerCaptor.getValue().onHover(mUdfpsView, moveEvent); moveEvent.recycle(); // THEN tick haptic is played verify(mVibrator).vibrate( anyInt(), anyString(), any(), eq("udfps-onStart-click"), eq(UdfpsController.UDFPS_VIBRATION_ATTRIBUTES)); // THEN make sure vibration attributes has so that it always will play the haptic, // even in battery saver mode assertEquals(VibrationAttributes.USAGE_COMMUNICATION_REQUEST, UdfpsController.UDFPS_VIBRATION_ATTRIBUTES.getUsage()); } @Test public void playHapticOnTouchUdfpsArea_a11yTouchExplorationEnabled_oneWayHapticsEnabled() throws RemoteException { when(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(true); // Configure UdfpsView to accept the ACTION_DOWN event when(mUdfpsView.isDisplayConfigured()).thenReturn(false); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN that the overlay is showing and a11y touch exploration enabled when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true); mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // WHEN ACTION_HOVER is received verify(mUdfpsView).setOnHoverListener(mHoverListenerCaptor.capture()); MotionEvent enterEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0, 0, 0); mHoverListenerCaptor.getValue().onHover(mUdfpsView, enterEvent); enterEvent.recycle(); MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_MOVE, 0, 0, 0); mHoverListenerCaptor.getValue().onHover(mUdfpsView, moveEvent); moveEvent.recycle(); // THEN context click haptic is played verify(mVibrator).performHapticFeedback( any(), eq(HapticFeedbackConstants.CONTEXT_CLICK) ); } @Test public void noHapticOnTouchUdfpsArea_a11yTouchExplorationDisabled() throws RemoteException { // Configure UdfpsView to accept the ACTION_DOWN event when(mUdfpsView.isDisplayConfigured()).thenReturn(false); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN that the overlay is showing and a11y touch exploration NOT enabled when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // WHEN ACTION_DOWN is received verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); mBiometricExecutor.runAllReady(); moveEvent.recycle(); // THEN NO haptic played verify(mVibrator, never()).vibrate( anyInt(), anyString(), any(), anyString(), any()); } @Test public void noHapticOnTouchUdfpsArea_a11yTouchExplorationDisabled__oneWayHapticsEnabled() throws RemoteException { when(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(true); // Configure UdfpsView to accept the ACTION_DOWN event when(mUdfpsView.isDisplayConfigured()).thenReturn(false); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN that the overlay is showing and a11y touch exploration NOT enabled when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // WHEN ACTION_DOWN is received verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); mBiometricExecutor.runAllReady(); moveEvent.recycle(); // THEN NO haptic played verify(mVibrator, never()).performHapticFeedback(any(), anyInt()); } @Test public void onTouch_withoutNewTouchDetection_shouldCallOldFingerprintManagerPath() throws RemoteException { // Disable new touch detection. when(mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)).thenReturn(false); // 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(true); // GIVEN that the overlay is showing and a11y touch exploration NOT enabled 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 MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); // AND ACTION_MOVE is received MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); mBiometricExecutor.runAllReady(); moveEvent.recycle(); // AND ACTION_UP is received MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent); mBiometricExecutor.runAllReady(); upEvent.recycle(); // THEN the old FingerprintManager path is invoked. verify(mFingerprintManager).onPointerDown(anyLong(), anyInt(), anyInt(), anyInt(), anyFloat(), anyFloat()); verify(mFingerprintManager).onPointerUp(anyLong(), anyInt()); } @Test public void fingerDown_falsingManagerInformed() throws RemoteException { final Pair touchProcessorResult = givenAcceptFingerDownEvent(); // WHEN ACTION_DOWN is received when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( touchProcessorResult.first); MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); // THEN falsing manager is informed of the touch verify(mFalsingManager).isFalseTouch(UDFPS_AUTHENTICATION); } @Test public void onTouch_withNewTouchDetection_shouldCallNewFingerprintManagerPath() throws RemoteException { final Pair processorResultDownAndUp = givenAcceptFingerDownEvent(); // WHEN ACTION_DOWN is received when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( processorResultDownAndUp.first); MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); // AND ACTION_UP is received when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( processorResultDownAndUp.second); MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent); mBiometricExecutor.runAllReady(); upEvent.recycle(); // THEN the new FingerprintManager path is invoked. verify(mFingerprintManager).onPointerDown(anyLong(), anyInt(), anyInt(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean()); verify(mFingerprintManager).onPointerUp(anyLong(), anyInt(), anyInt(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean()); } private Pair givenAcceptFingerDownEvent() throws RemoteException { final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, 0L); final TouchProcessorResult processorResultDown = new TouchProcessorResult.ProcessedTouch( InteractionEvent.DOWN, 1 /* pointerId */, touchData); final TouchProcessorResult processorResultUp = new TouchProcessorResult.ProcessedTouch( InteractionEvent.UP, 1 /* pointerId */, 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(true); // GIVEN that the overlay is showing and a11y touch exploration NOT enabled when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); return new Pair<>(processorResultDown, processorResultUp); } @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 onTouch_withNewTouchDetection_ignoreIfAuthPaused() throws RemoteException { final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, 0L); final TouchProcessorResult processorResultDown = new TouchProcessorResult.ProcessedTouch(InteractionEvent.DOWN, 1 /* pointerId */, 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(true); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN that auth is paused when(mUdfpsAnimationViewController.shouldPauseAuth()).thenReturn(true); // GIVEN that the overlay is showing and a11y touch exploration NOT enabled 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 and touch is within sensor 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(); downEvent.recycle(); // THEN the touch is ignored verify(mInputManager, never()).pilferPointers(any()); verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean()); } @Test public void onTouch_withNewTouchDetection_ignoreAuthPauseIfFingerDown() throws RemoteException { final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, 0L); final TouchProcessorResult processorResultDown = new TouchProcessorResult.ProcessedTouch(InteractionEvent.DOWN, 0 /* pointerId */, touchData); final TouchProcessorResult processorResultUp = new TouchProcessorResult.ProcessedTouch(InteractionEvent.UP, -1 /* pointerId */, 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(true); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN that the overlay is showing and a11y touch exploration NOT enabled 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 and touch is within sensor when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( processorResultDown); MotionEvent event = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); // THEN the down touch is received verify(mInputManager).pilferPointers(any()); verify(mFingerprintManager).onPointerDown(anyLong(), anyInt(), anyInt(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean()); // GIVEN that auth is paused when(mUdfpsAnimationViewController.shouldPauseAuth()).thenReturn(true); // WHEN ACTION_UP is received and touch is within sensor when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( processorResultUp); event.setAction(ACTION_UP); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); // THEN the UP is still received verify(mInputManager).pilferPointers(any()); verify(mFingerprintManager).onPointerUp(anyLong(), anyInt(), anyInt(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean()); } @Test public void onTouch_withNewTouchDetection_pilferPointer() throws RemoteException { final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, 0L); final TouchProcessorResult processorResultDown = new TouchProcessorResult.ProcessedTouch( InteractionEvent.DOWN, 1 /* pointerId */, 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(true); // GIVEN that the overlay is showing and a11y touch exploration NOT enabled 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 event = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); // WHEN ACTION_MOVE is received after final TouchProcessorResult processorResultUnchanged = new TouchProcessorResult.ProcessedTouch( InteractionEvent.UNCHANGED, 1 /* pointerId */, touchData); when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( processorResultUnchanged); event.setAction(ACTION_MOVE); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); event.recycle(); // THEN only pilfer once on the initial down verify(mInputManager).pilferPointers(any()); } @Test public void onTouch_withNewTouchDetection_doNotPilferPointer() throws RemoteException { final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, 0L); final TouchProcessorResult processorResultUnchanged = new TouchProcessorResult.ProcessedTouch(InteractionEvent.UNCHANGED, 1 /* pointerId */, 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 not 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(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 and touch is not within sensor when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( processorResultUnchanged); MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); // THEN the touch is NOT pilfered verify(mInputManager, never()).pilferPointers(any()); } @Test public void onTouch_withNewTouchDetection_pilferPointerWhenAltBouncerShowing() throws RemoteException { final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, 0L); final TouchProcessorResult processorResultUnchanged = new TouchProcessorResult.ProcessedTouch(InteractionEvent.UNCHANGED, 1 /* pointerId */, 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 not accept the ACTION_DOWN event when(mUdfpsView.isDisplayConfigured()).thenReturn(false); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(false); // GIVEN that the alternate bouncer is showing and a11y touch exploration NOT enabled when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); // WHEN ACTION_DOWN is received and touch is not within sensor when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( processorResultUnchanged); MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); mBiometricExecutor.runAllReady(); downEvent.recycle(); // THEN the touch is pilfered verify(mInputManager).pilferPointers(any()); } @Test public void onTouch_withNewTouchDetection_qsDrag_processesTouchWhenAlternateBouncerVisible() throws RemoteException { final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, 0L); final TouchProcessorResult processorResultMove = new TouchProcessorResult.ProcessedTouch(InteractionEvent.DOWN, 1 /* pointerId */, 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_MOVE event when(mUdfpsView.isDisplayConfigured()).thenReturn(false); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN that the alternate bouncer is showing and a11y touch exploration NOT enabled when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); // GIVEN swipe down for QS when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(false); when(mLockscreenShadeTransitionController.getQSDragProgress()).thenReturn(1f); // WHEN ACTION_MOVE is received and touch is within sensor when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( processorResultMove); MotionEvent moveEvent = MotionEvent.obtain(0, 0, ACTION_MOVE, 0, 0, 0); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); mBiometricExecutor.runAllReady(); moveEvent.recycle(); // THEN the touch is still processed verify(mFingerprintManager).onPointerDown(anyLong(), anyInt(), anyInt(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean()); } @Test public void onAodInterrupt_onAcquiredGood_fingerNoLongerDown() throws RemoteException { // GIVEN UDFPS overlay is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // GIVEN there's been an AoD interrupt when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true); mScreenObserver.onScreenTurnedOn(); mUdfpsController.onAodInterrupt(0, 0, 0, 0); // THEN finger is considered down assertTrue(mUdfpsController.isFingerDown()); // WHEN udfps receives an ACQUIRED_GOOD after the display is configured when(mUdfpsView.isDisplayConfigured()).thenReturn(true); verify(mFingerprintManager).setUdfpsOverlayController( mUdfpsOverlayControllerCaptor.capture()); mUdfpsOverlayControllerCaptor.getValue().onAcquired(0, FINGERPRINT_ACQUIRED_GOOD); mFgExecutor.runAllReady(); // THEN is fingerDown should be FALSE assertFalse(mUdfpsController.isFingerDown()); } @Test public void playHaptic_onAodInterrupt_oneWayHapticsDisabled_onAcquiredBad_usesVibrate() throws RemoteException { // GIVEN UDFPS overlay is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // GIVEN there's been an AoD interrupt when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(false); mScreenObserver.onScreenTurnedOn(); mUdfpsController.onAodInterrupt(0, 0, 0, 0); // THEN vibrate is used verify(mVibrator).vibrate( anyInt(), anyString(), eq(UdfpsController.EFFECT_CLICK), eq("aod-lock-icon-longpress"), eq(UdfpsController.LOCK_ICON_VIBRATION_ATTRIBUTES) ); } @Test public void playHaptic_onAodInterrupt_oneWayHapticsEnabled_onAcquiredBad_performHapticFeedback() throws RemoteException { when(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(true); // GIVEN UDFPS overlay is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); // GIVEN there's been an AoD interrupt when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(false); mScreenObserver.onScreenTurnedOn(); mUdfpsController.onAodInterrupt(0, 0, 0, 0); // THEN vibrate is used verify(mVibrator).performHapticFeedback(any(), eq(UdfpsController.LONG_PRESS)); } @Test public void aodInterrupt_withNewTouchDetection() throws RemoteException { mUdfpsController.cancelAodSendFingerUpAction(); final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, 0L); final TouchProcessorResult processorResultDown = new TouchProcessorResult.ProcessedTouch(InteractionEvent.DOWN, 1 /* pointerId */, 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 */); // GIVEN that the overlay is showing and screen is on and fp is running mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, 0, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); // WHEN fingerprint is requested because of AOD interrupt mUdfpsController.onAodInterrupt(0, 0, 2f, 3f); // Check case where touch driver sends touch to UdfpsView as well verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); 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 only one onPointerDown is sent verify(mFingerprintManager).onPointerDown(anyLong(), anyInt(), anyInt(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean()); } }