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