3/n: For passive modalities, add plumbing for "try again"

When "try again" is showing, authentication is canceled internally.
BiometricService caches the client's info so that authentication can
be restarted when "try again" is pressed. Because authentication
is not running when "try again" is showing, BiometricService also needs
to have a TaskStackListener so that BP can be dismissed and an error can
be sent to the client when the app loses focus.

IBiometricServiceReceiver has been split into two. One for BiometricPrompt
to receive messages from BiometricService, and another for BiometricService
to receive messages from SystemUI/<Biometric>Services.

When we get locked out, don't send the last onAuthenticationFailed
to the client, since "Authentication failed" will be shown briefly
and be replaced by "Device locked out" which is janky

Bug: 111461540

Test: Tested with requireConfirmation enabled/disabled
Test: Tested onConfigurationChange corner cases, e.g. when "try again"
      or "confirm" buttons are showing, rotate the device. Buttons
      persist correctly and don't appear when unexpected
Test: Tested task stack corner cases, e.g. when "try again" is showing,
      press home button. BP dismisses and client receives ERROR_CANCELED
Test: BiometricPromptDemo receives all callbacks

Change-Id: I62126708ce8db8b358c666a07aa7c39607642c9d
diff --git a/Android.bp b/Android.bp
index 8bce769..a3ab594 100644
--- a/Android.bp
+++ b/Android.bp
@@ -159,6 +159,7 @@
         "core/java/android/hardware/biometrics/IBiometricEnabledOnKeyguardCallback.aidl",
         "core/java/android/hardware/biometrics/IBiometricService.aidl",
         "core/java/android/hardware/biometrics/IBiometricServiceReceiver.aidl",
+        "core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl",
         "core/java/android/hardware/biometrics/IBiometricServiceLockoutResetCallback.aidl",
         "core/java/android/hardware/display/IDisplayManager.aidl",
         "core/java/android/hardware/display/IDisplayManagerCallback.aidl",
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 3c7ba14..b238d77 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -263,12 +263,6 @@
         }
 
         @Override
-        public void onAuthenticationSucceededInternal(boolean requireConfirmation, byte[] bytes)
-                throws RemoteException {
-            throw new UnsupportedOperationException("Operation not supported!");
-        }
-
-        @Override
         public void onAuthenticationFailed() throws RemoteException {
             mExecutor.execute(() -> {
                 mAuthenticationCallback.onAuthenticationFailed();
@@ -283,11 +277,6 @@
         }
 
         @Override
-        public void onErrorInternal(int error, String message, int cookie) throws RemoteException {
-            throw new UnsupportedOperationException("Operation not supported!");
-        }
-
-        @Override
         public void onAcquired(int acquireInfo, String message) throws RemoteException {
             mExecutor.execute(() -> {
                 mAuthenticationCallback.onAuthenticationHelp(acquireInfo, message);
diff --git a/core/java/android/hardware/biometrics/IBiometricServiceReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricServiceReceiver.aidl
index 62222a3..22ef33e 100644
--- a/core/java/android/hardware/biometrics/IBiometricServiceReceiver.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricServiceReceiver.aidl
@@ -16,28 +16,16 @@
 package android.hardware.biometrics;
 
 /**
- * Communication channel from
- *   1) BiometricDialogImpl (SysUI) back to BiometricService
- *   2) <Biometric>Service back to BiometricService
- *   3) BiometricService back to BiometricPrompt
- * BiometricPrompt sends a receiver to BiometricService, BiometricService contains another
- * "trampoline" receiver which intercepts messages from <Biometric>Service and does some
- * logic before forwarding results as necessary to BiometricPrompt.
+ * Communication channel from BiometricService back to BiometricPrompt
  * @hide
  */
 oneway interface IBiometricServiceReceiver {
     // Notify BiometricPrompt that authentication was successful
     void onAuthenticationSucceeded();
-    // Notify BiometricService that authentication was successful. If user confirmation is required,
-    // the auth token must be submitted into KeyStore.
-    void onAuthenticationSucceededInternal(boolean requireConfirmation, in byte[] token);
     // Noties that authentication failed.
     void onAuthenticationFailed();
     // Notify BiometricPrompt that an error has occurred.
     void onError(int error, String message);
-    // Notify BiometricService than an error has occured. Forward to the correct receiver depending
-    // on the cookie.
-    void onErrorInternal(int error, String message, int cookie);
     // Notifies that a biometric has been acquired.
     void onAcquired(int acquiredInfo, String message);
     // Notifies that the SystemUI dialog has been dismissed.
diff --git a/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl b/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl
new file mode 100644
index 0000000..180daaf
--- /dev/null
+++ b/core/java/android/hardware/biometrics/IBiometricServiceReceiverInternal.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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 android.hardware.biometrics;
+
+/**
+ * Communication channel from
+ *   1) BiometricDialogImpl (SysUI) back to BiometricService
+ *   2) <Biometric>Service back to BiometricService
+ * Receives messages from the above and does some handling before forwarding to BiometricPrompt
+ * via IBiometricServiceReceiver.
+ * @hide
+ */
+oneway interface IBiometricServiceReceiverInternal {
+    // Notify BiometricService that authentication was successful. If user confirmation is required,
+    // the auth token must be submitted into KeyStore.
+    void onAuthenticationSucceeded(boolean requireConfirmation, in byte[] token);
+    // Notify BiometricService that an error has occurred.
+    void onAuthenticationFailed(int cookie, boolean requireConfirmation);
+    // Notify BiometricService than an error has occured. Forward to the correct receiver depending
+    // on the cookie.
+    void onError(int cookie, int error, String message);
+    // Notifies that a biometric has been acquired.
+    void onAcquired(int acquiredInfo, String message);
+    // Notifies that the SystemUI dialog has been dismissed.
+    void onDialogDismissed(int reason);
+    // Notifies that the user has pressed the "try again" button on SystemUI
+    void onTryAgainPressed();
+}
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index f17bfc4..a15dcec 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -15,7 +15,7 @@
  */
 package android.hardware.face;
 
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
 import android.hardware.face.IFaceServiceReceiver;
 import android.hardware.face.Face;
@@ -36,7 +36,7 @@
     // by BiometricService. To start authentication after the clients are ready, use
     // startPreparedClient().
     void prepareForAuthentication(boolean requireConfirmation, IBinder token, long sessionId,
-            int userId, IBiometricServiceReceiver wrapperReceiver, String opPackageName,
+            int userId, IBiometricServiceReceiverInternal wrapperReceiver, String opPackageName,
             int cookie, int callingUid, int callingPid, int callingUserId);
 
     // Starts authentication with the previously prepared client.
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index b859720..dd6b29d 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -15,7 +15,7 @@
  */
 package android.hardware.fingerprint;
 
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
 import android.hardware.fingerprint.IFingerprintClientActiveCallback;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
@@ -39,7 +39,7 @@
     // by BiometricService. To start authentication after the clients are ready, use
     // startPreparedClient().
     void prepareForAuthentication(IBinder token, long sessionId, int userId,
-            IBiometricServiceReceiver wrapperReceiver, String opPackageName, int cookie,
+            IBiometricServiceReceiverInternal wrapperReceiver, String opPackageName, int cookie,
             int callingUid, int callingPid, int callingUserId);
 
     // Starts authentication with the previously prepared client.
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index bb00900..600b1b3 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -18,7 +18,7 @@
 
 import android.content.ComponentName;
 import android.graphics.Rect;
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.os.Bundle;
 import android.service.notification.StatusBarNotification;
 
@@ -141,7 +141,7 @@
     void showShutdownUi(boolean isReboot, String reason);
 
     // Used to show the dialog when BiometricService starts authentication
-    void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiver receiver, int type,
+    void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type,
             boolean requireConfirmation, int userId);
     // Used to hide the dialog when a biometric is authenticated
     void onBiometricAuthenticated();
@@ -151,4 +151,6 @@
     void onBiometricError(String error);
     // Used to hide the biometric dialog when the AuthenticationClient is stopped
     void hideBiometricDialog();
+    // Used to request the "try again" button for authentications which requireConfirmation=true
+    void showBiometricTryAgain();
 }
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 40a7812..22cec17 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -20,7 +20,7 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.service.notification.StatusBarNotification;
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 
 import com.android.internal.statusbar.IStatusBar;
 import com.android.internal.statusbar.StatusBarIcon;
@@ -91,7 +91,7 @@
     void showPinningEscapeToast();
 
     // Used to show the dialog when BiometricService starts authentication
-    void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiver receiver, int type,
+    void showBiometricDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int type,
             boolean requireConfirmation, int userId);
     // Used to hide the dialog when a biometric is authenticated
     void onBiometricAuthenticated();
@@ -101,4 +101,6 @@
     void onBiometricError(String error);
     // Used to hide the biometric dialog when the AuthenticationClient is stopped
     void hideBiometricDialog();
+    // Used to request the "try again" button for authentications which requireConfirmation=true
+    void showBiometricTryAgain();
 }
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d16df59..98f591f 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1444,11 +1444,14 @@
 
     <!-- Title shown when the system-provided biometric dialog is shown, asking the user to authenticate. [CHAR LIMIT=40] -->
     <string name="biometric_dialog_default_title">Application <xliff:g id="app" example="Gmail">%s</xliff:g> wants to authenticate.</string>
-
     <!-- Message shown when biometric hardware is not available [CHAR LIMIT=50] -->
     <string name="biometric_error_hw_unavailable">Biometric hardware unavailable</string>
     <!-- Message shown when biometric authentication was canceled by the user [CHAR LIMIT=50] -->
     <string name="biometric_error_user_canceled">Authentication canceled</string>
+    <!-- Message shown by the biometric dialog when biometric is not recognized -->
+    <string name="biometric_not_recognized">Not recognized</string>
+    <!-- Message shown when biometric authentication has been canceled [CHAR LIMIT=50] -->
+    <string name="biometric_error_canceled">Authentication canceled</string>
 
     <!-- Message shown during fingerprint acquisision when the fingerprint cannot be recognized -->
     <string name="fingerprint_acquired_partial">Partial fingerprint detected. Please try again.</string>
@@ -1464,8 +1467,6 @@
     <string-array name="fingerprint_acquired_vendor">
     </string-array>
 
-    <!-- Message shown by the biometric dialog when biometric is not recognized -->
-    <string name="biometric_not_recognized">Not recognized</string>
     <!-- Accessibility message announced when a fingerprint has been authenticated [CHAR LIMIT=NONE] -->
     <string name="fingerprint_authenticated">Fingerprint authenticated</string>
     <!-- Accessibility message announced when a face has been authenticated [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 18b1206..7ab34aa 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2404,6 +2404,7 @@
   <java-symbol type="string" name="biometric_error_hw_unavailable" />
   <java-symbol type="string" name="biometric_error_user_canceled" />
   <java-symbol type="string" name="biometric_not_recognized" />
+  <java-symbol type="string" name="biometric_error_canceled" />
 
   <!-- Fingerprint messages -->
   <java-symbol type="string" name="fingerprint_error_unable_to_process" />
diff --git a/packages/SystemUI/res/layout/biometric_dialog.xml b/packages/SystemUI/res/layout/biometric_dialog.xml
index 5ca34b0..1e8cd5a7 100644
--- a/packages/SystemUI/res/layout/biometric_dialog.xml
+++ b/packages/SystemUI/res/layout/biometric_dialog.xml
@@ -160,6 +160,15 @@
                         android:maxLines="2"
                         android:text="@string/biometric_dialog_confirm"
                         android:visibility="gone"/>
+                    <!-- Try Again Button -->
+                    <Button android:id="@+id/button_try_again"
+                        android:layout_width="wrap_content"
+                        android:layout_height="match_parent"
+                        style="@*android:style/Widget.DeviceDefault.Button.Colored"
+                        android:gravity="center"
+                        android:maxLines="2"
+                        android:text="@string/biometric_dialog_try_again"
+                        android:visibility="gone"/>
                     <Space android:id="@+id/rightSpacer"
                         android:layout_width="12dip"
                         android:layout_height="match_parent"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 4a0bc9b..c49410e 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -272,6 +272,8 @@
     <string name="accessibility_biometric_dialog_help_area">Help message area</string>
     <!-- Message shown when a biometric is authenticated, asking the user to confirm authentication [CHAR LIMIT=30] -->
     <string name="biometric_dialog_confirm">Confirm</string>
+    <!-- Button name on BiometricPrompt shown when a biometric is detected but not authenticated. Tapping the button resumes authentication [CHAR_LIMIT=30] -->
+    <string name="biometric_dialog_try_again">Try again</string>
 
     <!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication -->
     <string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
index 404f749..a90a7d2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
@@ -21,7 +21,7 @@
 import android.content.res.Configuration;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricPrompt;
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
@@ -52,15 +52,20 @@
     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_BIOMETRIC_SHOW_TRY_AGAIN = 9;
+    private static final int MSG_TRY_AGAIN_PRESSED = 10;
 
     private Map<Integer, BiometricDialogView> mDialogs; // BiometricAuthenticator type, view
     private SomeArgs mCurrentDialogArgs;
     private BiometricDialogView mCurrentDialog;
     private WindowManager mWindowManager;
-    private IBiometricServiceReceiver mReceiver;
+    private IBiometricServiceReceiverInternal mReceiver;
     private boolean mDialogShowing;
     private Callback mCallback = new Callback();
 
+    private boolean mTryAgainShowing; // No good place to save state before config change :/
+    private boolean mConfirmShowing; // No good place to save state before config change :/
+
     private Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
@@ -89,6 +94,15 @@
                 case MSG_BUTTON_POSITIVE:
                     handleButtonPositive();
                     break;
+                case MSG_BIOMETRIC_SHOW_TRY_AGAIN:
+                    handleShowTryAgain();
+                    break;
+                case MSG_TRY_AGAIN_PRESSED:
+                    handleTryAgainPressed();
+                    break;
+                default:
+                    Log.w(TAG, "Unknown message: " + msg.what);
+                    break;
             }
         }
     };
@@ -96,7 +110,7 @@
     private class Callback implements DialogViewCallback {
         @Override
         public void onUserCanceled() {
-            mHandler.obtainMessage(BiometricDialogImpl.MSG_USER_CANCELED).sendToTarget();
+            mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget();
         }
 
         @Override
@@ -107,12 +121,17 @@
 
         @Override
         public void onNegativePressed() {
-            mHandler.obtainMessage(BiometricDialogImpl.MSG_BUTTON_NEGATIVE).sendToTarget();
+            mHandler.obtainMessage(MSG_BUTTON_NEGATIVE).sendToTarget();
         }
 
         @Override
         public void onPositivePressed() {
-            mHandler.obtainMessage(BiometricDialogImpl.MSG_BUTTON_POSITIVE).sendToTarget();
+            mHandler.obtainMessage(MSG_BUTTON_POSITIVE).sendToTarget();
+        }
+
+        @Override
+        public void onTryAgainPressed() {
+            mHandler.obtainMessage(MSG_TRY_AGAIN_PRESSED).sendToTarget();
         }
     }
 
@@ -139,8 +158,8 @@
     }
 
     @Override
-    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiver receiver, int type,
-            boolean requireConfirmation, int userId) {
+    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
+            int type, boolean requireConfirmation, int userId) {
         if (DEBUG) Log.d(TAG, "showBiometricDialog, type: " + type);
         // Remove these messages as they are part of the previous client
         mHandler.removeMessages(MSG_BIOMETRIC_ERROR);
@@ -180,6 +199,12 @@
         mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget();
     }
 
+    @Override
+    public void showBiometricTryAgain() {
+        if (DEBUG) Log.d(TAG, "showBiometricTryAgain");
+        mHandler.obtainMessage(MSG_BIOMETRIC_SHOW_TRY_AGAIN).sendToTarget();
+    }
+
     private void handleShowDialog(SomeArgs args, boolean skipAnimation) {
         mCurrentDialogArgs = args;
         final int type = args.argi1;
@@ -194,11 +219,13 @@
             Log.w(TAG, "Dialog already showing");
             return;
         }
-        mReceiver = (IBiometricServiceReceiver) args.arg2;
+        mReceiver = (IBiometricServiceReceiverInternal) args.arg2;
         mCurrentDialog.setBundle((Bundle)args.arg1);
         mCurrentDialog.setRequireConfirmation((boolean) args.arg3);
         mCurrentDialog.setUserId(args.argi2);
         mCurrentDialog.setSkipIntro(skipAnimation);
+        mCurrentDialog.setPendingTryAgain(mTryAgainShowing);
+        mCurrentDialog.setPendingConfirm(mConfirmShowing);
         mWindowManager.addView(mCurrentDialog, mCurrentDialog.getLayoutParams());
         mDialogShowing = true;
     }
@@ -210,7 +237,8 @@
                 mContext.getResources()
                         .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId()));
         if (mCurrentDialog.requiresConfirmation()) {
-            mCurrentDialog.showConfirmationButton();
+            mConfirmShowing = true;
+            mCurrentDialog.showConfirmationButton(true /* show */);
         } else {
             handleHideDialog(false /* userCanceled */);
         }
@@ -227,6 +255,7 @@
             if (DEBUG) Log.d(TAG, "Dialog already dismissed");
             return;
         }
+        mTryAgainShowing = false;
         mCurrentDialog.showErrorMessage(error);
     }
 
@@ -247,6 +276,8 @@
         }
         mReceiver = null;
         mDialogShowing = false;
+        mConfirmShowing = false;
+        mTryAgainShowing = false;
         mCurrentDialog.startDismiss();
     }
 
@@ -260,6 +291,7 @@
         } catch (RemoteException e) {
             Log.e(TAG, "Remote exception when handling negative button", e);
         }
+        mTryAgainShowing = false;
         handleHideDialog(false /* userCanceled */);
     }
 
@@ -273,13 +305,31 @@
         } catch (RemoteException e) {
             Log.e(TAG, "Remote exception when handling positive button", e);
         }
+        mConfirmShowing = false;
         handleHideDialog(false /* userCanceled */);
     }
 
     private void handleUserCanceled() {
+        mTryAgainShowing = false;
+        mConfirmShowing = false;
         handleHideDialog(true /* userCanceled */);
     }
 
+    private void handleShowTryAgain() {
+        mCurrentDialog.showTryAgainButton(true /* show */);
+        mTryAgainShowing = true;
+    }
+
+    private void handleTryAgainPressed() {
+        try {
+            mCurrentDialog.clearTemporaryMessage();
+            mTryAgainShowing = false;
+            mReceiver.onTryAgainPressed();
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException when handling try again", e);
+        }
+    }
+
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java
index f969f75..e085f23 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java
@@ -87,6 +87,9 @@
     protected boolean mRequireConfirmation;
     private int mUserId; // used to determine if we should show work background
 
+    private boolean mPendingShowTryAgain;
+    private boolean mPendingShowConfirm;
+
     protected abstract void updateIcon(int lastState, int newState);
     protected abstract int getHintStringResourceId();
     protected abstract int getAuthenticatedAccessibilityResourceId();
@@ -178,6 +181,7 @@
         final Button negative = mLayout.findViewById(R.id.button2);
         final Button positive = mLayout.findViewById(R.id.button1);
         final ImageView icon = mLayout.findViewById(R.id.biometric_icon);
+        final Button tryAgain = mLayout.findViewById(R.id.button_try_again);
 
         icon.setContentDescription(getResources().getString(getIconDescriptionResourceId()));
 
@@ -193,6 +197,11 @@
             mCallback.onPositivePressed();
         });
 
+        tryAgain.setOnClickListener((View v) -> {
+            showTryAgainButton(false /* show */);
+            mCallback.onTryAgainPressed();
+        });
+
         mLayout.setFocusableInTouchMode(true);
         mLayout.requestFocus();
     }
@@ -207,7 +216,6 @@
         final TextView subtitle = mLayout.findViewById(R.id.subtitle);
         final TextView description = mLayout.findViewById(R.id.description);
         final Button negative = mLayout.findViewById(R.id.button2);
-        final Button positive = mLayout.findViewById(R.id.button1);
         final ImageView backgroundView = mLayout.findViewById(R.id.background);
 
         if (mUserManager.isManagedProfile(mUserId)) {
@@ -233,8 +241,6 @@
         title.setText(titleText);
         title.setSelected(true);
 
-        positive.setVisibility(View.INVISIBLE);
-
         final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
         if (TextUtils.isEmpty(subtitleText)) {
             subtitle.setVisibility(View.GONE);
@@ -243,7 +249,8 @@
             subtitle.setText(subtitleText);
         }
 
-        final CharSequence descriptionText = mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
+        final CharSequence descriptionText =
+                mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
         if (TextUtils.isEmpty(descriptionText)) {
             description.setVisibility(View.GONE);
         } else {
@@ -253,6 +260,9 @@
 
         negative.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));
 
+        showTryAgainButton(mPendingShowTryAgain);
+        showConfirmationButton(mPendingShowConfirm);
+
         if (mWasForceRemoved || mSkipIntro) {
             // Show the dialog immediately
             mLayout.animate().cancel();
@@ -281,6 +291,7 @@
     public void startDismiss() {
         mAnimatingAway = true;
 
+        // This is where final cleanup should occur.
         final Runnable endActionRunnable = new Runnable() {
             @Override
             public void run() {
@@ -288,6 +299,9 @@
                 mAnimatingAway = false;
                 // Set the icons / text back to normal state
                 handleClearMessage();
+                showTryAgainButton(false /* show */);
+                mPendingShowTryAgain = false;
+                mPendingShowConfirm = false;
             }
         };
 
@@ -347,9 +361,13 @@
         return mRequireConfirmation;
     }
 
-    public void showConfirmationButton() {
+    public void showConfirmationButton(boolean show) {
         final Button positive = mLayout.findViewById(R.id.button1);
-        positive.setVisibility(View.VISIBLE);
+        if (show) {
+            positive.setVisibility(View.VISIBLE);
+        } else {
+            positive.setVisibility(View.GONE);
+        }
     }
 
     public void setUserId(int userId) {
@@ -378,12 +396,18 @@
                 BiometricPrompt.HIDE_DIALOG_DELAY);
     }
 
+    public void clearTemporaryMessage() {
+        mHandler.removeMessages(MSG_CLEAR_MESSAGE);
+        mHandler.obtainMessage(MSG_CLEAR_MESSAGE).sendToTarget();
+    }
+
     public void showHelpMessage(String message) {
         showTemporaryMessage(message);
     }
 
     public void showErrorMessage(String error) {
         showTemporaryMessage(error);
+        showTryAgainButton(false /* show */);
         mCallback.onErrorShown();
     }
 
@@ -392,6 +416,25 @@
         mLastState = newState;
     }
 
+    public void showTryAgainButton(boolean show) {
+        final Button tryAgain = mLayout.findViewById(R.id.button_try_again);
+        if (show) {
+            tryAgain.setVisibility(View.VISIBLE);
+        } else {
+            tryAgain.setVisibility(View.GONE);
+        }
+    }
+
+    // Set the state before the window is attached, so we know if the dialog should be started
+    // with or without the button. This is because there's no good onPause signal
+    public void setPendingTryAgain(boolean show) {
+        mPendingShowTryAgain = show;
+    }
+
+    public void setPendingConfirm(boolean show) {
+        mPendingShowConfirm = show;
+    }
+
     public WindowManager.LayoutParams getLayoutParams() {
         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java
index f388d9c..24fd22e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java
@@ -43,4 +43,9 @@
      * should be dismissed.
      */
     void onPositivePressed();
+
+    /**
+     * Invoked when the "try again" button is pressed.
+     */
+    void onTryAgainPressed();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 8bf5d14..8b93995 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -21,7 +21,7 @@
 import android.app.StatusBarManager;
 import android.content.ComponentName;
 import android.graphics.Rect;
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -96,6 +96,7 @@
     private static final int MSG_SHOW_CHARGING_ANIMATION       = 44 << MSG_SHIFT;
     private static final int MSG_SHOW_PINNING_TOAST_ENTER_EXIT = 45 << MSG_SHIFT;
     private static final int MSG_SHOW_PINNING_TOAST_ESCAPE     = 46 << MSG_SHIFT;
+    private static final int MSG_BIOMETRIC_TRY_AGAIN           = 47 << MSG_SHIFT;
 
     public static final int FLAG_EXCLUDE_NONE = 0;
     public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
@@ -163,12 +164,13 @@
 
         default void onRotationProposal(int rotation, boolean isValid) { }
 
-        default void showBiometricDialog(Bundle bundle, IBiometricServiceReceiver receiver,
+        default void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
                 int type, boolean requireConfirmation, int userId) { }
         default void onBiometricAuthenticated() { }
         default void onBiometricHelp(String message) { }
         default void onBiometricError(String error) { }
         default void hideBiometricDialog() { }
+        default void showBiometricTryAgain() { }
     }
 
     @VisibleForTesting
@@ -523,8 +525,8 @@
     }
 
     @Override
-    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiver receiver, int type,
-            boolean requireConfirmation, int userId) {
+    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
+            int type, boolean requireConfirmation, int userId) {
         synchronized (mLock) {
             SomeArgs args = SomeArgs.obtain();
             args.arg1 = bundle;
@@ -565,6 +567,13 @@
         }
     }
 
+    @Override
+    public void showBiometricTryAgain() {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_BIOMETRIC_TRY_AGAIN).sendToTarget();
+        }
+    }
+
     private final class H extends Handler {
         private H(Looper l) {
             super(l);
@@ -774,7 +783,7 @@
                     for (int i = 0; i < mCallbacks.size(); i++) {
                         mCallbacks.get(i).showBiometricDialog(
                                 (Bundle) someArgs.arg1,
-                                (IBiometricServiceReceiver) someArgs.arg2,
+                                (IBiometricServiceReceiverInternal) someArgs.arg2,
                                 someArgs.argi1 /* type */,
                                 (boolean) someArgs.arg3 /* requireConfirmation */,
                                 someArgs.argi2 /* userId */);
@@ -816,6 +825,11 @@
                         mCallbacks.get(i).showPinningEscapeToast();
                     }
                     break;
+                case MSG_BIOMETRIC_TRY_AGAIN:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).showBiometricTryAgain();
+                    }
+                    break;
             }
         }
     }
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/AuthenticationClient.java
index 40a6d9e..eaa7a83 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationClient.java
@@ -131,10 +131,8 @@
                 }
             } else {
                 if (listener != null) {
-                    listener.onAuthenticationFailed(getHalDeviceId());
                     vibrateError();
                 }
-
                 // Allow system-defined limit of number of attempts before giving up
                 final int lockoutMode = handleFailedAttempt();
                 if (lockoutMode != LOCKOUT_NONE) {
@@ -148,6 +146,18 @@
                         listener.onError(getHalDeviceId(), errorCode, 0 /* vendorCode */,
                                 getCookie());
                     }
+                } else {
+                    // Don't send onAuthenticationFailed if we're in lockout, it causes a
+                    // janky UI on Keyguard/BiometricPrompt since "authentication failed"
+                    // will show briefly and be replaced by "device locked out" message.
+                    if (listener != null) {
+                        if (isBiometricPrompt()) {
+                            listener.onAuthenticationFailedInternal(getCookie(),
+                                    getRequireConfirmation());
+                        } else {
+                            listener.onAuthenticationFailed(getHalDeviceId());
+                        }
+                    }
                 }
                 result |= lockoutMode != LOCKOUT_NONE; // in a lockout mode
             }
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index ac81b44..add55ea 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -26,7 +26,10 @@
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
 
 import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
 import android.app.AppOpsManager;
+import android.app.IActivityTaskManager;
+import android.app.TaskStackListener;
 import android.app.UserSwitchObserver;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -39,6 +42,7 @@
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.hardware.face.FaceManager;
 import android.hardware.face.IFaceService;
 import android.hardware.fingerprint.FingerprintManager;
@@ -245,29 +249,67 @@
          * Authentication started, BiometricPrompt is showing and the hardware is authenticating.
          */
         private static final int STATE_AUTH_STARTED = 2;
+        /**
+         * Authentication is paused, waiting for the user to press "try again" button. Since the
+         * try again button requires us to cancel authentication, this represents the state where
+         * ERROR_CANCELED is not received yet.
+         */
+        private static final int STATE_AUTH_PAUSED = 3;
+        /**
+         * Same as above, except the ERROR_CANCELED has been received.
+         */
+        private static final int STATE_AUTH_PAUSED_CANCELED = 4;
+        /**
+         * Authentication is successful, but we're waiting for the user to press "confirm" button.
+         */
+        private static final int STATE_AUTH_PENDING_CONFIRM = 5;
 
-        class AuthSession {
-            // Original receiver from BiometricPrompt.
-            final IBiometricServiceReceiver mClientReceiver;
+        final class AuthSession {
             // Map of Authenticator/Cookie pairs. We expect to receive the cookies back from
             // <Biometric>Services before we can start authenticating. Pairs that have been returned
             // are moved to mModalitiesMatched.
             final HashMap<Integer, Integer> mModalitiesWaiting;
             // Pairs that have been matched.
             final HashMap<Integer, Integer> mModalitiesMatched = new HashMap<>();
+
+            // The following variables are passed to authenticateInternal, which initiates the
+            // appropriate <Biometric>Services.
+            final IBinder mToken;
+            final long mSessionId;
+            final int mUserId;
+            // Original receiver from BiometricPrompt.
+            final IBiometricServiceReceiver mClientReceiver;
+            final String mOpPackageName;
             // Info to be shown on BiometricDialog when all cookies are returned.
             final Bundle mBundle;
+            final int mCallingUid;
+            final int mCallingPid;
+            final int mCallingUserId;
+            // Continue authentication with the same modality/modalities after "try again" is
+            // pressed
+            final int mModality;
+
             // The current state, which can be either idle, called, or started
             private int mState = STATE_AUTH_IDLE;
             // For explicit confirmation, do not send to keystore until the user has confirmed
             // the authentication.
             byte[] mTokenEscrow;
 
-            AuthSession(HashMap<Integer, Integer> modalities, Bundle bundle,
-                    IBiometricServiceReceiver receiver) {
+            AuthSession(HashMap<Integer, Integer> modalities, IBinder token, long sessionId,
+                    int userId, IBiometricServiceReceiver receiver, String opPackageName,
+                    Bundle bundle, int callingUid, int callingPid, int callingUserId,
+                    int modality) {
                 mModalitiesWaiting = modalities;
-                mBundle = bundle;
+                mToken = token;
+                mSessionId = sessionId;
+                mUserId = userId;
                 mClientReceiver = receiver;
+                mOpPackageName = opPackageName;
+                mBundle = bundle;
+                mCallingUid = callingUid;
+                mCallingPid = callingPid;
+                mCallingUserId = callingUserId;
+                mModality = modality;
             }
 
             boolean containsCookie(int cookie) {
@@ -281,29 +323,58 @@
             }
         }
 
+        final class BiometricTaskStackListener extends TaskStackListener {
+            @Override
+            public void onTaskStackChanged() {
+                try {
+                    final List<ActivityManager.RunningTaskInfo> runningTasks =
+                            mActivityTaskManager.getTasks(1);
+                    if (!runningTasks.isEmpty()) {
+                        final String topPackage = runningTasks.get(0).topActivity.getPackageName();
+                        if (mCurrentAuthSession != null
+                                && !topPackage.contentEquals(mCurrentAuthSession.mOpPackageName)
+                                && mCurrentAuthSession.mState != STATE_AUTH_STARTED) {
+                            // We only care about this state, since <Biometric>Service will
+                            // cancel any client that's still in STATE_AUTH_STARTED
+                            mStatusBarService.hideBiometricDialog();
+                            mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
+                            mCurrentAuthSession.mClientReceiver.onError(
+                                    BiometricConstants.BIOMETRIC_ERROR_CANCELED,
+                                    getContext().getString(
+                                            com.android.internal.R.string.biometric_error_canceled)
+                            );
+                            mCurrentAuthSession.mState = STATE_AUTH_IDLE;
+                            mCurrentAuthSession = null;
+                        }
+                    }
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to get running tasks", e);
+                }
+            }
+        }
+
+        private final IActivityTaskManager mActivityTaskManager = getContext().getSystemService(
+                ActivityTaskManager.class).getService();
         private final IStatusBarService mStatusBarService = IStatusBarService.Stub.asInterface(
                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+        private final BiometricTaskStackListener mTaskStackListener =
+                new BiometricTaskStackListener();
         private final Random mRandom = new Random();
 
-
         // The current authentication session, null if idle/done. We need to track both the current
         // and pending sessions since errors may be sent to either.
         private AuthSession mCurrentAuthSession;
         private AuthSession mPendingAuthSession;
 
         // Wrap the client's receiver so we can do things with the BiometricDialog first
-        private final IBiometricServiceReceiver mWrapperReceiver =
-                new IBiometricServiceReceiver.Stub() {
+        private final IBiometricServiceReceiverInternal mInternalReceiver =
+                new IBiometricServiceReceiverInternal.Stub() {
             @Override
-            public void onAuthenticationSucceeded() throws RemoteException {
-                throw new UnsupportedOperationException("Operation not supported");
-            }
-
-            @Override
-            public void onAuthenticationSucceededInternal(boolean requireConfirmation,
-                    byte[] token) throws RemoteException {
+            public void onAuthenticationSucceeded(boolean requireConfirmation, byte[] token)
+                    throws RemoteException {
                 try {
                     if (!requireConfirmation) {
+                        mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
                         KeyStore.getInstance().addAuthToken(token);
                         mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded();
                         mCurrentAuthSession.mState = STATE_AUTH_IDLE;
@@ -312,6 +383,7 @@
                         // Store the auth token and submit it to keystore after the confirmation
                         // button has been pressed.
                         mCurrentAuthSession.mTokenEscrow = token;
+                        mCurrentAuthSession.mState = STATE_AUTH_PENDING_CONFIRM;
                     }
 
                     // Notify SysUI that the biometric has been authenticated. SysUI already knows
@@ -323,10 +395,20 @@
             }
 
             @Override
-            public void onAuthenticationFailed() throws RemoteException {
+            public void onAuthenticationFailed(int cookie, boolean requireConfirmation)
+                    throws RemoteException {
                 try {
                     mStatusBarService.onBiometricHelp(getContext().getResources().getString(
                             com.android.internal.R.string.biometric_not_recognized));
+                    if (requireConfirmation) {
+                        mCurrentAuthSession.mState = STATE_AUTH_PAUSED;
+                        mStatusBarService.showBiometricTryAgain();
+                        // Cancel authentication. Skip the token/package check since we are
+                        // cancelling from system server. The interface is permission protected so
+                        // this is fine.
+                        cancelInternal(null /* token */, null /* package */,
+                                false /* fromClient */);
+                    }
                     mCurrentAuthSession.mClientReceiver.onAuthenticationFailed();
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Remote exception", e);
@@ -334,13 +416,7 @@
             }
 
             @Override
-            public void onError(int error, String message) throws RemoteException {
-                throw new UnsupportedOperationException("Operation not supported!");
-            }
-
-            @Override
-            public void onErrorInternal(int error, String message, int cookie)
-                    throws RemoteException {
+            public void onError(int cookie, int error, String message) throws RemoteException {
                 Slog.d(TAG, "Error: " + error + " cookie: " + cookie);
                 // Errors can either be from the current auth session or the pending auth session.
                 // The pending auth session may receive errors such as ERROR_LOCKOUT before
@@ -349,15 +425,15 @@
                 // to be started. Thus we must match error messages with their cookies to be sure
                 // of their intended receivers.
                 try {
-                    mStatusBarService.onBiometricError(message);
-
                     if (mCurrentAuthSession != null && mCurrentAuthSession.containsCookie(cookie)) {
                         if (mCurrentAuthSession.mState == STATE_AUTH_STARTED) {
+                            mStatusBarService.onBiometricError(message);
+                            mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
                             if (error == BiometricConstants.BIOMETRIC_ERROR_CANCELED) {
-                                mCurrentAuthSession.mClientReceiver.onError(error, message);
-                                mCurrentAuthSession.mState = STATE_AUTH_IDLE;
-                                mCurrentAuthSession = null;
-                                mStatusBarService.hideBiometricDialog();
+                                    mCurrentAuthSession.mClientReceiver.onError(error, message);
+                                    mCurrentAuthSession.mState = STATE_AUTH_IDLE;
+                                    mCurrentAuthSession = null;
+                                    mStatusBarService.hideBiometricDialog();
                             } else {
                                 // Send errors after the dialog is dismissed.
                                 mHandler.postDelayed(() -> {
@@ -370,6 +446,24 @@
                                     }
                                 }, BiometricPrompt.HIDE_DIALOG_DELAY);
                             }
+                        } else if (mCurrentAuthSession.mState == STATE_AUTH_PAUSED
+                                || mCurrentAuthSession.mState == STATE_AUTH_PAUSED_CANCELED) {
+                            if (mCurrentAuthSession.mState == STATE_AUTH_PAUSED
+                                    && error == BiometricConstants.BIOMETRIC_ERROR_CANCELED) {
+                                // Skip the first ERROR_CANCELED message when this happens, since
+                                // "try again" requires us to cancel authentication but keep
+                                // the prompt showing.
+                                mCurrentAuthSession.mState = STATE_AUTH_PAUSED_CANCELED;
+                            } else {
+                                // In the "try again" state, we should forward canceled errors to
+                                // the client and and clean up.
+                                mCurrentAuthSession.mClientReceiver.onError(error, message);
+                                mStatusBarService.onBiometricError(message);
+                                mActivityTaskManager.unregisterTaskStackListener(
+                                        mTaskStackListener);
+                                mCurrentAuthSession.mState = STATE_AUTH_IDLE;
+                                mCurrentAuthSession = null;
+                            }
                         } else {
                             Slog.e(TAG, "Impossible session error state: "
                                     + mCurrentAuthSession.mState);
@@ -422,9 +516,29 @@
                     KeyStore.getInstance().addAuthToken(mCurrentAuthSession.mTokenEscrow);
                     mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded();
                 }
+                mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
                 mCurrentAuthSession.mState = STATE_AUTH_IDLE;
                 mCurrentAuthSession = null;
             }
+
+            @Override
+            public void onTryAgainPressed() {
+                Slog.d(TAG, "onTryAgainPressed");
+                // No need to check permission, since it can only be invoked by SystemUI
+                // (or system server itself).
+                mHandler.post(() -> {
+                    authenticateInternal(mCurrentAuthSession.mToken,
+                            mCurrentAuthSession.mSessionId,
+                            mCurrentAuthSession.mUserId,
+                            mCurrentAuthSession.mClientReceiver,
+                            mCurrentAuthSession.mOpPackageName,
+                            mCurrentAuthSession.mBundle,
+                            mCurrentAuthSession.mCallingUid,
+                            mCurrentAuthSession.mCallingPid,
+                            mCurrentAuthSession.mCallingUserId,
+                            mCurrentAuthSession.mModality);
+                });
+            }
         };
 
         @Override // Binder call
@@ -444,6 +558,8 @@
             }
 
             if (mPendingAuthSession.mModalitiesWaiting.isEmpty()) {
+                final boolean mContinuing = mCurrentAuthSession != null
+                        && mCurrentAuthSession.mState == STATE_AUTH_PAUSED;
                 mCurrentAuthSession = mPendingAuthSession;
                 mPendingAuthSession = null;
 
@@ -465,8 +581,11 @@
                         modality |= pair.getKey();
                     }
 
-                    mStatusBarService.showBiometricDialog(mCurrentAuthSession.mBundle,
-                            mWrapperReceiver, modality, requireConfirmation, userId);
+                    if (!mContinuing) {
+                        mStatusBarService.showBiometricDialog(mCurrentAuthSession.mBundle,
+                                mInternalReceiver, modality, requireConfirmation, userId);
+                        mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
+                    }
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Remote exception", e);
                 }
@@ -475,8 +594,8 @@
 
         @Override // Binder call
         public void authenticate(IBinder token, long sessionId, int userId,
-                IBiometricServiceReceiver receiver, String opPackageName,
-                Bundle bundle) throws RemoteException {
+                IBiometricServiceReceiver receiver, String opPackageName, Bundle bundle)
+                throws RemoteException {
             final int callingUid = Binder.getCallingUid();
             final int callingPid = Binder.getCallingPid();
             final int callingUserId = UserHandle.getCallingUserId();
@@ -503,6 +622,34 @@
                     bundle.getBoolean(BiometricPrompt.KEY_USE_DEFAULT_TITLE, false);
             if (useDefaultTitle) {
                 checkInternalPermission();
+                // Set the default title if necessary
+                try {
+                    if (useDefaultTitle) {
+                        final List<ActivityManager.RunningAppProcessInfo> procs =
+                                ActivityManager.getService().getRunningAppProcesses();
+                        for (int i = 0; i < procs.size(); i++) {
+                            final ActivityManager.RunningAppProcessInfo info = procs.get(i);
+                            if (info.uid == callingUid
+                                    && info.importance == IMPORTANCE_FOREGROUND) {
+                                PackageManager pm = getContext().getPackageManager();
+                                final CharSequence label = pm.getApplicationLabel(
+                                        pm.getApplicationInfo(info.processName,
+                                                PackageManager.GET_META_DATA));
+                                final String title = getContext()
+                                        .getString(R.string.biometric_dialog_default_title, label);
+                                if (TextUtils.isEmpty(
+                                        bundle.getCharSequence(BiometricPrompt.KEY_TITLE))) {
+                                    bundle.putCharSequence(BiometricPrompt.KEY_TITLE, title);
+                                }
+                                break;
+                            }
+                        }
+                    }
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Remote exception", e);
+                } catch (PackageManager.NameNotFoundException e) {
+                    Slog.e(TAG, "Name not found", e);
+                }
             }
 
             mHandler.post(() -> {
@@ -536,80 +683,91 @@
                     return;
                 }
 
-                // Set the default title if necessary
-                try {
-                    if (useDefaultTitle) {
-                        final List<ActivityManager.RunningAppProcessInfo> procs =
-                                ActivityManager.getService().getRunningAppProcesses();
-                        for (int i = 0; i < procs.size(); i++) {
-                            final ActivityManager.RunningAppProcessInfo info = procs.get(i);
-                            if (info.uid == callingUid
-                                    && info.importance == IMPORTANCE_FOREGROUND) {
-                                PackageManager pm = getContext().getPackageManager();
-                                final CharSequence label = pm.getApplicationLabel(
-                                        pm.getApplicationInfo(info.processName,
-                                                PackageManager.GET_META_DATA));
-                                final String title = getContext()
-                                        .getString(R.string.biometric_dialog_default_title, label);
-                                if (TextUtils.isEmpty(
-                                        bundle.getCharSequence(BiometricPrompt.KEY_TITLE))) {
-                                    bundle.putCharSequence(BiometricPrompt.KEY_TITLE, title);
-                                }
-                                break;
-                            }
-                        }
-                    }
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Remote exception", e);
-                } catch (PackageManager.NameNotFoundException e) {
-                    Slog.e(TAG, "Name not found", e);
-                }
+                mCurrentModality = modality;
 
                 // Actually start authentication
-                mCurrentModality = modality;
-                try {
-                    // Generate random cookies to pass to the services that should prepare to start
-                    // authenticating. Store the cookie here and wait for all services to "ack"
-                    // with the cookie. Once all cookies are received, we can show the prompt
-                    // and let the services start authenticating. The cookie should be non-zero.
-                    final int cookie = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
-                    Slog.d(TAG, "Creating auth session. Modality: " + mCurrentModality
-                            + ", cookie: " + cookie);
-                    HashMap<Integer, Integer> authenticators = new HashMap<>();
-                    authenticators.put(mCurrentModality, cookie);
-                    mPendingAuthSession = new AuthSession(authenticators, bundle, receiver);
-                    mPendingAuthSession.mState = STATE_AUTH_CALLED;
-                    // No polymorphism :(
-                    if ((mCurrentModality & TYPE_FINGERPRINT) != 0) {
-                        mFingerprintService.prepareForAuthentication(token, sessionId, userId,
-                                mWrapperReceiver, opPackageName, cookie,
-                                callingUid, callingPid, callingUserId);
-                    }
-                    if ((mCurrentModality & TYPE_IRIS) != 0) {
-                        Slog.w(TAG, "Iris unsupported");
-                    }
-                    if ((mCurrentModality & TYPE_FACE) != 0) {
-                        mFaceService.prepareForAuthentication(true /* requireConfirmation */,
-                                token, sessionId, userId, mWrapperReceiver, opPackageName,
-                                cookie, callingUid, callingPid, callingUserId);
-                    }
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Unable to start authentication", e);
-                }
-
+                authenticateInternal(token, sessionId, userId, receiver, opPackageName, bundle,
+                        callingUid, callingPid, callingUserId, modality);
             });
         }
 
+        /**
+         * authenticate() (above) which is called from BiometricPrompt determines which
+         * modality/modalities to start authenticating with. authenticateInternal() should only be
+         * used for:
+         * 1) Preparing <Biometric>Services for authentication when BiometricPrompt#authenticate is,
+         *    invoked, shortly after which BiometricPrompt is shown and authentication starts
+         * 2) Preparing <Biometric>Services for authentication when BiometricPrompt is already shown
+         *    and the user has pressed "try again"
+         */
+        private void authenticateInternal(IBinder token, long sessionId, int userId,
+                IBiometricServiceReceiver receiver, String opPackageName, Bundle bundle,
+                int callingUid, int callingPid, int callingUserId, int modality) {
+            try {
+                // Generate random cookies to pass to the services that should prepare to start
+                // authenticating. Store the cookie here and wait for all services to "ack"
+                // with the cookie. Once all cookies are received, we can show the prompt
+                // and let the services start authenticating. The cookie should be non-zero.
+                final int cookie = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
+                Slog.d(TAG, "Creating auth session. Modality: " + modality
+                        + ", cookie: " + cookie);
+                final HashMap<Integer, Integer> authenticators = new HashMap<>();
+                authenticators.put(modality, cookie);
+                mPendingAuthSession = new AuthSession(authenticators, token, sessionId, userId,
+                        receiver, opPackageName, bundle, callingUid, callingPid, callingUserId,
+                        modality);
+                mPendingAuthSession.mState = STATE_AUTH_CALLED;
+                // No polymorphism :(
+                if ((modality & TYPE_FINGERPRINT) != 0) {
+                    mFingerprintService.prepareForAuthentication(token, sessionId, userId,
+                            mInternalReceiver, opPackageName, cookie,
+                            callingUid, callingPid, callingUserId);
+                }
+                if ((modality & TYPE_IRIS) != 0) {
+                    Slog.w(TAG, "Iris unsupported");
+                }
+                if ((modality & TYPE_FACE) != 0) {
+                    mFaceService.prepareForAuthentication(true /* requireConfirmation */,
+                            token, sessionId, userId, mInternalReceiver, opPackageName,
+                            cookie, callingUid, callingPid, callingUserId);
+                }
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Unable to start authentication", e);
+            }
+        }
+
         @Override // Binder call
         public void cancelAuthentication(IBinder token, String opPackageName)
                 throws RemoteException {
             checkPermission();
-
             if (token == null || opPackageName == null) {
                 Slog.e(TAG, "Unable to cancel, one or more null arguments");
                 return;
             }
-            cancelInternal(token, opPackageName, true /* fromClient */);
+
+            // We need to check the current authenticators state. If we're pending confirm
+            // or idle, we need to dismiss the dialog and send an ERROR_CANCELED to the client,
+            // since we won't be getting an onError from the driver.
+            if (mCurrentAuthSession != null && mCurrentAuthSession.mState != STATE_AUTH_STARTED) {
+                mHandler.post(() -> {
+                    try {
+                        // Send error to client
+                        mCurrentAuthSession.mClientReceiver.onError(
+                                BiometricConstants.BIOMETRIC_ERROR_CANCELED,
+                                getContext().getString(
+                                        com.android.internal.R.string.biometric_error_user_canceled)
+                        );
+
+                        mCurrentAuthSession.mState = STATE_AUTH_IDLE;
+                        mCurrentAuthSession = null;
+                        mStatusBarService.hideBiometricDialog();
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Remote exception", e);
+                    }
+                });
+            } else {
+                cancelInternal(token, opPackageName, true /* fromClient */);
+            }
         }
 
         @Override // Binder call
diff --git a/services/core/java/com/android/server/biometrics/BiometricServiceBase.java b/services/core/java/com/android/server/biometrics/BiometricServiceBase.java
index 3207db4..9649ccd 100644
--- a/services/core/java/com/android/server/biometrics/BiometricServiceBase.java
+++ b/services/core/java/com/android/server/biometrics/BiometricServiceBase.java
@@ -37,7 +37,7 @@
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.hardware.fingerprint.Fingerprint;
 import android.os.Binder;
 import android.os.Bundle;
@@ -349,8 +349,14 @@
             throw new UnsupportedOperationException("Stub!");
         }
 
-        void onAuthenticationFailed(long deviceId)
-                throws RemoteException;
+        default void onAuthenticationFailed(long deviceId) throws RemoteException {
+            throw new UnsupportedOperationException("Stub!");
+        }
+
+        default void onAuthenticationFailedInternal(int cookie, boolean requireConfirmation)
+                throws RemoteException {
+            throw new UnsupportedOperationException("Stub!");
+        }
 
         void onError(long deviceId, int error, int vendorCode, int cookie) throws RemoteException;
 
@@ -365,14 +371,13 @@
      * Wraps the callback interface from Service -> BiometricPrompt
      */
     protected abstract class BiometricServiceListener implements ServiceListener {
-        // We should send results using the wrapper receiver.
-        private IBiometricServiceReceiver mWrapperReceiver;
+        private IBiometricServiceReceiverInternal mWrapperReceiver;
 
-        public BiometricServiceListener(IBiometricServiceReceiver wrapperReceiver) {
+        public BiometricServiceListener(IBiometricServiceReceiverInternal wrapperReceiver) {
             mWrapperReceiver = wrapperReceiver;
         }
 
-        public IBiometricServiceReceiver getWrapperReceiver() {
+        public IBiometricServiceReceiverInternal getWrapperReceiver() {
             return mWrapperReceiver;
         }
 
@@ -380,14 +385,15 @@
         public void onAuthenticationSucceededInternal(boolean requireConfirmation, byte[] token)
                 throws RemoteException {
             if (getWrapperReceiver() != null) {
-                getWrapperReceiver().onAuthenticationSucceededInternal(requireConfirmation, token);
+                getWrapperReceiver().onAuthenticationSucceeded(requireConfirmation, token);
             }
         }
 
         @Override
-        public void onAuthenticationFailed(long deviceId) throws RemoteException {
+        public void onAuthenticationFailedInternal(int cookie, boolean requireConfirmation)
+                throws RemoteException {
             if (getWrapperReceiver() != null) {
-                getWrapperReceiver().onAuthenticationFailed();
+                getWrapperReceiver().onAuthenticationFailed(cookie, requireConfirmation);
             }
         }
     }
diff --git a/services/core/java/com/android/server/biometrics/face/FaceService.java b/services/core/java/com/android/server/biometrics/face/FaceService.java
index 2a0fbee1..557af04 100644
--- a/services/core/java/com/android/server/biometrics/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/face/FaceService.java
@@ -28,7 +28,7 @@
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.hardware.biometrics.face.V1_0.IBiometricsFace;
 import android.hardware.biometrics.face.V1_0.IBiometricsFaceClientCallback;
 import android.hardware.biometrics.face.V1_0.Status;
@@ -147,8 +147,9 @@
 
         @Override // Binder call
         public void prepareForAuthentication(boolean requireConfirmation, IBinder token, long opId,
-                int groupId, IBiometricServiceReceiver wrapperReceiver, String opPackageName,
-                int cookie, int callingUid, int callingPid, int callingUserId) {
+                int groupId, IBiometricServiceReceiverInternal wrapperReceiver,
+                String opPackageName, int cookie, int callingUid, int callingPid,
+                int callingUserId) {
             checkPermission(USE_BIOMETRIC_INTERNAL);
             final boolean restricted = true; // BiometricPrompt is always restricted
             final AuthenticationClientImpl client = new FaceAuthClient(getContext(),
@@ -389,7 +390,7 @@
      * BiometricPrompt.
      */
     private class BiometricPromptServiceListenerImpl extends BiometricServiceListener {
-        BiometricPromptServiceListenerImpl(IBiometricServiceReceiver wrapperReceiver) {
+        BiometricPromptServiceListenerImpl(IBiometricServiceReceiverInternal wrapperReceiver) {
             super(wrapperReceiver);
         }
 
@@ -410,9 +411,8 @@
         public void onError(long deviceId, int error, int vendorCode, int cookie)
                 throws RemoteException {
             if (getWrapperReceiver() != null) {
-                getWrapperReceiver().onErrorInternal(error,
-                        FaceManager.getErrorString(getContext(), error, vendorCode),
-                        cookie);
+                getWrapperReceiver().onError(cookie, error,
+                        FaceManager.getErrorString(getContext(), error, vendorCode));
             }
         }
     }
diff --git a/services/core/java/com/android/server/biometrics/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/fingerprint/FingerprintService.java
index 3cb1b00..6a5bc61 100644
--- a/services/core/java/com/android/server/biometrics/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/fingerprint/FingerprintService.java
@@ -31,7 +31,7 @@
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprintClientCallback;
 import android.hardware.fingerprint.Fingerprint;
@@ -169,7 +169,7 @@
 
         @Override // Binder call
         public void prepareForAuthentication(IBinder token, long opId, int groupId,
-                IBiometricServiceReceiver wrapperReceiver, String opPackageName,
+                IBiometricServiceReceiverInternal wrapperReceiver, String opPackageName,
                 int cookie, int callingUid, int callingPid, int callingUserId) {
             checkPermission(MANAGE_BIOMETRIC);
             final boolean restricted = true; // BiometricPrompt is always restricted
@@ -375,7 +375,7 @@
      * BiometricPrompt.
      */
     private class BiometricPromptServiceListenerImpl extends BiometricServiceListener {
-        BiometricPromptServiceListenerImpl(IBiometricServiceReceiver wrapperReceiver) {
+        BiometricPromptServiceListenerImpl(IBiometricServiceReceiverInternal wrapperReceiver) {
             super(wrapperReceiver);
         }
 
@@ -392,9 +392,8 @@
         public void onError(long deviceId, int error, int vendorCode, int cookie)
                 throws RemoteException {
             if (getWrapperReceiver() != null) {
-                getWrapperReceiver().onErrorInternal(error,
-                        FingerprintManager.getErrorString(getContext(), error, vendorCode),
-                        cookie);
+                getWrapperReceiver().onError(cookie, error,
+                        FingerprintManager.getErrorString(getContext(), error, vendorCode));
             }
         }
     }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 45452be..64bcea2 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -23,7 +23,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Rect;
-import android.hardware.biometrics.IBiometricServiceReceiver;
+import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
@@ -567,8 +567,8 @@
     }
 
     @Override
-    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiver receiver, int type,
-            boolean requireConfirmation, int userId) {
+    public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
+            int type, boolean requireConfirmation, int userId) {
         enforceBiometricDialog();
         if (mBar != null) {
             try {
@@ -623,6 +623,17 @@
     }
 
     @Override
+    public void showBiometricTryAgain() {
+        enforceBiometricDialog();
+        if (mBar != null) {
+            try {
+                mBar.showBiometricTryAgain();
+            } catch (RemoteException ex) {
+            }
+        }
+    }
+
+    @Override
     public void disable(int what, IBinder token, String pkg) {
         disableForUser(what, token, pkg, mCurrentUserId);
     }