1/n: Update BiometricDialog (SystemUI side) with cleaner lifecycle

1) Clean up BiometricDialogImpl. As a side-effect of 2, which cleans up
   the dialog lifecycle, we no longer need to have a handler here. This
   greatly simplifies the code here.
2) Clean up interface between BiometricDialogImpl and the UI
   (BiometricDialogView).
3) Clean up interface between BimetricService and BiometricDialogImpl.
   SystemUI is now responsible for dismissing the dialog.

Test: atest BiometricDialogImplTest
Bug: 138628043

Change-Id: Ic1fea4c05c27dfc7eb6fc661f517f0380b9fff99
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java
new file mode 100644
index 0000000..ca9d372
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import com.android.systemui.biometrics.ui.BiometricDialogView;
+
+/**
+ * Interface for the biometric dialog UI.
+ */
+public interface BiometricDialog {
+
+    // TODO: Clean up save/restore state
+    String[] KEYS_TO_BACKUP = {
+            BiometricPrompt.KEY_TITLE,
+            BiometricPrompt.KEY_USE_DEFAULT_TITLE,
+            BiometricPrompt.KEY_SUBTITLE,
+            BiometricPrompt.KEY_DESCRIPTION,
+            BiometricPrompt.KEY_POSITIVE_TEXT,
+            BiometricPrompt.KEY_NEGATIVE_TEXT,
+            BiometricPrompt.KEY_REQUIRE_CONFIRMATION,
+            BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL,
+            BiometricPrompt.KEY_FROM_CONFIRM_DEVICE_CREDENTIAL,
+
+            BiometricDialogView.KEY_TRY_AGAIN_VISIBILITY,
+            BiometricDialogView.KEY_CONFIRM_VISIBILITY,
+            BiometricDialogView.KEY_CONFIRM_ENABLED,
+            BiometricDialogView.KEY_STATE,
+            BiometricDialogView.KEY_ERROR_TEXT_VISIBILITY,
+            BiometricDialogView.KEY_ERROR_TEXT_STRING,
+            BiometricDialogView.KEY_ERROR_TEXT_IS_TEMPORARY,
+            BiometricDialogView.KEY_ERROR_TEXT_COLOR,
+    };
+
+    /**
+     * Show the dialog.
+     * @param wm
+     * @param skipIntroAnimation
+     */
+    void show(WindowManager wm, boolean skipIntroAnimation);
+
+    /**
+     * Dismiss the dialog without sending a callback. Only used when the system detects a case
+     * where the error won't come from the UI (e.g. task stack changed).
+     * @param animate
+     */
+    void dismissWithoutCallback(boolean animate);
+
+    /**
+     * Biometric authenticated. May be pending user confirmation, or completed.
+     */
+    void onAuthenticationSucceeded();
+
+    /**
+     * Authentication failed (reject, timeout). Dialog stays showing.
+     * @param failureReason
+     */
+    void onAuthenticationFailed(String failureReason);
+
+    /**
+     * Authentication rejected, or help message received.
+     * @param help
+     */
+    void onHelp(String help);
+
+    /**
+     * Authentication failed. Dialog going away.
+     * @param error
+     */
+    void onError(String error);
+
+    /**
+     * Save the current state.
+     * @param outState
+     */
+    void onSaveState(Bundle outState);
+
+    /**
+     * Restore a previous state.
+     * @param savedState
+     */
+    void restoreState(Bundle savedState);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
index e66a8fa..0eea9ef 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
@@ -19,132 +19,88 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
-import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.WindowManager;
 
 import com.android.internal.os.SomeArgs;
-import com.android.systemui.Dependency;
 import com.android.systemui.SystemUI;
-import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.biometrics.ui.BiometricDialogView;
 import com.android.systemui.statusbar.CommandQueue;
 
 /**
- * Receives messages sent from AuthenticationClient and shows the appropriate biometric UI (e.g.
- * BiometricDialogView).
+ * Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
+ * appropriate biometric UI (e.g. BiometricDialogView).
  */
-public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks {
+public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks,
+        DialogViewCallback {
     private static final String TAG = "BiometricDialogImpl";
     private static final boolean DEBUG = true;
 
-    private static final int MSG_SHOW_DIALOG = 1;
-    private static final int MSG_BIOMETRIC_AUTHENTICATED = 2;
-    private static final int MSG_BIOMETRIC_HELP = 3;
-    private static final int MSG_BIOMETRIC_ERROR = 4;
-    private static final int MSG_HIDE_DIALOG = 5;
-    private static final int MSG_BUTTON_NEGATIVE = 6;
-    private static final int MSG_USER_CANCELED = 7;
-    private static final int MSG_BUTTON_POSITIVE = 8;
-    private static final int MSG_TRY_AGAIN_PRESSED = 9;
-
+    // TODO: These should just be saved from onSaveState
     private SomeArgs mCurrentDialogArgs;
-    private BiometricDialogView mCurrentDialog;
+    private BiometricDialog mCurrentDialog;
+
     private WindowManager mWindowManager;
     private IBiometricServiceReceiverInternal mReceiver;
-    private boolean mDialogShowing;
-    private Callback mCallback = new Callback();
-    private WakefulnessLifecycle mWakefulnessLifecycle;
 
-    private Handler mHandler = new Handler(Looper.getMainLooper()) {
-        @Override
-        public void handleMessage(Message msg) {
-            switch(msg.what) {
-                case MSG_SHOW_DIALOG:
-                    handleShowDialog((SomeArgs) msg.obj, false /* skipAnimation */,
-                            null /* savedState */);
-                    break;
-                case MSG_BIOMETRIC_AUTHENTICATED: {
-                    SomeArgs args = (SomeArgs) msg.obj;
-                    handleBiometricAuthenticated((boolean) args.arg1 /* authenticated */,
-                            (String) args.arg2 /* failureReason */);
-                    args.recycle();
-                    break;
-                }
-                case MSG_BIOMETRIC_HELP: {
-                    SomeArgs args = (SomeArgs) msg.obj;
-                    handleBiometricHelp((String) args.arg1 /* message */);
-                    args.recycle();
-                    break;
-                }
-                case MSG_BIOMETRIC_ERROR:
-                    handleBiometricError((String) msg.obj);
-                    break;
-                case MSG_HIDE_DIALOG:
-                    handleHideDialog((Boolean) msg.obj);
-                    break;
-                case MSG_BUTTON_NEGATIVE:
-                    handleButtonNegative();
-                    break;
-                case MSG_USER_CANCELED:
-                    handleUserCanceled();
-                    break;
-                case MSG_BUTTON_POSITIVE:
-                    handleButtonPositive();
-                    break;
-                case MSG_TRY_AGAIN_PRESSED:
-                    handleTryAgainPressed();
-                    break;
-                default:
-                    Log.w(TAG, "Unknown message: " + msg.what);
-                    break;
-            }
-        }
-    };
-
-    private class Callback implements DialogViewCallback {
-        @Override
-        public void onUserCanceled() {
-            mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget();
-        }
-
-        @Override
-        public void onErrorShown() {
-            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_HIDE_DIALOG,
-                    false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY);
-        }
-
-        @Override
-        public void onNegativePressed() {
-            mHandler.obtainMessage(MSG_BUTTON_NEGATIVE).sendToTarget();
-        }
-
-        @Override
-        public void onPositivePressed() {
-            mHandler.obtainMessage(MSG_BUTTON_POSITIVE).sendToTarget();
-        }
-
-        @Override
-        public void onTryAgainPressed() {
-            mHandler.obtainMessage(MSG_TRY_AGAIN_PRESSED).sendToTarget();
+    @Override
+    public void onTryAgainPressed() {
+        try {
+            mReceiver.onTryAgainPressed();
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException when handling try again", e);
         }
     }
 
-    final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
-        @Override
-        public void onStartedGoingToSleep() {
-            if (mDialogShowing) {
-                if (DEBUG) Log.d(TAG, "User canceled due to screen off");
-                mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget();
-            }
+    @Override
+    public void onDismissed(int reason) {
+        switch (reason) {
+            case DialogViewCallback.DISMISSED_USER_CANCELED:
+                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
+                break;
+
+            case DialogViewCallback.DISMISSED_BUTTON_NEGATIVE:
+                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
+                break;
+
+            case DialogViewCallback.DISMISSED_BUTTON_POSITIVE:
+                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
+                break;
+
+            case DialogViewCallback.DISMISSED_AUTHENTICATED:
+                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
+                // TODO: BiometricService currently sends the result immediately. This should
+                // actually happen when the animation is completed.
+                break;
+
+            case DialogViewCallback.DISMISSED_ERROR:
+                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR);
+                // TODO: Make sure error isn't received until dialog is dismissed
+                // TODO: Similarly, BiometricService currently sends the result immediately.
+                // This should happen when the animation is completed.
+                break;
+            default:
+                Log.e(TAG, "Unhandled reason: " + reason);
+                break;
         }
-    };
+    }
+
+    private void sendResultAndCleanUp(int result) {
+        if (mReceiver == null) {
+            Log.e(TAG, "Receiver is null");
+            return;
+        }
+        try {
+            mReceiver.onDialogDismissed(result);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Remote exception", e);
+        }
+        onDialogDismissed();
+    }
 
     @Override
     public void start() {
@@ -154,8 +110,6 @@
                 || pm.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
             getComponent(CommandQueue.class).addCallback(this);
             mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
-            mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
-            mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
         }
     }
 
@@ -166,18 +120,19 @@
             Log.d(TAG, "showBiometricDialog, type: " + type
                     + ", requireConfirmation: " + requireConfirmation);
         }
-        // Remove these messages as they are part of the previous client
-        mHandler.removeMessages(MSG_BIOMETRIC_ERROR);
-        mHandler.removeMessages(MSG_BIOMETRIC_HELP);
-        mHandler.removeMessages(MSG_BIOMETRIC_AUTHENTICATED);
-        mHandler.removeMessages(MSG_HIDE_DIALOG);
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = bundle;
         args.arg2 = receiver;
         args.argi1 = type;
         args.arg3 = requireConfirmation;
         args.argi2 = userId;
-        mHandler.obtainMessage(MSG_SHOW_DIALOG, args).sendToTarget();
+
+        boolean skipAnimation = false;
+        if (mCurrentDialog != null) {
+            Log.w(TAG, "mCurrentDialog: " + mCurrentDialog);
+            skipAnimation = true;
+        }
+        showDialog(args, skipAnimation /* skipAnimation */, null /* savedState */);
     }
 
     @Override
@@ -185,185 +140,106 @@
         if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: " + authenticated
                 + " reason: " + failureReason);
 
-        SomeArgs args = SomeArgs.obtain();
-        args.arg1 = authenticated;
-        args.arg2 = failureReason;
-        mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget();
-    }
-
-    @Override
-    public void onBiometricHelp(String message) {
-        if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message);
-        SomeArgs args = SomeArgs.obtain();
-        args.arg1 = message;
-        mHandler.obtainMessage(MSG_BIOMETRIC_HELP, args).sendToTarget();
-    }
-
-    @Override
-    public void onBiometricError(String error) {
-        if (DEBUG) Log.d(TAG, "onBiometricError: " + error);
-        mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, error).sendToTarget();
-    }
-
-    @Override
-    public void hideBiometricDialog() {
-        if (DEBUG) Log.d(TAG, "hideBiometricDialog");
-        mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget();
-    }
-
-    private void handleShowDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
-        mCurrentDialogArgs = args;
-        final int type = args.argi1;
-
-        // Create a new dialog but do not replace the current one yet.
-        BiometricDialogView newDialog;
-        if (type == BiometricAuthenticator.TYPE_FINGERPRINT) {
-            newDialog = new FingerprintDialogView(mContext, mCallback);
-        } else if (type == BiometricAuthenticator.TYPE_FACE) {
-            newDialog = new FaceDialogView(mContext, mCallback);
-        } else {
-            Log.e(TAG, "Unsupported type: " + type);
-            return;
-        }
-
-        if (DEBUG) Log.d(TAG, "handleShowDialog, "
-                + " savedState: " + savedState
-                + " mCurrentDialog: " + mCurrentDialog
-                + " newDialog: " + newDialog
-                + " type: " + type);
-
-        if (savedState != null) {
-            // SavedState is only non-null if it's from onConfigurationChanged. Restore the state
-            // even though it may be removed / re-created again
-            newDialog.restoreState(savedState);
-        } else if (mCurrentDialog != null && mDialogShowing) {
-            // If somehow we're asked to show a dialog, the old one doesn't need to be animated
-            // away. This can happen if the app cancels and re-starts auth during configuration
-            // change. This is ugly because we also have to do things on onConfigurationChanged
-            // here.
-            mCurrentDialog.forceRemove();
-        }
-
-        mReceiver = (IBiometricServiceReceiverInternal) args.arg2;
-        newDialog.setBundle((Bundle) args.arg1);
-        newDialog.setRequireConfirmation((boolean) args.arg3);
-        newDialog.setUserId(args.argi2);
-        newDialog.setSkipIntro(skipAnimation);
-        mCurrentDialog = newDialog;
-        mWindowManager.addView(mCurrentDialog, mCurrentDialog.getLayoutParams());
-        mDialogShowing = true;
-    }
-
-    private void handleBiometricAuthenticated(boolean authenticated, String failureReason) {
-        if (DEBUG) Log.d(TAG, "handleBiometricAuthenticated: " + authenticated);
-
         if (authenticated) {
-            mCurrentDialog.announceForAccessibility(
-                    mContext.getResources()
-                            .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId()));
-            if (mCurrentDialog.requiresConfirmation()) {
-                mCurrentDialog.updateState(BiometricDialogView.STATE_PENDING_CONFIRMATION);
-            } else {
-                mCurrentDialog.updateState(BiometricDialogView.STATE_AUTHENTICATED);
-                mHandler.postDelayed(() -> {
-                    handleHideDialog(false /* userCanceled */);
-                }, mCurrentDialog.getDelayAfterAuthenticatedDurationMs());
-            }
+            mCurrentDialog.onAuthenticationSucceeded();
         } else {
             mCurrentDialog.onAuthenticationFailed(failureReason);
         }
     }
 
-    private void handleBiometricHelp(String message) {
-        if (DEBUG) Log.d(TAG, "handleBiometricHelp: " + message);
-        mCurrentDialog.onHelpReceived(message);
+    @Override
+    public void onBiometricHelp(String message) {
+        if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message);
+
+        mCurrentDialog.onHelp(message);
     }
 
-    private void handleBiometricError(String error) {
-        if (DEBUG) Log.d(TAG, "handleBiometricError: " + error);
-        if (!mDialogShowing) {
-            if (DEBUG) Log.d(TAG, "Dialog already dismissed");
-            return;
-        }
-        mCurrentDialog.onErrorReceived(error);
+    @Override
+    public void onBiometricError(String error) {
+        if (DEBUG) Log.d(TAG, "onBiometricError: " + error);
+        mCurrentDialog.onError(error);
     }
 
-    private void handleHideDialog(boolean userCanceled) {
-        if (DEBUG) Log.d(TAG, "handleHideDialog, userCanceled: " + userCanceled);
-        if (!mDialogShowing) {
-            // This can happen if there's a race and we get called from both
-            // onAuthenticated and onError, etc.
-            Log.w(TAG, "Dialog already dismissed, userCanceled: " + userCanceled);
+    @Override
+    public void hideBiometricDialog() {
+        if (DEBUG) Log.d(TAG, "hideBiometricDialog");
+
+        // TODO: I think we need to remove this interface
+        mCurrentDialog.dismissWithoutCallback(true /* animate */);
+    }
+
+    private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
+        mCurrentDialogArgs = args;
+        final int type = args.argi1;
+
+        // Create a new dialog but do not replace the current one yet.
+        final BiometricDialog newDialog = buildDialog(
+                (Bundle) args.arg1 /* bundle */,
+                (boolean) args.arg3 /* requireConfirmation */,
+                args.argi2 /* userId */,
+                type);
+
+        if (newDialog == null) {
+            Log.e(TAG, "Unsupported type: " + type);
             return;
         }
-        if (userCanceled) {
-            try {
-                mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
-            } catch (RemoteException e) {
-                Log.e(TAG, "RemoteException when hiding dialog", e);
-            }
+
+        if (DEBUG) {
+            Log.d(TAG, "showDialog, "
+                    + " savedState: " + savedState
+                    + " mCurrentDialog: " + mCurrentDialog
+                    + " newDialog: " + newDialog
+                    + " type: " + type);
+        }
+
+        if (savedState != null) {
+            // SavedState is only non-null if it's from onConfigurationChanged. Restore the state
+            // even though it may be removed / re-created again
+            newDialog.restoreState(savedState);
+        } else if (mCurrentDialog != null) {
+            // If somehow we're asked to show a dialog, the old one doesn't need to be animated
+            // away. This can happen if the app cancels and re-starts auth during configuration
+            // change. This is ugly because we also have to do things on onConfigurationChanged
+            // here.
+            mCurrentDialog.dismissWithoutCallback(false /* animate */);
+        }
+
+        mReceiver = (IBiometricServiceReceiverInternal) args.arg2;
+        mCurrentDialog = newDialog;
+        mCurrentDialog.show(mWindowManager, skipAnimation);
+    }
+
+    private void onDialogDismissed() {
+        if (DEBUG) Log.d(TAG, "onDialogDismissed");
+        if (mCurrentDialog == null) {
+            Log.w(TAG, "Dialog already dismissed");
         }
         mReceiver = null;
-        mDialogShowing = false;
-        mCurrentDialog.startDismiss();
-    }
-
-    private void handleButtonNegative() {
-        if (mReceiver == null) {
-            Log.e(TAG, "Receiver is null");
-            return;
-        }
-        try {
-            mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Remote exception when handling negative button", e);
-        }
-        handleHideDialog(false /* userCanceled */);
-    }
-
-    private void handleButtonPositive() {
-        if (mReceiver == null) {
-            Log.e(TAG, "Receiver is null");
-            return;
-        }
-        try {
-            mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_POSITIVE);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Remote exception when handling positive button", e);
-        }
-        handleHideDialog(false /* userCanceled */);
-    }
-
-    private void handleUserCanceled() {
-        handleHideDialog(true /* userCanceled */);
-    }
-
-    private void handleTryAgainPressed() {
-        try {
-            mReceiver.onTryAgainPressed();
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException when handling try again", e);
-        }
+        mCurrentDialog = null;
     }
 
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        final boolean wasShowing = mDialogShowing;
 
         // Save the state of the current dialog (buttons showing, etc)
-        final Bundle savedState = new Bundle();
         if (mCurrentDialog != null) {
+            final Bundle savedState = new Bundle();
             mCurrentDialog.onSaveState(savedState);
-        }
+            mCurrentDialog.dismissWithoutCallback(false /* animate */);
+            mCurrentDialog = null;
 
-        if (mDialogShowing) {
-            mCurrentDialog.forceRemove();
-            mDialogShowing = false;
+            showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
         }
+    }
 
-        if (wasShowing) {
-            handleShowDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
-        }
+    protected BiometricDialog buildDialog(Bundle biometricPromptBundle,
+            boolean requireConfirmation, int userId, int type) {
+        return new BiometricDialogView.Builder(mContext)
+                .setCallback(this)
+                .setBiometricPromptBundle(biometricPromptBundle)
+                .setRequireConfirmation(requireConfirmation)
+                .setUserId(userId)
+                .build(type);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java
index 24fd22e..acad62a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java
@@ -21,31 +21,22 @@
  * FingerprintDialogImpl) and passed into their views (e.g. FingerprintDialogView).
  */
 public interface DialogViewCallback {
-    /**
-     * Invoked when the user cancels authentication by tapping outside the prompt, etc. The dialog
-     * should be dismissed.
-     */
-    void onUserCanceled();
+
+    int DISMISSED_USER_CANCELED = 1;
+    int DISMISSED_BUTTON_NEGATIVE = 2;
+    int DISMISSED_BUTTON_POSITIVE = 3;
+
+    int DISMISSED_AUTHENTICATED = 4;
+    int DISMISSED_ERROR = 5;
 
     /**
-     * Invoked when an error is shown. The dialog should be dismissed after a set amount of time.
+     * Invoked when the dialog is dismissed
+     * @param reason
      */
-    void onErrorShown();
+    void onDismissed(int reason);
 
     /**
-     * Invoked when the negative button is pressed. The client should be notified and the dialog
-     * should be dismissed.
-     */
-    void onNegativePressed();
-
-    /**
-     * Invoked when the positive button is pressed. The client should be notified and the dialog
-     * should be dismissed.
-     */
-    void onPositivePressed();
-
-    /**
-     * Invoked when the "try again" button is pressed.
+     * Invoked when the "try again" button is clicked
      */
     void onTryAgainPressed();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java
rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java
index ce67577..7a5c3e3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java
@@ -11,10 +11,10 @@
  * 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
+ * limitations under the License.
  */
 
-package com.android.systemui.biometrics;
+package com.android.systemui.biometrics.ui;
 
 import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
 
@@ -23,6 +23,7 @@
 import android.graphics.PixelFormat;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricPrompt;
 import android.os.Binder;
 import android.os.Bundle;
@@ -46,25 +47,29 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import com.android.systemui.Dependency;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
+import com.android.systemui.biometrics.BiometricDialog;
+import com.android.systemui.biometrics.DialogViewCallback;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.util.leak.RotationUtils;
 
 /**
  * Abstract base class. Shows a dialog for BiometricPrompt.
  */
-public abstract class BiometricDialogView extends LinearLayout {
+public abstract class BiometricDialogView extends LinearLayout implements BiometricDialog {
 
     private static final String TAG = "BiometricDialogView";
 
-    private static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility";
-    private static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility";
-    private static final String KEY_CONFIRM_ENABLED = "key_confirm_enabled";
-    private static final String KEY_STATE = "key_state";
-    private static final String KEY_ERROR_TEXT_VISIBILITY = "key_error_text_visibility";
-    private static final String KEY_ERROR_TEXT_STRING = "key_error_text_string";
-    private static final String KEY_ERROR_TEXT_IS_TEMPORARY = "key_error_text_is_temporary";
-    private static final String KEY_ERROR_TEXT_COLOR = "key_error_text_color";
+    public static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility";
+    public static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility";
+    public static final String KEY_CONFIRM_ENABLED = "key_confirm_enabled";
+    public static final String KEY_STATE = "key_state";
+    public static final String KEY_ERROR_TEXT_VISIBILITY = "key_error_text_visibility";
+    public static final String KEY_ERROR_TEXT_STRING = "key_error_text_string";
+    public static final String KEY_ERROR_TEXT_IS_TEMPORARY = "key_error_text_is_temporary";
+    public static final String KEY_ERROR_TEXT_COLOR = "key_error_text_color";
 
     private static final int ANIMATION_DURATION_SHOW = 250; // ms
     private static final int ANIMATION_DURATION_AWAY = 350; // ms
@@ -77,6 +82,7 @@
     protected static final int STATE_PENDING_CONFIRMATION = 3;
     protected static final int STATE_AUTHENTICATED = 4;
 
+    private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final AccessibilityManager mAccessibilityManager;
     private final IBinder mWindowToken = new Binder();
     private final Interpolator mLinearOutSlowIn;
@@ -105,7 +111,6 @@
     private Bundle mRestoredState;
 
     private int mState = STATE_IDLE;
-    private boolean mAnimatingAway;
     private boolean mWasForceRemoved;
     private boolean mSkipIntro;
     protected boolean mRequireConfirmation;
@@ -141,6 +146,14 @@
         }
     };
 
+    private final WakefulnessLifecycle.Observer mWakefulnessObserver =
+            new WakefulnessLifecycle.Observer() {
+                @Override
+                public void onStartedGoingToSleep() {
+                    animateAway(DialogViewCallback.DISMISSED_USER_CANCELED);
+                }
+            };
+
     protected Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
@@ -155,8 +168,63 @@
         }
     };
 
-    public BiometricDialogView(Context context, DialogViewCallback callback) {
+    /**
+     * Builds the dialog with specified parameters.
+     */
+    public static class Builder {
+        public static final int TYPE_FINGERPRINT = BiometricAuthenticator.TYPE_FINGERPRINT;
+        public static final int TYPE_FACE = BiometricAuthenticator.TYPE_FACE;
+
+        private Context mContext;
+        private DialogViewCallback mCallback;
+        private Bundle mBundle;
+        private boolean mRequireConfirmation;
+        private int mUserId;
+
+        public Builder(Context context) {
+            mContext = context;
+        }
+
+        public Builder setCallback(DialogViewCallback callback) {
+            mCallback = callback;
+            return this;
+        }
+
+        public Builder setBiometricPromptBundle(Bundle bundle) {
+            mBundle = bundle;
+            return this;
+        }
+
+        public Builder setRequireConfirmation(boolean requireConfirmation) {
+            mRequireConfirmation = requireConfirmation;
+            return this;
+        }
+
+        public Builder setUserId(int userId) {
+            mUserId = userId;
+            return this;
+        }
+
+        public BiometricDialogView build(int type) {
+            BiometricDialogView dialog;
+            if (type == TYPE_FINGERPRINT) {
+                dialog = new FingerprintDialogView(mContext, mCallback);
+            } else if (type == TYPE_FACE) {
+                dialog = new FaceDialogView(mContext, mCallback);
+            } else {
+                return null;
+            }
+            dialog.setBundle(mBundle);
+            dialog.setRequireConfirmation(mRequireConfirmation);
+            dialog.setUserId(mUserId);
+            return dialog;
+        }
+    }
+
+    protected BiometricDialogView(Context context, DialogViewCallback callback) {
         super(context);
+        mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
+
         mCallback = callback;
         mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
         mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
@@ -178,19 +246,19 @@
         addView(mLayout);
 
         mLayout.setOnKeyListener(new View.OnKeyListener() {
-            boolean downPressed = false;
+            boolean mDownPressed = false;
             @Override
             public boolean onKey(View v, int keyCode, KeyEvent event) {
                 if (keyCode != KeyEvent.KEYCODE_BACK) {
                     return false;
                 }
-                if (event.getAction() == KeyEvent.ACTION_DOWN && downPressed == false) {
-                    downPressed = true;
+                if (event.getAction() == KeyEvent.ACTION_DOWN && mDownPressed == false) {
+                    mDownPressed = true;
                 } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
-                    downPressed = false;
-                } else if (event.getAction() == KeyEvent.ACTION_UP && downPressed == true) {
-                    downPressed = false;
-                    mCallback.onUserCanceled();
+                    mDownPressed = false;
+                } else if (event.getAction() == KeyEvent.ACTION_UP && mDownPressed == true) {
+                    mDownPressed = false;
+                    animateAway(DialogViewCallback.DISMISSED_USER_CANCELED);
                 }
                 return true;
             }
@@ -219,16 +287,16 @@
 
         mNegativeButton.setOnClickListener((View v) -> {
             if (mState == STATE_PENDING_CONFIRMATION || mState == STATE_AUTHENTICATED) {
-                mCallback.onUserCanceled();
+                animateAway(DialogViewCallback.DISMISSED_USER_CANCELED);
             } else {
-                mCallback.onNegativePressed();
+                animateAway(DialogViewCallback.DISMISSED_BUTTON_NEGATIVE);
             }
         });
 
         mPositiveButton.setOnClickListener((View v) -> {
             updateState(STATE_AUTHENTICATED);
             mHandler.postDelayed(() -> {
-                mCallback.onPositivePressed();
+                animateAway(DialogViewCallback.DISMISSED_BUTTON_POSITIVE);
             }, getDelayAfterAuthenticatedDurationMs());
         });
 
@@ -248,21 +316,12 @@
         mLayout.requestFocus();
     }
 
-    public void onSaveState(Bundle bundle) {
-        bundle.putInt(KEY_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility());
-        bundle.putInt(KEY_CONFIRM_VISIBILITY, mPositiveButton.getVisibility());
-        bundle.putBoolean(KEY_CONFIRM_ENABLED, mPositiveButton.isEnabled());
-        bundle.putInt(KEY_STATE, mState);
-        bundle.putInt(KEY_ERROR_TEXT_VISIBILITY, mErrorText.getVisibility());
-        bundle.putCharSequence(KEY_ERROR_TEXT_STRING, mErrorText.getText());
-        bundle.putBoolean(KEY_ERROR_TEXT_IS_TEMPORARY, mHandler.hasMessages(MSG_RESET_MESSAGE));
-        bundle.putInt(KEY_ERROR_TEXT_COLOR, mErrorText.getCurrentTextColor());
-    }
-
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
 
+        mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
+
         final ImageView backgroundView = mLayout.findViewById(R.id.background);
 
         if (mUserManager.isManagedProfile(mUserId)) {
@@ -346,34 +405,48 @@
         mSkipIntro = false;
     }
 
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
+    }
+
     private void setDismissesDialog(View v) {
         v.setClickable(true);
         v.setOnClickListener(v1 -> {
             if (mState != STATE_AUTHENTICATED && shouldGrayAreaDismissDialog()) {
-                mCallback.onUserCanceled();
+                animateAway(DialogViewCallback.DISMISSED_USER_CANCELED);
             }
         });
     }
 
-    public void startDismiss() {
+    private void animateAway(int reason) {
+        animateAway(true /* sendReason */, reason);
+    }
+    /**
+     * Animate the dialog away
+     * @param reason one of the {@link DialogViewCallback} codes
+     */
+    private void animateAway(boolean sendReason, int reason) {
         if (!mCompletedAnimatingIn) {
             Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
             mPendingDismissDialog = true;
             return;
         }
 
-        mAnimatingAway = true;
-
         // This is where final cleanup should occur.
         final Runnable endActionRunnable = new Runnable() {
             @Override
             public void run() {
                 mWindowManager.removeView(BiometricDialogView.this);
-                mAnimatingAway = false;
                 // Set the icons / text back to normal state
                 handleResetMessage();
                 showTryAgainButton(false /* show */);
                 updateState(STATE_IDLE);
+                if (sendReason) {
+                    mCallback.onDismissed(reason);
+                }
             }
         };
 
@@ -398,49 +471,28 @@
     }
 
     /**
-     * Force remove the window, cancelling any animation that's happening. This should only be
-     * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method
-     * will cause the dialog to show without an animation the next time it's attached.
-     */
-    public void forceRemove() {
-        mLayout.animate().cancel();
-        mDialog.animate().cancel();
-        mWindowManager.removeView(BiometricDialogView.this);
-        mAnimatingAway = false;
-        mWasForceRemoved = true;
-    }
-
-    /**
      * Skip the intro animation
      */
-    public void setSkipIntro(boolean skip) {
+    private void setSkipIntro(boolean skip) {
         mSkipIntro = skip;
     }
 
-    public boolean isAnimatingAway() {
-        return mAnimatingAway;
-    }
-
-    public void setBundle(Bundle bundle) {
+    private void setBundle(Bundle bundle) {
         mBundle = bundle;
     }
 
-    public void setRequireConfirmation(boolean requireConfirmation) {
+    private void setRequireConfirmation(boolean requireConfirmation) {
         mRequireConfirmation = requireConfirmation;
     }
 
-    public boolean requiresConfirmation() {
+    protected boolean requiresConfirmation() {
         return mRequireConfirmation;
     }
 
-    public void setUserId(int userId) {
+    private void setUserId(int userId) {
         mUserId = userId;
     }
 
-    public ViewGroup getLayout() {
-        return mLayout;
-    }
-
     // Shows an error/help message
     protected void showTemporaryMessage(String message) {
         mHandler.removeMessages(MSG_RESET_MESSAGE);
@@ -452,17 +504,58 @@
                 BiometricPrompt.HIDE_DIALOG_DELAY);
     }
 
+    @Override
+    public void show(WindowManager wm, boolean skipIntroAnimation) {
+        setSkipIntro(skipIntroAnimation);
+        wm.addView(this, getLayoutParams(mWindowToken));
+    }
+
+    /**
+     * Force remove the window, cancelling any animation that's happening. This should only be
+     * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method
+     * will cause the dialog to show without an animation the next time it's attached.
+     */
+    @Override
+    public void dismissWithoutCallback(boolean animate) {
+        if (animate) {
+            animateAway(false /* sendReason */, 0);
+        } else {
+            mLayout.animate().cancel();
+            mDialog.animate().cancel();
+            mWindowManager.removeView(BiometricDialogView.this);
+            mWasForceRemoved = true;
+        }
+    }
+
+    @Override
+    public void onAuthenticationSucceeded() {
+        announceForAccessibility(getResources().getText(getAuthenticatedAccessibilityResourceId()));
+
+        if (requiresConfirmation()) {
+            updateState(STATE_PENDING_CONFIRMATION);
+        } else {
+            mHandler.postDelayed(() -> {
+                animateAway(DialogViewCallback.DISMISSED_AUTHENTICATED);
+            }, getDelayAfterAuthenticatedDurationMs());
+
+            updateState(STATE_AUTHENTICATED);
+        }
+    }
+
+
+    @Override
+    public void onAuthenticationFailed(String message) {
+        updateState(STATE_ERROR);
+        showTemporaryMessage(message);
+    }
+
     /**
      * Transient help message (acquire) is received, dialog stays showing. Sensor stays in
      * "authenticating" state.
      * @param message
      */
-    public void onHelpReceived(String message) {
-        updateState(STATE_ERROR);
-        showTemporaryMessage(message);
-    }
-
-    public void onAuthenticationFailed(String message) {
+    @Override
+    public void onHelp(String message) {
         updateState(STATE_ERROR);
         showTemporaryMessage(message);
     }
@@ -471,14 +564,57 @@
      * Hard error is received, dialog will be dismissed soon.
      * @param error
      */
-    public void onErrorReceived(String error) {
+    @Override
+    public void onError(String error) {
         updateState(STATE_ERROR);
         showTemporaryMessage(error);
         showTryAgainButton(false /* show */);
-        mCallback.onErrorShown(); // TODO: Split between fp and face
+
+        // TODO: Is this still used to synchronize animation and client onError timing?
+        mHandler.postDelayed(() -> {
+            animateAway(DialogViewCallback.DISMISSED_ERROR);
+        }, BiometricPrompt.HIDE_DIALOG_DELAY);
     }
 
-    public void updateState(int newState) {
+
+    @Override
+    public void onSaveState(Bundle bundle) {
+        bundle.putInt(KEY_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility());
+        bundle.putInt(KEY_CONFIRM_VISIBILITY, mPositiveButton.getVisibility());
+        bundle.putBoolean(KEY_CONFIRM_ENABLED, mPositiveButton.isEnabled());
+        bundle.putInt(KEY_STATE, mState);
+        bundle.putInt(KEY_ERROR_TEXT_VISIBILITY, mErrorText.getVisibility());
+        bundle.putCharSequence(KEY_ERROR_TEXT_STRING, mErrorText.getText());
+        bundle.putBoolean(KEY_ERROR_TEXT_IS_TEMPORARY, mHandler.hasMessages(MSG_RESET_MESSAGE));
+        bundle.putInt(KEY_ERROR_TEXT_COLOR, mErrorText.getCurrentTextColor());
+    }
+
+    @Override
+    public void restoreState(Bundle bundle) {
+        mRestoredState = bundle;
+        final int tryAgainVisibility = bundle.getInt(KEY_TRY_AGAIN_VISIBILITY);
+        mTryAgainButton.setVisibility(tryAgainVisibility);
+        final int confirmVisibility = bundle.getInt(KEY_CONFIRM_VISIBILITY);
+        mPositiveButton.setVisibility(confirmVisibility);
+        final boolean confirmEnabled = bundle.getBoolean(KEY_CONFIRM_ENABLED);
+        mPositiveButton.setEnabled(confirmEnabled);
+        mState = bundle.getInt(KEY_STATE);
+        mErrorText.setText(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
+        mErrorText.setContentDescription(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
+        final int errorTextVisibility = bundle.getInt(KEY_ERROR_TEXT_VISIBILITY);
+        mErrorText.setVisibility(errorTextVisibility);
+        if (errorTextVisibility == View.INVISIBLE || tryAgainVisibility == View.INVISIBLE
+                || confirmVisibility == View.INVISIBLE) {
+            announceAccessibilityEvent();
+        }
+        mErrorText.setTextColor(bundle.getInt(KEY_ERROR_TEXT_COLOR));
+        if (bundle.getBoolean(KEY_ERROR_TEXT_IS_TEMPORARY)) {
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE),
+                    BiometricPrompt.HIDE_DIALOG_DELAY);
+        }
+    }
+
+    protected void updateState(int newState) {
         if (newState == STATE_PENDING_CONFIRMATION) {
             mHandler.removeMessages(MSG_RESET_MESSAGE);
             mErrorText.setTextColor(mTextColor);
@@ -505,48 +641,24 @@
         mState = newState;
     }
 
-    public void showTryAgainButton(boolean show) {
+    protected void showTryAgainButton(boolean show) {
     }
 
-    public void onDialogAnimatedIn() {
+    protected void onDialogAnimatedIn() {
         mCompletedAnimatingIn = true;
 
         if (mPendingDismissDialog) {
             Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
-            startDismiss();
+            animateAway(false /* sendReason */, 0);
             mPendingDismissDialog = false;
         }
     }
 
-    public void restoreState(Bundle bundle) {
-        mRestoredState = bundle;
-        final int tryAgainVisibility = bundle.getInt(KEY_TRY_AGAIN_VISIBILITY);
-        mTryAgainButton.setVisibility(tryAgainVisibility);
-        final int confirmVisibility = bundle.getInt(KEY_CONFIRM_VISIBILITY);
-        mPositiveButton.setVisibility(confirmVisibility);
-        final boolean confirmEnabled = bundle.getBoolean(KEY_CONFIRM_ENABLED);
-        mPositiveButton.setEnabled(confirmEnabled);
-        mState = bundle.getInt(KEY_STATE);
-        mErrorText.setText(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
-        mErrorText.setContentDescription(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
-        final int errorTextVisibility = bundle.getInt(KEY_ERROR_TEXT_VISIBILITY);
-        mErrorText.setVisibility(errorTextVisibility);
-        if (errorTextVisibility == View.INVISIBLE || tryAgainVisibility == View.INVISIBLE
-                || confirmVisibility == View.INVISIBLE) {
-            announceAccessibilityEvent();
-        }
-        mErrorText.setTextColor(bundle.getInt(KEY_ERROR_TEXT_COLOR));
-        if (bundle.getBoolean(KEY_ERROR_TEXT_IS_TEMPORARY)) {
-            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE),
-                    BiometricPrompt.HIDE_DIALOG_DELAY);
-        }
-    }
-
-    protected int getState() {
-        return mState;
-    }
-
-    public WindowManager.LayoutParams getLayoutParams() {
+    /**
+     * @param windowToken token for the window
+     * @return
+     */
+    public static WindowManager.LayoutParams getLayoutParams(IBinder windowToken) {
         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT,
@@ -555,7 +667,7 @@
                 PixelFormat.TRANSLUCENT);
         lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
         lp.setTitle("BiometricDialogView");
-        lp.token = mWindowToken;
+        lp.token = windowToken;
         return lp;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java
similarity index 98%
rename from packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java
rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java
index ae6cb5c..cfa17ee 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java
@@ -11,10 +11,10 @@
  * 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
+ * limitations under the License.
  */
 
-package com.android.systemui.biometrics;
+package com.android.systemui.biometrics.ui;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -34,6 +34,7 @@
 import android.view.ViewOutlineProvider;
 
 import com.android.systemui.R;
+import com.android.systemui.biometrics.DialogViewCallback;
 
 /**
  * This class loads the view for the system-provided dialog. The view consists of:
@@ -152,7 +153,7 @@
         announceAccessibilityEvent();
     };
 
-    public FaceDialogView(Context context,
+    protected FaceDialogView(Context context,
             DialogViewCallback callback) {
         super(context, callback);
         mIconController = new IconController();
@@ -339,8 +340,8 @@
     }
 
     @Override
-    public void onErrorReceived(String error) {
-        super.onErrorReceived(error);
+    public void onError(String error) {
+        super.onError(error);
         // All error messages will cause the dialog to go from small -> big. Error messages
         // are messages such as lockout, auth failed, etc.
         if (mSize == SIZE_SMALL) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java
similarity index 95%
rename from packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java
rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java
index 183933e..1c339ba 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java
@@ -11,10 +11,10 @@
  * 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
+ * limitations under the License.
  */
 
-package com.android.systemui.biometrics;
+package com.android.systemui.biometrics.ui;
 
 import android.content.Context;
 import android.graphics.drawable.AnimatedVectorDrawable;
@@ -22,6 +22,7 @@
 import android.util.Log;
 
 import com.android.systemui.R;
+import com.android.systemui.biometrics.DialogViewCallback;
 
 /**
  * This class loads the view for the system-provided dialog. The view consists of:
@@ -32,7 +33,7 @@
 
     private static final String TAG = "FingerprintDialogView";
 
-    public FingerprintDialogView(Context context,
+    protected FingerprintDialogView(Context context,
             DialogViewCallback callback) {
         super(context, callback);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java
new file mode 100644
index 0000000..3122051
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2019 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 junit.framework.Assert.assertEquals;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
+import android.os.Bundle;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.testing.TestableLooper.RunWithLooper;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.StatusBar;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+@SmallTest
+public class BiometricDialogImplTest extends SysuiTestCase {
+
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private IBiometricServiceReceiverInternal mReceiver;
+    @Mock
+    private BiometricDialog mDialog1;
+    @Mock
+    private BiometricDialog mDialog2;
+
+    private TestableBiometricDialogImpl mBiometricDialogImpl;
+
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        TestableContext context = spy(mContext);
+
+        mContext.putComponent(StatusBar.class, mock(StatusBar.class));
+        mContext.putComponent(CommandQueue.class, mock(CommandQueue.class));
+
+        when(context.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE))
+            .thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+            .thenReturn(true);
+
+        mBiometricDialogImpl = new TestableBiometricDialogImpl();
+        mBiometricDialogImpl.mContext = context;
+        mBiometricDialogImpl.mComponents = mContext.getComponents();
+
+        mBiometricDialogImpl.start();
+    }
+
+    // Callback tests
+
+    @Test
+    public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_USER_CANCELED);
+        verify(mReceiver, times(1))
+                .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
+    }
+
+    @Test
+    public void testSendsReasonNegative_whenDismissedByButtonNegative() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_BUTTON_NEGATIVE);
+        verify(mReceiver, times(1))
+                .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
+    }
+
+    @Test
+    public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_BUTTON_POSITIVE);
+        verify(mReceiver, times(1))
+                .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
+    }
+
+    @Test
+    public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception {
+        // TODO: Modify BiometricService / BiometricDialog
+        showDialog(BiometricPrompt.TYPE_FACE);
+        mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_AUTHENTICATED);
+        verify(mReceiver, times(1))
+                .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
+    }
+
+    @Test
+    public void testSendsReasonError_whenDismissedByError() throws Exception {
+        // TODO: Modify BiometricService / BiometricDialog
+        showDialog(BiometricPrompt.TYPE_FACE);
+        mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_ERROR);
+        verify(mReceiver, times(1))
+                .onDialogDismissed(BiometricPrompt.DISMISSED_REASON_ERROR);
+    }
+
+    // Statusbar tests
+
+    @Test
+    public void testShowInvoked_whenSystemRequested()
+            throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        verify(mDialog1, times(1)).show(any(), eq(false) /* skipIntro */);
+    }
+
+    @Test
+    public void testOnAuthenticationSucceededInvoked_whenSystemRequested() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        mBiometricDialogImpl.onBiometricAuthenticated(true, null /* failureReason */);
+        verify(mDialog1, times(1)).onAuthenticationSucceeded();
+    }
+
+    @Test
+    public void testOnAuthenticationFailedInvoked_whenSystemRequested() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        final String failureReason = "failure reason";
+        mBiometricDialogImpl.onBiometricAuthenticated(false, failureReason);
+
+        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+        verify(mDialog1, times(1)).onAuthenticationFailed(captor.capture());
+
+        assertEquals(captor.getValue(), failureReason);
+    }
+
+    @Test
+    public void testOnHelpInvoked_whenSystemRequested() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        final String helpMessage = "help";
+        mBiometricDialogImpl.onBiometricHelp(helpMessage);
+
+        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+        verify(mDialog1, times(1)).onHelp(captor.capture());
+
+        assertEquals(captor.getValue(), helpMessage);
+    }
+
+    @Test
+    public void testOnErrorInvoked_whenSystemRequested() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        final String errMessage = "error message";
+        mBiometricDialogImpl.onBiometricError(errMessage);
+
+        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+        verify(mDialog1, times(1)).onError(captor.capture());
+
+        assertEquals(captor.getValue(), errMessage);
+    }
+
+    @Test
+    public void testDismissWithoutCallbackInvoked_whenSystemRequested() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        mBiometricDialogImpl.hideBiometricDialog();
+
+        verify(mDialog1, times(1)).dismissWithoutCallback(eq(true) /* animate */);
+    }
+
+    // Corner case tests
+
+    @Test
+    public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        verify(mDialog1, times(1)).show(any(), eq(false) /* skipIntro */);
+
+        showDialog(BiometricPrompt.TYPE_FACE);
+
+        // First dialog should be dismissed without animation
+        verify(mDialog1, times(1)).dismissWithoutCallback(eq(false) /* animate */);
+
+        // Second dialog should be shown without animation
+        verify(mDialog2, times(1)).show(any(), eq(true)) /* skipIntro */;
+    }
+
+    @Test
+    public void testConfigurationPersists_whenOnConfigurationChanged() throws Exception {
+        showDialog(BiometricPrompt.TYPE_FACE);
+        verify(mDialog1, times(1)).show(any(), eq(false) /* skipIntro */);
+
+        mBiometricDialogImpl.onConfigurationChanged(new Configuration());
+
+        ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mDialog1, times(1)).onSaveState(captor.capture());
+
+        // Old dialog doesn't animate
+        verify(mDialog1, times(1)).dismissWithoutCallback(eq(false) /* animate */);
+
+        // Saved state is restored into new dialog
+        ArgumentCaptor<Bundle> captor2 = ArgumentCaptor.forClass(Bundle.class);
+        verify(mDialog2, times(1)).restoreState(captor2.capture());
+
+        // Dialog for new configuration skips intro
+        verify(mDialog2, times(1)).show(any(), eq(true) /* skipIntro */);
+
+        // TODO: This should check all values we want to save/restore
+        assertEquals(captor.getValue(), captor2.getValue());
+    }
+
+
+    // Helpers
+
+    private void showDialog(int type) {
+        mBiometricDialogImpl.showBiometricDialog(createTestDialogBundle(),
+                mReceiver /* receiver */,
+                type,
+                true /* requireConfirmation */,
+                0 /* userId */);
+    }
+
+    private Bundle createTestDialogBundle() {
+        Bundle bundle = new Bundle();
+
+        bundle.putCharSequence(BiometricPrompt.KEY_TITLE, "Title");
+        bundle.putCharSequence(BiometricPrompt.KEY_SUBTITLE, "Subtitle");
+        bundle.putCharSequence(BiometricPrompt.KEY_DESCRIPTION, "Description");
+        bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, "Negative Button");
+
+        // RequireConfirmation is a hint to BiometricService. This can be forced to be required
+        // by user settings, and should be tested in BiometricService.
+        bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, true);
+
+        return bundle;
+    }
+
+    private final class TestableBiometricDialogImpl extends BiometricDialogImpl {
+        private int mBuildCount = 0;
+
+        @Override
+        protected BiometricDialog buildDialog(Bundle biometricPromptBundle,
+                boolean requireConfirmation, int userId, int type) {
+            BiometricDialog dialog;
+            if (mBuildCount == 0) {
+                dialog = mDialog1;
+            } else if (mBuildCount == 1) {
+                dialog = mDialog2;
+            } else {
+                dialog = null;
+            }
+            mBuildCount++;
+            return dialog;
+        }
+    }
+}
+
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 1e1f2156..f68c11b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -365,4 +365,43 @@
         waitForIdleSync();
         verify(mCallbacks).onRecentsAnimationStateChanged(eq(true));
     }
+
+    @Test
+    public void testShowBiometricDialog() {
+        Bundle bundle = new Bundle();
+        mCommandQueue.showBiometricDialog(bundle, null /* receiver */, 1, true, 3);
+        waitForIdleSync();
+        verify(mCallbacks).showBiometricDialog(eq(bundle), eq(null), eq(1), eq(true), eq(3));
+    }
+
+    @Test
+    public void testOnBiometricAuthenticated() {
+        String failureReason = "test_failure_reason";
+        mCommandQueue.onBiometricAuthenticated(true /* authenticated */, failureReason);
+        waitForIdleSync();
+        verify(mCallbacks).onBiometricAuthenticated(eq(true), eq(failureReason));
+    }
+
+    @Test
+    public void testOnBiometricHelp() {
+        String helpMessage = "test_help_message";
+        mCommandQueue.onBiometricHelp(helpMessage);
+        waitForIdleSync();
+        verify(mCallbacks).onBiometricHelp(eq(helpMessage));
+    }
+
+    @Test
+    public void testOnBiometricError() {
+        String errorMessage = "test_error_message";
+        mCommandQueue.onBiometricError(errorMessage);
+        waitForIdleSync();
+        verify(mCallbacks).onBiometricError(eq(errorMessage));
+    }
+
+    @Test
+    public void testHideBiometricDialog() {
+        mCommandQueue.hideBiometricDialog();
+        waitForIdleSync();
+        verify(mCallbacks).hideBiometricDialog();
+    }
 }