2/n: Refactor out common BiometricService code

Bug: 109900227

Test: fingerprint enrolls (up to 5)
Test: fingerprint authenticates in Settings/Keyguard
Test: removing fingerprints one by one works
Test: removing all fingerprints works
Test: enumerate works (update fingerprintutils to not store in framework)
      "extra fingerprint in hw" situation
Test: enumerate works (update fingerprintutils to store extra in framework)
      "extra fingerprint in framework" situation
Test: launch FP settings, lock screen, auth twice, repeat many times
Test: multi-user - fp for one user does not work for another
Test: multi-user - fp for secondary user works
Test: lockout reset works, per-user
Test: adb shell dumpsys fingerprint
Test: test app works
Test: test app gets automatically canceled when losing foreground
Test: test app without fingerprint/biometric permission is not allowed
Test: manually inspected that each incoming binder call does the heavy
      work on a handler
Test: log tags are per specific biometric for easier log tracking

Change-Id: Id9812e290e6a8098f73cd9902eaca43c02685990
diff --git a/services/core/java/com/android/server/biometrics/common/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/common/AuthenticationClient.java
new file mode 100644
index 0000000..1ed2847
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/common/AuthenticationClient.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2018 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.common;
+
+import android.content.Context;
+import android.hardware.biometrics.BiometricConstants;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.IBiometricPromptReceiver;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.statusbar.IStatusBarService;
+
+/**
+ * A class to keep track of the authentication state for a given client.
+ */
+public abstract class AuthenticationClient extends ClientMonitor {
+    private long mOpId;
+
+    public abstract int handleFailedAttempt();
+    public abstract void resetFailedAttempts();
+
+    public static final int LOCKOUT_NONE = 0;
+    public static final int LOCKOUT_TIMED = 1;
+    public static final int LOCKOUT_PERMANENT = 2;
+
+    // Callback mechanism received from the client
+    // (BiometricPrompt -> FingerprintManager -> FingerprintService -> AuthenticationClient)
+    private IBiometricPromptReceiver mDialogReceiverFromClient;
+    private Bundle mBundle;
+    private IStatusBarService mStatusBarService;
+    private boolean mInLockout;
+    // TODO: BiometricManager, after other biometric modalities are introduced.
+    private final FingerprintManager mFingerprintManager;
+    protected boolean mDialogDismissed;
+
+    // Receives events from SystemUI and handles them before forwarding them to FingerprintDialog
+    protected IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() {
+        @Override // binder call
+        public void onDialogDismissed(int reason) {
+            if (mBundle != null && mDialogReceiverFromClient != null) {
+                try {
+                    mDialogReceiverFromClient.onDialogDismissed(reason);
+                    if (reason == BiometricPrompt.DISMISSED_REASON_USER_CANCEL) {
+                        onError(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED,
+                                0 /* vendorCode */);
+                    }
+                    mDialogDismissed = true;
+                } catch (RemoteException e) {
+                    Slog.e(getLogTag(), "Unable to notify dialog dismissed", e);
+                }
+                stop(true /* initiatedByClient */);
+            }
+        }
+    };
+
+    /**
+     * This method is called when authentication starts.
+     */
+    public abstract void onStart();
+
+    /**
+     * This method is called when a fingerprint is authenticated or authentication is stopped
+     * (cancelled by the user, or an error such as lockout has occurred).
+     */
+    public abstract void onStop();
+
+    public AuthenticationClient(Context context, Metrics metrics,
+            BiometricService.DaemonWrapper daemon, long halDeviceId, IBinder token,
+            BiometricService.ServiceListener listener, int targetUserId, int groupId, long opId,
+            boolean restricted, String owner, Bundle bundle,
+            IBiometricPromptReceiver dialogReceiver, IStatusBarService statusBarService) {
+        super(context, metrics, daemon, halDeviceId, token, listener, targetUserId, groupId,
+                restricted, owner);
+        mOpId = opId;
+        mBundle = bundle;
+        mDialogReceiverFromClient = dialogReceiver;
+        mStatusBarService = statusBarService;
+        mFingerprintManager = (FingerprintManager) getContext()
+                .getSystemService(Context.FINGERPRINT_SERVICE);
+    }
+
+    @Override
+    public void binderDied() {
+        super.binderDied();
+        // When the binder dies, we should stop the client. This probably belongs in
+        // ClientMonitor's binderDied(), but testing all the cases would be tricky.
+        // AuthenticationClient is the most user-visible case.
+        stop(false /* initiatedByClient */);
+    }
+
+    @Override
+    public boolean onAcquired(int acquiredInfo, int vendorCode) {
+        // If the dialog is showing, the client doesn't need to receive onAcquired messages.
+        if (mBundle != null) {
+            try {
+                if (acquiredInfo != BiometricConstants.BIOMETRIC_ACQUIRED_GOOD) {
+                    mStatusBarService.onFingerprintHelp(
+                            mFingerprintManager.getAcquiredString(acquiredInfo, vendorCode));
+                }
+                return false; // acquisition continues
+            } catch (RemoteException e) {
+                Slog.e(getLogTag(), "Remote exception when sending acquired message", e);
+                return true; // client failed
+            } finally {
+                // Good scans will keep the device awake
+                if (acquiredInfo == BiometricConstants.BIOMETRIC_ACQUIRED_GOOD) {
+                    notifyUserActivity();
+                }
+            }
+        } else {
+            return super.onAcquired(acquiredInfo, vendorCode);
+        }
+    }
+
+    @Override
+    public boolean onError(int error, int vendorCode) {
+        if (mDialogDismissed) {
+            // If user cancels authentication, the application has already received the
+            // FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED message from onDialogDismissed()
+            // and stopped the fingerprint hardware, so there is no need to send a
+            // FingerprintManager.FINGERPRINT_ERROR_CANCELED message.
+            return true;
+        }
+        if (mBundle != null) {
+            try {
+                mStatusBarService.onFingerprintError(
+                        mFingerprintManager.getErrorString(error, vendorCode));
+            } catch (RemoteException e) {
+                Slog.e(getLogTag(), "Remote exception when sending error", e);
+            }
+        }
+        return super.onError(error, vendorCode);
+    }
+
+    @Override
+    public boolean onAuthenticated(int fingerId, int groupId) {
+        boolean result = false;
+        boolean authenticated = fingerId != 0;
+
+        // If the fingerprint dialog is showing, notify authentication succeeded
+        if (mBundle != null) {
+            try {
+                if (authenticated) {
+                    mStatusBarService.onFingerprintAuthenticated();
+                } else {
+                    mStatusBarService.onFingerprintHelp(getContext().getResources().getString(
+                            com.android.internal.R.string.fingerprint_not_recognized));
+                }
+            } catch (RemoteException e) {
+                Slog.e(getLogTag(), "Failed to notify Authenticated:", e);
+            }
+        }
+
+        final BiometricService.ServiceListener listener = getListener();
+        if (listener != null) {
+            try {
+                mMetricsLogger.action(mMetrics.actionBiometricAuth(), authenticated);
+                if (!authenticated) {
+                    listener.onAuthenticationFailed(getHalDeviceId());
+                } else {
+                    if (DEBUG) {
+                        Slog.v(getLogTag(), "onAuthenticated(owner=" + getOwnerString()
+                                + ", id=" + fingerId + ", gp=" + groupId + ")");
+                    }
+                    Fingerprint fp = !getIsRestricted()
+                            ? new Fingerprint("" /* TODO */, groupId, fingerId, getHalDeviceId())
+                            : null;
+                    listener.onAuthenticationSucceeded(getHalDeviceId(), fp, getTargetUserId());
+                }
+            } catch (RemoteException e) {
+                Slog.w(getLogTag(), "Failed to notify Authenticated:", e);
+                result = true; // client failed
+            }
+        } else {
+            result = true; // client not listening
+        }
+        if (!authenticated) {
+            if (listener != null) {
+                vibrateError();
+            }
+            // allow system-defined limit of number of attempts before giving up
+            int lockoutMode =  handleFailedAttempt();
+            if (lockoutMode != LOCKOUT_NONE) {
+                try {
+                    mInLockout = true;
+                    Slog.w(getLogTag(), "Forcing lockout (fp driver code should do this!), mode(" +
+                            lockoutMode + ")");
+                    stop(false);
+                    int errorCode = lockoutMode == LOCKOUT_TIMED ?
+                            BiometricConstants.BIOMETRIC_ERROR_LOCKOUT :
+                            BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
+
+                    // TODO: if the dialog is showing, this error should be delayed. On a similar
+                    // note, AuthenticationClient should override onError and delay all other errors
+                    // as well, if the dialog is showing
+                    listener.onError(getHalDeviceId(), errorCode, 0 /* vendorCode */);
+
+                    // Send the lockout message to the system dialog
+                    if (mBundle != null) {
+                        mStatusBarService.onFingerprintError(
+                                mFingerprintManager.getErrorString(errorCode, 0 /* vendorCode */));
+                    }
+                } catch (RemoteException e) {
+                    Slog.w(getLogTag(), "Failed to notify lockout:", e);
+                }
+            }
+            result |= lockoutMode != LOCKOUT_NONE; // in a lockout mode
+        } else {
+            if (listener != null) {
+                vibrateSuccess();
+            }
+            result |= true; // we have a valid fingerprint, done
+            resetFailedAttempts();
+            onStop();
+        }
+        return result;
+    }
+
+    /**
+     * Start authentication
+     */
+    @Override
+    public int start() {
+        onStart();
+        try {
+            final int result = getDaemonWrapper().authenticate(mOpId, getGroupId());
+            if (result != 0) {
+                Slog.w(getLogTag(), "startAuthentication failed, result=" + result);
+                mMetricsLogger.histogram(mMetrics.tagAuthStartError(), result);
+                onError(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */);
+                return result;
+            }
+            if (DEBUG) Slog.w(getLogTag(), "client " + getOwnerString() + " is authenticating...");
+
+            // If authenticating with system dialog, show the dialog
+            if (mBundle != null) {
+                try {
+                    mStatusBarService.showFingerprintDialog(mBundle, mDialogReceiver);
+                } catch (RemoteException e) {
+                    Slog.e(getLogTag(), "Unable to show fingerprint dialog", e);
+                }
+            }
+        } catch (RemoteException e) {
+            Slog.e(getLogTag(), "startAuthentication failed", e);
+            return ERROR_ESRCH;
+        }
+        return 0; // success
+    }
+
+    @Override
+    public int stop(boolean initiatedByClient) {
+        if (mAlreadyCancelled) {
+            Slog.w(getLogTag(), "stopAuthentication: already cancelled!");
+            return 0;
+        }
+
+        onStop();
+
+        try {
+            final int result = getDaemonWrapper().cancel();
+            if (result != 0) {
+                Slog.w(getLogTag(), "stopAuthentication failed, result=" + result);
+                return result;
+            }
+            if (DEBUG) Slog.w(getLogTag(), "client " + getOwnerString() + " is no longer authenticating");
+        } catch (RemoteException e) {
+            Slog.e(getLogTag(), "stopAuthentication failed", e);
+            return ERROR_ESRCH;
+        } finally {
+            // If the user already cancelled authentication (via some interaction with the
+            // dialog, we do not need to hide it since it's already hidden.
+            // If the device is in lockout, don't hide the dialog - it will automatically hide
+            // after BiometricPrompt.HIDE_DIALOG_DELAY
+            if (mBundle != null && !mDialogDismissed && !mInLockout) {
+                try {
+                    mStatusBarService.hideFingerprintDialog();
+                } catch (RemoteException e) {
+                    Slog.e(getLogTag(), "Unable to hide fingerprint dialog", e);
+                }
+            }
+        }
+
+        mAlreadyCancelled = true;
+        return 0; // success
+    }
+
+    @Override
+    public boolean onEnrollResult(int fingerId, int groupId, int remaining) {
+        if (DEBUG) Slog.w(getLogTag(), "onEnrollResult() called for authenticate!");
+        return true; // Invalid for Authenticate
+    }
+
+    @Override
+    public boolean onRemoved(int fingerId, int groupId, int remaining) {
+        if (DEBUG) Slog.w(getLogTag(), "onRemoved() called for authenticate!");
+        return true; // Invalid for Authenticate
+    }
+
+    @Override
+    public boolean onEnumerationResult(int fingerId, int groupId, int remaining) {
+        if (DEBUG) Slog.w(getLogTag(), "onEnumerationResult() called for authenticate!");
+        return true; // Invalid for Authenticate
+    }
+}