diff options
21 files changed, 1676 insertions, 169 deletions
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 085bfca158c1..5c1da1112e2a 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -938,7 +938,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing public void onPointerDown(long requestId, int sensorId, int x, int y, float minor, float major) { if (mService == null) { - Slog.w(TAG, "onFingerDown: no fingerprint service"); + Slog.w(TAG, "onPointerDown: no fingerprint service"); return; } @@ -955,7 +955,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing @RequiresPermission(USE_BIOMETRIC_INTERNAL) public void onPointerUp(long requestId, int sensorId) { if (mService == null) { - Slog.w(TAG, "onFingerDown: no fingerprint service"); + Slog.w(TAG, "onPointerUp: no fingerprint service"); return; } @@ -967,6 +967,58 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } /** + * TODO(b/218388821): The parameter list should be replaced with PointerContext. + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void onPointerDown( + long requestId, + int sensorId, + int pointerId, + float x, + float y, + float minor, + float major, + float orientation, + long time, + long gestureStart, + boolean isAod) { + if (mService == null) { + Slog.w(TAG, "onPointerDown: no fingerprint service"); + return; + } + + // TODO(b/218388821): Propagate all the parameters to FingerprintService. + Slog.e(TAG, "onPointerDown: not implemented!"); + } + + /** + * TODO(b/218388821): The parameter list should be replaced with PointerContext. + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void onPointerUp( + long requestId, + int sensorId, + int pointerId, + float x, + float y, + float minor, + float major, + float orientation, + long time, + long gestureStart, + boolean isAod) { + if (mService == null) { + Slog.w(TAG, "onPointerUp: no fingerprint service"); + return; + } + + // TODO(b/218388821): Propagate all the parameters to FingerprintService. + Slog.e(TAG, "onPointerUp: not implemented!"); + } + + /** * @hide */ @RequiresPermission(USE_BIOMETRIC_INTERNAL) diff --git a/core/java/android/util/RotationUtils.java b/core/java/android/util/RotationUtils.java index c54d9b604ba2..3e7c67e72031 100644 --- a/core/java/android/util/RotationUtils.java +++ b/core/java/android/util/RotationUtils.java @@ -25,6 +25,7 @@ import android.annotation.Dimension; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.view.Surface.Rotation; import android.view.SurfaceControl; @@ -193,6 +194,29 @@ public class RotationUtils { } /** + * Same as {@link #rotatePoint}, but for float coordinates. + */ + public static void rotatePointF(PointF inOutPoint, @Rotation int rotation, + float parentW, float parentH) { + float origX = inOutPoint.x; + switch (rotation) { + case ROTATION_0: + return; + case ROTATION_90: + inOutPoint.x = inOutPoint.y; + inOutPoint.y = parentW - origX; + return; + case ROTATION_180: + inOutPoint.x = parentW - inOutPoint.x; + inOutPoint.y = parentH - inOutPoint.y; + return; + case ROTATION_270: + inOutPoint.x = parentH - inOutPoint.y; + inOutPoint.y = origX; + } + } + + /** * Sets a matrix such that given a rotation, it transforms physical display * coordinates to that rotation's logical coordinates. * diff --git a/core/tests/coretests/src/android/util/RotationUtilsTest.java b/core/tests/coretests/src/android/util/RotationUtilsTest.java index 826eb3070c7e..1b1ee4fb1a1c 100644 --- a/core/tests/coretests/src/android/util/RotationUtilsTest.java +++ b/core/tests/coretests/src/android/util/RotationUtilsTest.java @@ -18,6 +18,7 @@ package android.util; import static android.util.RotationUtils.rotateBounds; import static android.util.RotationUtils.rotatePoint; +import static android.util.RotationUtils.rotatePointF; import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -25,6 +26,7 @@ import static android.view.Surface.ROTATION_90; import static org.junit.Assert.assertEquals; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -79,4 +81,26 @@ public class RotationUtilsTest { rotatePoint(testResult, ROTATION_270, parentW, parentH); assertEquals(new Point(560, 60), testResult); } + + @Test + public void testRotatePointF() { + float parentW = 1000f; + float parentH = 600f; + PointF testPt = new PointF(60f, 40f); + + PointF testResult = new PointF(testPt); + rotatePointF(testResult, ROTATION_90, parentW, parentH); + assertEquals(40f, testResult.x, .1f); + assertEquals(940f, testResult.y, .1f); + + testResult.set(testPt.x, testPt.y); + rotatePointF(testResult, ROTATION_180, parentW, parentH); + assertEquals(940f, testResult.x, .1f); + assertEquals(560f, testResult.y, .1f); + + testResult.set(testPt.x, testPt.y); + rotatePointF(testResult, ROTATION_270, parentW, parentH); + assertEquals(560f, testResult.x, .1f); + assertEquals(60f, testResult.y, .1f); + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 45595c80d36d..5a81bd3e01b6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -61,6 +61,11 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Dumpable; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.biometrics.dagger.BiometricsBackground; +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.TouchProcessor; +import com.android.systemui.biometrics.udfps.TouchProcessorResult; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; @@ -142,6 +147,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener; @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator; @NonNull private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; + @Nullable private final TouchProcessor mTouchProcessor; // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @@ -165,7 +171,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { // The current request from FingerprintService. Null if no current request. @Nullable UdfpsControllerOverlay mOverlay; - @Nullable private UdfpsEllipseDetection mUdfpsEllipseDetection; // The fingerprint AOD trigger doesn't provide an ACTION_UP/ACTION_CANCEL event to tell us when // to turn off high brightness mode. To get around this limitation, the state of the AOD @@ -322,10 +327,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { if (!mOverlayParams.equals(overlayParams)) { mOverlayParams = overlayParams; - if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { - mUdfpsEllipseDetection.updateOverlayParams(overlayParams); - } - final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateBouncer(); // When the bounds change it's always necessary to re-create the overlay's window with @@ -434,8 +435,99 @@ public class UdfpsController implements DozeReceiver, Dumpable { return portraitTouch; } + private void tryDismissingKeyguard() { + if (!mOnFingerDown) { + playStartHaptic(); + } + mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); + mAttemptedToDismissKeyguard = true; + } + @VisibleForTesting boolean onTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) { + if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + return newOnTouch(requestId, event, fromUdfpsView); + } else { + return oldOnTouch(requestId, event, fromUdfpsView); + } + } + + private boolean newOnTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) { + if (!fromUdfpsView) { + Log.e(TAG, "ignoring the touch injected from outside of UdfpsView"); + return false; + } + if (mOverlay == null) { + Log.w(TAG, "ignoring onTouch with null overlay"); + return false; + } + if (!mOverlay.matchesRequestId(requestId)) { + Log.w(TAG, "ignoring stale touch event: " + requestId + " current: " + + mOverlay.getRequestId()); + return false; + } + + final TouchProcessorResult result = mTouchProcessor.processTouch(event, mActivePointerId, + mOverlayParams); + if (result instanceof TouchProcessorResult.Failure) { + Log.w(TAG, ((TouchProcessorResult.Failure) result).getReason()); + return false; + } + + final TouchProcessorResult.ProcessedTouch processedTouch = + (TouchProcessorResult.ProcessedTouch) result; + final NormalizedTouchData data = processedTouch.getTouchData(); + + mActivePointerId = processedTouch.getPointerOnSensorId(); + switch (processedTouch.getEvent()) { + case DOWN: + if (shouldTryToDismissKeyguard()) { + tryDismissingKeyguard(); + } + onFingerDown(requestId, + data.getPointerId(), + data.getX(), + data.getY(), + data.getMinor(), + data.getMajor(), + data.getOrientation(), + data.getTime(), + data.getGestureStart(), + mStatusBarStateController.isDozing()); + break; + + case UP: + case CANCEL: + if (InteractionEvent.CANCEL.equals(processedTouch.getEvent())) { + Log.w(TAG, "This is a CANCEL event that's reported as an UP event!"); + } + mAttemptedToDismissKeyguard = false; + onFingerUp(requestId, + mOverlay.getOverlayView(), + data.getPointerId(), + data.getX(), + data.getY(), + data.getMinor(), + data.getMajor(), + data.getOrientation(), + data.getTime(), + data.getGestureStart(), + mStatusBarStateController.isDozing()); + mFalsingManager.isFalseTouch(UDFPS_AUTHENTICATION); + break; + + + default: + break; + } + + // 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()); + } + + private boolean oldOnTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) { if (mOverlay == null) { Log.w(TAG, "ignoring onTouch with null overlay"); return false; @@ -465,23 +557,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { mVelocityTracker.clear(); } - boolean withinSensorArea; - if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { - if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { - // Ellipse detection - withinSensorArea = mUdfpsEllipseDetection.isGoodEllipseOverlap(event); - } else { - // Centroid with expanded overlay - withinSensorArea = - isWithinSensorArea(udfpsView, event.getRawX(), - event.getRawY(), fromUdfpsView); - } - } else { - // Centroid with sensor sized view - withinSensorArea = + final boolean withinSensorArea = isWithinSensorArea(udfpsView, event.getX(), event.getY(), fromUdfpsView); - } - if (withinSensorArea) { Trace.beginAsyncSection("UdfpsController.e2e.onPointerDown", 0); Log.v(TAG, "onTouch | action down"); @@ -495,11 +572,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { } if ((withinSensorArea || fromUdfpsView) && shouldTryToDismissKeyguard()) { Log.v(TAG, "onTouch | dismiss keyguard ACTION_DOWN"); - if (!mOnFingerDown) { - playStartHaptic(); - } - mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); - mAttemptedToDismissKeyguard = true; + tryDismissingKeyguard(); } Trace.endSection(); @@ -512,33 +585,13 @@ public class UdfpsController implements DozeReceiver, Dumpable { ? event.getPointerId(0) : event.findPointerIndex(mActivePointerId); if (idx == event.getActionIndex()) { - boolean actionMoveWithinSensorArea; - if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { - if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { - // Ellipse detection - actionMoveWithinSensorArea = - mUdfpsEllipseDetection.isGoodEllipseOverlap(event); - } else { - // Centroid with expanded overlay - actionMoveWithinSensorArea = - isWithinSensorArea(udfpsView, event.getRawX(idx), - event.getRawY(idx), fromUdfpsView); - } - } else { - // Centroid with sensor sized view - actionMoveWithinSensorArea = - isWithinSensorArea(udfpsView, event.getX(idx), - event.getY(idx), fromUdfpsView); - } - + final boolean actionMoveWithinSensorArea = + isWithinSensorArea(udfpsView, event.getX(idx), event.getY(idx), + fromUdfpsView); if ((fromUdfpsView || actionMoveWithinSensorArea) && shouldTryToDismissKeyguard()) { Log.v(TAG, "onTouch | dismiss keyguard ACTION_MOVE"); - if (!mOnFingerDown) { - playStartHaptic(); - } - mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); - mAttemptedToDismissKeyguard = true; + tryDismissingKeyguard(); break; } // Map the touch to portrait mode if the device is in landscape mode. @@ -663,7 +716,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull ActivityLaunchAnimator activityLaunchAnimator, @NonNull Optional<AlternateUdfpsTouchProvider> alternateTouchProvider, @NonNull @BiometricsBackground Executor biometricsExecutor, - @NonNull PrimaryBouncerInteractor primaryBouncerInteractor) { + @NonNull PrimaryBouncerInteractor primaryBouncerInteractor, + @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor) { mContext = context; mExecution = execution; mVibrator = vibrator; @@ -704,6 +758,9 @@ public class UdfpsController implements DozeReceiver, Dumpable { mBiometricExecutor = biometricsExecutor; mPrimaryBouncerInteractor = primaryBouncerInteractor; + mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) + ? singlePointerTouchProcessor : null; + mDumpManager.registerDumpable(TAG, this); mOrientationListener = new BiometricDisplayListener( @@ -728,10 +785,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { udfpsHapticsSimulator.setUdfpsController(this); udfpsShell.setUdfpsOverlayController(mUdfpsOverlayController); - - if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { - mUdfpsEllipseDetection = new UdfpsEllipseDetection(mOverlayParams); - } } /** @@ -913,7 +966,36 @@ public class UdfpsController implements DozeReceiver, Dumpable { return mOnFingerDown; } - private void onFingerDown(long requestId, int x, int y, float minor, float major) { + private void onFingerDown( + long requestId, + int x, + int y, + float minor, + float major) { + onFingerDown( + requestId, + MotionEvent.INVALID_POINTER_ID /* pointerId */, + x, + y, + minor, + major, + 0f /* orientation */, + 0L /* time */, + 0L /* gestureStart */, + false /* isAod */); + } + + private void onFingerDown( + long requestId, + int pointerId, + float x, + float y, + float minor, + float major, + float orientation, + long time, + long gestureStart, + boolean isAod) { mExecution.assertIsMainThread(); if (mOverlay == null) { @@ -942,7 +1024,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mOnFingerDown = true; if (mAlternateTouchProvider != null) { mBiometricExecutor.execute(() -> { - mAlternateTouchProvider.onPointerDown(requestId, x, y, minor, major); + mAlternateTouchProvider.onPointerDown(requestId, (int) x, (int) y, minor, major); }); mFgExecutor.execute(() -> { if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) { @@ -950,7 +1032,13 @@ public class UdfpsController implements DozeReceiver, Dumpable { } }); } else { - mFingerprintManager.onPointerDown(requestId, mSensorProps.sensorId, x, y, minor, major); + if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + mFingerprintManager.onPointerDown(requestId, mSensorProps.sensorId, pointerId, x, y, + minor, major, orientation, time, gestureStart, isAod); + } else { + mFingerprintManager.onPointerDown(requestId, mSensorProps.sensorId, (int) x, + (int) y, minor, major); + } } Trace.endAsyncSection("UdfpsController.e2e.onPointerDown", 0); final UdfpsView view = mOverlay.getOverlayView(); @@ -974,6 +1062,32 @@ public class UdfpsController implements DozeReceiver, Dumpable { } private void onFingerUp(long requestId, @NonNull UdfpsView view) { + onFingerUp( + requestId, + view, + MotionEvent.INVALID_POINTER_ID /* pointerId */, + 0f /* x */, + 0f /* y */, + 0f /* minor */, + 0f /* major */, + 0f /* orientation */, + 0L /* time */, + 0L /* gestureStart */, + false /* isAod */); + } + + private void onFingerUp( + long requestId, + @NonNull UdfpsView view, + int pointerId, + float x, + float y, + float minor, + float major, + float orientation, + long time, + long gestureStart, + boolean isAod) { mExecution.assertIsMainThread(); mActivePointerId = -1; mAcquiredReceived = false; @@ -988,7 +1102,12 @@ public class UdfpsController implements DozeReceiver, Dumpable { } }); } else { - mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId); + if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId, pointerId, x, + y, minor, major, orientation, time, gestureStart, isAod); + } else { + mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId); + } } for (Callback cb : mCallbacks) { cb.onFingerUp(); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt deleted file mode 100644 index 8ae4775467df..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2022 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 android.graphics.Point -import android.graphics.Rect -import android.util.RotationUtils -import android.view.MotionEvent -import kotlin.math.cos -import kotlin.math.pow -import kotlin.math.sin - -private const val TAG = "UdfpsEllipseDetection" - -private const val NEEDED_POINTS = 2 - -class UdfpsEllipseDetection(overlayParams: UdfpsOverlayParams) { - var sensorRect = Rect() - var points: Array<Point> = emptyArray() - - init { - sensorRect = Rect(overlayParams.sensorBounds) - - points = calculateSensorPoints(sensorRect) - } - - fun updateOverlayParams(params: UdfpsOverlayParams) { - sensorRect = Rect(params.sensorBounds) - - val rot = params.rotation - RotationUtils.rotateBounds( - sensorRect, - params.naturalDisplayWidth, - params.naturalDisplayHeight, - rot - ) - - points = calculateSensorPoints(sensorRect) - } - - fun isGoodEllipseOverlap(event: MotionEvent): Boolean { - return points.count { checkPoint(event, it) } >= NEEDED_POINTS - } - - private fun checkPoint(event: MotionEvent, point: Point): Boolean { - // Calculate if sensor point is within ellipse - // Formula: ((cos(o)(xE - xS) + sin(o)(yE - yS))^2 / a^2) + ((sin(o)(xE - xS) + cos(o)(yE - - // yS))^2 / b^2) <= 1 - val a: Float = cos(event.orientation) * (point.x - event.rawX) - val b: Float = sin(event.orientation) * (point.y - event.rawY) - val c: Float = sin(event.orientation) * (point.x - event.rawX) - val d: Float = cos(event.orientation) * (point.y - event.rawY) - val result = - (a + b).pow(2) / (event.touchMinor / 2).pow(2) + - (c - d).pow(2) / (event.touchMajor / 2).pow(2) - - return result <= 1 - } -} - -fun calculateSensorPoints(sensorRect: Rect): Array<Point> { - val sensorX = sensorRect.centerX() - val sensorY = sensorRect.centerY() - val cornerOffset: Int = sensorRect.width() / 4 - val sideOffset: Int = sensorRect.width() / 3 - - return arrayOf( - Point(sensorX - cornerOffset, sensorY - cornerOffset), - Point(sensorX, sensorY - sideOffset), - Point(sensorX + cornerOffset, sensorY - cornerOffset), - Point(sensorX - sideOffset, sensorY), - Point(sensorX, sensorY), - Point(sensorX + sideOffset, sensorY), - Point(sensorX - cornerOffset, sensorY + cornerOffset), - Point(sensorX, sensorY + sideOffset), - Point(sensorX + cornerOffset, sensorY + cornerOffset) - ) -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt index 98d4c22d927d..7f3846ca4e40 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt @@ -7,17 +7,23 @@ import android.view.Surface.Rotation /** * Collection of parameters that define an under-display fingerprint sensor (UDFPS) overlay. * - * @property sensorBounds coordinates of the bounding box around the sensor, in natural orientation, - * in pixels, for the current resolution. - * @property naturalDisplayWidth width of the physical display, in natural orientation, in pixels, - * for the current resolution. - * @property naturalDisplayHeight height of the physical display, in natural orientation, in pixels, - * for the current resolution. - * @property scaleFactor ratio of a dimension in the current resolution to the corresponding - * dimension in the native resolution. - * @property rotation current rotation of the display. + * [sensorBounds] coordinates of the bounding box around the sensor in natural orientation, in + * pixels, for the current resolution. + * + * [overlayBounds] coordinates of the UI overlay in natural orientation, in pixels, for the current + * resolution. + * + * [naturalDisplayWidth] width of the physical display in natural orientation, in pixels, for the + * current resolution. + * + * [naturalDisplayHeight] height of the physical display in natural orientation, in pixels, for the + * current resolution. + * + * [scaleFactor] ratio of a dimension in the current resolution to the corresponding dimension in + * the native resolution. + * + * [rotation] current rotation of the display. */ - data class UdfpsOverlayParams( val sensorBounds: Rect = Rect(), val overlayBounds: Rect = Rect(), @@ -26,19 +32,23 @@ data class UdfpsOverlayParams( val scaleFactor: Float = 1f, @Rotation val rotation: Int = Surface.ROTATION_0 ) { + + /** Same as [sensorBounds], but in native resolution. */ + val nativeSensorBounds = Rect(sensorBounds).apply { scale(1f / scaleFactor) } + /** See [android.view.DisplayInfo.logicalWidth] */ - val logicalDisplayWidth - get() = if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { + val logicalDisplayWidth = + if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { naturalDisplayHeight } else { naturalDisplayWidth } /** See [android.view.DisplayInfo.logicalHeight] */ - val logicalDisplayHeight - get() = if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { + val logicalDisplayHeight = + if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { naturalDisplayWidth } else { naturalDisplayHeight } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt new file mode 100644 index 000000000000..001fed76c124 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 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.dagger + +import com.android.systemui.biometrics.udfps.BoundingBoxOverlapDetector +import com.android.systemui.biometrics.udfps.EllipseOverlapDetector +import com.android.systemui.biometrics.udfps.OverlapDetector +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import dagger.Module +import dagger.Provides + +/** Dagger module for all things UDFPS. TODO(b/260558624): Move to BiometricsModule. */ +@Module +interface UdfpsModule { + companion object { + + @Provides + @SysUISingleton + fun providesOverlapDetector(featureFlags: FeatureFlags): OverlapDetector { + return if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { + EllipseOverlapDetector() + } else { + BoundingBoxOverlapDetector() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt new file mode 100644 index 000000000000..79a0acb8bbc1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.graphics.Rect +import com.android.systemui.dagger.SysUISingleton + +/** Returns whether the touch coordinates are within the sensor's bounding box. */ +@SysUISingleton +class BoundingBoxOverlapDetector : OverlapDetector { + override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean = + touchData.isWithinSensor(nativeSensorBounds) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt new file mode 100644 index 000000000000..857224290752 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.graphics.Point +import android.graphics.Rect +import com.android.systemui.dagger.SysUISingleton +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin + +/** + * Approximates the touch as an ellipse and determines whether the ellipse has a sufficient overlap + * with the sensor. + */ +@SysUISingleton +class EllipseOverlapDetector(private val neededPoints: Int = 2) : OverlapDetector { + + override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean { + val points = calculateSensorPoints(nativeSensorBounds) + return points.count { checkPoint(it, touchData) } >= neededPoints + } + + private fun checkPoint(point: Point, touchData: NormalizedTouchData): Boolean { + // Calculate if sensor point is within ellipse + // Formula: ((cos(o)(xE - xS) + sin(o)(yE - yS))^2 / a^2) + ((sin(o)(xE - xS) + cos(o)(yE - + // yS))^2 / b^2) <= 1 + val a: Float = cos(touchData.orientation) * (point.x - touchData.x) + val b: Float = sin(touchData.orientation) * (point.y - touchData.y) + val c: Float = sin(touchData.orientation) * (point.x - touchData.x) + val d: Float = cos(touchData.orientation) * (point.y - touchData.y) + val result = + (a + b).pow(2) / (touchData.minor / 2).pow(2) + + (c - d).pow(2) / (touchData.major / 2).pow(2) + + return result <= 1 + } + + private fun calculateSensorPoints(sensorBounds: Rect): List<Point> { + val sensorX = sensorBounds.centerX() + val sensorY = sensorBounds.centerY() + val cornerOffset: Int = sensorBounds.width() / 4 + val sideOffset: Int = sensorBounds.width() / 3 + + return listOf( + Point(sensorX - cornerOffset, sensorY - cornerOffset), + Point(sensorX, sensorY - sideOffset), + Point(sensorX + cornerOffset, sensorY - cornerOffset), + Point(sensorX - sideOffset, sensorY), + Point(sensorX, sensorY), + Point(sensorX + sideOffset, sensorY), + Point(sensorX - cornerOffset, sensorY + cornerOffset), + Point(sensorX, sensorY + sideOffset), + Point(sensorX + cornerOffset, sensorY + cornerOffset) + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/InteractionEvent.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/InteractionEvent.kt new file mode 100644 index 000000000000..6e47dadc4545 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/InteractionEvent.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.view.MotionEvent + +/** Interaction event between a finger and the under-display fingerprint sensor (UDFPS). */ +enum class InteractionEvent { + /** + * A finger entered the sensor area. This can originate from either [MotionEvent.ACTION_DOWN] or + * [MotionEvent.ACTION_MOVE]. + */ + DOWN, + + /** + * A finger left the sensor area. This can originate from either [MotionEvent.ACTION_UP] or + * [MotionEvent.ACTION_MOVE]. + */ + UP, + + /** + * The touch reporting has stopped. This corresponds to [MotionEvent.ACTION_CANCEL]. This should + * not be confused with [UP]. If there was a finger on the sensor, it may or may not still be on + * the sensor. + */ + CANCEL, + + /** + * The interaction hasn't changed since the previous event. The can originate from any of + * [MotionEvent.ACTION_DOWN], [MotionEvent.ACTION_MOVE], or [MotionEvent.ACTION_UP] if one of + * these is true: + * - There was previously a finger on the sensor, and that finger is still on the sensor. + * - There was previously no finger on the sensor, and there still isn't. + */ + UNCHANGED, +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt new file mode 100644 index 000000000000..62bedc627b07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.graphics.Rect +import android.view.MotionEvent + +/** Touch data in natural orientation and native resolution. */ +data class NormalizedTouchData( + + /** + * Value obtained from [MotionEvent.getPointerId], or [MotionEvent.INVALID_POINTER_ID] if the ID + * is not available. + */ + val pointerId: Int, + + /** [MotionEvent.getRawX] mapped to natural orientation and native resolution. */ + val x: Float, + + /** [MotionEvent.getRawY] mapped to natural orientation and native resolution. */ + val y: Float, + + /** [MotionEvent.getTouchMinor] mapped to natural orientation and native resolution. */ + val minor: Float, + + /** [MotionEvent.getTouchMajor] mapped to natural orientation and native resolution. */ + val major: Float, + + /** [MotionEvent.getOrientation] mapped to natural orientation. */ + val orientation: Float, + + /** [MotionEvent.getEventTime]. */ + val time: Long, + + /** [MotionEvent.getDownTime]. */ + val gestureStart: Long, +) { + + /** + * [nativeSensorBounds] contains the location and dimensions of the sensor area in native + * resolution and natural orientation. + * + * Returns whether the coordinates of the given pointer are within the sensor's bounding box. + */ + fun isWithinSensor(nativeSensorBounds: Rect): Boolean { + return nativeSensorBounds.left <= x && + nativeSensorBounds.right >= x && + nativeSensorBounds.top <= y && + nativeSensorBounds.bottom >= y + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt new file mode 100644 index 000000000000..0fec8ffbaa0a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.graphics.Rect + +/** Determines whether the touch has a sufficient overlap with the sensor. */ +interface OverlapDetector { + fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt new file mode 100644 index 000000000000..338bf66d197e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.graphics.PointF +import android.util.RotationUtils +import android.view.MotionEvent +import android.view.MotionEvent.INVALID_POINTER_ID +import android.view.Surface +import com.android.systemui.biometrics.UdfpsOverlayParams +import com.android.systemui.biometrics.udfps.TouchProcessorResult.Failure +import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +/** + * TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations. + */ +@SysUISingleton +class SinglePointerTouchProcessor @Inject constructor(val overlapDetector: OverlapDetector) : + TouchProcessor { + + override fun processTouch( + event: MotionEvent, + previousPointerOnSensorId: Int, + overlayParams: UdfpsOverlayParams, + ): TouchProcessorResult { + + fun preprocess(): PreprocessedTouch { + // TODO(b/253085297): Add multitouch support. pointerIndex can be > 0 for ACTION_MOVE. + val pointerIndex = 0 + val touchData = event.normalize(pointerIndex, overlayParams) + val isGoodOverlap = + overlapDetector.isGoodOverlap(touchData, overlayParams.nativeSensorBounds) + return PreprocessedTouch(touchData, previousPointerOnSensorId, isGoodOverlap) + } + + return when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> processActionDown(preprocess()) + MotionEvent.ACTION_MOVE -> processActionMove(preprocess()) + MotionEvent.ACTION_UP -> processActionUp(preprocess()) + MotionEvent.ACTION_CANCEL -> + processActionCancel(event.normalize(pointerIndex = 0, overlayParams)) + else -> + Failure("Unsupported MotionEvent." + MotionEvent.actionToString(event.actionMasked)) + } + } +} + +private data class PreprocessedTouch( + val data: NormalizedTouchData, + val previousPointerOnSensorId: Int, + val isGoodOverlap: Boolean, +) + +private fun processActionDown(touch: PreprocessedTouch): TouchProcessorResult { + return if (touch.isGoodOverlap) { + ProcessedTouch(InteractionEvent.DOWN, pointerOnSensorId = touch.data.pointerId, touch.data) + } else { + val event = + if (touch.data.pointerId == touch.previousPointerOnSensorId) { + InteractionEvent.UP + } else { + InteractionEvent.UNCHANGED + } + ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data) + } +} + +private fun processActionMove(touch: PreprocessedTouch): TouchProcessorResult { + val hadPointerOnSensor = touch.previousPointerOnSensorId != INVALID_POINTER_ID + val interactionEvent = + when { + touch.isGoodOverlap && !hadPointerOnSensor -> InteractionEvent.DOWN + !touch.isGoodOverlap && hadPointerOnSensor -> InteractionEvent.UP + else -> InteractionEvent.UNCHANGED + } + val pointerOnSensorId = + when (interactionEvent) { + InteractionEvent.UNCHANGED -> touch.previousPointerOnSensorId + InteractionEvent.DOWN -> touch.data.pointerId + else -> INVALID_POINTER_ID + } + return ProcessedTouch(interactionEvent, pointerOnSensorId, touch.data) +} + +private fun processActionUp(touch: PreprocessedTouch): TouchProcessorResult { + return if (touch.isGoodOverlap) { + ProcessedTouch(InteractionEvent.UP, pointerOnSensorId = INVALID_POINTER_ID, touch.data) + } else { + val event = + if (touch.previousPointerOnSensorId != INVALID_POINTER_ID) { + InteractionEvent.UP + } else { + InteractionEvent.UNCHANGED + } + ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data) + } +} + +private fun processActionCancel(data: NormalizedTouchData): TouchProcessorResult { + return ProcessedTouch(InteractionEvent.CANCEL, pointerOnSensorId = INVALID_POINTER_ID, data) +} + +/** + * Returns the touch information from the given [MotionEvent] with the relevant fields mapped to + * natural orientation and native resolution. + */ +private fun MotionEvent.normalize( + pointerIndex: Int, + overlayParams: UdfpsOverlayParams +): NormalizedTouchData { + val naturalTouch: PointF = rotateToNaturalOrientation(pointerIndex, overlayParams) + val nativeX = naturalTouch.x / overlayParams.scaleFactor + val nativeY = naturalTouch.y / overlayParams.scaleFactor + val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor + val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor + return NormalizedTouchData( + pointerId = getPointerId(pointerIndex), + x = nativeX, + y = nativeY, + minor = nativeMinor, + major = nativeMajor, + // TODO(b/259311354): touch orientation should be reported relative to Surface.ROTATION_O. + orientation = getOrientation(pointerIndex), + time = eventTime, + gestureStart = downTime, + ) +} + +/** + * Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device + * is in the [Surface.ROTATION_0] orientation. + */ +private fun MotionEvent.rotateToNaturalOrientation( + pointerIndex: Int, + overlayParams: UdfpsOverlayParams +): PointF { + val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex)) + val rot = overlayParams.rotation + if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { + RotationUtils.rotatePointF( + touchPoint, + RotationUtils.deltaRotation(rot, Surface.ROTATION_0), + overlayParams.logicalDisplayWidth.toFloat(), + overlayParams.logicalDisplayHeight.toFloat() + ) + } + return touchPoint +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt new file mode 100644 index 000000000000..ffcebf9cff75 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.view.MotionEvent +import com.android.systemui.biometrics.UdfpsOverlayParams + +/** + * Determines whether a finger entered or left the area of the under-display fingerprint sensor + * (UDFPS). Maps the touch information from a [MotionEvent] to the orientation and scale independent + * [NormalizedTouchData]. + */ +interface TouchProcessor { + + /** + * [event] touch event to be processed. + * + * [previousPointerOnSensorId] pointerId for the finger that was on the sensor prior to this + * event. See [MotionEvent.getPointerId]. If there was no finger on the sensor, this should be + * set to [MotionEvent.INVALID_POINTER_ID]. + * + * [overlayParams] contains the location and dimensions of the sensor area, as well as the scale + * factor and orientation of the overlay. See [UdfpsOverlayParams]. + * + * Returns [TouchProcessorResult.ProcessedTouch] on success, and [TouchProcessorResult.Failure] + * on failure. + */ + fun processTouch( + event: MotionEvent, + previousPointerOnSensorId: Int, + overlayParams: UdfpsOverlayParams, + ): TouchProcessorResult +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessorResult.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessorResult.kt new file mode 100644 index 000000000000..be75bb0d3821 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessorResult.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.view.MotionEvent + +/** Contains all the possible returns types for [TouchProcessor.processTouch] */ +sealed class TouchProcessorResult { + + /** + * [event] whether a finger entered or left the sensor area. See [InteractionEvent]. + * + * [pointerOnSensorId] pointerId fof the finger that's currently on the sensor. See + * [MotionEvent.getPointerId]. If there is no finger on the sensor, the value is set to + * [MotionEvent.INVALID_POINTER_ID]. + * + * [touchData] relevant data from the MotionEvent, mapped to natural orientation and native + * resolution. See [NormalizedTouchData]. + */ + data class ProcessedTouch( + val event: InteractionEvent, + val pointerOnSensorId: Int, + val touchData: NormalizedTouchData + ) : TouchProcessorResult() + + /** [reason] the reason for the failure. */ + data class Failure(val reason: String = "") : TouchProcessorResult() +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 95919c6b2c0d..b8e66735c740 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -33,6 +33,7 @@ import com.android.systemui.assist.AssistModule; import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; +import com.android.systemui.biometrics.dagger.UdfpsModule; import com.android.systemui.classifier.FalsingModule; import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule; import com.android.systemui.controls.dagger.ControlsModule; @@ -156,6 +157,7 @@ import dagger.Provides; TelephonyRepositoryModule.class, TemporaryDisplayModule.class, TunerModule.class, + UdfpsModule.class, UserModule.class, UtilModule.class, NoteTaskModule.class, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index acdafe3e1c7d..b267a5c23a49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -70,8 +70,13 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.R; 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.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.PrimaryBouncerInteractor; import com.android.systemui.plugins.FalsingManager; @@ -190,6 +195,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private AlternateUdfpsTouchProvider mAlternateTouchProvider; @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; + @Mock + private SinglePointerTouchProcessor mSinglePointerTouchProcessor; // Capture listeners so that they can be used to send events @Captor @@ -275,7 +282,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mDisplayManager, mHandler, mConfigurationController, mSystemClock, mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker, mActivityLaunchAnimator, alternateTouchProvider, mBiometricsExecutor, - mPrimaryBouncerInteractor); + mPrimaryBouncerInteractor, mSinglePointerTouchProcessor); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); @@ -1086,4 +1093,100 @@ public class UdfpsControllerTest extends SysuiTestCase { anyString(), any()); } + + @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); + mBiometricsExecutor.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); + mBiometricsExecutor.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); + mBiometricsExecutor.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 onTouch_withNewTouchDetection_shouldCallOldFingerprintManagerPath() + 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()); + + // 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); + mBiometricsExecutor.runAllReady(); + downEvent.recycle(); + + // AND ACTION_UP is received + when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( + processorResultUp); + MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent); + mBiometricsExecutor.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()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt new file mode 100644 index 000000000000..4f89b69108f4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.graphics.Rect +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@SmallTest +@RunWith(Parameterized::class) +class BoundingBoxOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { + val underTest = BoundingBoxOverlapDetector() + + @Test + fun isGoodOverlap() { + val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat()) + val actual = underTest.isGoodOverlap(touchData, SENSOR) + + assertThat(actual).isEqualTo(testCase.expected) + } + + data class TestCase(val x: Int, val y: Int, val expected: Boolean) + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): List<TestCase> = + listOf( + genPositiveTestCases( + validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) + ), + genNegativeTestCases( + invalidXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + invalidYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) + ) + ) + .flatten() + } +} + +/* Placeholder touch parameters. */ +private const val POINTER_ID = 42 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 1.23f +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + +/* Template [NormalizedTouchData]. */ +private val TOUCH_DATA = + NormalizedTouchData( + POINTER_ID, + x = 0f, + y = 0f, + NATIVE_MINOR, + NATIVE_MAJOR, + ORIENTATION, + TIME, + GESTURE_START + ) + +private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 500 /* bottom */) + +private fun genTestCases( + xs: List<Int>, + ys: List<Int>, + expected: Boolean +): List<BoundingBoxOverlapDetectorTest.TestCase> { + return xs.flatMap { x -> + ys.map { y -> BoundingBoxOverlapDetectorTest.TestCase(x, y, expected) } + } +} + +private fun genPositiveTestCases( + validXs: List<Int>, + validYs: List<Int>, +) = genTestCases(validXs, validYs, expected = true) + +private fun genNegativeTestCases( + invalidXs: List<Int>, + invalidYs: List<Int>, + validXs: List<Int>, + validYs: List<Int>, +): List<BoundingBoxOverlapDetectorTest.TestCase> { + return genTestCases(invalidXs, validYs, expected = false) + + genTestCases(validXs, invalidYs, expected = false) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt new file mode 100644 index 000000000000..834d0a69e427 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt @@ -0,0 +1,90 @@ +package com.android.systemui.biometrics.udfps + +import android.graphics.Rect +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@SmallTest +@RunWith(Parameterized::class) +class NormalizedTouchDataTest(val testCase: TestCase) : SysuiTestCase() { + + @Test + fun isWithinSensor() { + val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat()) + val actual = touchData.isWithinSensor(SENSOR) + + assertThat(actual).isEqualTo(testCase.expected) + } + + data class TestCase(val x: Int, val y: Int, val expected: Boolean) + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): List<TestCase> = + listOf( + genPositiveTestCases( + validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) + ), + genNegativeTestCases( + invalidXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + invalidYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) + ) + ) + .flatten() + } +} + +/* Placeholder touch parameters. */ +private const val POINTER_ID = 42 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 1.23f +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + +/* Template [NormalizedTouchData]. */ +private val TOUCH_DATA = + NormalizedTouchData( + POINTER_ID, + x = 0f, + y = 0f, + NATIVE_MINOR, + NATIVE_MAJOR, + ORIENTATION, + TIME, + GESTURE_START + ) + +private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 500 /* bottom */) + +private fun genTestCases( + xs: List<Int>, + ys: List<Int>, + expected: Boolean +): List<NormalizedTouchDataTest.TestCase> { + return xs.flatMap { x -> ys.map { y -> NormalizedTouchDataTest.TestCase(x, y, expected) } } +} + +private fun genPositiveTestCases( + validXs: List<Int>, + validYs: List<Int>, +) = genTestCases(validXs, validYs, expected = true) + +private fun genNegativeTestCases( + invalidXs: List<Int>, + invalidYs: List<Int>, + validXs: List<Int>, + validYs: List<Int>, +): List<NormalizedTouchDataTest.TestCase> { + return genTestCases(invalidXs, validYs, expected = false) + + genTestCases(validXs, invalidYs, expected = false) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt new file mode 100644 index 000000000000..95c53b408056 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.graphics.Rect +import android.view.MotionEvent +import android.view.MotionEvent.INVALID_POINTER_ID +import android.view.MotionEvent.PointerProperties +import android.view.Surface +import android.view.Surface.Rotation +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.UdfpsOverlayParams +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@SmallTest +@RunWith(Parameterized::class) +class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() { + private val overlapDetector = FakeOverlapDetector() + private val underTest = SinglePointerTouchProcessor(overlapDetector) + + @Test + fun processTouch() { + overlapDetector.shouldReturn = testCase.isGoodOverlap + + val actual = + underTest.processTouch( + testCase.event, + testCase.previousPointerOnSensorId, + testCase.overlayParams, + ) + + assertThat(actual).isInstanceOf(testCase.expected.javaClass) + if (actual is TouchProcessorResult.ProcessedTouch) { + assertThat(actual).isEqualTo(testCase.expected) + } + } + + data class TestCase( + val event: MotionEvent, + val isGoodOverlap: Boolean, + val previousPointerOnSensorId: Int, + val overlayParams: UdfpsOverlayParams, + val expected: TouchProcessorResult, + ) { + override fun toString(): String { + val expectedOutput = + if (expected is TouchProcessorResult.ProcessedTouch) { + expected.event.toString() + + ", (x: ${expected.touchData.x}, y: ${expected.touchData.y})" + + ", pointerOnSensorId: ${expected.pointerOnSensorId}" + + ", ..." + } else { + TouchProcessorResult.Failure().toString() + } + return "{" + + MotionEvent.actionToString(event.action) + + ", (x: ${event.x}, y: ${event.y})" + + ", scale: ${overlayParams.scaleFactor}" + + ", rotation: " + + Surface.rotationToString(overlayParams.rotation) + + ", previousPointerOnSensorId: $previousPointerOnSensorId" + + ", ...} expected: {$expectedOutput}" + } + } + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): List<TestCase> = + listOf( + // MotionEvent.ACTION_DOWN + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_DOWN, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.DOWN, + expectedPointerOnSensorId = POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_DOWN, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.DOWN, + expectedPointerOnSensorId = POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_DOWN, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_DOWN, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + // MotionEvent.ACTION_MOVE + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.DOWN, + expectedPointerOnSensorId = POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + // MotionEvent.ACTION_UP + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_UP, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_UP, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_UP, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_UP, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + // MotionEvent.ACTION_CANCEL + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_CANCEL, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.CANCEL, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_CANCEL, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.CANCEL, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_CANCEL, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.CANCEL, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_CANCEL, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.CANCEL, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + ) + .flatten() + + listOf( + // Unsupported MotionEvent actions. + genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_DOWN), + genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_UP), + genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_ENTER), + genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_MOVE), + genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_EXIT), + ) + .flatten() + } +} + +/* Display dimensions in native resolution and natural orientation. */ +private const val ROTATION_0_NATIVE_DISPLAY_WIDTH = 400 +private const val ROTATION_0_NATIVE_DISPLAY_HEIGHT = 600 + +/* + * ROTATION_0 map: + * _ _ _ _ + * _ _ O _ + * _ _ _ _ + * _ S _ _ + * _ S _ _ + * _ _ _ _ + * + * (_) empty space + * (S) sensor + * (O) touch outside of the sensor + */ +private val ROTATION_0_NATIVE_SENSOR_BOUNDS = + Rect( + 100, /* left */ + 300, /* top */ + 200, /* right */ + 500, /* bottom */ + ) +private val ROTATION_0_INPUTS = + OrientationBasedInputs( + rotation = Surface.ROTATION_0, + nativeXWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterX(), + nativeYWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterY(), + nativeXOutsideSensor = 250f, + nativeYOutsideSensor = 150f, + ) + +/* + * ROTATION_90 map: + * _ _ _ _ _ _ + * _ O _ _ _ _ + * _ _ _ S S _ + * _ _ _ _ _ _ + * + * (_) empty space + * (S) sensor + * (O) touch outside of the sensor + */ +private val ROTATION_90_NATIVE_SENSOR_BOUNDS = + Rect( + 300, /* left */ + 200, /* top */ + 500, /* right */ + 300, /* bottom */ + ) +private val ROTATION_90_INPUTS = + OrientationBasedInputs( + rotation = Surface.ROTATION_90, + nativeXWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterX(), + nativeYWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterY(), + nativeXOutsideSensor = 150f, + nativeYOutsideSensor = 150f, + ) + +/* ROTATION_180 is not supported. It's treated the same as ROTATION_0. */ +private val ROTATION_180_INPUTS = + ROTATION_0_INPUTS.copy( + rotation = Surface.ROTATION_180, + ) + +/* + * ROTATION_270 map: + * _ _ _ _ _ _ + * _ S S _ _ _ + * _ _ _ _ O _ + * _ _ _ _ _ _ + * + * (_) empty space + * (S) sensor + * (O) touch outside of the sensor + */ +private val ROTATION_270_NATIVE_SENSOR_BOUNDS = + Rect( + 100, /* left */ + 100, /* top */ + 300, /* right */ + 200, /* bottom */ + ) +private val ROTATION_270_INPUTS = + OrientationBasedInputs( + rotation = Surface.ROTATION_270, + nativeXWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterX(), + nativeYWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterY(), + nativeXOutsideSensor = 450f, + nativeYOutsideSensor = 250f, + ) + +/* Placeholder touch parameters. */ +private const val POINTER_ID = 42 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 1.23f +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + +/* Template [MotionEvent]. */ +private val MOTION_EVENT = + obtainMotionEvent( + action = 0, + pointerId = POINTER_ID, + x = 0f, + y = 0f, + minor = 0f, + major = 0f, + orientation = ORIENTATION, + time = TIME, + gestureStart = GESTURE_START, + ) + +/* Template [NormalizedTouchData]. */ +private val NORMALIZED_TOUCH_DATA = + NormalizedTouchData( + POINTER_ID, + x = 0f, + y = 0f, + NATIVE_MINOR, + NATIVE_MAJOR, + ORIENTATION, + TIME, + GESTURE_START + ) + +/* + * Contains test inputs that are tied to a particular device orientation. + * + * "native" means in native resolution (not scaled). + */ +private data class OrientationBasedInputs( + @Rotation val rotation: Int, + val nativeXWithinSensor: Float, + val nativeYWithinSensor: Float, + val nativeXOutsideSensor: Float, + val nativeYOutsideSensor: Float, +) { + + fun toOverlayParams(scaleFactor: Float): UdfpsOverlayParams = + UdfpsOverlayParams( + sensorBounds = ROTATION_0_NATIVE_SENSOR_BOUNDS.scaled(scaleFactor), + overlayBounds = ROTATION_0_NATIVE_SENSOR_BOUNDS.scaled(scaleFactor), + naturalDisplayHeight = (ROTATION_0_NATIVE_DISPLAY_HEIGHT * scaleFactor).toInt(), + naturalDisplayWidth = (ROTATION_0_NATIVE_DISPLAY_WIDTH * scaleFactor).toInt(), + scaleFactor = scaleFactor, + rotation = rotation + ) + + fun getNativeX(isWithinSensor: Boolean): Float { + return if (isWithinSensor) nativeXWithinSensor else nativeXOutsideSensor + } + + fun getNativeY(isWithinSensor: Boolean): Float { + return if (isWithinSensor) nativeYWithinSensor else nativeYOutsideSensor + } +} + +private fun genPositiveTestCases( + motionEventAction: Int, + previousPointerOnSensorId: Int, + isGoodOverlap: Boolean, + expectedInteractionEvent: InteractionEvent, + expectedPointerOnSensorId: Int +): List<SinglePointerTouchProcessorTest.TestCase> { + val scaleFactors = listOf(0.75f, 1f, 1.5f) + val orientations = + listOf( + ROTATION_0_INPUTS, + ROTATION_90_INPUTS, + ROTATION_180_INPUTS, + ROTATION_270_INPUTS, + ) + return scaleFactors.flatMap { scaleFactor -> + orientations.map { orientation -> + val overlayParams = orientation.toOverlayParams(scaleFactor) + val nativeX = orientation.getNativeX(isGoodOverlap) + val nativeY = orientation.getNativeY(isGoodOverlap) + val event = + MOTION_EVENT.copy( + action = motionEventAction, + x = nativeX * scaleFactor, + y = nativeY * scaleFactor, + minor = NATIVE_MINOR * scaleFactor, + major = NATIVE_MAJOR * scaleFactor, + ) + val expectedTouchData = + NORMALIZED_TOUCH_DATA.copy( + x = ROTATION_0_INPUTS.getNativeX(isGoodOverlap), + y = ROTATION_0_INPUTS.getNativeY(isGoodOverlap), + ) + val expected = + TouchProcessorResult.ProcessedTouch( + event = expectedInteractionEvent, + pointerOnSensorId = expectedPointerOnSensorId, + touchData = expectedTouchData, + ) + SinglePointerTouchProcessorTest.TestCase( + event = event, + isGoodOverlap = isGoodOverlap, + previousPointerOnSensorId = previousPointerOnSensorId, + overlayParams = overlayParams, + expected = expected, + ) + } + } +} + +private fun genTestCasesForUnsupportedAction( + motionEventAction: Int +): List<SinglePointerTouchProcessorTest.TestCase> { + val isGoodOverlap = true + val previousPointerOnSensorIds = listOf(INVALID_POINTER_ID, POINTER_ID) + return previousPointerOnSensorIds.map { previousPointerOnSensorId -> + val overlayParams = ROTATION_0_INPUTS.toOverlayParams(scaleFactor = 1f) + val nativeX = ROTATION_0_INPUTS.getNativeX(isGoodOverlap) + val nativeY = ROTATION_0_INPUTS.getNativeY(isGoodOverlap) + val event = + MOTION_EVENT.copy( + action = motionEventAction, + x = nativeX, + y = nativeY, + minor = NATIVE_MINOR, + major = NATIVE_MAJOR, + ) + SinglePointerTouchProcessorTest.TestCase( + event = event, + isGoodOverlap = isGoodOverlap, + previousPointerOnSensorId = previousPointerOnSensorId, + overlayParams = overlayParams, + expected = TouchProcessorResult.Failure(), + ) + } +} + +private fun obtainMotionEvent( + action: Int, + pointerId: Int, + x: Float, + y: Float, + minor: Float, + major: Float, + orientation: Float, + time: Long, + gestureStart: Long, +): MotionEvent { + val pp = PointerProperties() + pp.id = pointerId + val pc = MotionEvent.PointerCoords() + pc.x = x + pc.y = y + pc.touchMinor = minor + pc.touchMajor = major + pc.orientation = orientation + return MotionEvent.obtain( + gestureStart /* downTime */, + time /* eventTime */, + action /* action */, + 1 /* pointerCount */, + arrayOf(pp) /* pointerProperties */, + arrayOf(pc) /* pointerCoords */, + 0 /* metaState */, + 0 /* buttonState */, + 1f /* xPrecision */, + 1f /* yPrecision */, + 0 /* deviceId */, + 0 /* edgeFlags */, + 0 /* source */, + 0 /* flags */ + ) +} + +private fun MotionEvent.copy( + action: Int = this.action, + pointerId: Int = this.getPointerId(0), + x: Float = this.rawX, + y: Float = this.rawY, + minor: Float = this.touchMinor, + major: Float = this.touchMajor, + orientation: Float = this.orientation, + time: Long = this.eventTime, + gestureStart: Long = this.downTime, +) = obtainMotionEvent(action, pointerId, x, y, minor, major, orientation, time, gestureStart) + +private fun Rect.scaled(scaleFactor: Float) = Rect(this).apply { scale(scaleFactor) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt new file mode 100644 index 000000000000..8176dd07b84a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 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.udfps + +import android.graphics.Rect + +class FakeOverlapDetector : OverlapDetector { + var shouldReturn: Boolean = false + + override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean { + return shouldReturn + } +} |