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:
committed by
Android (Google) Code Review
commit
0672ae40db
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user