Merge changes from topic "new-udfps-touch" into tm-qpr-dev

* changes:
  Integrate new touch architecture with UdfpsController
  Change computed properties to regular in UdfpsOverlayParams
  Introduce testable UDFPS touch architecture
  Add rotatePointF to RotationUtils
  Add tryDismissingKeyguard() to UdfpsController
This commit is contained in:
Ilya Matyukhin
2022-11-30 06:12:34 +00:00
committed by Android (Google) Code Review
21 changed files with 1676 additions and 169 deletions

View File

@@ -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;
}
@@ -966,6 +966,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
*/

View File

@@ -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;
@@ -192,6 +193,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.

View File

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

View File

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

View File

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

View File

@@ -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
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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,
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

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

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
}