Merge changes from topic "new-udfps-touch" into tm-qpr-dev am: 0672ae40db
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20550191 Change-Id: Idad692926ac241f051d6580ddfe1b1318e8cad08 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -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