Let BiometricPrompt directly launch CDC activity

Currently, BiometricPrompt fails with an error when attempting to
authenticate with no supported biometrics enrolled on API < 29. If
allowing device credential auth, however, the prompt could simply fall
back to that form of authentication in this case. This commit makes that
change, bringing support for this option up to parity with API 29.

Test: ./gradlew biometric:connectedAndroidTest
Test: Manually, using biometric demo app on API 23, 27, 28, and 29.

Bug: 140750912
Change-Id: I863daed71c1267f0670ba8dfc91d0c95bf46cd2c
diff --git a/biometric/src/androidTest/java/androidx/biometric/DeviceCredentialHandlerBridgeTest.java b/biometric/src/androidTest/java/androidx/biometric/DeviceCredentialHandlerBridgeTest.java
index 1d55d79..1c82fc1 100644
--- a/biometric/src/androidTest/java/androidx/biometric/DeviceCredentialHandlerBridgeTest.java
+++ b/biometric/src/androidTest/java/androidx/biometric/DeviceCredentialHandlerBridgeTest.java
@@ -176,6 +176,15 @@
     }
 
     @Test
+    public void testConfirmingDeviceCredential_CanSetAndGet() {
+        final DeviceCredentialHandlerBridge bridge = DeviceCredentialHandlerBridge.getInstance();
+        assertThat(bridge.isConfirmingDeviceCredential()).isFalse();
+
+        bridge.setConfirmingDeviceCredential(true);
+        assertThat(bridge.isConfirmingDeviceCredential()).isTrue();
+    }
+
+    @Test
     @UiThreadTest
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
     public void testReset_ClearsBiometricFragment_WhenNotIgnoringReset() {
@@ -187,17 +196,25 @@
 
     @Test
     @UiThreadTest
-    public void testReset_ClearsMostState_WhenNotIgnoringReset() {
+    public void testReset_ClearsState_WhenNotIgnoringReset() {
         final DeviceCredentialHandlerBridge bridge = DeviceCredentialHandlerBridge.getInstance();
+        bridge.setClientThemeResId(1);
         bridge.setFingerprintFragments(
                 FingerprintDialogFragment.newInstance(), FingerprintHelperFragment.newInstance());
         bridge.setCallbacks(EXECUTOR, CLICK_LISTENER, AUTH_CALLBACK);
+        bridge.setDeviceCredentialResult(DeviceCredentialHandlerBridge.RESULT_SUCCESS);
+        bridge.setConfirmingDeviceCredential(true);
+
         bridge.reset();
+        assertThat(bridge.getClientThemeResId()).isEqualTo(0);
         assertThat(bridge.getFingerprintDialogFragment()).isNull();
         assertThat(bridge.getFingerprintHelperFragment()).isNull();
         assertThat(bridge.getExecutor()).isNull();
         assertThat(bridge.getOnClickListener()).isNull();
         assertThat(bridge.getAuthenticationCallback()).isNull();
+        assertThat(bridge.getDeviceCredentialResult()).isEqualTo(
+                DeviceCredentialHandlerBridge.RESULT_NONE);
+        assertThat(bridge.isConfirmingDeviceCredential()).isFalse();
         assertThat(DeviceCredentialHandlerBridge.getInstanceIfNotNull()).isNull();
     }
 
diff --git a/biometric/src/main/java/androidx/biometric/BiometricPrompt.java b/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
index 5959f79..974a3d90 100644
--- a/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
+++ b/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
@@ -301,10 +301,10 @@
              * will be returned in
              * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
              *
-             * Note that {@link Builder#setNegativeButtonText(CharSequence)} should not be set
+             * <p>Note that {@link Builder#setNegativeButtonText(CharSequence)} should not be set
              * if this is set to true.
              *
-             * On versions P and below, once the device credential prompt is shown,
+             * <p>On versions P and below, once the device credential prompt is shown,
              * {@link #cancelAuthentication()} will not work, since the library internally launches
              * {@link android.app.KeyguardManager#createConfirmDeviceCredentialIntent(CharSequence,
              * CharSequence)}, which does not have a public API for cancellation.
@@ -664,10 +664,37 @@
 
     private void authenticateInternal(@NonNull PromptInfo info, @Nullable CryptoObject crypto) {
         mIsHandlingDeviceCredential = info.isHandlingDeviceCredentialResult();
-        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && info.isDeviceCredentialAllowed()
-                && !mIsHandlingDeviceCredential) {
-            launchDeviceCredentialHandler(info);
-            return;
+        if (info.isDeviceCredentialAllowed() && Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+            // Launch handler activity to support device credential on older versions.
+            if (!mIsHandlingDeviceCredential) {
+                launchDeviceCredentialHandler(info);
+                return;
+            }
+
+            // Fall back to device credential immediately if no biometrics are enrolled.
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                final FragmentActivity activity = getActivity();
+                if (activity == null) {
+                    Log.e(TAG, "Failed to authenticate with device credential. Activity was null.");
+                    return;
+                }
+
+                final DeviceCredentialHandlerBridge bridge =
+                        DeviceCredentialHandlerBridge.getInstanceIfNotNull();
+                if (bridge == null) {
+                    Log.e(TAG, "Failed to authenticate with device credential. Bridge was null.");
+                    return;
+                }
+
+                if (!bridge.isConfirmingDeviceCredential()) {
+                    final BiometricManager biometricManager = BiometricManager.from(activity);
+                    if (biometricManager.canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) {
+                        Utils.launchDeviceCredentialConfirmation(
+                                TAG, activity, info.getBundle(), null /* onLaunch */);
+                        return;
+                    }
+                }
+            }
         }
 
         final Bundle bundle = info.getBundle();
@@ -757,8 +784,8 @@
      * Cancels the biometric authentication, and dismisses the dialog upon confirmation from the
      * biometric service.
      *
-     * On P or below, calling this method when the device credential prompt is shown will NOT work
-     * as expected. See {@link PromptInfo.Builder#setDeviceCredentialAllowed(boolean)} for more
+     * <p>On P or below, calling this method when the device credential prompt is shown will NOT
+     * work as expected. See {@link PromptInfo.Builder#setDeviceCredentialAllowed(boolean)} for more
      * details.
      */
     public void cancelAuthentication() {
diff --git a/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerActivity.java b/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerActivity.java
index 4d8f784..3a65608 100644
--- a/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerActivity.java
+++ b/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerActivity.java
@@ -39,15 +39,12 @@
 
     static final String EXTRA_PROMPT_INFO_BUNDLE = "prompt_info_bundle";
 
-    @Nullable
-    private DeviceCredentialHandlerBridge mBridge;
-
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
+        final DeviceCredentialHandlerBridge bridge = DeviceCredentialHandlerBridge.getInstance();
+
         // Apply the client activity's theme to ensure proper dialog styling.
-        DeviceCredentialHandlerBridge bridge =
-                DeviceCredentialHandlerBridge.getInstanceIfNotNull();
-        if (bridge != null && bridge.getClientThemeResId() != 0) {
+        if (bridge.getClientThemeResId() != 0) {
             setTheme(bridge.getClientThemeResId());
             getTheme().applyStyle(R.style.TransparentStyle, true /* force */);
         }
@@ -57,13 +54,12 @@
         setTitle(null);
         setContentView(R.layout.device_credential_handler_activity);
 
-        mBridge = DeviceCredentialHandlerBridge.getInstance();
-        if (mBridge.getExecutor() == null || mBridge.getAuthenticationCallback() == null) {
+        if (bridge.getExecutor() == null || bridge.getAuthenticationCallback() == null) {
             Log.e(TAG, "onCreate: Executor and/or callback was null!");
         } else {
             // (Re)connect to and launch a biometric prompt within this activity.
             final BiometricPrompt biometricPrompt = new BiometricPrompt(this,
-                    mBridge.getExecutor(), mBridge.getAuthenticationCallback());
+                    bridge.getExecutor(), bridge.getAuthenticationCallback());
             final Bundle infoBundle = getIntent().getBundleExtra(EXTRA_PROMPT_INFO_BUNDLE);
             final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo(infoBundle);
             biometricPrompt.authenticate(info);
@@ -75,24 +71,38 @@
         super.onPause();
 
         // Prevent the client from resetting the bridge in onPause if just changing configuration.
-        if (isChangingConfigurations() && mBridge != null) {
-            mBridge.ignoreNextReset();
+        final DeviceCredentialHandlerBridge bridge =
+                DeviceCredentialHandlerBridge.getInstanceIfNotNull();
+        if (isChangingConfigurations() && bridge != null) {
+            bridge.ignoreNextReset();
         }
     }
 
-    // Handles the result of startActivity invoked by the attached BiometricPrompt.
     @Override
     protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
+        handleDeviceCredentialResult(resultCode);
+    }
 
-        // Handle result from ConfirmDeviceCredentialActivity.
-        if (mBridge == null || mBridge.getAuthenticationCallback() == null) {
+    /**
+     * Handles a result from the confirm device credential Settings activity.
+     *
+     * @param resultCode The (actual or simulated) result code from the device credential
+     *                   Settings activity. Typically, either {@link android.app.Activity#RESULT_OK}
+     *                   or {@link android.app.Activity#RESULT_CANCELED}.
+     */
+    void handleDeviceCredentialResult(int resultCode) {
+        final DeviceCredentialHandlerBridge bridge =
+                DeviceCredentialHandlerBridge.getInstanceIfNotNull();
+        if (bridge == null) {
             Log.e(TAG, "onActivityResult: Bridge or callback was null!");
         } else if (resultCode == RESULT_OK) {
-            mBridge.setDeviceCredentialResult(DeviceCredentialHandlerBridge.RESULT_SUCCESS);
+            bridge.setDeviceCredentialResult(DeviceCredentialHandlerBridge.RESULT_SUCCESS);
+            bridge.setConfirmingDeviceCredential(false);
         } else {
             // Treat any non-OK result as a user cancellation.
-            mBridge.setDeviceCredentialResult(DeviceCredentialHandlerBridge.RESULT_ERROR);
+            bridge.setDeviceCredentialResult(DeviceCredentialHandlerBridge.RESULT_ERROR);
+            bridge.setConfirmingDeviceCredential(false);
         }
 
         finish();
diff --git a/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerBridge.java b/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerBridge.java
index cfb2c47..821ad7a 100644
--- a/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerBridge.java
+++ b/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerBridge.java
@@ -61,6 +61,8 @@
     @Nullable
     private BiometricPrompt.AuthenticationCallback mAuthenticationCallback;
 
+    private boolean mConfirmingDeviceCredential;
+
     // Possible results from launching the confirm device credential Settings activity.
     static final int RESULT_NONE = 0;
     static final int RESULT_SUCCESS = 1;
@@ -163,7 +165,7 @@
      * Registers dialog and authentication callbacks to the bridge, along with an executor that can
      * be used to run them.
      *
-     * If a {@link BiometricFragment} has been registered via
+     * <p>If a {@link BiometricFragment} has been registered via
      * {@link #setBiometricFragment(BiometricFragment)}, or if a {@link FingerprintDialogFragment}
      * and {@link FingerprintHelperFragment} have been registered via
      * {@link #setFingerprintFragments(FingerprintDialogFragment, FingerprintHelperFragment)}, then
@@ -232,6 +234,19 @@
     }
 
     /**
+     * Sets a flag indicating whether the confirm device credential Settings activity is currently
+     * being shown.
+     */
+    void setConfirmingDeviceCredential(boolean confirmingDeviceCredential) {
+        mConfirmingDeviceCredential = confirmingDeviceCredential;
+    }
+
+    /** @return See {@link #setConfirmingDeviceCredential(boolean)}. */
+    boolean isConfirmingDeviceCredential() {
+        return mConfirmingDeviceCredential;
+    }
+
+    /**
      * Indicates that the bridge should ignore the next call to {@link #reset}. Calling this method
      * after {@link #startIgnoringReset()} but before {@link #stopIgnoringReset()} has no effect.
      */
@@ -260,7 +275,7 @@
     /**
      * Clears all data associated with the bridge, returning it to its default state.
      *
-     * Note that calls to this method may be ignored if {@link #ignoreNextReset()} or
+     * <p>Note that calls to this method may be ignored if {@link #ignoreNextReset()} or
      * {@link #startIgnoringReset()} has been called without a corresponding call to
      * {@link #stopIgnoringReset()}.
      */
@@ -274,12 +289,16 @@
             return;
         }
 
+        mClientThemeResId = 0;
         mBiometricFragment = null;
         mFingerprintDialogFragment = null;
         mFingerprintHelperFragment = null;
         mExecutor = null;
         mOnClickListener = null;
         mAuthenticationCallback = null;
+        mDeviceCredentialResult = RESULT_NONE;
+        mConfirmingDeviceCredential = false;
+
         sInstance = null;
     }
 }
diff --git a/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java b/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
index 905b2bc..e9fe877 100644
--- a/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
+++ b/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
@@ -43,7 +43,6 @@
 import androidx.appcompat.app.AlertDialog;
 import androidx.core.content.ContextCompat;
 import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.FragmentManager;
 
 /**
  * This class implements a custom AlertDialog that prompts the user for fingerprint authentication.
@@ -156,27 +155,15 @@
                             return;
                         }
 
-                        final Runnable onLaunch = new Runnable() {
-                            @Override
-                            public void run() {
-                                // Dismiss the fingerprint dialog without forwarding errors to
-                                // the client.
-                                final FragmentManager fragmentManager =
-                                        FingerprintDialogFragment.this.getFragmentManager();
-                                final String fragmentTag =
-                                        BiometricPrompt.FINGERPRINT_HELPER_FRAGMENT_TAG;
-                                final FingerprintHelperFragment fragment =
-                                        (FingerprintHelperFragment) fragmentManager
-                                                .findFragmentByTag(fragmentTag);
-                                if (fragment != null) {
-                                    fragment.setConfirmingDeviceCredential(true);
-                                }
-                                FingerprintDialogFragment.this.onCancel(dialog);
-                            }
-                        };
-
-                        Utils.launchDeviceCredentialConfirmation(TAG,
-                                FingerprintDialogFragment.this.getActivity(), mBundle, onLaunch);
+                        Utils.launchDeviceCredentialConfirmation(
+                                TAG, FingerprintDialogFragment.this.getActivity(), mBundle,
+                                new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        // Dismiss the fingerprint dialog without forwarding errors.
+                                        FingerprintDialogFragment.this.onCancel(dialog);
+                                    }
+                                });
                     }
                 }
             };
diff --git a/biometric/src/main/java/androidx/biometric/FingerprintHelperFragment.java b/biometric/src/main/java/androidx/biometric/FingerprintHelperFragment.java
index 7e3f2f9..fb15fbf 100644
--- a/biometric/src/main/java/androidx/biometric/FingerprintHelperFragment.java
+++ b/biometric/src/main/java/androidx/biometric/FingerprintHelperFragment.java
@@ -72,9 +72,6 @@
     private int mCanceledFrom;
     private CancellationSignal mCancellationSignal;
 
-    // Whether this fragment is launching the confirm device credential Settings activity.
-    private boolean mConfirmingDeviceCredential;
-
     // Also created once and retained.
     @SuppressWarnings("deprecation")
     private final androidx.core.hardware.fingerprint.FingerprintManagerCompat.AuthenticationCallback
@@ -86,7 +83,7 @@
                         final CharSequence errString) {
                     mHandler.obtainMessage(FingerprintDialogFragment.MSG_DISMISS_DIALOG_ERROR)
                             .sendToTarget();
-                    if (!mConfirmingDeviceCredential) {
+                    if (!isConfirmingDeviceCredential()) {
                         mExecutor.execute(
                                 new Runnable() {
                                     @Override
@@ -126,7 +123,7 @@
 
                         mHandler.obtainMessage(FingerprintDialogFragment.MSG_SHOW_ERROR,
                                 errMsgIdToSend, 0, errStringNonNull).sendToTarget();
-                        if (!mConfirmingDeviceCredential) {
+                        if (!isConfirmingDeviceCredential()) {
                             mHandler.postDelayed(
                                     new Runnable() {
                                         @Override
@@ -254,14 +251,6 @@
     }
 
     /**
-     * Indicates whether this fragment has or is about to launch the confirm device credential
-     * Settings activity and should therefore stop sending error signals back to the client.
-     */
-    void setConfirmingDeviceCredential(boolean confirmingDeviceCredential) {
-        mConfirmingDeviceCredential = confirmingDeviceCredential;
-    }
-
-    /**
      * Cancel the authentication.
      *
      * @param canceledFrom one of the USER_CANCELED_FROM* constants
@@ -287,7 +276,8 @@
         if (getFragmentManager() != null) {
             getFragmentManager().beginTransaction().detach(this).commitAllowingStateLoss();
         }
-        if (!mConfirmingDeviceCredential) {
+
+        if (!isConfirmingDeviceCredential()) {
             Utils.maybeFinishHandler(activity);
         }
     }
@@ -316,7 +306,7 @@
      * @param error The error code that will be sent to the client.
      */
     private void sendErrorToClient(final int error) {
-        if (!mConfirmingDeviceCredential) {
+        if (!isConfirmingDeviceCredential()) {
             mClientAuthenticationCallback.onAuthenticationError(error,
                     getErrorString(mContext, error));
         }
@@ -376,4 +366,9 @@
             return null;
         }
     }
+
+    private static boolean isConfirmingDeviceCredential() {
+        DeviceCredentialHandlerBridge bridge = DeviceCredentialHandlerBridge.getInstanceIfNotNull();
+        return bridge != null && bridge.isConfirmingDeviceCredential();
+    }
 }
diff --git a/biometric/src/main/java/androidx/biometric/Utils.java b/biometric/src/main/java/androidx/biometric/Utils.java
index e979660..0be493f 100644
--- a/biometric/src/main/java/androidx/biometric/Utils.java
+++ b/biometric/src/main/java/androidx/biometric/Utils.java
@@ -16,6 +16,7 @@
 
 package androidx.biometric;
 
+import android.app.Activity;
 import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.Intent;
@@ -69,7 +70,7 @@
      * @param loggingTag The tag to be used for logging events.
      * @param activity Activity that will launch the CDC activity and handle its result. Should be
      *                 {@link DeviceCredentialHandlerActivity}; all other activities will fail to
-     *                 launch the CDC activity and will instead log an error.
+     *                 launch the CDC activity and instead log an error.
      * @param bundle Bundle of extras forwarded from {@link BiometricPrompt}.
      * @param onLaunch Optional callback to be run before launching the new activity.
      */
@@ -81,15 +82,18 @@
             Log.e(loggingTag, "Failed to check device credential. Parent handler not found.");
             return;
         }
+        final DeviceCredentialHandlerActivity handlerActivity =
+                (DeviceCredentialHandlerActivity) activity;
 
         // Get the KeyguardManager service in whichever way the platform supports.
         final KeyguardManager keyguardManager;
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            keyguardManager = activity.getSystemService(KeyguardManager.class);
+            keyguardManager = handlerActivity.getSystemService(KeyguardManager.class);
         } else {
-            final Object service = activity.getSystemService(Context.KEYGUARD_SERVICE);
+            final Object service = handlerActivity.getSystemService(Context.KEYGUARD_SERVICE);
             if (!(service instanceof KeyguardManager)) {
                 Log.e(loggingTag, "Failed to check device credential. KeyguardManager not found.");
+                handlerActivity.handleDeviceCredentialResult(Activity.RESULT_CANCELED);
                 return;
             }
             keyguardManager = (KeyguardManager) service;
@@ -97,14 +101,10 @@
 
         if (keyguardManager == null) {
             Log.e(loggingTag, "Failed to check device credential. KeyguardManager was null.");
+            handlerActivity.handleDeviceCredentialResult(Activity.RESULT_CANCELED);
             return;
         }
 
-        // There's no longer a chance of returning early, so run the onLaunch callback.
-        if (onLaunch != null) {
-            onLaunch.run();
-        }
-
         // Pass along the title and subtitle from the biometric prompt.
         final CharSequence title;
         final CharSequence subtitle;
@@ -116,17 +116,27 @@
             subtitle = null;
         }
 
+        @SuppressWarnings("deprecation")
+        final Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(title, subtitle);
+        if (intent == null) {
+            Log.e(loggingTag, "Failed to check device credential. Got null intent from Keyguard.");
+            handlerActivity.handleDeviceCredentialResult(Activity.RESULT_CANCELED);
+            return;
+        }
+
         // Prevent the bridge from resetting until the confirmation activity finishes.
-        DeviceCredentialHandlerBridge bridge = DeviceCredentialHandlerBridge.getInstanceIfNotNull();
-        if (bridge != null) {
-            bridge.startIgnoringReset();
+        final DeviceCredentialHandlerBridge bridge = DeviceCredentialHandlerBridge.getInstance();
+        bridge.setConfirmingDeviceCredential(true);
+        bridge.startIgnoringReset();
+
+        // Run callback after the CDC flag is set but before launching the activity.
+        if (onLaunch != null) {
+            onLaunch.run();
         }
 
         // Launch a new instance of the confirm device credential Settings activity.
-        @SuppressWarnings("deprecation")
-        final Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(title, subtitle);
         intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
-        activity.startActivityForResult(intent, 0 /* requestCode */);
+        handlerActivity.startActivityForResult(intent, 0 /* requestCode */);
     }
 
     /**