12/n: Add LockPatternView for setDeviceCredentialAllowed(true)

Includes lock icon, title, subtitle, description, lock pattern view.
Corner radius and padding animates nicely from !=0 --> 0.

Support for password/pin will come in a subsequent CL.
Unit tests for AuthCredentialView will be added when
password/pin are implemented.

Support for persisting across configuration changes
and landscape view will also be added in a subsequent
change.

Test: BiometricPromptDemo with the following:
      1) Confirm pattern, callback received
      2) Rejected, error string shown
      3) Lockout (5 attempts), countdown string shown,
         pattern view disabled until countdown is over
      4) Cancel pattern auth, callback received
Test: atest BiometricServiceTest
Test: atest com.android.systemui.biometrics

Change-Id: Idc01e33be0074a6c8a43f60b172a4391bfbe5e8a
This commit is contained in:
Kevin Chyn
2019-09-16 16:04:38 -07:00
parent 396a84129f
commit ff168dc49d
20 changed files with 696 additions and 86 deletions

View File

@@ -95,7 +95,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
/**
* @hide
*/
public static final int DISMISSED_REASON_CONFIRMED = 1;
public static final int DISMISSED_REASON_BIOMETRIC_CONFIRMED = 1;
/**
* Dialog is done animating away after user clicked on the button set via
@@ -114,7 +114,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
* Authenticated, confirmation not required. Dialog animated away.
* @hide
*/
public static final int DISMISSED_REASON_CONFIRM_NOT_REQUIRED = 4;
public static final int DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED = 4;
/**
* Error message shown on SystemUI. When BiometricService receives this, the UI is already
@@ -129,6 +129,11 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
*/
public static final int DISMISSED_REASON_SERVER_REQUESTED = 6;
/**
* @hide
*/
public static final int DISMISSED_REASON_CREDENTIAL_CONFIRMED = 7;
private static class ButtonInfo {
Executor executor;
DialogInterface.OnClickListener listener;
@@ -368,7 +373,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
@Override
public void onDialogDismissed(int reason) throws RemoteException {
// Check the reason and invoke OnClickListener(s) if necessary
if (reason == DISMISSED_REASON_CONFIRMED) {
if (reason == DISMISSED_REASON_BIOMETRIC_CONFIRMED) {
mPositiveButtonInfo.executor.execute(() -> {
mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE);
});

View File

@@ -38,4 +38,6 @@ oneway interface IBiometricServiceReceiverInternal {
void onDialogDismissed(int reason);
// Notifies that the user has pressed the "try again" button on SystemUI
void onTryAgainPressed();
// Notifies that the user has pressed the "use password" button on SystemUI
void onDeviceCredentialPressed();
}

View File

@@ -0,0 +1,27 @@
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:attr/colorAccent"
android:pathData="M12,15m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
<path
android:fillColor="?android:attr/colorAccent"
android:pathData="M18,8h-1.5V5.5C16.5,3.01 14.49,1 12,1S7.5,3.01 7.5,5.5V8H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10C20,8.9 19.1,8 18,8zM9.5,5.5C9.5,4.12 10.62,3 12,3c1.38,0 2.5,1.12 2.5,2.5V8h-5V5.5zM18,20H6V10h1.5h9H18V20z"/>
</vector>

View File

@@ -34,7 +34,7 @@
android:elevation="@dimen/biometric_dialog_elevation"/>
<ScrollView
android:id="@+id/scrollview"
android:id="@+id/biometric_scrollview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"

View File

@@ -0,0 +1,98 @@
<!--
~ Copyright (C) 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.
-->
<com.android.systemui.biometrics.AuthCredentialView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:elevation="@dimen/biometric_dialog_elevation">
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/auth_dialog_lock"/>
<TextView
android:id="@+id/title"
android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="12dp"
android:textSize="20sp"
android:gravity="center"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:gravity="center"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="8dp"
android:gravity="center"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"/>
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="3"/>
<TextView
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:textSize="16sp"
android:gravity="center"
android:textColor="?android:attr/colorError"/>
<com.android.internal.widget.LockPatternView
android:id="@+id/lockPattern"
android:layout_marginBottom="20dp"
android:layout_marginLeft="40dp"
android:layout_marginRight="40dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clipChildren="false"
android:clipToPadding="false"
style="@style/LockPatternStyleBiometricPrompt"/>
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
</com.android.systemui.biometrics.AuthCredentialView>

View File

@@ -1009,10 +1009,15 @@
<!-- Biometric Dialog values -->
<dimen name="biometric_dialog_biometric_icon_size">64dp</dimen>
<dimen name="biometric_dialog_corner_size">4dp</dimen>
<!-- Y translation when showing/dismissing the dialog-->
<dimen name="biometric_dialog_animation_translation_offset">350dp</dimen>
<dimen name="biometric_dialog_border_padding">4dp</dimen>
<dimen name="biometric_dialog_elevation">1dp</dimen>
<dimen name="biometric_dialog_icon_padding">16dp</dimen>
<!-- Y translation for biometric contents when transitioning to device credential UI -->
<dimen name="biometric_dialog_medium_to_large_translation_offset">100dp</dimen>
<!-- Y translation for credential contents when animating in -->
<dimen name="biometric_dialog_credential_translation_offset">60dp</dimen>
<!-- Wireless Charging Animation values -->
<dimen name="wireless_charging_dots_radius_start">0dp</dimen>

View File

@@ -317,6 +317,14 @@
<string name="biometric_dialog_use_pattern">Use pattern</string>
<!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pass) [CHAR LIMIT=30] -->
<string name="biometric_dialog_use_password">Use password</string>
<!-- Error string shown when the user enters an incorrect PIN [CHAR LIMIT=40]-->
<string name="biometric_dialog_wrong_pin">Wrong PIN</string>
<!-- Error string shown when the user enters an incorrect pattern [CHAR LIMIT=40]-->
<string name="biometric_dialog_wrong_pattern">Wrong pattern</string>
<!-- Error string shown when the user enters an incorrect password [CHAR LIMIT=40]-->
<string name="biometric_dialog_wrong_password">Wrong password</string>
<!-- Error string shown when the user enters too many incorrect attempts [CHAR LIMIT=120]-->
<string name="biometric_dialog_credential_too_many_attempts">Too many incorrect attempts.\nTry again in <xliff:g id="number">%d</xliff:g> seconds.</string>
<!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication -->
<string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string>

View File

@@ -314,6 +314,12 @@
<item name="*android:errorColor">?android:attr/colorError</item>
</style>
<style name="LockPatternStyleBiometricPrompt">
<item name="*android:regularColor">?android:attr/colorForeground</item>
<item name="*android:successColor">?android:attr/colorForeground</item>
<item name="*android:errorColor">?android:attr/colorError</item>
</style>
<style name="qs_theme" parent="@*android:style/Theme.DeviceDefault.QuickSettings">
<item name="lightIconTheme">@style/QSIconTheme</item>
<item name="darkIconTheme">@style/QSIconTheme</item>

View File

@@ -147,8 +147,12 @@ public abstract class AuthBiometricView extends LinearLayout {
return BiometricPrompt.HIDE_DIALOG_DELAY;
}
public int getAnimationDuration() {
return AuthDialog.ANIMATE_DURATION_MS;
public int getMediumToLargeAnimationDurationMs() {
return AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS;
}
public int getAnimateCredentialStartDelayMs() {
return AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS;
}
}
@@ -159,7 +163,7 @@ public abstract class AuthBiometricView extends LinearLayout {
private final int mTextColorHint;
private AuthPanelController mPanelController;
private Bundle mBundle;
private Bundle mBiometricPromptBundle;
private boolean mRequireConfirmation;
private int mUserId;
@AuthDialog.DialogSize int mSize = AuthDialog.SIZE_UNKNOWN;
@@ -265,7 +269,7 @@ public abstract class AuthBiometricView extends LinearLayout {
}
public void setBiometricPromptBundle(Bundle bundle) {
mBundle = bundle;
mBiometricPromptBundle = bundle;
}
public void setCallback(Callback callback) {
@@ -300,7 +304,7 @@ public abstract class AuthBiometricView extends LinearLayout {
final int newHeight = mIconView.getHeight() + 2 * (int) iconPadding;
mPanelController.updateForContentDimensions(mMediumWidth, newHeight,
false /* animate */);
0 /* animateDurationMs */);
mSize = newSize;
} else if (mSize == AuthDialog.SIZE_SMALL && newSize == AuthDialog.SIZE_MEDIUM) {
@@ -318,7 +322,6 @@ public abstract class AuthBiometricView extends LinearLayout {
// Animate the text
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1);
opacityAnimator.setDuration(mInjector.getAnimationDuration());
opacityAnimator.addUpdateListener((animation) -> {
final float opacity = (float) animation.getAnimatedValue();
mTitleView.setAlpha(opacity);
@@ -336,7 +339,7 @@ public abstract class AuthBiometricView extends LinearLayout {
// Choreograph together
final AnimatorSet as = new AnimatorSet();
as.setDuration(mInjector.getAnimationDuration());
as.setDuration(AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS);
as.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -367,41 +370,51 @@ public abstract class AuthBiometricView extends LinearLayout {
as.start();
// Animate the panel
mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight,
true /* animate */);
AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS);
} else if (newSize == AuthDialog.SIZE_MEDIUM) {
mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight,
false /* animate */);
0 /* animateDurationMs */);
mSize = newSize;
} else if (newSize == AuthDialog.SIZE_LARGE) {
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
opacityAnimator.setDuration(mInjector.getAnimationDuration());
opacityAnimator.addUpdateListener((animation) -> {
final float opacity = (float) animation.getAnimatedValue();
mTitleView.setAlpha(opacity);
mSubtitleView.setAlpha(opacity);
mDescriptionView.setAlpha(opacity);
mIconView.setAlpha(opacity);
mIndicatorView.setAlpha(opacity);
mNegativeButton.setAlpha(opacity);
final float translationY = getResources().getDimension(
R.dimen.biometric_dialog_medium_to_large_translation_offset);
final AuthBiometricView biometricView = this;
// Translate at full duration
final ValueAnimator translationAnimator = ValueAnimator.ofFloat(
biometricView.getY(), biometricView.getY() - translationY);
translationAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs());
translationAnimator.addUpdateListener((animation) -> {
final float translation = (float) animation.getAnimatedValue();
biometricView.setTranslationY(translation);
});
opacityAnimator.addListener(new AnimatorListenerAdapter() {
translationAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
AuthBiometricView view = AuthBiometricView.this;
if (view.getParent() != null) {
((ViewGroup) view.getParent()).removeView(view);
if (biometricView.getParent() != null) {
((ViewGroup) biometricView.getParent()).removeView(biometricView);
}
mSize = newSize;
}
});
// Opacity to 0 in half duration
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
opacityAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs() / 2);
opacityAnimator.addUpdateListener((animation) -> {
final float opacity = (float) animation.getAnimatedValue();
biometricView.setAlpha(opacity);
});
mPanelController.setUseFullScreen(true);
mPanelController.updateForContentDimensions(
mPanelController.getContainerWidth(),
mPanelController.getContainerHeight(),
true /* animate */);
opacityAnimator.start();
mInjector.getMediumToLargeAnimationDurationMs());
AnimatorSet as = new AnimatorSet();
as.play(translationAnimator).with(opacityAnimator);
as.start();
} else {
Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize);
}
@@ -572,7 +585,10 @@ public abstract class AuthBiometricView extends LinearLayout {
} else {
if (isDeviceCredentialAllowed()) {
updateSize(AuthDialog.SIZE_LARGE);
mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
mHandler.postDelayed(() -> {
mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
}, mInjector.getAnimateCredentialStartDelayMs());
} else {
mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
}
@@ -603,7 +619,7 @@ public abstract class AuthBiometricView extends LinearLayout {
*/
@VisibleForTesting
void onAttachedToWindowInternal() {
setText(mTitleView, mBundle.getString(BiometricPrompt.KEY_TITLE));
setText(mTitleView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_TITLE));
final String negativeText;
if (isDeviceCredentialAllowed()) {
@@ -628,12 +644,14 @@ public abstract class AuthBiometricView extends LinearLayout {
}
} else {
negativeText = mBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT);
negativeText = mBiometricPromptBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT);
}
setText(mNegativeButton, negativeText);
setTextOrHide(mSubtitleView, mBundle.getString(BiometricPrompt.KEY_SUBTITLE));
setTextOrHide(mDescriptionView, mBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
setTextOrHide(mSubtitleView,
mBiometricPromptBundle.getString(BiometricPrompt.KEY_SUBTITLE));
setTextOrHide(mDescriptionView,
mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
if (mSavedState == null) {
updateState(STATE_AUTHENTICATING_ANIMATING_IN);
@@ -730,6 +748,6 @@ public abstract class AuthBiometricView extends LinearLayout {
}
private boolean isDeviceCredentialAllowed() {
return mBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
return mBiometricPromptBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
}
}

View File

@@ -37,6 +37,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@@ -78,12 +79,14 @@ public class AuthContainerView extends LinearLayout
private final AuthPanelController mPanelController;
private final Interpolator mLinearOutSlowIn;
@VisibleForTesting final BiometricCallback mBiometricCallback;
private final CredentialCallback mCredentialCallback;
private final ViewGroup mContainerView;
@VisibleForTesting final FrameLayout mFrameLayout;
private final AuthBiometricView mBiometricView;
@VisibleForTesting AuthCredentialView mCredentialView;
private final ImageView mBackgroundView;
private final ScrollView mScrollView;
private final ScrollView mBiometricScrollView;
private final View mPanelView;
private final float mTranslationY;
@@ -156,7 +159,7 @@ public class AuthContainerView extends LinearLayout
public void onAction(int action) {
switch (action) {
case AuthBiometricView.Callback.ACTION_AUTHENTICATED:
animateAway(AuthDialogCallback.DISMISSED_AUTHENTICATED);
animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
break;
case AuthBiometricView.Callback.ACTION_USER_CANCELED:
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
@@ -171,7 +174,8 @@ public class AuthContainerView extends LinearLayout
animateAway(AuthDialogCallback.DISMISSED_ERROR);
break;
case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL:
Log.v(TAG, "ACTION_USE_DEVICE_CREDENTIAL");
mConfig.mCallback.onDeviceCredentialPressed();
showCredentialView();
break;
default:
Log.e(TAG, "Unhandled action: " + action);
@@ -179,6 +183,13 @@ public class AuthContainerView extends LinearLayout
}
}
final class CredentialCallback implements AuthCredentialView.Callback {
@Override
public void onCredentialMatched() {
animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
}
}
@VisibleForTesting
AuthContainerView(Config config) {
super(config.mContext);
@@ -191,12 +202,13 @@ public class AuthContainerView extends LinearLayout
.getDimension(R.dimen.biometric_dialog_animation_translation_offset);
mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
mBiometricCallback = new BiometricCallback();
mCredentialCallback = new CredentialCallback();
final LayoutInflater factory = LayoutInflater.from(mContext);
mContainerView = (ViewGroup) factory.inflate(
mFrameLayout = (FrameLayout) factory.inflate(
R.layout.auth_container_view, this, false /* attachToRoot */);
mPanelView = mContainerView.findViewById(R.id.panel);
mPanelView = mFrameLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
// TODO: Update with new controllers if multi-modal authentication can occur simultaneously
@@ -210,11 +222,11 @@ public class AuthContainerView extends LinearLayout
Log.e(TAG, "Unsupported modality mask: " + config.mModalityMask);
mBiometricView = null;
mBackgroundView = null;
mScrollView = null;
mBiometricScrollView = null;
return;
}
mBackgroundView = mContainerView.findViewById(R.id.background);
mBackgroundView = mFrameLayout.findViewById(R.id.background);
UserManager userManager = mContext.getSystemService(UserManager.class);
DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
@@ -234,9 +246,9 @@ public class AuthContainerView extends LinearLayout
mBiometricView.setBackgroundView(mBackgroundView);
mBiometricView.setUserId(mConfig.mUserId);
mScrollView = mContainerView.findViewById(R.id.scrollview);
mScrollView.addView(mBiometricView);
addView(mContainerView);
mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
mBiometricScrollView.addView(mBiometricView);
addView(mFrameLayout);
setOnKeyListener((v, keyCode, event) -> {
if (keyCode != KeyEvent.KEYCODE_BACK) {
@@ -252,6 +264,16 @@ public class AuthContainerView extends LinearLayout
requestFocus();
}
private void showCredentialView() {
final LayoutInflater factory = LayoutInflater.from(mContext);
mCredentialView = (AuthCredentialView) factory.inflate(
R.layout.auth_credential_view, null, false);
mCredentialView.setUser(mConfig.mUserId);
mCredentialView.setCallback(mCredentialCallback);
mCredentialView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle);
mFrameLayout.addView(mCredentialView);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -270,7 +292,7 @@ public class AuthContainerView extends LinearLayout
// The background panel and content are different views since we need to be able to
// animate them separately in other places.
mPanelView.setY(mTranslationY);
mScrollView.setY(mTranslationY);
mBiometricScrollView.setY(mTranslationY);
setAlpha(0f);
postOnAnimation(() -> {
@@ -281,7 +303,7 @@ public class AuthContainerView extends LinearLayout
.withLayer()
.withEndAction(this::onDialogAnimatedIn)
.start();
mScrollView.animate()
mBiometricScrollView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setInterpolator(mLinearOutSlowIn)
@@ -396,12 +418,20 @@ public class AuthContainerView extends LinearLayout
.withLayer()
.withEndAction(endActionRunnable)
.start();
mScrollView.animate()
mBiometricScrollView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
mCredentialView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
}
animate()
.alpha(0f)
.setDuration(ANIMATION_DURATION_AWAY_MS)

View File

@@ -104,6 +104,15 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
}
}
@Override
public void onDeviceCredentialPressed() {
try {
mReceiver.onDeviceCredentialPressed();
} catch (RemoteException e) {
Log.e(TAG, "RemoteException when handling credential button", e);
}
}
@Override
public void onDismissed(@DismissedReason int reason) {
switch (reason) {
@@ -116,11 +125,12 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
break;
case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED);
break;
case AuthDialogCallback.DISMISSED_AUTHENTICATED:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
case AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED:
sendResultAndCleanUp(
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED);
break;
case AuthDialogCallback.DISMISSED_ERROR:
@@ -131,6 +141,10 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED);
break;
case AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED);
break;
default:
Log.e(TAG, "Unhandled reason: " + reason);
break;

View File

@@ -0,0 +1,261 @@
/*
* Copyright (C) 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 com.android.systemui.biometrics;
import android.content.Context;
import android.hardware.biometrics.BiometricPrompt;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.internal.widget.LockPatternChecker;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockPatternView;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import java.util.List;
/**
* Shows Pin, Pattern, or Password for
* {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)}
*/
public class AuthCredentialView extends LinearLayout {
private static final int ERROR_DURATION_MS = 3000;
private final AccessibilityManager mAccessibilityManager;
private final LockPatternUtils mLockPatternUtils;
private final Handler mHandler;
private LockPatternView mLockPatternView;
private int mUserId;
private AsyncTask<?, ?, ?> mPendingLockCheck;
private Callback mCallback;
private ErrorTimer mErrorTimer;
private Bundle mBiometricPromptBundle;
private TextView mTitleView;
private TextView mSubtitleView;
private TextView mDescriptionView;
private TextView mErrorView;
interface Callback {
void onCredentialMatched();
}
private static class ErrorTimer extends CountDownTimer {
private final TextView mErrorView;
private final Context mContext;
/**
* @param millisInFuture The number of millis in the future from the call
* to {@link #start()} until the countdown is done and {@link
* #onFinish()}
* is called.
* @param countDownInterval The interval along the way to receive
* {@link #onTick(long)} callbacks.
*/
public ErrorTimer(Context context, long millisInFuture, long countDownInterval,
TextView errorView) {
super(millisInFuture, countDownInterval);
mErrorView = errorView;
mContext = context;
}
@Override
public void onTick(long millisUntilFinished) {
final int secondsCountdown = (int) (millisUntilFinished / 1000);
mErrorView.setText(mContext.getString(
R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown));
}
@Override
public void onFinish() {
mErrorView.setText("");
}
}
private class UnlockPatternListener implements LockPatternView.OnPatternListener {
@Override
public void onPatternStart() {
}
@Override
public void onPatternCleared() {
}
@Override
public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
}
@Override
public void onPatternDetected(List<LockPatternView.Cell> pattern) {
if (mPendingLockCheck != null) {
mPendingLockCheck.cancel(false);
}
mLockPatternView.setEnabled(false);
if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
// Pattern size is less than the minimum, do not count it as a failed attempt.
onPatternChecked(false /* matched */, 0 /* timeoutMs */);
return;
}
mPendingLockCheck = LockPatternChecker.checkPattern(
mLockPatternUtils,
pattern,
mUserId,
this::onPatternChecked);
}
private void onPatternChecked(boolean matched, int timeoutMs) {
mLockPatternView.setEnabled(true);
if (matched) {
mClearErrorRunnable.run();
mCallback.onCredentialMatched();
} else {
if (timeoutMs > 0) {
mHandler.removeCallbacks(mClearErrorRunnable);
mLockPatternView.setEnabled(false);
long deadline = mLockPatternUtils.setLockoutAttemptDeadline(mUserId, timeoutMs);
mErrorTimer = new ErrorTimer(mContext,
deadline - SystemClock.elapsedRealtime(),
LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
mErrorView) {
@Override
public void onFinish() {
mClearErrorRunnable.run();
mLockPatternView.setEnabled(true);
}
};
mErrorTimer.start();
} else {
showError(getResources().getString(R.string.biometric_dialog_wrong_pattern));
}
}
}
}
private final Runnable mClearErrorRunnable = new Runnable() {
@Override
public void run() {
mErrorView.setText("");
}
};
public AuthCredentialView(Context context, AttributeSet attrs) {
super(context, attrs);
mHandler = new Handler(Looper.getMainLooper());
mLockPatternUtils = new LockPatternUtils(mContext);
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
}
private void showError(String error) {
mHandler.removeCallbacks(mClearErrorRunnable);
mErrorView.setText(error);
mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
}
private void setTextOrHide(TextView view, String string) {
if (TextUtils.isEmpty(string)) {
view.setVisibility(View.GONE);
} else {
view.setText(string);
}
Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
}
private void setText(TextView view, String string) {
view.setText(string);
}
void setUser(int user) {
mUserId = user;
}
void setCallback(Callback callback) {
mCallback = callback;
}
void setBiometricPromptBundle(Bundle bundle) {
mBiometricPromptBundle = bundle;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setText(mTitleView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_TITLE));
setTextOrHide(mSubtitleView,
mBiometricPromptBundle.getString(BiometricPrompt.KEY_SUBTITLE));
setTextOrHide(mDescriptionView,
mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
setTranslationY(getResources()
.getDimension(R.dimen.biometric_dialog_credential_translation_offset));
setAlpha(0);
postOnAnimation(() -> {
animate().translationY(0)
.setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS)
.alpha(1.f)
.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
.withLayer()
.start();
});
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mErrorTimer != null) {
mErrorTimer.cancel();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTitleView = findViewById(R.id.title);
mSubtitleView = findViewById(R.id.subtitle);
mDescriptionView = findViewById(R.id.description);
mErrorView = findViewById(R.id.error);
mLockPatternView = findViewById(R.id.lockPattern);
mLockPatternView.setOnPatternListener(new UnlockPatternListener());
mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(mUserId));
mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
}
}

View File

@@ -40,17 +40,38 @@ public interface AuthDialog {
String KEY_BIOMETRIC_DIALOG_SIZE = "size";
int SIZE_UNKNOWN = 0;
/**
* Minimal UI, showing only biometric icon.
*/
int SIZE_SMALL = 1;
/**
* Normal-sized biometric UI, showing title, icon, buttons, etc.
*/
int SIZE_MEDIUM = 2;
/**
* Full-screen credential UI.
*/
int SIZE_LARGE = 3;
@Retention(RetentionPolicy.SOURCE)
@IntDef({SIZE_UNKNOWN, SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE})
@interface DialogSize {}
/**
* Animation duration, e.g. small to medium dialog, icon translation, etc.
* Animation duration, from small to medium dialog, including back panel, icon translation, etc
*/
int ANIMATE_DURATION_MS = 150;
int ANIMATE_SMALL_TO_MEDIUM_DURATION_MS = 150;
/**
* Animation duration from medium to large dialog, including biometric fade out, back panel, etc
*/
int ANIMATE_MEDIUM_TO_LARGE_DURATION_MS = 450;
/**
* Delay before notifying {@link AuthCredentialView} to start animating in.
*/
int ANIMATE_CREDENTIAL_START_DELAY_MS = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS * 2 / 3;
/**
* Animation duration when sliding in credential UI
*/
int ANIMATE_CREDENTIAL_INITIAL_DURATION_MS = 150;
/**
* Show the dialog.

View File

@@ -27,17 +27,18 @@ public interface AuthDialogCallback {
int DISMISSED_USER_CANCELED = 1;
int DISMISSED_BUTTON_NEGATIVE = 2;
int DISMISSED_BUTTON_POSITIVE = 3;
int DISMISSED_AUTHENTICATED = 4;
int DISMISSED_BIOMETRIC_AUTHENTICATED = 4;
int DISMISSED_ERROR = 5;
int DISMISSED_BY_SYSTEM_SERVER = 6;
int DISMISSED_CREDENTIAL_AUTHENTICATED = 7;
@IntDef({DISMISSED_USER_CANCELED,
DISMISSED_BUTTON_NEGATIVE,
DISMISSED_BUTTON_POSITIVE,
DISMISSED_AUTHENTICATED,
DISMISSED_BIOMETRIC_AUTHENTICATED,
DISMISSED_ERROR,
DISMISSED_BY_SYSTEM_SERVER})
DISMISSED_BY_SYSTEM_SERVER,
DISMISSED_CREDENTIAL_AUTHENTICATED})
@interface DismissedReason {}
/**
@@ -50,4 +51,9 @@ public interface AuthDialogCallback {
* Invoked when the "try again" button is clicked
*/
void onTryAgainPressed();
/**
* Invoked when the "use password" button is clicked
*/
void onDeviceCredentialPressed();
}

View File

@@ -32,12 +32,10 @@ import com.android.systemui.R;
public class AuthPanelController extends ViewOutlineProvider {
private static final String TAG = "BiometricPrompt/AuthPanelController";
private static final boolean DEBUG = true;
private static final boolean DEBUG = false;
private final Context mContext;
private final View mPanelView;
private final float mCornerRadius;
private final int mBiometricMargin;
private boolean mUseFullScreen;
@@ -47,19 +45,24 @@ public class AuthPanelController extends ViewOutlineProvider {
private int mContentWidth;
private int mContentHeight;
private float mCornerRadius;
private int mMargin;
@Override
public void getOutline(View view, Outline outline) {
final int left = (mContainerWidth - mContentWidth) / 2;
final int right = mContainerWidth - left;
final int margin = mUseFullScreen ? 0 : mBiometricMargin;
final float cornerRadius = mUseFullScreen ? 0 : mCornerRadius;
// If the content fits within the container, shrink the height to wrap the content.
// Otherwise, set the outline to be the display size minus the margin - the content within
// is scrollable.
final int top = mContentHeight < mContainerHeight
? mContainerHeight - mContentHeight - margin
: margin;
final int bottom = mContainerHeight - margin;
outline.setRoundRect(left, top, right, bottom, cornerRadius);
? mContainerHeight - mContentHeight - mMargin
: mMargin;
// TODO(b/139954942) Likely don't need to "+1" after we resolve the navbar styling.
final int bottom = mContainerHeight - mMargin + 1;
outline.setRoundRect(left, top, right, bottom, mCornerRadius);
}
public void setContainerDimensions(int containerWidth, int containerHeight) {
@@ -74,11 +77,12 @@ public class AuthPanelController extends ViewOutlineProvider {
mUseFullScreen = fullScreen;
}
public void updateForContentDimensions(int contentWidth, int contentHeight, boolean animate) {
public void updateForContentDimensions(int contentWidth, int contentHeight,
int animateDurationMs) {
if (DEBUG) {
Log.v(TAG, "Content Width: " + contentWidth
+ " Height: " + contentHeight
+ " Animate: " + animate);
+ " Animate: " + animateDurationMs);
}
if (mContainerWidth == 0 || mContainerHeight == 0) {
@@ -86,7 +90,24 @@ public class AuthPanelController extends ViewOutlineProvider {
return;
}
if (animate) {
if (animateDurationMs > 0) {
// Animate margin
final int margin = mUseFullScreen ? 0 : (int) mContext.getResources()
.getDimension(R.dimen.biometric_dialog_border_padding);
ValueAnimator marginAnimator = ValueAnimator.ofInt(mMargin, margin);
marginAnimator.addUpdateListener((animation) -> {
mMargin = (int) animation.getAnimatedValue();
});
// Animate corners
final float cornerRadius = mUseFullScreen ? 0 : mContext.getResources()
.getDimension(R.dimen.biometric_dialog_corner_size);
ValueAnimator cornerAnimator = ValueAnimator.ofFloat(mCornerRadius, cornerRadius);
cornerAnimator.addUpdateListener((animation) -> {
mCornerRadius = (float) animation.getAnimatedValue();
});
// Animate height
ValueAnimator heightAnimator = ValueAnimator.ofInt(mContentHeight, contentHeight);
heightAnimator.addUpdateListener((animation) -> {
mContentHeight = (int) animation.getAnimatedValue();
@@ -94,14 +115,16 @@ public class AuthPanelController extends ViewOutlineProvider {
});
heightAnimator.start();
// Animate width
ValueAnimator widthAnimator = ValueAnimator.ofInt(mContentWidth, contentWidth);
widthAnimator.addUpdateListener((animation) -> {
mContentWidth = (int) animation.getAnimatedValue();
});
// Play together
AnimatorSet as = new AnimatorSet();
as.setDuration(AuthDialog.ANIMATE_DURATION_MS);
as.play(heightAnimator).with(widthAnimator);
as.setDuration(animateDurationMs);
as.playTogether(cornerAnimator, heightAnimator, widthAnimator, marginAnimator);
as.start();
} else {
mContentWidth = contentWidth;
@@ -123,7 +146,7 @@ public class AuthPanelController extends ViewOutlineProvider {
mPanelView = panelView;
mCornerRadius = context.getResources()
.getDimension(R.dimen.biometric_dialog_corner_size);
mBiometricMargin = (int) context.getResources()
mMargin = (int) context.getResources()
.getDimension(R.dimen.biometric_dialog_border_padding);
mPanelView.setOutlineProvider(this);
mPanelView.setClipToOutline(true);

View File

@@ -364,7 +364,12 @@ public class AuthBiometricViewTest extends SysuiTestCase {
}
@Override
public int getAnimationDuration() {
public int getMediumToLargeAnimationDurationMs() {
return 0;
}
@Override
public int getAnimateCredentialStartDelayMs() {
return 0;
}
}

View File

@@ -16,6 +16,7 @@
package com.android.systemui.biometrics;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
@@ -54,7 +55,7 @@ public class AuthContainerViewTest extends SysuiTestCase {
public void testActionAuthenticated_sendsDismissedAuthenticated() {
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_AUTHENTICATED);
verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_AUTHENTICATED));
verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED));
}
@Test
@@ -85,6 +86,17 @@ public class AuthContainerViewTest extends SysuiTestCase {
verify(mCallback).onDismissed(AuthDialogCallback.DISMISSED_ERROR);
}
@Test
public void testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() {
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL);
verify(mCallback).onDeviceCredentialPressed();
// Credential view is attached to the frame layout
waitForIdleSync();
assertEquals(mAuthContainer.mFrameLayout, mAuthContainer.mCredentialView.getParent());
}
private class TestableAuthContainer extends AuthContainerView {
TestableAuthContainer(AuthContainerView.Config config) {
super(config);

View File

@@ -117,14 +117,15 @@ public class AuthControllerTest extends SysuiTestCase {
public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED);
}
@Test
public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_AUTHENTICATED);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
verify(mReceiver).onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED);
}
@Test
@@ -135,12 +136,20 @@ public class AuthControllerTest extends SysuiTestCase {
}
@Test
public void testSendsReasonDismissedBySystemServer_whenDismissedByServer() throws Exception {
public void testSendsReasonServerRequested_whenDismissedByServer() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED);
}
@Test
public void testSendsReasonCredentialConfirmed_whenDeviceCredentialAuthenticated()
throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED);
}
// Statusbar tests
@Test

View File

@@ -93,6 +93,7 @@ public class BiometricService extends SystemService {
private static final int MSG_AUTHENTICATE = 9;
private static final int MSG_CANCEL_AUTHENTICATION = 10;
private static final int MSG_ON_AUTHENTICATION_TIMED_OUT = 11;
private static final int MSG_ON_DEVICE_CREDENTIAL_PRESSED = 12;
private static final int[] FEATURE_ID = {
TYPE_FINGERPRINT,
TYPE_IRIS,
@@ -132,6 +133,10 @@ public class BiometricService extends SystemService {
* Biometric error, waiting for SysUI to finish animation
*/
static final int STATE_ERROR_PENDING_SYSUI = 7;
/**
* Device credential in AuthController is showing
*/
static final int STATE_SHOWING_DEVICE_CREDENTIAL = 8;
final class AuthSession {
// Map of Authenticator/Cookie pairs. We expect to receive the cookies back from
@@ -326,6 +331,11 @@ public class BiometricService extends SystemService {
break;
}
case MSG_ON_DEVICE_CREDENTIAL_PRESSED: {
handleOnDeviceCredentialPressed();
break;
}
default:
Slog.e(TAG, "Unknown message: " + msg);
break;
@@ -545,6 +555,11 @@ public class BiometricService extends SystemService {
public void onTryAgainPressed() {
mHandler.sendEmptyMessage(MSG_ON_TRY_AGAIN_PRESSED);
}
@Override
public void onDeviceCredentialPressed() {
mHandler.sendEmptyMessage(MSG_ON_DEVICE_CREDENTIAL_PRESSED);
}
};
@@ -958,7 +973,7 @@ public class BiometricService extends SystemService {
}
private void logDialogDismissed(int reason) {
if (reason == BiometricPrompt.DISMISSED_REASON_CONFIRMED) {
if (reason == BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED) {
// Explicit auth, authentication confirmed.
// Latency in this case is authenticated -> confirmed. <Biometric>Service
// should have the first half (first acquired -> authenticated).
@@ -1133,6 +1148,9 @@ public class BiometricService extends SystemService {
mCurrentAuthSession.mClientReceiver.onError(error, message);
mStatusBarService.hideBiometricDialog();
mCurrentAuthSession = null;
} else if (mCurrentAuthSession.mState == STATE_SHOWING_DEVICE_CREDENTIAL) {
Slog.d(TAG, "Biometric canceled, ignoring from state: "
+ mCurrentAuthSession.mState);
} else {
Slog.e(TAG, "Impossible session error state: "
+ mCurrentAuthSession.mState);
@@ -1183,9 +1201,12 @@ public class BiometricService extends SystemService {
try {
switch (reason) {
case BiometricPrompt.DISMISSED_REASON_CONFIRMED:
case BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED:
mKeyStore.addAuthToken(mCurrentAuthSession.mTokenEscrow);
case BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED:
case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED:
case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED:
if (mCurrentAuthSession.mTokenEscrow != null) {
mKeyStore.addAuthToken(mCurrentAuthSession.mTokenEscrow);
}
mCurrentAuthSession.mClientReceiver.onAuthenticationSucceeded();
break;
@@ -1240,6 +1261,20 @@ public class BiometricService extends SystemService {
mCurrentAuthSession.mModality);
}
private void handleOnDeviceCredentialPressed() {
Slog.d(TAG, "onDeviceCredentialPressed");
if (mCurrentAuthSession == null) {
Slog.e(TAG, "Auth session null");
return;
}
// Cancel authentication. Skip the token/package check since we are cancelling
// from system server. The interface is permission protected so this is fine.
cancelInternal(null /* token */, null /* package */, false /* fromClient */);
mCurrentAuthSession.mState = STATE_SHOWING_DEVICE_CREDENTIAL;
}
private void handleOnReadyForAuthentication(int cookie, boolean requireConfirmation,
int userId) {
Iterator it = mPendingAuthSession.mModalitiesWaiting.entrySet().iterator();

View File

@@ -333,7 +333,7 @@ public class BiometricServiceTest {
// SystemUI sends callback with dismissed reason
mBiometricService.mInternalReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED);
waitForIdle();
// HAT sent to keystore
verify(mBiometricService.mKeyStore).addAuthToken(any(byte[].class));
@@ -362,7 +362,7 @@ public class BiometricServiceTest {
// SystemUI sends confirm, HAT is sent to keystore and client is notified.
mBiometricService.mInternalReceiver.onDialogDismissed(
BiometricPrompt.DISMISSED_REASON_CONFIRMED);
BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED);
waitForIdle();
verify(mBiometricService.mKeyStore).addAuthToken(any(byte[].class));
verify(mReceiver1).onAuthenticationSucceeded();
@@ -548,6 +548,31 @@ public class BiometricServiceTest {
assertNull(mBiometricService.mCurrentAuthSession);
}
@Test
public void testErrorFromHal_whileShowingDeviceCredential_doesntNotifySystemUI()
throws Exception {
setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT);
invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
false /* requireConfirmation */);
mBiometricService.mInternalReceiver.onDeviceCredentialPressed();
waitForIdle();
assertEquals(BiometricService.STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.mState);
verify(mReceiver1, never()).onError(anyInt(), anyString());
mBiometricService.mInternalReceiver.onError(
getCookieForCurrentSession(mBiometricService.mCurrentAuthSession),
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
ERROR_CANCELED);
waitForIdle();
assertEquals(BiometricService.STATE_SHOWING_DEVICE_CREDENTIAL,
mBiometricService.mCurrentAuthSession.mState);
verify(mReceiver1, never()).onError(anyInt(), anyString());
}
@Test
public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication()
throws Exception {