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:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
27
packages/SystemUI/res/drawable/auth_dialog_lock.xml
Normal file
27
packages/SystemUI/res/drawable/auth_dialog_lock.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
98
packages/SystemUI/res/layout/auth_credential_view.xml
Normal file
98
packages/SystemUI/res/layout/auth_credential_view.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -364,7 +364,12 @@ public class AuthBiometricViewTest extends SysuiTestCase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAnimationDuration() {
|
||||
public int getMediumToLargeAnimationDurationMs() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAnimateCredentialStartDelayMs() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user