| /* |
| * 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.systemui.biometrics; |
| |
| import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD; |
| import static android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD; |
| |
| import static com.android.internal.util.Preconditions.checkNotNull; |
| import static com.android.systemui.classifier.Classifier.LOCK_ICON; |
| import static com.android.systemui.classifier.Classifier.UDFPS_AUTHENTICATION; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.graphics.Point; |
| import android.hardware.biometrics.BiometricFingerprintConstants; |
| import android.hardware.display.DisplayManager; |
| import android.hardware.fingerprint.FingerprintManager; |
| import android.hardware.fingerprint.IUdfpsOverlayController; |
| import android.hardware.fingerprint.IUdfpsOverlayControllerCallback; |
| import android.os.Handler; |
| import android.os.PowerManager; |
| import android.os.Process; |
| import android.os.Trace; |
| import android.os.VibrationAttributes; |
| import android.os.VibrationEffect; |
| import android.util.Log; |
| import android.util.RotationUtils; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.Surface; |
| import android.view.VelocityTracker; |
| import android.view.WindowManager; |
| import android.view.accessibility.AccessibilityManager; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.LatencyTracker; |
| import com.android.keyguard.KeyguardUpdateMonitor; |
| import com.android.systemui.animation.ActivityLaunchAnimator; |
| import com.android.systemui.biometrics.dagger.BiometricsBackground; |
| import com.android.systemui.dagger.SysUISingleton; |
| import com.android.systemui.dagger.qualifiers.Main; |
| import com.android.systemui.doze.DozeReceiver; |
| import com.android.systemui.dump.DumpManager; |
| import com.android.systemui.keyguard.ScreenLifecycle; |
| import com.android.systemui.plugins.FalsingManager; |
| import com.android.systemui.plugins.statusbar.StatusBarStateController; |
| import com.android.systemui.statusbar.LockscreenShadeTransitionController; |
| import com.android.systemui.statusbar.VibratorHelper; |
| import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; |
| import com.android.systemui.statusbar.phone.SystemUIDialogManager; |
| import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; |
| import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; |
| import com.android.systemui.statusbar.policy.ConfigurationController; |
| import com.android.systemui.statusbar.policy.KeyguardStateController; |
| import com.android.systemui.util.concurrency.DelayableExecutor; |
| import com.android.systemui.util.concurrency.Execution; |
| import com.android.systemui.util.time.SystemClock; |
| |
| import java.util.HashSet; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.Executor; |
| |
| import javax.inject.Inject; |
| |
| import kotlin.Unit; |
| |
| /** |
| * Shows and hides the under-display fingerprint sensor (UDFPS) overlay, handles UDFPS touch events, |
| * and coordinates triggering of the high-brightness mode (HBM). |
| * |
| * Note that the current architecture is designed so that a single {@link UdfpsController} |
| * controls/manages all UDFPS sensors. In other words, a single controller is registered with |
| * {@link com.android.server.biometrics.sensors.fingerprint.FingerprintService}, and interfaces such |
| * as {@link FingerprintManager#onPointerDown(long, int, int, int, float, float)} or |
| * {@link IUdfpsOverlayController#showUdfpsOverlay} should all have |
| * {@code sensorId} parameters. |
| */ |
| @SuppressWarnings("deprecation") |
| @SysUISingleton |
| public class UdfpsController implements DozeReceiver { |
| private static final String TAG = "UdfpsController"; |
| private static final long AOD_INTERRUPT_TIMEOUT_MILLIS = 1000; |
| |
| // Minimum required delay between consecutive touch logs in milliseconds. |
| private static final long MIN_TOUCH_LOG_INTERVAL = 50; |
| |
| private final Context mContext; |
| private final Execution mExecution; |
| private final FingerprintManager mFingerprintManager; |
| @NonNull private final LayoutInflater mInflater; |
| private final WindowManager mWindowManager; |
| private final DelayableExecutor mFgExecutor; |
| @NonNull private final Executor mBiometricExecutor; |
| @NonNull private final PanelExpansionStateManager mPanelExpansionStateManager; |
| @NonNull private final StatusBarStateController mStatusBarStateController; |
| @NonNull private final KeyguardStateController mKeyguardStateController; |
| @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager; |
| @NonNull private final DumpManager mDumpManager; |
| @NonNull private final SystemUIDialogManager mDialogManager; |
| @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; |
| @NonNull private final VibratorHelper mVibrator; |
| @NonNull private final FalsingManager mFalsingManager; |
| @NonNull private final PowerManager mPowerManager; |
| @NonNull private final AccessibilityManager mAccessibilityManager; |
| @NonNull private final LockscreenShadeTransitionController mLockscreenShadeTransitionController; |
| @Nullable private final UdfpsHbmProvider mHbmProvider; |
| @NonNull private final ConfigurationController mConfigurationController; |
| @NonNull private final SystemClock mSystemClock; |
| @NonNull private final UnlockedScreenOffAnimationController |
| mUnlockedScreenOffAnimationController; |
| @NonNull private final LatencyTracker mLatencyTracker; |
| @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener; |
| @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator; |
| |
| // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple |
| // sensors, this, in addition to a lot of the code here, will be updated. |
| @VisibleForTesting int mSensorId; |
| private boolean mHalControlsIllumination; |
| @VisibleForTesting @NonNull UdfpsOverlayParams mOverlayParams = new UdfpsOverlayParams(); |
| // TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this. |
| @Nullable private Runnable mAuthControllerUpdateUdfpsLocation; |
| @Nullable private final AlternateUdfpsTouchProvider mAlternateTouchProvider; |
| |
| // Tracks the velocity of a touch to help filter out the touches that move too fast. |
| @Nullable private VelocityTracker mVelocityTracker; |
| // The ID of the pointer for which ACTION_DOWN has occurred. -1 means no pointer is active. |
| private int mActivePointerId = -1; |
| // The timestamp of the most recent touch log. |
| private long mTouchLogTime; |
| // Sensor has a capture (good or bad) for this touch. Do not need to illuminate for this |
| // particular touch event anymore. In other words, do not illuminate until user lifts and |
| // touches the sensor area again. |
| // TODO: We should probably try to make touch/illumination things more of a FSM |
| private boolean mAcquiredReceived; |
| |
| // The current request from FingerprintService. Null if no current request. |
| @Nullable UdfpsControllerOverlay mOverlay; |
| |
| // The fingerprint AOD trigger doesn't provide an ACTION_UP/ACTION_CANCEL event to tell us when |
| // to turn off high brightness mode. To get around this limitation, the state of the AOD |
| // interrupt is being tracked and a timeout is used as a last resort to turn off high brightness |
| // mode. |
| private boolean mIsAodInterruptActive; |
| @Nullable private Runnable mCancelAodTimeoutAction; |
| private boolean mScreenOn; |
| private Runnable mAodInterruptRunnable; |
| private boolean mOnFingerDown; |
| private boolean mAttemptedToDismissKeyguard; |
| private final Set<Callback> mCallbacks = new HashSet<>(); |
| |
| @VisibleForTesting |
| public static final VibrationAttributes UDFPS_VIBRATION_ATTRIBUTES = |
| new VibrationAttributes.Builder() |
| // vibration will bypass battery saver mode: |
| .setUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST) |
| .build(); |
| @VisibleForTesting |
| public static final VibrationAttributes LOCK_ICON_VIBRATION_ATTRIBUTES = |
| new VibrationAttributes.Builder() |
| .setUsage(VibrationAttributes.USAGE_TOUCH) |
| .build(); |
| |
| // haptic to use for successful device entry |
| public static final VibrationEffect EFFECT_CLICK = |
| VibrationEffect.get(VibrationEffect.EFFECT_CLICK); |
| |
| private final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() { |
| @Override |
| public void onScreenTurnedOn() { |
| mScreenOn = true; |
| if (mAodInterruptRunnable != null) { |
| mAodInterruptRunnable.run(); |
| mAodInterruptRunnable = null; |
| } |
| } |
| |
| @Override |
| public void onScreenTurnedOff() { |
| mScreenOn = false; |
| } |
| }; |
| |
| public class UdfpsOverlayController extends IUdfpsOverlayController.Stub { |
| @Override |
| public void showUdfpsOverlay(long requestId, int sensorId, int reason, |
| @NonNull IUdfpsOverlayControllerCallback callback) { |
| mFgExecutor.execute(() -> UdfpsController.this.showUdfpsOverlay( |
| new UdfpsControllerOverlay(mContext, mFingerprintManager, mInflater, |
| mWindowManager, mAccessibilityManager, mStatusBarStateController, |
| mPanelExpansionStateManager, mKeyguardViewManager, |
| mKeyguardUpdateMonitor, mDialogManager, mDumpManager, |
| mLockscreenShadeTransitionController, mConfigurationController, |
| mSystemClock, mKeyguardStateController, |
| mUnlockedScreenOffAnimationController, mHalControlsIllumination, |
| mHbmProvider, requestId, reason, callback, |
| (view, event, fromUdfpsView) -> onTouch(requestId, event, |
| fromUdfpsView), mActivityLaunchAnimator))); |
| } |
| |
| @Override |
| public void hideUdfpsOverlay(int sensorId) { |
| mFgExecutor.execute(() -> { |
| if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) { |
| // if we get here, we expect keyguardUpdateMonitor's fingerprintRunningState |
| // to be updated shortly afterwards |
| Log.d(TAG, "hiding udfps overlay when " |
| + "mKeyguardUpdateMonitor.isFingerprintDetectionRunning()=true"); |
| } |
| |
| UdfpsController.this.hideUdfpsOverlay(); |
| }); |
| } |
| |
| @Override |
| public void onAcquired( |
| int sensorId, |
| @BiometricFingerprintConstants.FingerprintAcquired int acquiredInfo |
| ) { |
| if (BiometricFingerprintConstants.shouldTurnOffHbm(acquiredInfo)) { |
| boolean acquiredGood = acquiredInfo == FINGERPRINT_ACQUIRED_GOOD; |
| mFgExecutor.execute(() -> { |
| if (mOverlay == null) { |
| Log.e(TAG, "Null request when onAcquired for sensorId: " + sensorId |
| + " acquiredInfo=" + acquiredInfo); |
| return; |
| } |
| mAcquiredReceived = true; |
| final UdfpsView view = mOverlay.getOverlayView(); |
| if (view != null) { |
| view.stopIllumination(); // turn off HBM |
| } |
| if (acquiredGood) { |
| mOverlay.onAcquiredGood(); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onEnrollmentProgress(int sensorId, int remaining) { |
| mFgExecutor.execute(() -> { |
| if (mOverlay == null) { |
| Log.e(TAG, "onEnrollProgress received but serverRequest is null"); |
| return; |
| } |
| mOverlay.onEnrollmentProgress(remaining); |
| }); |
| } |
| |
| @Override |
| public void onEnrollmentHelp(int sensorId) { |
| mFgExecutor.execute(() -> { |
| if (mOverlay == null) { |
| Log.e(TAG, "onEnrollmentHelp received but serverRequest is null"); |
| return; |
| } |
| mOverlay.onEnrollmentHelp(); |
| }); |
| } |
| |
| @Override |
| public void setDebugMessage(int sensorId, String message) { |
| mFgExecutor.execute(() -> { |
| if (mOverlay == null || mOverlay.isHiding()) { |
| return; |
| } |
| mOverlay.getOverlayView().setDebugMessage(message); |
| }); |
| } |
| } |
| |
| /** |
| * Updates the overlay parameters and reconstructs or redraws the overlay, if necessary. |
| * |
| * @param sensorId sensor for which the overlay is getting updated. |
| * @param overlayParams See {@link UdfpsOverlayParams}. |
| */ |
| public void updateOverlayParams(int sensorId, @NonNull UdfpsOverlayParams overlayParams) { |
| if (sensorId != mSensorId) { |
| mSensorId = sensorId; |
| Log.w(TAG, "updateUdfpsParams | sensorId has changed"); |
| } |
| |
| if (!mOverlayParams.equals(overlayParams)) { |
| mOverlayParams = overlayParams; |
| |
| final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateAuth(); |
| |
| // When the bounds change it's always necessary to re-create the overlay's window with |
| // new LayoutParams. If the overlay needs to be shown, this will re-create and show the |
| // overlay with the updated LayoutParams. Otherwise, the overlay will remain hidden. |
| redrawOverlay(); |
| if (wasShowingAltAuth) { |
| mKeyguardViewManager.showGenericBouncer(true); |
| } |
| } |
| } |
| |
| // TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this. |
| public void setAuthControllerUpdateUdfpsLocation(@Nullable Runnable r) { |
| mAuthControllerUpdateUdfpsLocation = r; |
| } |
| |
| // TODO(b/229290039): UDFPS controller should manage its properties on its own. Remove this. |
| public void setHalControlsIllumination(boolean value) { |
| mHalControlsIllumination = value; |
| } |
| |
| /** |
| * Calculate the pointer speed given a velocity tracker and the pointer id. |
| * This assumes that the velocity tracker has already been passed all relevant motion events. |
| */ |
| public static float computePointerSpeed(@NonNull VelocityTracker tracker, int pointerId) { |
| final float vx = tracker.getXVelocity(pointerId); |
| final float vy = tracker.getYVelocity(pointerId); |
| return (float) Math.sqrt(Math.pow(vx, 2.0) + Math.pow(vy, 2.0)); |
| } |
| |
| /** |
| * Whether the velocity exceeds the acceptable UDFPS debouncing threshold. |
| */ |
| public static boolean exceedsVelocityThreshold(float velocity) { |
| return velocity > 750f; |
| } |
| |
| private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (mOverlay != null |
| && mOverlay.getRequestReason() != REASON_AUTH_KEYGUARD |
| && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { |
| Log.d(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, mRequestReason: " |
| + mOverlay.getRequestReason()); |
| mOverlay.cancel(); |
| hideUdfpsOverlay(); |
| } |
| } |
| }; |
| |
| /** |
| * Forwards touches to the udfps controller / view |
| */ |
| public boolean onTouch(MotionEvent event) { |
| if (mOverlay == null || mOverlay.isHiding()) { |
| return false; |
| } |
| // TODO(b/225068271): may not be correct but no way to get the id yet |
| return onTouch(mOverlay.getRequestId(), event, false); |
| } |
| |
| /** |
| * @param x coordinate |
| * @param y coordinate |
| * @param relativeToUdfpsView true if the coordinates are relative to the udfps view; else, |
| * calculate from the display dimensions in portrait orientation |
| */ |
| private boolean isWithinSensorArea(UdfpsView udfpsView, float x, float y, |
| boolean relativeToUdfpsView) { |
| if (relativeToUdfpsView) { |
| // TODO: move isWithinSensorArea to UdfpsController. |
| return udfpsView.isWithinSensorArea(x, y); |
| } |
| |
| if (mOverlay == null || mOverlay.getAnimationViewController() == null) { |
| return false; |
| } |
| |
| return !mOverlay.getAnimationViewController().shouldPauseAuth() |
| && mOverlayParams.getSensorBounds().contains((int) x, (int) y); |
| } |
| |
| private Point getTouchInNativeCoordinates(@NonNull MotionEvent event, int idx) { |
| Point portraitTouch = new Point( |
| (int) event.getRawX(idx), |
| (int) event.getRawY(idx) |
| ); |
| final int rot = mOverlayParams.getRotation(); |
| if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { |
| RotationUtils.rotatePoint(portraitTouch, |
| RotationUtils.deltaRotation(rot, Surface.ROTATION_0), |
| mOverlayParams.getLogicalDisplayWidth(), |
| mOverlayParams.getLogicalDisplayHeight() |
| ); |
| } |
| |
| // Scale the coordinates to native resolution. |
| final float scale = mOverlayParams.getScaleFactor(); |
| portraitTouch.x = (int) (portraitTouch.x / scale); |
| portraitTouch.y = (int) (portraitTouch.y / scale); |
| return portraitTouch; |
| } |
| |
| @VisibleForTesting |
| boolean onTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) { |
| if (mOverlay == null) { |
| Log.w(TAG, "ignoring onTouch with null overlay"); |
| return false; |
| } |
| if (!mOverlay.matchesRequestId(requestId)) { |
| Log.w(TAG, "ignoring stale touch event: " + requestId + " current: " |
| + mOverlay.getRequestId()); |
| return false; |
| } |
| |
| final UdfpsView udfpsView = mOverlay.getOverlayView(); |
| final boolean isIlluminationRequested = udfpsView.isIlluminationRequested(); |
| boolean handled = false; |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_OUTSIDE: |
| udfpsView.onTouchOutsideView(); |
| return true; |
| case MotionEvent.ACTION_DOWN: |
| case MotionEvent.ACTION_HOVER_ENTER: |
| Trace.beginSection("UdfpsController.onTouch.ACTION_DOWN"); |
| // To simplify the lifecycle of the velocity tracker, make sure it's never null |
| // after ACTION_DOWN, and always null after ACTION_CANCEL or ACTION_UP. |
| if (mVelocityTracker == null) { |
| mVelocityTracker = VelocityTracker.obtain(); |
| } else { |
| // ACTION_UP or ACTION_CANCEL is not guaranteed to be called before a new |
| // ACTION_DOWN, in that case we should just reuse the old instance. |
| mVelocityTracker.clear(); |
| } |
| |
| boolean withinSensorArea = |
| isWithinSensorArea(udfpsView, event.getX(), event.getY(), fromUdfpsView); |
| if (withinSensorArea) { |
| Trace.beginAsyncSection("UdfpsController.e2e.onPointerDown", 0); |
| Log.v(TAG, "onTouch | action down"); |
| // The pointer that causes ACTION_DOWN is always at index 0. |
| // We need to persist its ID to track it during ACTION_MOVE that could include |
| // data for many other pointers because of multi-touch support. |
| mActivePointerId = event.getPointerId(0); |
| mVelocityTracker.addMovement(event); |
| handled = true; |
| mAcquiredReceived = false; |
| } |
| if ((withinSensorArea || fromUdfpsView) && shouldTryToDismissKeyguard()) { |
| Log.v(TAG, "onTouch | dismiss keyguard ACTION_DOWN"); |
| if (!mOnFingerDown) { |
| playStartHaptic(); |
| } |
| mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); |
| mAttemptedToDismissKeyguard = true; |
| } |
| |
| Trace.endSection(); |
| break; |
| |
| case MotionEvent.ACTION_MOVE: |
| case MotionEvent.ACTION_HOVER_MOVE: |
| Trace.beginSection("UdfpsController.onTouch.ACTION_MOVE"); |
| final int idx = mActivePointerId == -1 |
| ? event.getPointerId(0) |
| : event.findPointerIndex(mActivePointerId); |
| if (idx == event.getActionIndex()) { |
| boolean actionMoveWithinSensorArea = |
| isWithinSensorArea(udfpsView, event.getX(idx), event.getY(idx), |
| fromUdfpsView); |
| if ((fromUdfpsView || actionMoveWithinSensorArea) |
| && shouldTryToDismissKeyguard()) { |
| Log.v(TAG, "onTouch | dismiss keyguard ACTION_MOVE"); |
| if (!mOnFingerDown) { |
| playStartHaptic(); |
| } |
| mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); |
| mAttemptedToDismissKeyguard = true; |
| break; |
| } |
| // Map the touch to portrait mode if the device is in landscape mode. |
| final Point scaledTouch = getTouchInNativeCoordinates(event, idx); |
| if (actionMoveWithinSensorArea) { |
| if (mVelocityTracker == null) { |
| // touches could be injected, so the velocity tracker may not have |
| // been initialized (via ACTION_DOWN). |
| mVelocityTracker = VelocityTracker.obtain(); |
| } |
| mVelocityTracker.addMovement(event); |
| // Compute pointer velocity in pixels per second. |
| mVelocityTracker.computeCurrentVelocity(1000); |
| // Compute pointer speed from X and Y velocities. |
| final float v = computePointerSpeed(mVelocityTracker, mActivePointerId); |
| final float minor = event.getTouchMinor(idx); |
| final float major = event.getTouchMajor(idx); |
| final boolean exceedsVelocityThreshold = exceedsVelocityThreshold(v); |
| final String touchInfo = String.format( |
| "minor: %.1f, major: %.1f, v: %.1f, exceedsVelocityThreshold: %b", |
| minor, major, v, exceedsVelocityThreshold); |
| final long sinceLastLog = mSystemClock.elapsedRealtime() - mTouchLogTime; |
| if (!isIlluminationRequested && !mAcquiredReceived |
| && !exceedsVelocityThreshold) { |
| |
| final float scale = mOverlayParams.getScaleFactor(); |
| float scaledMinor = minor / scale; |
| float scaledMajor = major / scale; |
| |
| onFingerDown(requestId, scaledTouch.x, scaledTouch.y, scaledMinor, |
| scaledMajor); |
| Log.v(TAG, "onTouch | finger down: " + touchInfo); |
| mTouchLogTime = mSystemClock.elapsedRealtime(); |
| mPowerManager.userActivity(mSystemClock.uptimeMillis(), |
| PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0); |
| handled = true; |
| } else if (sinceLastLog >= MIN_TOUCH_LOG_INTERVAL) { |
| Log.v(TAG, "onTouch | finger move: " + touchInfo); |
| mTouchLogTime = mSystemClock.elapsedRealtime(); |
| } |
| } else { |
| Log.v(TAG, "onTouch | finger outside"); |
| onFingerUp(requestId, udfpsView); |
| // Maybe announce for accessibility. |
| mFgExecutor.execute(() -> { |
| if (mOverlay == null) { |
| Log.e(TAG, "touch outside sensor area received" |
| + "but serverRequest is null"); |
| return; |
| } |
| // Scale the coordinates to native resolution. |
| final float scale = mOverlayParams.getScaleFactor(); |
| final float scaledSensorX = |
| mOverlayParams.getSensorBounds().centerX() / scale; |
| final float scaledSensorY = |
| mOverlayParams.getSensorBounds().centerY() / scale; |
| |
| mOverlay.onTouchOutsideOfSensorArea( |
| scaledTouch.x, scaledTouch.y, scaledSensorX, scaledSensorY, |
| mOverlayParams.getRotation()); |
| }); |
| } |
| } |
| Trace.endSection(); |
| break; |
| |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_HOVER_EXIT: |
| Trace.beginSection("UdfpsController.onTouch.ACTION_UP"); |
| mActivePointerId = -1; |
| if (mVelocityTracker != null) { |
| mVelocityTracker.recycle(); |
| mVelocityTracker = null; |
| } |
| Log.v(TAG, "onTouch | finger up"); |
| mAttemptedToDismissKeyguard = false; |
| onFingerUp(requestId, udfpsView); |
| mFalsingManager.isFalseTouch(UDFPS_AUTHENTICATION); |
| Trace.endSection(); |
| break; |
| |
| default: |
| // Do nothing. |
| } |
| return handled; |
| } |
| |
| private boolean shouldTryToDismissKeyguard() { |
| return mOverlay != null |
| && mOverlay.getAnimationViewController() instanceof UdfpsKeyguardViewController |
| && mKeyguardStateController.canDismissLockScreen() |
| && !mAttemptedToDismissKeyguard; |
| } |
| |
| @Inject |
| public UdfpsController(@NonNull Context context, |
| @NonNull Execution execution, |
| @NonNull LayoutInflater inflater, |
| @Nullable FingerprintManager fingerprintManager, |
| @NonNull WindowManager windowManager, |
| @NonNull StatusBarStateController statusBarStateController, |
| @Main DelayableExecutor fgExecutor, |
| @NonNull PanelExpansionStateManager panelExpansionStateManager, |
| @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager, |
| @NonNull DumpManager dumpManager, |
| @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, |
| @NonNull FalsingManager falsingManager, |
| @NonNull PowerManager powerManager, |
| @NonNull AccessibilityManager accessibilityManager, |
| @NonNull LockscreenShadeTransitionController lockscreenShadeTransitionController, |
| @NonNull ScreenLifecycle screenLifecycle, |
| @NonNull VibratorHelper vibrator, |
| @NonNull UdfpsHapticsSimulator udfpsHapticsSimulator, |
| @NonNull UdfpsShell udfpsShell, |
| @NonNull Optional<UdfpsHbmProvider> hbmProvider, |
| @NonNull KeyguardStateController keyguardStateController, |
| @NonNull DisplayManager displayManager, |
| @Main Handler mainHandler, |
| @NonNull ConfigurationController configurationController, |
| @NonNull SystemClock systemClock, |
| @NonNull UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, |
| @NonNull SystemUIDialogManager dialogManager, |
| @NonNull LatencyTracker latencyTracker, |
| @NonNull ActivityLaunchAnimator activityLaunchAnimator, |
| @NonNull Optional<AlternateUdfpsTouchProvider> aternateTouchProvider, |
| @BiometricsBackground Executor biometricsExecutor) { |
| mContext = context; |
| mExecution = execution; |
| mVibrator = vibrator; |
| mInflater = inflater; |
| // The fingerprint manager is queried for UDFPS before this class is constructed, so the |
| // fingerprint manager should never be null. |
| mFingerprintManager = checkNotNull(fingerprintManager); |
| mWindowManager = windowManager; |
| mFgExecutor = fgExecutor; |
| mPanelExpansionStateManager = panelExpansionStateManager; |
| mStatusBarStateController = statusBarStateController; |
| mKeyguardStateController = keyguardStateController; |
| mKeyguardViewManager = statusBarKeyguardViewManager; |
| mDumpManager = dumpManager; |
| mDialogManager = dialogManager; |
| mKeyguardUpdateMonitor = keyguardUpdateMonitor; |
| mFalsingManager = falsingManager; |
| mPowerManager = powerManager; |
| mAccessibilityManager = accessibilityManager; |
| mLockscreenShadeTransitionController = lockscreenShadeTransitionController; |
| mHbmProvider = hbmProvider.orElse(null); |
| screenLifecycle.addObserver(mScreenObserver); |
| mScreenOn = screenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_ON; |
| mConfigurationController = configurationController; |
| mSystemClock = systemClock; |
| mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; |
| mLatencyTracker = latencyTracker; |
| mActivityLaunchAnimator = activityLaunchAnimator; |
| mAlternateTouchProvider = aternateTouchProvider.orElse(null); |
| mBiometricExecutor = biometricsExecutor; |
| |
| mOrientationListener = new BiometricDisplayListener( |
| context, |
| displayManager, |
| mainHandler, |
| BiometricDisplayListener.SensorType.UnderDisplayFingerprint.INSTANCE, |
| () -> { |
| if (mAuthControllerUpdateUdfpsLocation != null) { |
| mAuthControllerUpdateUdfpsLocation.run(); |
| } |
| return Unit.INSTANCE; |
| }); |
| |
| final UdfpsOverlayController mUdfpsOverlayController = new UdfpsOverlayController(); |
| mFingerprintManager.setUdfpsOverlayController(mUdfpsOverlayController); |
| |
| final IntentFilter filter = new IntentFilter(); |
| filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); |
| context.registerReceiver(mBroadcastReceiver, filter, |
| Context.RECEIVER_EXPORTED_UNAUDITED); |
| |
| udfpsHapticsSimulator.setUdfpsController(this); |
| udfpsShell.setUdfpsOverlayController(mUdfpsOverlayController); |
| } |
| |
| /** |
| * If a11y touchExplorationEnabled, play haptic to signal UDFPS scanning started. |
| */ |
| @VisibleForTesting |
| public void playStartHaptic() { |
| if (mAccessibilityManager.isTouchExplorationEnabled()) { |
| mVibrator.vibrate( |
| Process.myUid(), |
| mContext.getOpPackageName(), |
| EFFECT_CLICK, |
| "udfps-onStart-click", |
| UDFPS_VIBRATION_ATTRIBUTES); |
| } |
| } |
| |
| @Override |
| public void dozeTimeTick() { |
| if (mOverlay != null) { |
| final UdfpsView view = mOverlay.getOverlayView(); |
| if (view != null) { |
| view.dozeTimeTick(); |
| } |
| } |
| } |
| |
| private void redrawOverlay() { |
| UdfpsControllerOverlay overlay = mOverlay; |
| if (overlay != null) { |
| hideUdfpsOverlay(); |
| showUdfpsOverlay(overlay); |
| } |
| } |
| |
| private void showUdfpsOverlay(@NonNull UdfpsControllerOverlay overlay) { |
| mExecution.assertIsMainThread(); |
| |
| mOverlay = overlay; |
| final int requestReason = overlay.getRequestReason(); |
| if (requestReason == REASON_AUTH_KEYGUARD |
| && !mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) { |
| Log.d(TAG, "Attempting to showUdfpsOverlay when fingerprint detection" |
| + " isn't running on keyguard. Skip show."); |
| return; |
| } |
| if (overlay.show(this, mOverlayParams)) { |
| Log.v(TAG, "showUdfpsOverlay | adding window reason=" + requestReason); |
| mOnFingerDown = false; |
| mAttemptedToDismissKeyguard = false; |
| mOrientationListener.enable(); |
| } else { |
| Log.v(TAG, "showUdfpsOverlay | the overlay is already showing"); |
| } |
| } |
| |
| private void hideUdfpsOverlay() { |
| mExecution.assertIsMainThread(); |
| |
| if (mOverlay != null) { |
| // Reset the controller back to its starting state. |
| final UdfpsView oldView = mOverlay.getOverlayView(); |
| if (oldView != null) { |
| onFingerUp(mOverlay.getRequestId(), oldView); |
| } |
| final boolean removed = mOverlay.hide(); |
| if (mKeyguardViewManager.isShowingAlternateAuth()) { |
| mKeyguardViewManager.resetAlternateAuth(true); |
| } |
| Log.v(TAG, "hideUdfpsOverlay | removing window: " + removed); |
| } else { |
| Log.v(TAG, "hideUdfpsOverlay | the overlay is already hidden"); |
| } |
| |
| mOverlay = null; |
| mOrientationListener.disable(); |
| } |
| |
| /** |
| * Request fingerprint scan. |
| * |
| * This is intended to be called in response to a sensor that triggers an AOD interrupt for the |
| * fingerprint sensor. |
| */ |
| void onAodInterrupt(int screenX, int screenY, float major, float minor) { |
| if (mIsAodInterruptActive) { |
| return; |
| } |
| |
| if (!mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) { |
| if (mFalsingManager.isFalseTouch(LOCK_ICON)) { |
| Log.v(TAG, "aod lock icon long-press rejected by the falsing manager."); |
| return; |
| } |
| mKeyguardViewManager.showBouncer(true); |
| |
| // play the same haptic as the LockIconViewController longpress |
| mVibrator.vibrate( |
| Process.myUid(), |
| mContext.getOpPackageName(), |
| UdfpsController.EFFECT_CLICK, |
| "aod-lock-icon-longpress", |
| LOCK_ICON_VIBRATION_ATTRIBUTES); |
| return; |
| } |
| |
| // TODO(b/225068271): this may not be correct but there isn't a way to track it |
| final long requestId = mOverlay != null ? mOverlay.getRequestId() : -1; |
| mAodInterruptRunnable = () -> { |
| mIsAodInterruptActive = true; |
| // Since the sensor that triggers the AOD interrupt doesn't provide |
| // ACTION_UP/ACTION_CANCEL, we need to be careful about not letting the screen |
| // accidentally remain in high brightness mode. As a mitigation, queue a call to |
| // cancel the fingerprint scan. |
| mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelUdfps, |
| AOD_INTERRUPT_TIMEOUT_MILLIS); |
| // using a hard-coded value for major and minor until it is available from the sensor |
| onFingerDown(requestId, screenX, screenY, minor, major); |
| }; |
| |
| if (mScreenOn) { |
| mAodInterruptRunnable.run(); |
| mAodInterruptRunnable = null; |
| } |
| } |
| |
| /** |
| * Add a callback for fingerUp and fingerDown events |
| */ |
| public void addCallback(Callback cb) { |
| mCallbacks.add(cb); |
| } |
| |
| /** |
| * Remove callback |
| */ |
| public void removeCallback(Callback cb) { |
| mCallbacks.remove(cb); |
| } |
| |
| /** |
| * Cancel updfs scan affordances - ability to hide the HbmSurfaceView (white circle) before |
| * user explicitly lifts their finger. Generally, this should be called whenever udfps fails |
| * or errors. |
| * |
| * The sensor that triggers an AOD fingerprint interrupt (see onAodInterrupt) doesn't give |
| * ACTION_UP/ACTION_CANCEL events, so and AOD interrupt scan needs to be cancelled manually. |
| * This should be called when authentication either succeeds or fails. Failing to cancel the |
| * scan will leave the screen in high brightness mode and will show the HbmSurfaceView until |
| * the user lifts their finger. |
| */ |
| void onCancelUdfps() { |
| if (mOverlay != null && mOverlay.getOverlayView() != null) { |
| onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView()); |
| } |
| if (!mIsAodInterruptActive) { |
| return; |
| } |
| if (mCancelAodTimeoutAction != null) { |
| mCancelAodTimeoutAction.run(); |
| mCancelAodTimeoutAction = null; |
| } |
| mIsAodInterruptActive = false; |
| } |
| |
| public boolean isFingerDown() { |
| return mOnFingerDown; |
| } |
| |
| private void onFingerDown(long requestId, int x, int y, float minor, float major) { |
| mExecution.assertIsMainThread(); |
| |
| if (mOverlay == null) { |
| Log.w(TAG, "Null request in onFingerDown"); |
| return; |
| } |
| if (!mOverlay.matchesRequestId(requestId)) { |
| Log.w(TAG, "Mismatched fingerDown: " + requestId |
| + " current: " + mOverlay.getRequestId()); |
| return; |
| } |
| mLatencyTracker.onActionStart(LatencyTracker.ACTION_UDFPS_ILLUMINATE); |
| |
| if (!mOnFingerDown) { |
| playStartHaptic(); |
| |
| if (!mKeyguardUpdateMonitor.isFaceDetectionRunning()) { |
| mKeyguardUpdateMonitor.requestFaceAuth(/* userInitiatedRequest */ false); |
| } |
| } |
| mOnFingerDown = true; |
| if (mAlternateTouchProvider != null) { |
| mBiometricExecutor.execute(() -> { |
| mAlternateTouchProvider.onPointerDown(requestId, x, y, minor, major); |
| }); |
| mFgExecutor.execute(() -> { |
| if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) { |
| mKeyguardUpdateMonitor.onUdfpsPointerDown((int) requestId); |
| } |
| }); |
| } else { |
| mFingerprintManager.onPointerDown(requestId, mSensorId, x, y, minor, major); |
| } |
| Trace.endAsyncSection("UdfpsController.e2e.onPointerDown", 0); |
| final UdfpsView view = mOverlay.getOverlayView(); |
| if (view != null) { |
| view.startIllumination(() -> { |
| if (mAlternateTouchProvider != null) { |
| mBiometricExecutor.execute(() -> { |
| mAlternateTouchProvider.onUiReady(); |
| mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE); |
| }); |
| } else { |
| mFingerprintManager.onUiReady(requestId, mSensorId); |
| mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE); |
| } |
| }); |
| } |
| |
| for (Callback cb : mCallbacks) { |
| cb.onFingerDown(); |
| } |
| } |
| |
| private void onFingerUp(long requestId, @NonNull UdfpsView view) { |
| mExecution.assertIsMainThread(); |
| mActivePointerId = -1; |
| mAcquiredReceived = false; |
| if (mOnFingerDown) { |
| if (mAlternateTouchProvider != null) { |
| mBiometricExecutor.execute(() -> { |
| mAlternateTouchProvider.onPointerUp(requestId); |
| }); |
| mFgExecutor.execute(() -> { |
| if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) { |
| mKeyguardUpdateMonitor.onUdfpsPointerUp((int) requestId); |
| } |
| }); |
| } else { |
| mFingerprintManager.onPointerUp(requestId, mSensorId); |
| } |
| for (Callback cb : mCallbacks) { |
| cb.onFingerUp(); |
| } |
| } |
| mOnFingerDown = false; |
| if (view.isIlluminationRequested()) { |
| view.stopIllumination(); |
| } |
| } |
| |
| /** |
| * Callback for fingerUp and fingerDown events. |
| */ |
| public interface Callback { |
| /** |
| * Called onFingerUp events. Will only be called if the finger was previously down. |
| */ |
| void onFingerUp(); |
| |
| /** |
| * Called onFingerDown events. |
| */ |
| void onFingerDown(); |
| } |
| } |