Merge "AndroidX Webkit: Update WebViewAssetLoader#PathHandler docs" into androidx-master-dev
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/androidTest/java/androidx/biometric/UtilsTest.java b/biometric/src/androidTest/java/androidx/biometric/UtilsTest.java
new file mode 100644
index 0000000..dda354a
--- /dev/null
+++ b/biometric/src/androidTest/java/androidx/biometric/UtilsTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.biometric;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.os.Build;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class UtilsTest {
+    private static final String TAG = "UtilsTest";
+
+    @After
+    public void tearDown() {
+        // Ensure the bridge is fully reset after running each test.
+        final DeviceCredentialHandlerBridge bridge =
+                DeviceCredentialHandlerBridge.getInstanceIfNotNull();
+        if (bridge != null) {
+            bridge.stopIgnoringReset();
+            bridge.reset();
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    public void testLaunchDeviceCredentialConfirmation_ReturnsEarly_WithNullActivity() {
+        final Runnable onLaunch = mock(Runnable.class);
+
+        Utils.launchDeviceCredentialConfirmation(
+                TAG, null /* activity */, null /* bundle */, onLaunch);
+
+        verifyZeroInteractions(onLaunch);
+    }
+}
diff --git a/biometric/src/main/AndroidManifest.xml b/biometric/src/main/AndroidManifest.xml
index 8ff1f77..d61555c 100644
--- a/biometric/src/main/AndroidManifest.xml
+++ b/biometric/src/main/AndroidManifest.xml
@@ -23,7 +23,8 @@
     <application>
         <activity
             android:name=".DeviceCredentialHandlerActivity"
-            android:theme="@style/DeviceCredentialHandlerTheme" />
+            android:theme="@style/DeviceCredentialHandlerTheme"
+            android:exported="true" />
     </application>
 
 </manifest>
\ No newline at end of file
diff --git a/biometric/src/main/java/androidx/biometric/BiometricFragment.java b/biometric/src/main/java/androidx/biometric/BiometricFragment.java
index c44183f..11ed6e9 100644
--- a/biometric/src/main/java/androidx/biometric/BiometricFragment.java
+++ b/biometric/src/main/java/androidx/biometric/BiometricFragment.java
@@ -17,10 +17,8 @@
 package androidx.biometric;
 
 import android.annotation.SuppressLint;
-import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.content.Intent;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.CancellationSignal;
@@ -156,50 +154,13 @@
             };
 
     // Also created once and retained.
-    @SuppressWarnings("deprecation")
     private final DialogInterface.OnClickListener mDeviceCredentialButtonListener =
             new DialogInterface.OnClickListener() {
                 @Override
                 public void onClick(DialogInterface dialog, int which) {
                     if (which == DialogInterface.BUTTON_NEGATIVE) {
-                        final FragmentActivity activity = BiometricFragment.this.getActivity();
-                        if (!(activity instanceof DeviceCredentialHandlerActivity)) {
-                            Log.e(TAG, "Failed to check device credential."
-                                    + " Parent handler not found.");
-                            return;
-                        }
-
-                        final KeyguardManager km = activity.getSystemService(KeyguardManager.class);
-                        if (km == null) {
-                            Log.e(TAG, "Failed to check device credential."
-                                    + " KeyguardManager was null.");
-                            return;
-                        }
-
-                        // Pass along the title and subtitle from the biometric prompt.
-                        final CharSequence title;
-                        final CharSequence subtitle;
-                        if (mBundle != null) {
-                            title = mBundle.getCharSequence(BiometricPrompt.KEY_TITLE);
-                            subtitle = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
-                        } else {
-                            title = null;
-                            subtitle = null;
-                        }
-
-                        // Prevent bridge from resetting until the confirmation activity finishes.
-                        DeviceCredentialHandlerBridge bridge =
-                                DeviceCredentialHandlerBridge.getInstanceIfNotNull();
-                        if (bridge != null) {
-                            bridge.startIgnoringReset();
-                        }
-
-                        // Launch a new instance of the confirm device credential Settings activity.
-                        final Intent intent =
-                                km.createConfirmDeviceCredentialIntent(title, subtitle);
-                        intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK
-                                | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
-                        activity.startActivityForResult(intent, 0 /* requestCode */);
+                        Utils.launchDeviceCredentialConfirmation(TAG,
+                                BiometricFragment.this.getActivity(), mBundle, null /* onLaunch */);
                     }
                 }
             };
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 740b520..821ad7a 100644
--- a/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerBridge.java
+++ b/biometric/src/main/java/androidx/biometric/DeviceCredentialHandlerBridge.java
@@ -37,7 +37,7 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-class DeviceCredentialHandlerBridge {
+public class DeviceCredentialHandlerBridge {
     @Nullable
     private static DeviceCredentialHandlerBridge sInstance;
 
@@ -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 94b192b..e9fe877 100644
--- a/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
+++ b/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
@@ -18,10 +18,8 @@
 
 import android.annotation.SuppressLint;
 import android.app.Dialog;
-import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.content.Intent;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.drawable.AnimatedVectorDrawable;
@@ -45,7 +43,6 @@
 import androidx.appcompat.app.AlertDialog;
 import androidx.core.content.ContextCompat;
 import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.FragmentActivity;
 
 /**
  * This class implements a custom AlertDialog that prompts the user for fingerprint authentication.
@@ -147,11 +144,10 @@
     DialogInterface.OnClickListener mNegativeButtonListener;
 
     // Also created once and retained.
-    @SuppressWarnings("deprecation")
     private final DialogInterface.OnClickListener mDeviceCredentialButtonListener =
             new DialogInterface.OnClickListener() {
                 @Override
-                public void onClick(DialogInterface dialog, int which) {
+                public void onClick(final DialogInterface dialog, int which) {
                     if (which == DialogInterface.BUTTON_NEGATIVE) {
                         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                             Log.e(TAG, "Failed to check device credential."
@@ -159,57 +155,15 @@
                             return;
                         }
 
-                        final FragmentActivity activity =
-                                FingerprintDialogFragment.this.getActivity();
-                        if (!(activity instanceof DeviceCredentialHandlerActivity)) {
-                            Log.e(TAG, "Failed to check device credential."
-                                    + " Parent handler not found.");
-                            return;
-                        }
-
-                        final Object service = activity.getSystemService(Context.KEYGUARD_SERVICE);
-                        if (!(service instanceof KeyguardManager)) {
-                            Log.e(TAG, "Failed to check device credential."
-                                    + " KeyguardManager not found.");
-                            return;
-                        }
-                        final KeyguardManager km = (KeyguardManager) service;
-
-                        // Dismiss the fingerprint dialog without forwarding errors to the client.
-                        final FingerprintHelperFragment fingerprintHelperFragment =
-                                (FingerprintHelperFragment) FingerprintDialogFragment.this
-                                        .getFragmentManager()
-                                        .findFragmentByTag(
-                                                BiometricPrompt.FINGERPRINT_HELPER_FRAGMENT_TAG);
-                        if (fingerprintHelperFragment != null) {
-                            fingerprintHelperFragment.setConfirmingDeviceCredential(true);
-                        }
-                        FingerprintDialogFragment.this.onCancel(dialog);
-
-                        // Pass along the title and subtitle from the biometric prompt.
-                        final CharSequence title;
-                        final CharSequence subtitle;
-                        if (mBundle != null) {
-                            title = mBundle.getCharSequence(BiometricPrompt.KEY_TITLE);
-                            subtitle = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
-                        } else {
-                            title = null;
-                            subtitle = null;
-                        }
-
-                        // Prevent bridge from resetting until the confirmation activity finishes.
-                        DeviceCredentialHandlerBridge bridge =
-                                DeviceCredentialHandlerBridge.getInstanceIfNotNull();
-                        if (bridge != null) {
-                            bridge.startIgnoringReset();
-                        }
-
-                        // Launch a new instance of the confirm device credential Settings activity.
-                        final Intent intent =
-                                km.createConfirmDeviceCredentialIntent(title, subtitle);
-                        intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK
-                                | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
-                        activity.startActivityForResult(intent, 0 /* requestCode */);
+                        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 1250728..b6c7af36 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 0fa34f91..0be493f 100644
--- a/biometric/src/main/java/androidx/biometric/Utils.java
+++ b/biometric/src/main/java/androidx/biometric/Utils.java
@@ -16,7 +16,17 @@
 
 package androidx.biometric;
 
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.fragment.app.FragmentActivity;
 
@@ -24,13 +34,15 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-public class Utils {
+class Utils {
 
     /**
-     * @param errMsgId
-     * @return true if the error is not publicly defined
+     * Determines if the given ID fails to match any known error message.
+     *
+     * @param errMsgId Integer ID representing an error.
+     * @return true if the error is not publicly defined, or false otherwise.
      */
-    public static boolean isUnknownError(int errMsgId) {
+    static boolean isUnknownError(int errMsgId) {
         switch (errMsgId) {
             case BiometricPrompt.ERROR_HW_UNAVAILABLE:
             case BiometricPrompt.ERROR_UNABLE_TO_PROCESS:
@@ -52,10 +64,87 @@
     }
 
     /**
+     * Launches the confirm device credential (CDC) Settings activity to allow the user to
+     * authenticate with their device credential (PIN/pattern/password) on Android P and below.
+     *
+     * @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 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.
+     */
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    static void launchDeviceCredentialConfirmation(
+            @NonNull String loggingTag, @Nullable FragmentActivity activity,
+            @Nullable Bundle bundle, @Nullable Runnable onLaunch) {
+        if (!(activity instanceof DeviceCredentialHandlerActivity)) {
+            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 = handlerActivity.getSystemService(KeyguardManager.class);
+        } else {
+            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;
+        }
+
+        if (keyguardManager == null) {
+            Log.e(loggingTag, "Failed to check device credential. KeyguardManager was null.");
+            handlerActivity.handleDeviceCredentialResult(Activity.RESULT_CANCELED);
+            return;
+        }
+
+        // Pass along the title and subtitle from the biometric prompt.
+        final CharSequence title;
+        final CharSequence subtitle;
+        if (bundle != null) {
+            title = bundle.getCharSequence(BiometricPrompt.KEY_TITLE);
+            subtitle = bundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
+        } else {
+            title = null;
+            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.
+        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.
+        intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        handlerActivity.startActivityForResult(intent, 0 /* requestCode */);
+    }
+
+    /**
      * Finishes a given activity if and only if it's a {@link DeviceCredentialHandlerActivity}.
+     *
      * @param activity The activity to finish.
      */
-    public static void maybeFinishHandler(@Nullable FragmentActivity activity) {
+    static void maybeFinishHandler(@Nullable FragmentActivity activity) {
         if (activity instanceof DeviceCredentialHandlerActivity && !activity.isFinishing()) {
             activity.finish();
         }
diff --git a/buildSrc/src/main/kotlin/androidx/build/dokka/DokkaPublicDocs.kt b/buildSrc/src/main/kotlin/androidx/build/dokka/DokkaPublicDocs.kt
index 9b36868..adf6f52 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dokka/DokkaPublicDocs.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dokka/DokkaPublicDocs.kt
@@ -43,6 +43,14 @@
     private const val UNZIP_DEPS_TASK_NAME = "unzipDokkaPublicDocsDeps"
 
     val hiddenPackages = listOf(
+        "androidx.camera.camera2.impl",
+        "androidx.camera.camera2.impl.compat",
+        "androidx.camera.camera2.impl.compat.params",
+        "androidx.camera.core.impl",
+        "androidx.camera.core.impl.utils",
+        "androidx.camera.core.impl.utils.executor",
+        "androidx.camera.core.impl.utils.futures",
+        "androidx.camera.core.impl.utils.futures.internal",
         "androidx.core.internal",
         "androidx.preference.internal",
         "androidx.wear.internal.widget.drawer",
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysisNonBlockingAnalyzer.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysisNonBlockingAnalyzer.java
index fe8420e..0583b38 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysisNonBlockingAnalyzer.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysisNonBlockingAnalyzer.java
@@ -106,6 +106,7 @@
      */
     private synchronized void analyze(@NonNull ImageProxy imageProxy) {
         if (isClosed()) {
+            imageProxy.close();
             return;
         }
         long postedImageTimestamp = mPostedImageTimestamp.get();
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisNonBlockingAnalyzerTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisNonBlockingAnalyzerTest.java
new file mode 100644
index 0000000..b2d6555
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisNonBlockingAnalyzerTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class ImageAnalysisNonBlockingAnalyzerTest {
+    private ImageAnalysisNonBlockingAnalyzer mImageAnalysisNonBlockingAnalyzer;
+    private static final AtomicInteger ROTATION = new AtomicInteger(0);
+    private ImageAnalysis.Analyzer mAnalyzer;
+    private ImageReaderProxy mImageReaderProxy;
+    private ImageProxy mImageProxy;
+
+
+    @Before
+    public void setup() {
+        mImageProxy = mock(ImageProxy.class);
+        mImageReaderProxy = mock(ImageReaderProxy.class);
+
+        when(mImageReaderProxy.acquireLatestImage()).thenReturn(mImageProxy);
+
+        mAnalyzer = mock(ImageAnalysis.Analyzer.class);
+        mImageAnalysisNonBlockingAnalyzer = new ImageAnalysisNonBlockingAnalyzer(
+                new AtomicReference<ImageAnalysis.Analyzer>(mAnalyzer),
+                ROTATION,
+                new Handler(Looper.getMainLooper()),
+                CameraXExecutors.directExecutor()
+        );
+    }
+
+    @Test
+    public void imageClosedAfterAnalyzerClosed() {
+        mImageAnalysisNonBlockingAnalyzer.close();
+
+        mImageAnalysisNonBlockingAnalyzer.onImageAvailable(mImageReaderProxy);
+
+        verify(mImageProxy, times(1)).close();
+    }
+
+    @Test
+    public void analysisNotRunAfterAnalyzerClosed() {
+        mImageAnalysisNonBlockingAnalyzer.close();
+
+        mImageAnalysisNonBlockingAnalyzer.onImageAvailable(mImageReaderProxy);
+
+        verifyZeroInteractions(mAnalyzer);
+    }
+
+    @Test
+    public void imageClosedWhenAnalyzerOpen() {
+        mImageAnalysisNonBlockingAnalyzer.open();
+
+        mImageAnalysisNonBlockingAnalyzer.onImageAvailable(mImageReaderProxy);
+
+        verify(mImageProxy, times(1)).close();
+    }
+
+    @Test
+    public void analysisRunWhenAnalyzerOpen() {
+        mImageAnalysisNonBlockingAnalyzer.open();
+
+        mImageAnalysisNonBlockingAnalyzer.onImageAvailable(mImageReaderProxy);
+
+        verify(mAnalyzer, times(1)).analyze(mImageProxy, ROTATION.get());
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebViewAssetLoader.java b/webkit/src/main/java/androidx/webkit/WebViewAssetLoader.java
index 49ba845..84850a6 100644
--- a/webkit/src/main/java/androidx/webkit/WebViewAssetLoader.java
+++ b/webkit/src/main/java/androidx/webkit/WebViewAssetLoader.java
@@ -58,24 +58,23 @@
  * <p>
  * A typical usage would be like:
  * <pre class="prettyprint">
- *     final WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
- *              .addPathHandler("/assets/", new AssetsPathHandler(this))
- *              .addPathHandler("/res/", new ResourcesPathHandler(this))
- *              .build();
+ * final WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
+ *          .addPathHandler("/assets/", new AssetsPathHandler(this))
+ *          .addPathHandler("/res/", new ResourcesPathHandler(this))
+ *          .build();
  *
- *     webView.setWebViewClient(new WebViewClient() {
- *         {@literal @}Override
- *         public WebResourceResponse shouldInterceptRequest(WebView view,
- *                                          WebResourceRequest request) {
- *             return assetLoader.shouldInterceptRequest(request.getUrl());
- *         }
- *     });
- *     // Assets are hosted under http(s)://appassets.androidplatform.net/assets/... .
- *     // If the application's assets are in the "main/assets" folder this will read the file
- *     // from "main/assets/www/index.html" and load it as if it were hosted on:
- *     // https://appassets.androidplatform.net/assets/www/index.html
- *     webview.loadUrl("https://appassets.androidplatform.net/assets/www/index.html");
- *
+ * webView.setWebViewClient(new WebViewClient() {
+ *     {@literal @}Override
+ *     public WebResourceResponse shouldInterceptRequest(WebView view,
+ *                                      WebResourceRequest request) {
+ *         return assetLoader.shouldInterceptRequest(request.getUrl());
+ *     }
+ * });
+ * // Assets are hosted under http(s)://appassets.androidplatform.net/assets/... .
+ * // If the application's assets are in the "main/assets" folder this will read the file
+ * // from "main/assets/www/index.html" and load it as if it were hosted on:
+ * // https://appassets.androidplatform.net/assets/www/index.html
+ * webview.loadUrl("https://appassets.androidplatform.net/assets/www/index.html");
  * </pre>
  */
 public final class WebViewAssetLoader {
@@ -245,12 +244,12 @@
      * <p>
      * A typical usage would be like:
      * <pre class="prettyprint">
-     *     File publicDir = new File(context.getFilesDir(), "public");
-     *     // Host "files/public/" in app's data directory under:
-     *     // http://appassets.androidplatform.net/public/...
-     *     WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
-     *              .addPathHandler("/public/", new InternalStoragePathHandler(context, publicDir))
-     *              .build();
+     * File publicDir = new File(context.getFilesDir(), "public");
+     * // Host "files/public/" in app's data directory under:
+     * // http://appassets.androidplatform.net/public/...
+     * WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
+     *          .addPathHandler("/public/", new InternalStoragePathHandler(context, publicDir))
+     *          .build();
      * </pre>
      */
     public static final class InternalStoragePathHandler implements PathHandler {