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 {