diff --git a/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl b/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl
index 32530da74e897..a57726c4afe4b 100644
--- a/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl
+++ b/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl
@@ -25,4 +25,7 @@ oneway interface IUdfpsOverlayController {
// Hides the overlay.
void hideUdfpsOverlay();
+
+ // Shows debug messages on the UDFPS overlay.
+ void setDebugMessage(String message);
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index f99be880cbe33..613111d512be4 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4189,6 +4189,8 @@
+
+ false
15dp
+
+ 8dp
+ 8dp
+
8dp
0dp
48dp
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index ade106fb2223b..3cd4fca0e8771 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -49,6 +49,8 @@ import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.systemui.SystemUI;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
import java.util.List;
@@ -62,12 +64,13 @@ import javax.inject.Singleton;
*/
@Singleton
public class AuthController extends SystemUI implements CommandQueue.Callbacks,
- AuthDialogCallback {
+ AuthDialogCallback, DozeReceiver {
- private static final String TAG = "BiometricPrompt/AuthController";
+ private static final String TAG = "AuthController";
private static final boolean DEBUG = true;
private final CommandQueue mCommandQueue;
+ private final StatusBarStateController mStatusBarStateController;
private final Injector mInjector;
// TODO: These should just be saved from onSaveState
@@ -77,6 +80,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
private Handler mHandler = new Handler(Looper.getMainLooper());
private WindowManager mWindowManager;
+ @Nullable
private UdfpsController mUdfpsController;
@VisibleForTesting
IActivityTaskManager mActivityTaskManager;
@@ -142,6 +146,13 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
}
};
+ @Override
+ public void dozeTimeTick() {
+ if (mUdfpsController != null) {
+ mUdfpsController.dozeTimeTick();
+ }
+ }
+
@Override
public void onTryAgainPressed() {
if (mReceiver == null) {
@@ -251,14 +262,17 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
}
@Inject
- public AuthController(Context context, CommandQueue commandQueue) {
- this(context, commandQueue, new Injector());
+ public AuthController(Context context, CommandQueue commandQueue,
+ StatusBarStateController statusBarStateController) {
+ this(context, commandQueue, statusBarStateController, new Injector());
}
@VisibleForTesting
- AuthController(Context context, CommandQueue commandQueue, Injector injector) {
+ AuthController(Context context, CommandQueue commandQueue,
+ StatusBarStateController statusBarStateController, Injector injector) {
super(context);
mCommandQueue = commandQueue;
+ mStatusBarStateController = statusBarStateController;
mInjector = injector;
IntentFilter filter = new IntentFilter();
@@ -280,7 +294,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
fpm.getSensorProperties();
for (FingerprintSensorProperties props : fingerprintSensorProperties) {
if (props.sensorType == FingerprintSensorProperties.TYPE_UDFPS) {
- mUdfpsController = new UdfpsController(mContext);
+ mUdfpsController = new UdfpsController(mContext, mStatusBarStateController);
break;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index d29a8fb45bb54..82fb80892ab16 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -16,6 +16,7 @@
package com.android.systemui.biometrics;
+import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
@@ -29,6 +30,7 @@ import android.os.Looper;
import android.os.PowerManager;
import android.os.UserHandle;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.Log;
import android.util.MathUtils;
import android.util.Spline;
@@ -38,6 +40,8 @@ import android.view.WindowManager;
import com.android.internal.BrightnessSynchronizer;
import com.android.systemui.R;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
import java.io.FileWriter;
import java.io.IOException;
@@ -46,7 +50,7 @@ import java.io.IOException;
* Shows and hides the under-display fingerprint sensor (UDFPS) overlay, handles UDFPS touch events,
* and coordinates triggering of the high-brightness mode (HBM).
*/
-class UdfpsController {
+class UdfpsController implements DozeReceiver {
private static final String TAG = "UdfpsController";
// Gamma approximation for the sRGB color space.
private static final float DISPLAY_GAMMA = 2.2f;
@@ -61,6 +65,7 @@ class UdfpsController {
private final String mHbmPath;
private final String mHbmEnableCommand;
private final String mHbmDisableCommand;
+ private final boolean mHbmSupported;
// Brightness in nits in the high-brightness mode.
private final float mHbmNits;
// A spline mapping from the device's backlight value, normalized to the range [0, 1.0], to a
@@ -85,6 +90,11 @@ class UdfpsController {
public void hideUdfpsOverlay() {
UdfpsController.this.hideUdfpsOverlay();
}
+
+ @Override
+ public void setDebugMessage(String message) {
+ mView.setDebugMessage(message);
+ }
}
@SuppressLint("ClickableViewAccessibility")
@@ -116,7 +126,8 @@ class UdfpsController {
}
};
- UdfpsController(Context context) {
+ UdfpsController(@NonNull Context context,
+ @NonNull StatusBarStateController statusBarStateController) {
mFingerprintManager = context.getSystemService(FingerprintManager.class);
mWindowManager = context.getSystemService(WindowManager.class);
mContentResolver = context.getContentResolver();
@@ -129,6 +140,10 @@ class UdfpsController {
mHbmEnableCommand = context.getResources().getString(R.string.udfps_hbm_enable_command);
mHbmDisableCommand = context.getResources().getString(R.string.udfps_hbm_disable_command);
+ mHbmSupported = !TextUtils.isEmpty(mHbmPath);
+ mView.setHbmSupported(mHbmSupported);
+ statusBarStateController.addCallback(mView);
+
// This range only consists of the minimum and maximum values, which only cover
// non-high-brightness mode.
float[] nitsRange = toFloatArray(context.getResources().obtainTypedArray(
@@ -162,6 +177,11 @@ class UdfpsController {
mIsOverlayShowing = false;
}
+ @Override
+ public void dozeTimeTick() {
+ mView.dozeTimeTick();
+ }
+
private void showUdfpsOverlay() {
mHandler.post(() -> {
if (!mIsOverlayShowing) {
@@ -224,9 +244,11 @@ class UdfpsController {
mView.setScrimAlpha(computeScrimOpacity());
mView.showScrimAndDot();
try {
- FileWriter fw = new FileWriter(mHbmPath);
- fw.write(mHbmEnableCommand);
- fw.close();
+ if (mHbmSupported) {
+ FileWriter fw = new FileWriter(mHbmPath);
+ fw.write(mHbmEnableCommand);
+ fw.close();
+ }
mFingerprintManager.onFingerDown(x, y, minor, major);
} catch (IOException e) {
mView.hideScrimAndDot();
@@ -238,13 +260,15 @@ class UdfpsController {
mFingerprintManager.onFingerUp();
// Hiding the scrim before disabling HBM results in less noticeable flicker.
mView.hideScrimAndDot();
- try {
- FileWriter fw = new FileWriter(mHbmPath);
- fw.write(mHbmDisableCommand);
- fw.close();
- } catch (IOException e) {
- mView.showScrimAndDot();
- Log.e(TAG, "onFingerUp | failed to disable HBM: " + e.getMessage());
+ if (mHbmSupported) {
+ try {
+ FileWriter fw = new FileWriter(mHbmPath);
+ fw.write(mHbmDisableCommand);
+ fw.close();
+ } catch (IOException e) {
+ mView.showScrimAndDot();
+ Log.e(TAG, "onFingerUp | failed to disable HBM: " + e.getMessage());
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
index 45afc76b65eac..36353fa969560 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
@@ -16,6 +16,8 @@
package com.android.systemui.biometrics;
+import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@@ -23,25 +25,33 @@ import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.MathUtils;
import android.view.View;
import android.view.ViewTreeObserver;
import com.android.systemui.R;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
/**
* A full screen view with a configurable illumination dot and scrim.
*/
-public class UdfpsView extends View {
+public class UdfpsView extends View implements DozeReceiver,
+ StatusBarStateController.StateListener {
private static final String TAG = "UdfpsView";
// Values in pixels.
- private static final float SENSOR_SHADOW_OFFSET = 2.0f;
+ private static final float SENSOR_SHADOW_RADIUS = 2.0f;
private static final float SENSOR_OUTLINE_WIDTH = 2.0f;
+ private static final int DEBUG_TEXT_SIZE_PX = 32;
+
private final Rect mScrimRect;
private final Paint mScrimPaint;
+ private final Paint mDebugTextPaint;
private float mSensorX;
private float mSensorY;
@@ -50,11 +60,20 @@ public class UdfpsView extends View {
private final float mSensorRadius;
private final float mSensorMarginBottom;
private final float mSensorTouchAreaCoefficient;
+ private final int mMaxBurnInOffsetX;
+ private final int mMaxBurnInOffsetY;
private final Rect mTouchableRegion;
private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener;
+ // AOD anti-burn-in offsets
+ private float mInterpolatedDarkAmount;
+ private float mBurnInOffsetX;
+ private float mBurnInOffsetY;
+
private boolean mIsScrimShowing;
+ private boolean mHbmSupported;
+ private String mDebugMessage;
public UdfpsView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -80,18 +99,29 @@ public class UdfpsView extends View {
a.recycle();
}
+ mMaxBurnInOffsetX = getResources()
+ .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
+ mMaxBurnInOffsetY = getResources()
+ .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
+
mScrimRect = new Rect();
mScrimPaint = new Paint(0 /* flags */);
mScrimPaint.setColor(Color.BLACK);
mSensorRect = new RectF();
mSensorPaint = new Paint(0 /* flags */);
+ mSensorPaint.setAntiAlias(true);
mSensorPaint.setColor(Color.WHITE);
mSensorPaint.setStyle(Paint.Style.STROKE);
mSensorPaint.setStrokeWidth(SENSOR_OUTLINE_WIDTH);
- mSensorPaint.setShadowLayer(SENSOR_OUTLINE_WIDTH, 0, 0, Color.BLACK);
+ mSensorPaint.setShadowLayer(SENSOR_SHADOW_RADIUS, 0, 0, Color.BLACK);
mSensorPaint.setAntiAlias(true);
+ mDebugTextPaint = new Paint();
+ mDebugTextPaint.setAntiAlias(true);
+ mDebugTextPaint.setColor(Color.BLUE);
+ mDebugTextPaint.setTextSize(DEBUG_TEXT_SIZE_PX);
+
mTouchableRegion = new Rect();
mInsetsListener = internalInsetsInfo -> {
internalInsetsInfo.setTouchableInsets(
@@ -102,6 +132,29 @@ public class UdfpsView extends View {
mIsScrimShowing = false;
}
+ @Override
+ public void dozeTimeTick() {
+ updateAodPosition();
+ }
+
+ @Override
+ public void onDozeAmountChanged(float linear, float eased) {
+ mInterpolatedDarkAmount = eased;
+ updateAodPosition();
+ }
+
+ private void updateAodPosition() {
+ mBurnInOffsetX = MathUtils.lerp(0f,
+ getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */)
+ - mMaxBurnInOffsetX,
+ mInterpolatedDarkAmount);
+ mBurnInOffsetY = MathUtils.lerp(0f,
+ getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */)
+ - 0.5f * mMaxBurnInOffsetY,
+ mInterpolatedDarkAmount);
+ postInvalidate();
+ }
+
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -131,10 +184,29 @@ public class UdfpsView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
- if (mIsScrimShowing) {
+
+ if (mIsScrimShowing && mHbmSupported) {
+ // Only draw the scrim if HBM is supported.
canvas.drawRect(mScrimRect, mScrimPaint);
}
+
+ // Translation should affect everything but the scrim.
+ canvas.save();
+ canvas.translate(mBurnInOffsetX, mBurnInOffsetY);
+ if (!TextUtils.isEmpty(mDebugMessage)) {
+ canvas.drawText(mDebugMessage, 0, 160, mDebugTextPaint);
+ }
canvas.drawOval(mSensorRect, mSensorPaint);
+ canvas.restore();
+ }
+
+ void setHbmSupported(boolean hbmSupported) {
+ mHbmSupported = hbmSupported;
+ }
+
+ void setDebugMessage(String message) {
+ mDebugMessage = message;
+ postInvalidate();
}
boolean isValidTouch(float x, float y, float pressure) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index de74d4e04921a..5aca0dfd677ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -31,6 +31,7 @@ import android.view.View;
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.assist.AssistManager;
+import com.android.systemui.biometrics.AuthController;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.doze.DozeReceiver;
@@ -88,6 +89,7 @@ public final class DozeServiceHost implements DozeHost {
private final NotificationWakeUpCoordinator mNotificationWakeUpCoordinator;
private NotificationShadeWindowViewController mNotificationShadeWindowViewController;
private final LockscreenLockIconController mLockscreenLockIconController;
+ private final AuthController mAuthController;
private NotificationIconAreaController mNotificationIconAreaController;
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private NotificationPanelViewController mNotificationPanel;
@@ -110,7 +112,8 @@ public final class DozeServiceHost implements DozeHost {
PulseExpansionHandler pulseExpansionHandler,
NotificationShadeWindowController notificationShadeWindowController,
NotificationWakeUpCoordinator notificationWakeUpCoordinator,
- LockscreenLockIconController lockscreenLockIconController) {
+ LockscreenLockIconController lockscreenLockIconController,
+ AuthController authController) {
super();
mDozeLog = dozeLog;
mPowerManager = powerManager;
@@ -130,6 +133,7 @@ public final class DozeServiceHost implements DozeHost {
mNotificationShadeWindowController = notificationShadeWindowController;
mNotificationWakeUpCoordinator = notificationWakeUpCoordinator;
mLockscreenLockIconController = lockscreenLockIconController;
+ mAuthController = authController;
}
// TODO: we should try to not pass status bar in here if we can avoid it.
@@ -297,6 +301,7 @@ public final class DozeServiceHost implements DozeHost {
@Override
public void dozeTimeTick() {
mNotificationPanel.dozeTimeTick();
+ mAuthController.dozeTimeTick();
if (mAmbientIndicationContainer instanceof DozeReceiver) {
((DozeReceiver) mAmbientIndicationContainer).dozeTimeTick();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index d4a94c5b9e66c..c8566c599108e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -54,6 +54,7 @@ import android.testing.TestableLooper.RunWithLooper;
import com.android.internal.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
import org.junit.Before;
@@ -103,8 +104,8 @@ public class AuthControllerTest extends SysuiTestCase {
when(mDialog1.isAllowDeviceCredentials()).thenReturn(false);
when(mDialog2.isAllowDeviceCredentials()).thenReturn(false);
- mAuthController = new TestableAuthController(
- context, mock(CommandQueue.class), new MockInjector());
+ mAuthController = new TestableAuthController(context, mock(CommandQueue.class),
+ mock(StatusBarStateController.class), new MockInjector());
mAuthController.start();
}
@@ -502,8 +503,9 @@ public class AuthControllerTest extends SysuiTestCase {
private int mBuildCount = 0;
private PromptInfo mLastBiometricPromptInfo;
- TestableAuthController(Context context, CommandQueue commandQueue, Injector injector) {
- super(context, commandQueue, injector);
+ TestableAuthController(Context context, CommandQueue commandQueue,
+ StatusBarStateController statusBarStateController, Injector injector) {
+ super(context, commandQueue, statusBarStateController, injector);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index a5f4e51ea13c7..713a7c92e1cfc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -36,6 +36,7 @@ import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.assist.AssistManager;
+import com.android.systemui.biometrics.AuthController;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -90,6 +91,7 @@ public class DozeServiceHostTest extends SysuiTestCase {
@Mock private View mAmbientIndicationContainer;
@Mock private BiometricUnlockController mBiometricUnlockController;
@Mock private LockscreenLockIconController mLockscreenLockIconController;
+ @Mock private AuthController mAuthController;
@Before
public void setup() {
@@ -100,7 +102,7 @@ public class DozeServiceHostTest extends SysuiTestCase {
mKeyguardViewMediator, () -> mAssistManager, mDozeScrimController,
mKeyguardUpdateMonitor, mVisualStabilityManager, mPulseExpansionHandler,
mNotificationShadeWindowController, mNotificationWakeUpCoordinator,
- mLockscreenLockIconController);
+ mLockscreenLockIconController, mAuthController);
mDozeServiceHost.initialize(mStatusBar, mNotificationIconAreaController,
mStatusBarKeyguardViewManager, mNotificationShadeWindowViewController,
diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
index e87c8fe092fe9..73fc17aa26f1d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
@@ -45,7 +45,7 @@ public abstract class AcquisitionClient extends ClientMonitor implements I
private final PowerManager mPowerManager;
private final VibrationEffect mSuccessVibrationEffect;
private final VibrationEffect mErrorVibrationEffect;
- private boolean mShouldSendErrorToClient;
+ private boolean mShouldSendErrorToClient = true;
private boolean mAlreadyCancelled;
/**
@@ -85,11 +85,11 @@ public abstract class AcquisitionClient extends ClientMonitor implements I
// case (success, failure, or error) is received from the HAL (e.g. versions of fingerprint
// that do not handle lockout under the HAL. In these cases, ensure that the framework only
// sends errors once per ClientMonitor.
- if (!mShouldSendErrorToClient) {
+ if (mShouldSendErrorToClient) {
logOnError(getContext(), errorCode, vendorCode, getTargetUserId());
try {
if (getListener() != null) {
- mShouldSendErrorToClient = true;
+ mShouldSendErrorToClient = false;
getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode);
}
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index 47c839cb329a9..9128359250df9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -99,6 +99,14 @@ public abstract class AuthenticationClient extends AcquisitionClient
return getCookie() != 0;
}
+ public long getOperationId() {
+ return mOperationId;
+ }
+
+ public boolean isRestricted() {
+ return mIsRestricted;
+ }
+
@Override
protected boolean isCryptoOperation() {
return mOperationId != 0;
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index fffc07320f840..588e865112c28 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -175,7 +175,7 @@ public class BiometricScheduler {
@Nullable private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
@NonNull private final IBiometricService mBiometricService;
@NonNull private final Handler mHandler = new Handler(Looper.getMainLooper());
- @NonNull protected final InternalCallback mInternalCallback;
+ @NonNull private final InternalCallback mInternalCallback;
@NonNull private final Queue mPendingOperations;
@Nullable private Operation mCurrentOperation;
@NonNull private final ArrayDeque mCrashStates;
@@ -185,7 +185,15 @@ public class BiometricScheduler {
// starting the next client).
public class InternalCallback implements ClientMonitor.Callback {
@Override
- public void onClientFinished(ClientMonitor> clientMonitor, boolean success) {
+ public void onClientStarted(@NonNull ClientMonitor> clientMonitor) {
+ Slog.d(getTag(), "[Started] " + clientMonitor);
+ if (mCurrentOperation.mClientCallback != null) {
+ mCurrentOperation.mClientCallback.onClientStarted(clientMonitor);
+ }
+ }
+
+ @Override
+ public void onClientFinished(@NonNull ClientMonitor> clientMonitor, boolean success) {
mHandler.post(() -> {
if (mCurrentOperation == null) {
Slog.e(getTag(), "[Finishing] " + clientMonitor
@@ -235,6 +243,14 @@ public class BiometricScheduler {
mCrashStates = new ArrayDeque<>();
}
+ /**
+ * @return A reference to the internal callback that should be invoked whenever the scheduler
+ * needs to (e.g. client started, client finished).
+ */
+ @NonNull protected InternalCallback getInternalCallback() {
+ return mInternalCallback;
+ }
+
private String getTag() {
return BASE_TAG + "/" + mBiometricTag;
}
@@ -262,7 +278,7 @@ public class BiometricScheduler {
}
final Interruptable interruptable = (Interruptable) currentClient;
- interruptable.cancelWithoutStarting(mInternalCallback);
+ interruptable.cancelWithoutStarting(getInternalCallback());
// Now we wait for the client to send its FinishCallback, which kicks off the next
// operation.
return;
@@ -280,10 +296,7 @@ public class BiometricScheduler {
final boolean shouldStartNow = currentClient.getCookie() == 0;
if (shouldStartNow) {
Slog.d(getTag(), "[Starting] " + mCurrentOperation);
- if (mCurrentOperation.mClientCallback != null) {
- mCurrentOperation.mClientCallback.onClientStarted(currentClient);
- }
- currentClient.start(mInternalCallback);
+ currentClient.start(getInternalCallback());
mCurrentOperation.state = Operation.STATE_STARTED;
} else {
try {
@@ -326,11 +339,8 @@ public class BiometricScheduler {
}
Slog.d(getTag(), "[Starting] Prepared client: " + mCurrentOperation);
- if (mCurrentOperation.mClientCallback != null) {
- mCurrentOperation.mClientCallback.onClientStarted(mCurrentOperation.clientMonitor);
- }
mCurrentOperation.state = Operation.STATE_STARTED;
- mCurrentOperation.clientMonitor.start(mInternalCallback);
+ mCurrentOperation.clientMonitor.start(getInternalCallback());
}
/**
diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitor.java
index 81867b02aed9f..dec40e39fb291 100644
--- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitor.java
@@ -144,6 +144,7 @@ public abstract class ClientMonitor extends LoggableMonitor implements IBinde
*/
public void start(@NonNull Callback callback) {
mCallback = callback;
+ mCallback.onClientStarted(this);
}
/**
diff --git a/services/core/java/com/android/server/biometrics/sensors/LoggableMonitor.java b/services/core/java/com/android/server/biometrics/sensors/LoggableMonitor.java
index 1a4216f9d43c4..d85ab25cc8120 100644
--- a/services/core/java/com/android/server/biometrics/sensors/LoggableMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/LoggableMonitor.java
@@ -58,6 +58,10 @@ public abstract class LoggableMonitor {
mStatsClient = statsClient;
}
+ public int getStatsClient() {
+ return mStatsClient;
+ }
+
private boolean isAnyFieldUnknown() {
return mStatsModality == BiometricsProtoEnums.MODALITY_UNKNOWN
|| mStatsAction == BiometricsProtoEnums.ACTION_UNKNOWN
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21.java
index 20a180712b5b5..a12b54cfdd7f7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21.java
@@ -84,7 +84,7 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
private static final String TAG = "Fingerprint21";
private static final int ENROLL_TIMEOUT_SEC = 60;
- private final Context mContext;
+ final Context mContext;
private final IActivityTaskManager mActivityTaskManager;
private final FingerprintSensorProperties mSensorProperties;
private final BiometricScheduler mScheduler;
@@ -96,6 +96,7 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
private final Map mAuthenticatorIds;
@Nullable private IBiometricsFingerprint mDaemon;
+ @NonNull private final HalResultController mHalResultController;
@Nullable private IUdfpsOverlayController mUdfpsOverlayController;
private int mCurrentUserId = UserHandle.USER_NULL;
@@ -146,15 +147,37 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
}
};
- private final IBiometricsFingerprintClientCallback mDaemonCallback =
- new IBiometricsFingerprintClientCallback.Stub() {
+ public static class HalResultController extends IBiometricsFingerprintClientCallback.Stub {
+
+ /**
+ * Interface to sends results to the HalResultController's owner.
+ */
+ public interface Callback {
+ /**
+ * Invoked when the HAL sends ERROR_HW_UNAVAILABLE.
+ */
+ void onHardwareUnavailable();
+ }
+
+ @NonNull private final Context mContext;
+ @NonNull final Handler mHandler;
+ @NonNull final BiometricScheduler mScheduler;
+ @Nullable private Callback mCallback;
+
+ HalResultController(@NonNull Context context, @NonNull Handler handler,
+ @NonNull BiometricScheduler scheduler) {
+ mContext = context;
+ mHandler = handler;
+ mScheduler = scheduler;
+ }
+
+ public void setCallback(@Nullable Callback callback) {
+ mCallback = callback;
+ }
+
@Override
public void onEnrollResult(long deviceId, int fingerId, int groupId, int remaining) {
mHandler.post(() -> {
- final CharSequence name = FingerprintUtils.getInstance()
- .getUniqueName(mContext, mCurrentUserId);
- final Fingerprint fingerprint = new Fingerprint(name, groupId, fingerId, deviceId);
-
final ClientMonitor> client = mScheduler.getCurrentClient();
if (!(client instanceof FingerprintEnrollClient)) {
Slog.e(TAG, "onEnrollResult for non-enroll client: "
@@ -162,6 +185,11 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
return;
}
+ final int currentUserId = client.getTargetUserId();
+ final CharSequence name = FingerprintUtils.getInstance()
+ .getUniqueName(mContext, currentUserId);
+ final Fingerprint fingerprint = new Fingerprint(name, groupId, fingerId, deviceId);
+
final FingerprintEnrollClient enrollClient = (FingerprintEnrollClient) client;
enrollClient.onEnrollResult(fingerprint, remaining);
});
@@ -224,8 +252,9 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
if (error == BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE) {
Slog.e(TAG, "Got ERROR_HW_UNAVAILABLE");
- mDaemon = null;
- mCurrentUserId = UserHandle.USER_NULL;
+ if (mCallback != null) {
+ mCallback.onHardwareUnavailable();
+ }
}
});
}
@@ -262,20 +291,27 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
enumerateConsumer.onEnumerationResult(fp, remaining);
});
}
- };
+ }
- Fingerprint21(@NonNull Context context, int sensorId,
+ Fingerprint21(@NonNull Context context, @NonNull BiometricScheduler scheduler,
+ @NonNull Handler handler, int sensorId,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
- @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+ @NonNull HalResultController controller) {
mContext = context;
+ mScheduler = scheduler;
+ mHandler = handler;
mActivityTaskManager = ActivityTaskManager.getService();
- mHandler = new Handler(Looper.getMainLooper());
+
mTaskStackListener = new BiometricTaskStackListener();
mAuthenticatorIds = Collections.synchronizedMap(new HashMap<>());
mLazyDaemon = Fingerprint21.this::getDaemon;
mLockoutResetDispatcher = lockoutResetDispatcher;
mLockoutTracker = new LockoutFrameworkImpl(context, mLockoutResetCallback);
- mScheduler = new BiometricScheduler(TAG, gestureAvailabilityDispatcher);
+ mHalResultController = controller;
+ mHalResultController.setCallback(() -> {
+ mDaemon = null;
+ mCurrentUserId = UserHandle.USER_NULL;
+ });
try {
ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
@@ -303,6 +339,17 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
mSensorProperties = new FingerprintSensorProperties(sensorId, sensorType);
}
+ static Fingerprint21 newInstance(@NonNull Context context, int sensorId,
+ @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+ @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+ final Handler handler = new Handler(Looper.getMainLooper());
+ final BiometricScheduler scheduler =
+ new BiometricScheduler(TAG, gestureAvailabilityDispatcher);
+ final HalResultController controller = new HalResultController(context, handler, scheduler);
+ return new Fingerprint21(context, scheduler, handler, sensorId, lockoutResetDispatcher,
+ controller);
+ }
+
@Override
public void serviceDied(long cookie) {
Slog.e(TAG, "HAL died");
@@ -355,7 +402,7 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
// successfully set.
long halId = 0;
try {
- halId = mDaemon.setNotify(mDaemonCallback);
+ halId = mDaemon.setNotify(mHalResultController);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to set callback for fingerprint HAL", e);
mDaemon = null;
@@ -373,6 +420,9 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
return mDaemon;
}
+ @Nullable IUdfpsOverlayController getUdfpsOverlayController() {
+ return mUdfpsOverlayController;
+ }
@LockoutTracker.LockoutMode int getLockoutModeForUser(int userId) {
return mLockoutTracker.getLockoutModeForUser(userId);
}
@@ -492,7 +542,7 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
void scheduleAuthenticate(@NonNull IBinder token, long operationId, int userId, int cookie,
@NonNull ClientMonitorCallbackConverter listener, @NonNull String opPackageName,
- @Nullable Surface surface, boolean restricted, int statsClient) {
+ boolean restricted, int statsClient, boolean isKeyguard) {
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
@@ -500,8 +550,8 @@ class Fingerprint21 implements IHwBinder.DeathRecipient {
final FingerprintAuthenticationClient client = new FingerprintAuthenticationClient(
mContext, mLazyDaemon, token, listener, userId, operationId, restricted,
opPackageName, cookie, false /* requireConfirmation */,
- mSensorProperties.sensorId, isStrongBiometric, surface, statsClient,
- mTaskStackListener, mLockoutTracker, mUdfpsOverlayController);
+ mSensorProperties.sensorId, isStrongBiometric, statsClient,
+ mTaskStackListener, mLockoutTracker, mUdfpsOverlayController, isKeyguard);
mScheduler.scheduleClientMonitor(client);
});
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21UdfpsMock.java
new file mode 100644
index 0000000000000..c5567744851f3
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Fingerprint21UdfpsMock.java
@@ -0,0 +1,555 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics.sensors.fingerprint;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.trust.TrustManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
+import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
+import android.hardware.fingerprint.FingerprintSensorProperties;
+import android.hardware.fingerprint.IUdfpsOverlayController;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+import android.util.SparseBooleanArray;
+
+import com.android.server.biometrics.sensors.AuthenticationConsumer;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.ClientMonitor;
+import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+/**
+ * A mockable/testable provider of the {@link android.hardware.biometrics.fingerprint.V2_3} HIDL
+ * interface. This class is intended simulate UDFPS logic for devices that do not have an actual
+ * fingerprint@2.3 HAL (where UDFPS starts to become supported)
+ *
+ * UDFPS "accept" can only happen within a set amount of time after a sensor authentication. This is
+ * specified by {@link MockHalResultController#AUTH_VALIDITY_MS}. Touches after this duration will
+ * be treated as "reject".
+ *
+ * This class provides framework logic to emulate, for testing only, the UDFPS functionalies below:
+ *
+ * 1) IF either A) the caller is keyguard, and the device is not in a trusted state (authenticated
+ * via biometric sensor or unlocked with a trust agent {@see android.app.trust.TrustManager}, OR
+ * B) the caller is not keyguard, and regardless of trusted state, AND (following applies to both
+ * (A) and (B) above) {@link FingerprintManager#onFingerDown(int, int, float, float)} is
+ * received, this class will respond with {@link AuthenticationCallback#onAuthenticationFailed()}
+ * after a tunable flat_time + variance_time.
+ *
+ * In the case above (1), callers must not receive a successful authentication event here because
+ * the sensor has not actually been authenticated.
+ *
+ * 2) IF A) the caller is keyguard and the device is not in a trusted state, OR B) the caller is not
+ * keyguard and regardless of trusted state, AND (following applies to both (A) and (B)) the
+ * sensor is touched and the fingerprint is accepted by the HAL, and then
+ * {@link FingerprintManager#onFingerDown(int, int, float, float)} is received, this class will
+ * respond with {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)}
+ * after a tunable flat_time + variance_time. Note that the authentication callback from the
+ * sensor is held until {@link FingerprintManager#onFingerDown(int, int, float, float)} is
+ * invoked.
+ *
+ * In the case above (2), callers can receive a successful authentication callback because the
+ * real sensor was authenticated. Note that even though the real sensor was touched, keyguard
+ * fingerprint authentication does not put keyguard into a trusted state because the
+ * authentication callback is held until onFingerDown was invoked. This allows callers such as
+ * keyguard to simulate a realistic path.
+ *
+ * 3) IF the caller is keyguard AND the device in a trusted state and then
+ * {@link FingerprintManager#onFingerDown(int, int, float, float)} is received, this class will
+ * respond with {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)}
+ * after a tunable flat_time + variance time.
+ *
+ * In the case above (3), since the device is already unlockable via trust agent, it's fine to
+ * simulate the successful auth path. Non-keyguard clients will fall into either (1) or (2)
+ * above.
+ *
+ * This class currently does not simulate false rejection. Conversely, this class relies on the
+ * real hardware sensor so does not affect false acceptance.
+ */
+@SuppressWarnings("deprecation")
+public class Fingerprint21UdfpsMock extends Fingerprint21 implements TrustManager.TrustListener {
+
+ private static final String TAG = "Fingerprint21UdfpsMock";
+
+ // Secure setting integer. If true, the system will load this class to enable udfps testing.
+ public static final String CONFIG_ENABLE_TEST_UDFPS =
+ "com.android.server.biometrics.sensors.fingerprint.test_udfps.enable";
+ // Secure setting integer. A fixed duration intended to simulate something like the duration
+ // required for image capture.
+ private static final String CONFIG_AUTH_DELAY_PT1 =
+ "com.android.server.biometrics.sensors.fingerprint.test_udfps.auth_delay_pt1";
+ // Secure setting integer. A fixed duration intended to simulate something like the duration
+ // required for template matching to complete.
+ private static final String CONFIG_AUTH_DELAY_PT2 =
+ "com.android.server.biometrics.sensors.fingerprint.test_udfps.auth_delay_pt2";
+ // Secure setting integer. A random value between [-randomness, randomness] will be added to the
+ // capture delay above for each accept/reject.
+ private static final String CONFIG_AUTH_DELAY_RANDOMNESS =
+ "com.android.server.biometrics.sensors.fingerprint.test_udfps.auth_delay_randomness";
+
+ private static final int DEFAULT_AUTH_DELAY_PT1_MS = 300;
+ private static final int DEFAULT_AUTH_DELAY_PT2_MS = 400;
+ private static final int DEFAULT_AUTH_DELAY_RANDOMNESS_MS = 100;
+
+ @NonNull private final TestableBiometricScheduler mScheduler;
+ @NonNull private final Handler mHandler;
+ @NonNull private final FingerprintSensorProperties mSensorProperties;
+ @NonNull private final MockHalResultController mMockHalResultController;
+ @NonNull private final TrustManager mTrustManager;
+ @NonNull private final SparseBooleanArray mUserHasTrust;
+ @NonNull private final Random mRandom;
+ @NonNull private final FakeRejectRunnable mFakeRejectRunnable;
+ @NonNull private final FakeAcceptRunnable mFakeAcceptRunnable;
+ @NonNull private final RestartAuthRunnable mRestartAuthRunnable;
+
+ private static class TestableBiometricScheduler extends BiometricScheduler {
+ @NonNull private final TestableInternalCallback mInternalCallback;
+ @NonNull private Fingerprint21UdfpsMock mFingerprint21;
+
+ TestableBiometricScheduler(@NonNull String tag,
+ @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+ super(tag, gestureAvailabilityDispatcher);
+ mInternalCallback = new TestableInternalCallback();
+ }
+
+ class TestableInternalCallback extends InternalCallback {
+ @Override
+ public void onClientStarted(ClientMonitor> clientMonitor) {
+ super.onClientStarted(clientMonitor);
+ Slog.d(TAG, "Client started: " + clientMonitor);
+ mFingerprint21.setDebugMessage("Started: " + clientMonitor);
+ }
+
+ @Override
+ public void onClientFinished(ClientMonitor> clientMonitor, boolean success) {
+ super.onClientFinished(clientMonitor, success);
+ Slog.d(TAG, "Client finished: " + clientMonitor);
+ mFingerprint21.setDebugMessage("Finished: " + clientMonitor);
+ }
+ }
+
+ void init(@NonNull Fingerprint21UdfpsMock fingerprint21) {
+ mFingerprint21 = fingerprint21;
+ }
+
+ /**
+ * Expose the internal finish callback so it can be used for testing
+ */
+ @Override
+ @NonNull protected InternalCallback getInternalCallback() {
+ return mInternalCallback;
+ }
+ }
+
+ /**
+ * All of the mocking/testing should happen in here. This way we don't need to modify the
+ * {@link com.android.server.biometrics.sensors.ClientMonitor} implementations and can run the
+ * real path there.
+ */
+ private static class MockHalResultController extends HalResultController {
+
+ // Duration for which a sensor authentication can be treated as UDFPS success.
+ private static final int AUTH_VALIDITY_MS = 10 * 1000; // 10 seconds
+
+ static class LastAuthArgs {
+ @NonNull final AuthenticationConsumer lastAuthenticatedClient;
+ final long deviceId;
+ final int fingerId;
+ final int groupId;
+ @Nullable final ArrayList token;
+
+ LastAuthArgs(@NonNull AuthenticationConsumer authenticationConsumer, long deviceId,
+ int fingerId, int groupId, @Nullable ArrayList token) {
+ lastAuthenticatedClient = authenticationConsumer;
+ this.deviceId = deviceId;
+ this.fingerId = fingerId;
+ this.groupId = groupId;
+ if (token == null) {
+ this.token = null;
+ } else {
+ // Store a copy so the owner can be GC'd
+ this.token = new ArrayList<>(token);
+ }
+ }
+ }
+
+ // Initialized after the constructor, but before it's ever used.
+ @NonNull private RestartAuthRunnable mRestartAuthRunnable;
+ @NonNull private Fingerprint21UdfpsMock mFingerprint21;
+ @Nullable private LastAuthArgs mLastAuthArgs;
+
+ MockHalResultController(@NonNull Context context, @NonNull Handler handler,
+ @NonNull BiometricScheduler scheduler) {
+ super(context, handler, scheduler);
+ }
+
+ void init(@NonNull RestartAuthRunnable restartAuthRunnable,
+ @NonNull Fingerprint21UdfpsMock fingerprint21) {
+ mRestartAuthRunnable = restartAuthRunnable;
+ mFingerprint21 = fingerprint21;
+ }
+
+ @Nullable AuthenticationConsumer getLastAuthenticatedClient() {
+ return mLastAuthArgs != null ? mLastAuthArgs.lastAuthenticatedClient : null;
+ }
+
+ /**
+ * Intercepts the HAL authentication and holds it until the UDFPS simulation decides
+ * that authentication finished.
+ */
+ @Override
+ public void onAuthenticated(long deviceId, int fingerId, int groupId,
+ ArrayList token) {
+ mHandler.post(() -> {
+ final ClientMonitor> client = mScheduler.getCurrentClient();
+ if (!(client instanceof AuthenticationConsumer)) {
+ Slog.e(TAG, "Non authentication consumer: " + client);
+ return;
+ }
+
+ final boolean accepted = fingerId != 0;
+ if (accepted) {
+ mFingerprint21.setDebugMessage("Finger accepted");
+ } else {
+ mFingerprint21.setDebugMessage("Finger rejected");
+ }
+
+ final AuthenticationConsumer authenticationConsumer =
+ (AuthenticationConsumer) client;
+ mLastAuthArgs = new LastAuthArgs(authenticationConsumer, deviceId, fingerId,
+ groupId, token);
+
+ // Remove any existing restart runnbables since auth just started, and put a new
+ // one on the queue.
+ mHandler.removeCallbacks(mRestartAuthRunnable);
+ mRestartAuthRunnable.setLastAuthReference(authenticationConsumer);
+ mHandler.postDelayed(mRestartAuthRunnable, AUTH_VALIDITY_MS);
+ });
+ }
+
+ /**
+ * Calls through to the real interface and notifies clients of accept/reject.
+ */
+ void sendAuthenticated(long deviceId, int fingerId, int groupId,
+ ArrayList token) {
+ Slog.d(TAG, "sendAuthenticated: " + (fingerId != 0));
+ mFingerprint21.setDebugMessage("Udfps match: " + (fingerId != 0));
+ super.onAuthenticated(deviceId, fingerId, groupId, token);
+ }
+ }
+
+ static Fingerprint21UdfpsMock newInstance(@NonNull Context context, int sensorId,
+ @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+ @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+ Slog.d(TAG, "Creating Fingerprint23Mock!");
+
+ final Handler handler = new Handler(Looper.getMainLooper());
+ final TestableBiometricScheduler scheduler =
+ new TestableBiometricScheduler(TAG, gestureAvailabilityDispatcher);
+ final MockHalResultController controller =
+ new MockHalResultController(context, handler, scheduler);
+ return new Fingerprint21UdfpsMock(context, scheduler, handler, sensorId,
+ lockoutResetDispatcher, controller);
+ }
+
+ private static abstract class FakeFingerRunnable implements Runnable {
+ private long mFingerDownTime;
+ private int mCaptureDuration;
+
+ /**
+ * @param fingerDownTime System time when onFingerDown occurred
+ * @param captureDuration Duration that the finger needs to be down for
+ */
+ void setSimulationTime(long fingerDownTime, int captureDuration) {
+ mFingerDownTime = fingerDownTime;
+ mCaptureDuration = captureDuration;
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ boolean isImageCaptureComplete() {
+ return System.currentTimeMillis() - mFingerDownTime > mCaptureDuration;
+ }
+ }
+
+ private final class FakeRejectRunnable extends FakeFingerRunnable {
+ @Override
+ public void run() {
+ mMockHalResultController.sendAuthenticated(0, 0, 0, null);
+ }
+ }
+
+ private final class FakeAcceptRunnable extends FakeFingerRunnable {
+ @Override
+ public void run() {
+ if (mMockHalResultController.mLastAuthArgs == null) {
+ // This can happen if the user has trust agents enabled, which make lockscreen
+ // dismissable. Send a fake non-zero (accept) finger.
+ Slog.d(TAG, "Sending fake finger");
+ mMockHalResultController.sendAuthenticated(1 /* deviceId */,
+ 1 /* fingerId */, 1 /* groupId */, null /* token */);
+ } else {
+ mMockHalResultController.sendAuthenticated(
+ mMockHalResultController.mLastAuthArgs.deviceId,
+ mMockHalResultController.mLastAuthArgs.fingerId,
+ mMockHalResultController.mLastAuthArgs.groupId,
+ mMockHalResultController.mLastAuthArgs.token);
+ }
+ }
+ }
+
+ /**
+ * The fingerprint HAL allows multiple (5) fingerprint attempts per HIDL invocation of the
+ * authenticate method. However, valid fingerprint authentications are invalidated after
+ * {@link MockHalResultController#AUTH_VALIDITY_MS}, meaning UDFPS touches will be reported as
+ * rejects if touched after that duration. However, since a valid fingerprint was detected, the
+ * HAL and FingerprintService will not look for subsequent fingerprints.
+ *
+ * In order to keep the FingerprintManager API consistent (that multiple fingerprint attempts
+ * are allowed per auth lifecycle), we internally cancel and restart authentication so that the
+ * sensor is responsive again.
+ */
+ private static final class RestartAuthRunnable implements Runnable {
+ @NonNull private final Fingerprint21UdfpsMock mFingerprint21;
+ @NonNull private final TestableBiometricScheduler mScheduler;
+
+ // Store a reference to the auth consumer that should be invalidated.
+ private AuthenticationConsumer mLastAuthConsumer;
+
+ RestartAuthRunnable(@NonNull Fingerprint21UdfpsMock fingerprint21,
+ @NonNull TestableBiometricScheduler scheduler) {
+ mFingerprint21 = fingerprint21;
+ mScheduler = scheduler;
+ }
+
+ void setLastAuthReference(AuthenticationConsumer lastAuthConsumer) {
+ mLastAuthConsumer = lastAuthConsumer;
+ }
+
+ @Override
+ public void run() {
+ final ClientMonitor> client = mScheduler.getCurrentClient();
+
+ // We don't care about FingerprintDetectClient, since accept/rejects are both OK. UDFPS
+ // rejects will just simulate the path where non-enrolled fingers are presented.
+ if (!(client instanceof FingerprintAuthenticationClient)) {
+ Slog.e(TAG, "Non-FingerprintAuthenticationClient client: " + client);
+ return;
+ }
+
+ // Perhaps the runnable is stale, or the user stopped/started auth manually. Do not
+ // restart auth in this case.
+ if (client != mLastAuthConsumer) {
+ Slog.e(TAG, "Current client: " + client
+ + " does not match mLastAuthConsumer: " + mLastAuthConsumer);
+ return;
+ }
+
+ Slog.d(TAG, "Restarting auth, current: " + client);
+ mFingerprint21.setDebugMessage("Auth timed out");
+
+ final FingerprintAuthenticationClient authClient =
+ (FingerprintAuthenticationClient) client;
+ // Store the authClient parameters so it can be rescheduled
+ final IBinder token = client.getToken();
+ final long operationId = authClient.getOperationId();
+ final int user = client.getTargetUserId();
+ final int cookie = client.getCookie();
+ final ClientMonitorCallbackConverter listener = client.getListener();
+ final String opPackageName = client.getOwnerString();
+ final boolean restricted = authClient.isRestricted();
+ final int statsClient = client.getStatsClient();
+ final boolean isKeyguard = authClient.isKeyguard();
+
+ // Don't actually send cancel() to the HAL, since successful auth already finishes
+ // HAL authenticate() lifecycle. Just
+ mScheduler.getInternalCallback().onClientFinished(client, true /* success */);
+
+ // Schedule this only after we invoke onClientFinished for the previous client, so that
+ // internal preemption logic is not run.
+ mFingerprint21.scheduleAuthenticate(token, operationId, user, cookie,
+ listener, opPackageName, restricted, statsClient, isKeyguard);
+ }
+ }
+
+ private Fingerprint21UdfpsMock(@NonNull Context context,
+ @NonNull TestableBiometricScheduler scheduler,
+ @NonNull Handler handler, int sensorId,
+ @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+ @NonNull MockHalResultController controller) {
+ super(context, scheduler, handler, sensorId, lockoutResetDispatcher, controller);
+ mScheduler = scheduler;
+ mScheduler.init(this);
+ mHandler = handler;
+ mSensorProperties = new FingerprintSensorProperties(sensorId,
+ FingerprintSensorProperties.TYPE_UDFPS);
+ mMockHalResultController = controller;
+ mUserHasTrust = new SparseBooleanArray();
+ mTrustManager = context.getSystemService(TrustManager.class);
+ mTrustManager.registerTrustListener(this);
+ mRandom = new Random();
+ mFakeRejectRunnable = new FakeRejectRunnable();
+ mFakeAcceptRunnable = new FakeAcceptRunnable();
+ mRestartAuthRunnable = new RestartAuthRunnable(this, mScheduler);
+
+ // We can't initialize this during MockHalresultController's constructor due to a circular
+ // dependency.
+ mMockHalResultController.init(mRestartAuthRunnable, this);
+ }
+
+ @Override
+ public void onTrustChanged(boolean enabled, int userId, int flags) {
+ mUserHasTrust.put(userId, enabled);
+ }
+
+ @Override
+ public void onTrustManagedChanged(boolean enabled, int userId) {
+
+ }
+
+ @Override
+ public void onTrustError(CharSequence message) {
+
+ }
+
+ @Override
+ @NonNull
+ FingerprintSensorProperties getFingerprintSensorProperties() {
+ return mSensorProperties;
+ }
+
+ @Override
+ void onFingerDown(int x, int y, float minor, float major) {
+ mHandler.post(() -> {
+ Slog.d(TAG, "onFingerDown");
+ final AuthenticationConsumer lastAuthenticatedConsumer =
+ mMockHalResultController.getLastAuthenticatedClient();
+ final ClientMonitor> currentScheduledClient = mScheduler.getCurrentClient();
+
+ if (currentScheduledClient == null) {
+ Slog.d(TAG, "Not authenticating");
+ return;
+ }
+
+ mHandler.removeCallbacks(mFakeRejectRunnable);
+ mHandler.removeCallbacks(mFakeAcceptRunnable);
+
+ // The sensor was authenticated, is still the currently scheduled client, and the
+ // user touched the UDFPS affordance. Pretend that auth succeeded.
+ final boolean authenticatedClientIsCurrent = lastAuthenticatedConsumer != null
+ && lastAuthenticatedConsumer == currentScheduledClient;
+ // User is unlocked on keyguard via Trust Agent
+ final boolean keyguardAndTrusted;
+ if (currentScheduledClient instanceof FingerprintAuthenticationClient) {
+ keyguardAndTrusted = ((FingerprintAuthenticationClient) currentScheduledClient)
+ .isKeyguard()
+ && mUserHasTrust.get(currentScheduledClient.getTargetUserId(), false);
+ } else {
+ keyguardAndTrusted = false;
+ }
+
+ final int captureDuration = getNewCaptureDuration();
+ final int matchingDuration = getMatchingDuration();
+ final int totalDuration = captureDuration + matchingDuration;
+ setDebugMessage("Duration: " + totalDuration
+ + " (" + captureDuration + " + " + matchingDuration + ")");
+ if (authenticatedClientIsCurrent || keyguardAndTrusted) {
+ mFakeAcceptRunnable.setSimulationTime(System.currentTimeMillis(), captureDuration);
+ mHandler.postDelayed(mFakeAcceptRunnable, totalDuration);
+ } else if (currentScheduledClient instanceof AuthenticationConsumer) {
+ // Something is authenticating but authentication has not succeeded yet. Pretend
+ // that auth rejected.
+ mFakeRejectRunnable.setSimulationTime(System.currentTimeMillis(), captureDuration);
+ mHandler.postDelayed(mFakeRejectRunnable, totalDuration);
+ }
+ });
+ }
+
+ @Override
+ void onFingerUp() {
+ mHandler.post(() -> {
+ Slog.d(TAG, "onFingerUp");
+
+ // Only one of these can be on the handler at any given time (see onFingerDown). If
+ // image capture is not complete, send ACQUIRED_TOO_FAST and remove the runnable from
+ // the handler. Image capture (onFingerDown) needs to happen again.
+ if (mHandler.hasCallbacks(mFakeRejectRunnable)
+ && !mFakeRejectRunnable.isImageCaptureComplete()) {
+ mHandler.removeCallbacks(mFakeRejectRunnable);
+ mMockHalResultController.onAcquired(0 /* deviceId */,
+ FingerprintManager.FINGERPRINT_ACQUIRED_TOO_FAST,
+ 0 /* vendorCode */);
+ } else if (mHandler.hasCallbacks(mFakeAcceptRunnable)
+ && !mFakeAcceptRunnable.isImageCaptureComplete()) {
+ mHandler.removeCallbacks(mFakeAcceptRunnable);
+ mMockHalResultController.onAcquired(0 /* deviceId */,
+ FingerprintManager.FINGERPRINT_ACQUIRED_TOO_FAST,
+ 0 /* vendorCode */);
+ }
+ });
+ }
+
+ private int getNewCaptureDuration() {
+ final ContentResolver contentResolver = mContext.getContentResolver();
+ final int captureTime = Settings.Secure.getIntForUser(contentResolver,
+ CONFIG_AUTH_DELAY_PT1,
+ DEFAULT_AUTH_DELAY_PT1_MS,
+ UserHandle.USER_CURRENT);
+ final int randomDelayRange = Settings.Secure.getIntForUser(contentResolver,
+ CONFIG_AUTH_DELAY_RANDOMNESS,
+ DEFAULT_AUTH_DELAY_RANDOMNESS_MS,
+ UserHandle.USER_CURRENT);
+ final int randomDelay = mRandom.nextInt(randomDelayRange * 2) - randomDelayRange;
+
+ // Must be at least 0
+ return Math.max(captureTime + randomDelay, 0);
+ }
+
+ private int getMatchingDuration() {
+ final int matchingTime = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ CONFIG_AUTH_DELAY_PT2,
+ DEFAULT_AUTH_DELAY_PT2_MS,
+ UserHandle.USER_CURRENT);
+
+ // Must be at least 0
+ return Math.max(matchingTime, 0);
+ }
+
+ private void setDebugMessage(String message) {
+ try {
+ final IUdfpsOverlayController controller = getUdfpsOverlayController();
+ // Things can happen before SysUI loads and sets the controller.
+ if (controller != null) {
+ Slog.d(TAG, "setDebugMessage: " + message);
+ controller.setDebugMessage(message);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception when sending message: " + message, e);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticationClient.java
index 751df8ef99259..99d348a780f32 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticationClient.java
@@ -29,7 +29,6 @@ import android.hardware.fingerprint.IUdfpsOverlayController;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
-import android.view.Surface;
import com.android.server.biometrics.sensors.AuthenticationClient;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -47,6 +46,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient lazyDaemon, @NonNull IBinder token,
@NonNull ClientMonitorCallbackConverter listener, int targetUserId, long operationId,
boolean restricted, @NonNull String owner, int cookie, boolean requireConfirmation,
- int sensorId, boolean isStrongBiometric, @Nullable Surface surface, int statsClient,
+ int sensorId, boolean isStrongBiometric, int statsClient,
@NonNull TaskStackListener taskStackListener,
@NonNull LockoutFrameworkImpl lockoutTracker,
- @Nullable IUdfpsOverlayController udfpsOverlayController) {
+ @Nullable IUdfpsOverlayController udfpsOverlayController, boolean isKeyguard) {
super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
owner, cookie, requireConfirmation, sensorId, isStrongBiometric,
BiometricsProtoEnums.MODALITY_FINGERPRINT, statsClient, taskStackListener,
lockoutTracker);
mLockoutFrameworkImpl = lockoutTracker;
mUdfpsOverlayController = udfpsOverlayController;
+ mIsKeyguard = isKeyguard;
}
@Override
@@ -145,4 +146,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient