3/n: Move task stack listener to SystemUI

Simplifies the dismissal of the dialog, and allows for better
synchronization of when the client should receive the error. The only time
that BiometricService should dismiss the dialog is when authentication
is canceled due to another client, which is almost always due to
allowed-but-weird app behavior.

Bug: 135082347

Test: atest BiometricServiceTest
Test: atest BiometricDialogImplTest
Test: atest CommandQueueTest
Change-Id: I10daa798115e51af8a854759e30033c28e6636ba
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java
index ca9d372..d4baefd 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java
@@ -57,13 +57,16 @@
     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
+     * Dismiss the dialog without sending a callback.
      */
     void dismissWithoutCallback(boolean animate);
 
     /**
+     * Dismiss the dialog. Animate away.
+     */
+    void dismissFromSystemServer();
+
+    /**
      * Biometric authenticated. May be pending user confirmation, or completed.
      */
     void onAuthenticationSucceeded();
@@ -97,4 +100,9 @@
      * @param savedState
      */
     void restoreState(Bundle savedState);
+
+    /**
+     * Get the client's package name
+     */
+    String getOpPackageName();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
index 0c3cea7..a8e5722 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java
@@ -16,21 +16,30 @@
 
 package com.android.systemui.biometrics;
 
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.IActivityTaskManager;
+import android.app.TaskStackListener;
 import android.content.Context;
 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.os.Handler;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.WindowManager;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.SomeArgs;
 import com.android.systemui.SystemUI;
 import com.android.systemui.biometrics.ui.BiometricDialogView;
 import com.android.systemui.statusbar.CommandQueue;
 
+import java.util.List;
+
 /**
  * Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
  * appropriate biometric UI (e.g. BiometricDialogView).
@@ -40,12 +49,51 @@
     private static final String TAG = "BiometricDialogImpl";
     private static final boolean DEBUG = true;
 
+    private final Injector mInjector;
+
     // TODO: These should just be saved from onSaveState
     private SomeArgs mCurrentDialogArgs;
-    private BiometricDialog mCurrentDialog;
+    @VisibleForTesting
+    BiometricDialog mCurrentDialog;
 
+    private Handler mHandler = new Handler(Looper.getMainLooper());
     private WindowManager mWindowManager;
-    private IBiometricServiceReceiverInternal mReceiver;
+    @VisibleForTesting
+    IActivityTaskManager mActivityTaskManager;
+    @VisibleForTesting
+    BiometricTaskStackListener mTaskStackListener;
+    @VisibleForTesting
+    IBiometricServiceReceiverInternal mReceiver;
+
+    public class BiometricTaskStackListener extends TaskStackListener {
+        @Override
+        public void onTaskStackChanged() {
+            mHandler.post(mTaskStackChangedRunnable);
+        }
+    }
+
+    private final Runnable mTaskStackChangedRunnable = () -> {
+        if (mCurrentDialog != null) {
+            try {
+                final String clientPackage = mCurrentDialog.getOpPackageName();
+                Log.w(TAG, "Task stack changed, current client: " + clientPackage);
+                final List<ActivityManager.RunningTaskInfo> runningTasks =
+                        mActivityTaskManager.getTasks(1);
+                if (!runningTasks.isEmpty()) {
+                    final String topPackage = runningTasks.get(0).topActivity.getPackageName();
+                    if (!topPackage.contentEquals(clientPackage)) {
+                        Log.w(TAG, "Evicting client due to: " + topPackage);
+                        mCurrentDialog.dismissWithoutCallback(true /* animate */);
+                        mCurrentDialog = null;
+                        mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
+                        mReceiver = null;
+                    }
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Remote exception", e);
+            }
+        }
+    };
 
     @Override
     public void onTryAgainPressed() {
@@ -57,7 +105,7 @@
     }
 
     @Override
-    public void onDismissed(int reason) {
+    public void onDismissed(@DismissedReason int reason) {
         switch (reason) {
             case DialogViewCallback.DISMISSED_USER_CANCELED:
                 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
@@ -78,23 +126,43 @@
             case DialogViewCallback.DISMISSED_ERROR:
                 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR);
                 break;
+
+            case DialogViewCallback.DISMISSED_BY_SYSTEM_SERVER:
+                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED);
+                break;
+
             default:
                 Log.e(TAG, "Unhandled reason: " + reason);
                 break;
         }
     }
 
-    private void sendResultAndCleanUp(int result) {
+    private void sendResultAndCleanUp(@DismissedReason int reason) {
         if (mReceiver == null) {
             Log.e(TAG, "Receiver is null");
             return;
         }
         try {
-            mReceiver.onDialogDismissed(result);
+            mReceiver.onDialogDismissed(reason);
         } catch (RemoteException e) {
             Log.w(TAG, "Remote exception", e);
         }
-        onDialogDismissed();
+        onDialogDismissed(reason);
+    }
+
+    public static class Injector {
+        IActivityTaskManager getActivityTaskManager() {
+            return ActivityTaskManager.getService();
+        }
+    }
+
+    public BiometricDialogImpl() {
+        this(new Injector());
+    }
+
+    @VisibleForTesting
+    BiometricDialogImpl(Injector injector) {
+        mInjector = injector;
     }
 
     @Override
@@ -105,12 +173,20 @@
                 || pm.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
             getComponent(CommandQueue.class).addCallback(this);
             mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+            mActivityTaskManager = mInjector.getActivityTaskManager();
+
+            try {
+                mTaskStackListener = new BiometricTaskStackListener();
+                mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Unable to register task stack listener", e);
+            }
         }
     }
 
     @Override
     public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
-            int type, boolean requireConfirmation, int userId) {
+            int type, boolean requireConfirmation, int userId, String opPackageName) {
         if (DEBUG) {
             Log.d(TAG, "showBiometricDialog, type: " + type
                     + ", requireConfirmation: " + requireConfirmation);
@@ -121,13 +197,14 @@
         args.argi1 = type;
         args.arg3 = requireConfirmation;
         args.argi2 = userId;
+        args.arg4 = opPackageName;
 
         boolean skipAnimation = false;
         if (mCurrentDialog != null) {
             Log.w(TAG, "mCurrentDialog: " + mCurrentDialog);
             skipAnimation = true;
         }
-        showDialog(args, skipAnimation /* skipAnimation */, null /* savedState */);
+        showDialog(args, skipAnimation, null /* savedState */);
     }
 
     @Override
@@ -159,20 +236,24 @@
     public void hideBiometricDialog() {
         if (DEBUG) Log.d(TAG, "hideBiometricDialog");
 
-        // TODO: I think we need to remove this interface
-        mCurrentDialog.dismissWithoutCallback(true /* animate */);
+        mCurrentDialog.dismissFromSystemServer();
     }
 
     private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
         mCurrentDialogArgs = args;
         final int type = args.argi1;
+        final Bundle biometricPromptBundle = (Bundle) args.arg1;
+        final boolean requireConfirmation = (boolean) args.arg3;
+        final int userId = args.argi2;
+        final String opPackageName = (String) args.arg4;
 
         // 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);
+                biometricPromptBundle,
+                requireConfirmation,
+                userId,
+                type,
+                opPackageName);
 
         if (newDialog == null) {
             Log.e(TAG, "Unsupported type: " + type);
@@ -204,8 +285,8 @@
         mCurrentDialog.show(mWindowManager, skipAnimation);
     }
 
-    private void onDialogDismissed() {
-        if (DEBUG) Log.d(TAG, "onDialogDismissed");
+    private void onDialogDismissed(@DismissedReason int reason) {
+        if (DEBUG) Log.d(TAG, "onDialogDismissed: " + reason);
         if (mCurrentDialog == null) {
             Log.w(TAG, "Dialog already dismissed");
         }
@@ -229,12 +310,13 @@
     }
 
     protected BiometricDialog buildDialog(Bundle biometricPromptBundle,
-            boolean requireConfirmation, int userId, int type) {
+            boolean requireConfirmation, int userId, int type, String opPackageName) {
         return new BiometricDialogView.Builder(mContext)
                 .setCallback(this)
                 .setBiometricPromptBundle(biometricPromptBundle)
                 .setRequireConfirmation(requireConfirmation)
                 .setUserId(userId)
+                .setOpPackageName(opPackageName)
                 .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 acad62a..b65d1e8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.biometrics;
 
+import android.annotation.IntDef;
+
 /**
  * Callback interface for dialog views. These should be implemented by the controller (e.g.
  * FingerprintDialogImpl) and passed into their views (e.g. FingerprintDialogView).
@@ -28,12 +30,21 @@
 
     int DISMISSED_AUTHENTICATED = 4;
     int DISMISSED_ERROR = 5;
+    int DISMISSED_BY_SYSTEM_SERVER = 6;
+
+    @IntDef({DISMISSED_USER_CANCELED,
+            DISMISSED_BUTTON_NEGATIVE,
+            DISMISSED_BUTTON_POSITIVE,
+            DISMISSED_AUTHENTICATED,
+            DISMISSED_ERROR,
+            DISMISSED_BY_SYSTEM_SERVER})
+    @interface DismissedReason {}
 
     /**
      * Invoked when the dialog is dismissed
      * @param reason
      */
-    void onDismissed(int reason);
+    void onDismissed(@DismissedReason int reason);
 
     /**
      * Invoked when the "try again" button is clicked
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java
index a1687ac..66718c1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java
@@ -109,6 +109,7 @@
 
     private Bundle mBundle;
     private Bundle mRestoredState;
+    private String mOpPackageName;
 
     private int mState = STATE_IDLE;
     private boolean mWasForceRemoved;
@@ -180,6 +181,7 @@
         private Bundle mBundle;
         private boolean mRequireConfirmation;
         private int mUserId;
+        private String mOpPackageName;
 
         public Builder(Context context) {
             mContext = context;
@@ -205,6 +207,11 @@
             return this;
         }
 
+        public Builder setOpPackageName(String opPackageName) {
+            mOpPackageName = opPackageName;
+            return this;
+        }
+
         public BiometricDialogView build(int type) {
             BiometricDialogView dialog;
             if (type == TYPE_FINGERPRINT) {
@@ -217,6 +224,7 @@
             dialog.setBundle(mBundle);
             dialog.setRequireConfirmation(mRequireConfirmation);
             dialog.setUserId(mUserId);
+            dialog.setOpPackageName(mOpPackageName);
             return dialog;
         }
     }
@@ -246,18 +254,12 @@
         addView(mLayout);
 
         mLayout.setOnKeyListener(new View.OnKeyListener() {
-            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 && mDownPressed == false) {
-                    mDownPressed = true;
-                } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
-                    mDownPressed = false;
-                } else if (event.getAction() == KeyEvent.ACTION_UP && mDownPressed == true) {
-                    mDownPressed = false;
+                if (event.getAction() == KeyEvent.ACTION_UP) {
                     animateAway(DialogViewCallback.DISMISSED_USER_CANCELED);
                 }
                 return true;
@@ -421,14 +423,15 @@
         });
     }
 
-    private void animateAway(int reason) {
+    private void animateAway(@DialogViewCallback.DismissedReason 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) {
+    private void animateAway(boolean sendReason, @DialogViewCallback.DismissedReason int reason) {
         if (!mCompletedAnimatingIn) {
             Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
             mPendingDismissDialog = true;
@@ -493,6 +496,10 @@
         mUserId = userId;
     }
 
+    private void setOpPackageName(String opPackageName) {
+        mOpPackageName = opPackageName;
+    }
+
     // Shows an error/help message
     protected void showTemporaryMessage(String message) {
         mHandler.removeMessages(MSG_RESET_MESSAGE);
@@ -518,7 +525,7 @@
     @Override
     public void dismissWithoutCallback(boolean animate) {
         if (animate) {
-            animateAway(false /* sendReason */, 0);
+            animateAway(false /* sendReason */, 0 /* reason */);
         } else {
             mLayout.animate().cancel();
             mDialog.animate().cancel();
@@ -528,6 +535,11 @@
     }
 
     @Override
+    public void dismissFromSystemServer() {
+        animateAway(DialogViewCallback.DISMISSED_BY_SYSTEM_SERVER);
+    }
+
+    @Override
     public void onAuthenticationSucceeded() {
         announceForAccessibility(getResources().getText(getAuthenticatedAccessibilityResourceId()));
 
@@ -613,6 +625,11 @@
         }
     }
 
+    @Override
+    public String getOpPackageName() {
+        return mOpPackageName;
+    }
+
     protected void updateState(int newState) {
         if (newState == STATE_PENDING_CONFIRMATION) {
             mHandler.removeMessages(MSG_RESET_MESSAGE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index fa0fe13..134d4b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -271,7 +271,7 @@
         default void onRotationProposal(int rotation, boolean isValid) { }
 
         default void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
-                int type, boolean requireConfirmation, int userId) { }
+                int type, boolean requireConfirmation, int userId, String opPackageName) { }
         default void onBiometricAuthenticated(boolean authenticated, String failureReason) { }
         default void onBiometricHelp(String message) { }
         default void onBiometricError(String error) { }
@@ -741,7 +741,7 @@
 
     @Override
     public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
-            int type, boolean requireConfirmation, int userId) {
+            int type, boolean requireConfirmation, int userId, String opPackageName) {
         synchronized (mLock) {
             SomeArgs args = SomeArgs.obtain();
             args.arg1 = bundle;
@@ -749,6 +749,7 @@
             args.argi1 = type;
             args.arg3 = requireConfirmation;
             args.argi2 = userId;
+            args.arg4 = opPackageName;
             mHandler.obtainMessage(MSG_BIOMETRIC_SHOW, args)
                     .sendToTarget();
         }
@@ -1036,7 +1037,8 @@
                                 (IBiometricServiceReceiverInternal) someArgs.arg2,
                                 someArgs.argi1 /* type */,
                                 (boolean) someArgs.arg3 /* requireConfirmation */,
-                                someArgs.argi2 /* userId */);
+                                someArgs.argi2 /* userId */,
+                                (String) someArgs.arg4 /* opPackageName */);
                     }
                     someArgs.recycle();
                     break;