Merge changes from topic "biometric-ui-refactor"

* changes:
  4/n: Rename files to make more sense
  3/n: Tapping outside of the dialog should cancel authentication
  2/n: Start plumbing authentication signals to the UI
  1/n: Refactor BiometricPrompt UI hierarchy
This commit is contained in:
Kevin Chyn
2019-09-03 17:49:46 +00:00
committed by Android (Google) Code Review
21 changed files with 2021 additions and 90 deletions

View File

@@ -0,0 +1,118 @@
<!--
~ 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/title"
android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingTop="24dp"
android:gravity="@integer/biometric_dialog_text_gravity"
android:textSize="20sp"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingHorizontal="24dp"
android:gravity="@integer/biometric_dialog_text_gravity"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingBottom="48dp"
android:paddingTop="8dp"
android:gravity="@integer/biometric_dialog_text_gravity"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"/>
<ImageView
android:id="@+id/biometric_icon"
android:layout_width="@dimen/biometric_dialog_biometric_icon_size"
android:layout_height="@dimen/biometric_dialog_biometric_icon_size"
android:layout_gravity="center_horizontal"
android:scaleType="fitXY" />
<TextView
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingTop="16dp"
android:paddingBottom="24dp"
android:textSize="12sp"
android:gravity="center_horizontal"
android:accessibilityLiveRegion="polite"
android:textColor="@color/biometric_dialog_gray"/>
<LinearLayout
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="72dip"
android:paddingTop="24dp"
android:layout_gravity="center_vertical"
style="?android:attr/buttonBarStyle"
android:orientation="horizontal">
<Space android:id="@+id/leftSpacer"
android:layout_width="12dp"
android:layout_height="match_parent"
android:visibility="visible" />
<!-- Negative Button -->
<Button android:id="@+id/button_negative"
android:layout_width="wrap_content"
android:layout_height="match_parent"
style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
android:gravity="center"
android:maxLines="2"/>
<Space android:id="@+id/middleSpacer"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:visibility="visible"/>
<!-- Positive Button -->
<Button android:id="@+id/button_positive"
android:layout_width="wrap_content"
android:layout_height="match_parent"
style="@*android:style/Widget.DeviceDefault.Button.Colored"
android:gravity="center"
android:maxLines="2"
android:text="@string/biometric_dialog_confirm"
android:visibility="gone"/>
<!-- Try Again Button -->
<Button android:id="@+id/button_try_again"
android:layout_width="wrap_content"
android:layout_height="match_parent"
style="@*android:style/Widget.DeviceDefault.Button.Colored"
android:gravity="center"
android:maxLines="2"
android:text="@string/biometric_dialog_try_again"
android:visibility="gone"/>
<Space android:id="@+id/rightSpacer"
android:layout_width="12dip"
android:layout_height="match_parent"
android:visibility="visible" />
</LinearLayout>
</merge>

View File

@@ -0,0 +1,26 @@
<!--
~ 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.AuthBiometricFaceView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/auth_biometric_contents"/>
</com.android.systemui.biometrics.AuthBiometricFaceView>

View File

@@ -0,0 +1,43 @@
<!--
~ 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/biometric_dialog_dim_color"/>
<View
android:id="@+id/panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackgroundFloating"
android:elevation="@dimen/biometric_dialog_elevation"/>
<ScrollView
android:id="@+id/scrollview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:layout_margin="@dimen/biometric_dialog_border_padding"
android:elevation="@dimen/biometric_dialog_elevation"/>
</FrameLayout>

View File

@@ -291,7 +291,7 @@
<item>com.android.systemui.LatencyTester</item>
<item>com.android.systemui.globalactions.GlobalActionsComponent</item>
<item>com.android.systemui.ScreenDecorations</item>
<item>com.android.systemui.biometrics.BiometricDialogImpl</item>
<item>com.android.systemui.biometrics.AuthController</item>
<item>com.android.systemui.SliceBroadcastRelayHandler</item>
<item>com.android.systemui.SizeCompatModeActivityController</item>
<item>com.android.systemui.statusbar.notification.InstantAppNotifier</item>

View File

@@ -1010,6 +1010,8 @@
<dimen name="biometric_dialog_corner_size">4dp</dimen>
<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>
<!-- Wireless Charging Animation values -->
<dimen name="wireless_charging_dots_radius_start">0dp</dimen>

View File

@@ -0,0 +1,201 @@
/*
* 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.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
public class AuthBiometricFaceView extends AuthBiometricView {
private static final String TAG = "BiometricPrompt/AuthBiometricFaceView";
// Delay before dismissing after being authenticated/confirmed.
private static final int HIDE_DELAY_MS = 500;
public static class IconController extends Animatable2.AnimationCallback {
Context mContext;
ImageView mIconView;
TextView mTextView;
Handler mHandler;
boolean mLastPulseLightToDark; // false = dark to light, true = light to dark
@BiometricState int mState;
IconController(Context context, ImageView iconView, TextView textView) {
mContext = context;
mIconView = iconView;
mTextView = textView;
mHandler = new Handler(Looper.getMainLooper());
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
}
void animateOnce(int iconRes) {
animateIcon(iconRes, false);
}
public void showStaticDrawable(int iconRes) {
mIconView.setImageDrawable(mContext.getDrawable(iconRes));
}
void animateIcon(int iconRes, boolean repeat) {
final AnimatedVectorDrawable icon =
(AnimatedVectorDrawable) mContext.getDrawable(iconRes);
mIconView.setImageDrawable(icon);
icon.forceAnimationOnUI();
if (repeat) {
icon.registerAnimationCallback(this);
}
icon.start();
}
void startPulsing() {
mLastPulseLightToDark = false;
animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true);
}
void pulseInNextDirection() {
int iconRes = mLastPulseLightToDark ? R.drawable.face_dialog_pulse_dark_to_light
: R.drawable.face_dialog_pulse_light_to_dark;
animateIcon(iconRes, true /* repeat */);
mLastPulseLightToDark = !mLastPulseLightToDark;
}
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
if (mState == STATE_AUTHENTICATING || mState == STATE_HELP) {
pulseInNextDirection();
}
}
public void updateState(int lastState, int newState) {
final boolean lastStateIsErrorIcon =
lastState == STATE_ERROR || lastState == STATE_HELP;
if (newState == STATE_AUTHENTICATING_ANIMATING_IN) {
showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticating));
} else if (newState == STATE_AUTHENTICATING) {
startPulsing();
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticating));
} else if (lastState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_confirmed));
} else if (lastStateIsErrorIcon && newState == STATE_IDLE) {
animateOnce(R.drawable.face_dialog_error_to_idle);
} else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
} else if (newState == STATE_ERROR && lastState != STATE_ERROR) {
animateOnce(R.drawable.face_dialog_dark_to_error);
} else if (lastState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
animateOnce(R.drawable.face_dialog_dark_to_checkmark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == STATE_PENDING_CONFIRMATION) {
animateOnce(R.drawable.face_dialog_wink_from_dark);
mIconView.setContentDescription(mContext.getString(
R.string.biometric_dialog_face_icon_description_authenticated));
} else if (newState == STATE_IDLE) {
showStaticDrawable(R.drawable.face_dialog_idle_static);
} else {
Log.w(TAG, "Unhandled state: " + newState);
}
mState = newState;
}
}
@VisibleForTesting IconController mIconController;
public AuthBiometricFaceView(Context context) {
this(context, null);
}
public AuthBiometricFaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return HIDE_DELAY_MS;
}
@Override
protected int getStateForAfterError() {
return STATE_IDLE;
}
@Override
protected void handleResetAfterError() {
resetErrorView(mContext, mErrorView);
}
@Override
protected void handleResetAfterHelp() {
resetErrorView(mContext, mErrorView);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mIconController = new IconController(mContext, mIconView, mErrorView);
}
@Override
public void updateState(@BiometricState int newState) {
mIconController.updateState(mState, newState);
if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
(newState == STATE_AUTHENTICATING && mSize == AuthDialog.SIZE_MEDIUM)) {
resetErrorView(mContext, mErrorView);
}
// Do this last since the state variable gets updated.
super.updateState(newState);
}
@Override
public void onAuthenticationFailed(String failureReason) {
if (mSize == AuthDialog.SIZE_MEDIUM) {
mTryAgainButton.setVisibility(View.VISIBLE);
mPositiveButton.setVisibility(View.GONE);
}
// Do this last since wa want to know if the button is being animated (in the case of
// small -> medium dialog)
super.onAuthenticationFailed(failureReason);
}
static void resetErrorView(Context context, TextView textView) {
textView.setTextColor(context.getResources().getColor(
R.color.biometric_dialog_gray, context.getTheme()));
textView.setVisibility(View.INVISIBLE);
}
}

View File

@@ -0,0 +1,533 @@
/*
* 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.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.content.Context;
import android.hardware.biometrics.BiometricPrompt;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Contains the Biometric views (title, subtitle, icon, buttons, etc) and its controllers.
*/
public abstract class AuthBiometricView extends LinearLayout {
private static final String TAG = "BiometricPrompt/AuthBiometricView";
/**
* Authentication hardware idle.
*/
protected static final int STATE_IDLE = 0;
/**
* UI animating in, authentication hardware active.
*/
protected static final int STATE_AUTHENTICATING_ANIMATING_IN = 1;
/**
* UI animated in, authentication hardware active.
*/
protected static final int STATE_AUTHENTICATING = 2;
/**
* UI animated in, authentication hardware active.
*/
protected static final int STATE_HELP = 3;
/**
* Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle.
*/
protected static final int STATE_ERROR = 4;
/**
* Authenticated, waiting for user confirmation. Authentication hardware idle.
*/
protected static final int STATE_PENDING_CONFIRMATION = 5;
/**
* Authenticated, dialog animating away soon.
*/
protected static final int STATE_AUTHENTICATED = 6;
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_IDLE, STATE_AUTHENTICATING_ANIMATING_IN, STATE_AUTHENTICATING, STATE_HELP,
STATE_ERROR, STATE_PENDING_CONFIRMATION, STATE_AUTHENTICATED})
@interface BiometricState {}
/**
* Callback to the parent when a user action has occurred.
*/
interface Callback {
int ACTION_AUTHENTICATED = 1;
int ACTION_USER_CANCELED = 2;
int ACTION_BUTTON_NEGATIVE = 3;
int ACTION_BUTTON_TRY_AGAIN = 4;
/**
* When an action has occurred. The caller will only invoke this when the callback should
* be propagated. e.g. the caller will handle any necessary delay.
* @param action
*/
void onAction(int action);
}
@VisibleForTesting
static class Injector {
AuthBiometricView mBiometricView;
public Button getNegativeButton() {
return mBiometricView.findViewById(R.id.button_negative);
}
public Button getPositiveButton() {
return mBiometricView.findViewById(R.id.button_positive);
}
public Button getTryAgainButton() {
return mBiometricView.findViewById(R.id.button_try_again);
}
public TextView getTitleView() {
return mBiometricView.findViewById(R.id.title);
}
public TextView getSubtitleView() {
return mBiometricView.findViewById(R.id.subtitle);
}
public TextView getDescriptionView() {
return mBiometricView.findViewById(R.id.description);
}
public TextView getErrorView() {
return mBiometricView.findViewById(R.id.error);
}
public ImageView getIconView() {
return mBiometricView.findViewById(R.id.biometric_icon);
}
}
private final Injector mInjector;
private final Handler mHandler;
private final int mTextColorError;
private final int mTextColorHint;
private AuthPanelController mPanelController;
private Bundle mBundle;
private boolean mRequireConfirmation;
@AuthDialog.DialogSize int mSize = AuthDialog.SIZE_UNKNOWN;
private TextView mTitleView;
private TextView mSubtitleView;
private TextView mDescriptionView;
protected ImageView mIconView;
@VisibleForTesting protected TextView mErrorView;
@VisibleForTesting Button mNegativeButton;
@VisibleForTesting Button mPositiveButton;
@VisibleForTesting Button mTryAgainButton;
// Measurements when biometric view is showing text, buttons, etc.
private int mMediumHeight;
private int mMediumWidth;
private Callback mCallback;
protected @BiometricState int mState;
private float mIconOriginalY;
protected boolean mDialogSizeAnimating;
/**
* Delay after authentication is confirmed, before the dialog should be animated away.
*/
protected abstract int getDelayAfterAuthenticatedDurationMs();
/**
* State that the dialog/icon should be in after showing a help message.
*/
protected abstract int getStateForAfterError();
/**
* Invoked when the error message is being cleared.
*/
protected abstract void handleResetAfterError();
/**
* Invoked when the help message is being cleared.
*/
protected abstract void handleResetAfterHelp();
private final Runnable mResetErrorRunnable = () -> {
updateState(getStateForAfterError());
handleResetAfterError();
};
private final Runnable mResetHelpRunnable = () -> {
updateState(STATE_AUTHENTICATING);
handleResetAfterHelp();
};
private final OnClickListener mBackgroundClickListener = (view) -> {
if (mState == STATE_AUTHENTICATED) {
Log.w(TAG, "Ignoring background click after authenticated");
return;
} else if (mSize == AuthDialog.SIZE_SMALL) {
Log.w(TAG, "Ignoring background click during small dialog");
return;
}
mCallback.onAction(Callback.ACTION_USER_CANCELED);
};
public AuthBiometricView(Context context) {
this(context, null);
}
public AuthBiometricView(Context context, AttributeSet attrs) {
this(context, attrs, new Injector());
}
@VisibleForTesting
AuthBiometricView(Context context, AttributeSet attrs, Injector injector) {
super(context, attrs);
mHandler = new Handler(Looper.getMainLooper());
mTextColorError = getResources().getColor(
R.color.biometric_dialog_error, context.getTheme());
mTextColorHint = getResources().getColor(
R.color.biometric_dialog_gray, context.getTheme());
mInjector = injector;
mInjector.mBiometricView = this;
}
public void setPanelController(AuthPanelController panelController) {
mPanelController = panelController;
}
public void setBiometricPromptBundle(Bundle bundle) {
mBundle = bundle;
}
public void setCallback(Callback callback) {
mCallback = callback;
}
public void setBackgroundView(View backgroundView) {
backgroundView.setOnClickListener(mBackgroundClickListener);
}
public void setRequireConfirmation(boolean requireConfirmation) {
mRequireConfirmation = requireConfirmation;
}
@VisibleForTesting
void updateSize(@AuthDialog.DialogSize int newSize) {
Log.v(TAG, "Current: " + mSize + " New: " + newSize);
if (newSize == AuthDialog.SIZE_SMALL) {
mTitleView.setVisibility(View.GONE);
mSubtitleView.setVisibility(View.GONE);
mDescriptionView.setVisibility(View.GONE);
mErrorView.setVisibility(View.GONE);
mNegativeButton.setVisibility(View.GONE);
final float iconPadding = getResources()
.getDimension(R.dimen.biometric_dialog_icon_padding);
mIconView.setY(getHeight() - mIconView.getHeight() - iconPadding);
final int newHeight = mIconView.getHeight() + 2 * (int) iconPadding;
mPanelController.updateForContentDimensions(mMediumWidth, newHeight,
false /* animate */);
mSize = newSize;
} else if (mSize == AuthDialog.SIZE_SMALL && newSize == AuthDialog.SIZE_MEDIUM) {
if (mDialogSizeAnimating) {
return;
}
mDialogSizeAnimating = true;
// Animate the icon back to original position
final ValueAnimator iconAnimator =
ValueAnimator.ofFloat(mIconView.getY(), mIconOriginalY);
iconAnimator.addUpdateListener((animation) -> {
mIconView.setY((float) animation.getAnimatedValue());
});
// Animate the text
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1);
opacityAnimator.setDuration(AuthDialog.ANIMATE_DURATION_MS);
opacityAnimator.addUpdateListener((animation) -> {
final float opacity = (float) animation.getAnimatedValue();
mTitleView.setAlpha(opacity);
mErrorView.setAlpha(opacity);
mNegativeButton.setAlpha(opacity);
mTryAgainButton.setAlpha(opacity);
if (!TextUtils.isEmpty(mSubtitleView.getText())) {
mSubtitleView.setAlpha(opacity);
}
if (!TextUtils.isEmpty(mDescriptionView.getText())) {
mDescriptionView.setAlpha(opacity);
}
});
// Choreograph together
final AnimatorSet as = new AnimatorSet();
as.setDuration(AuthDialog.ANIMATE_DURATION_MS);
as.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mTitleView.setVisibility(View.VISIBLE);
mErrorView.setVisibility(View.VISIBLE);
mNegativeButton.setVisibility(View.VISIBLE);
mTryAgainButton.setVisibility(View.VISIBLE);
if (!TextUtils.isEmpty(mSubtitleView.getText())) {
mSubtitleView.setVisibility(View.VISIBLE);
}
if (!TextUtils.isEmpty(mDescriptionView.getText())) {
mDescriptionView.setVisibility(View.VISIBLE);
}
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mSize = newSize;
mDialogSizeAnimating = false;
}
});
as.play(iconAnimator).with(opacityAnimator);
as.start();
// Animate the panel
mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight,
true /* animate */);
} else if (newSize == AuthDialog.SIZE_MEDIUM) {
mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight,
false /* animate */);
mSize = newSize;
} else {
Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize);
}
}
public void updateState(@BiometricState int newState) {
Log.v(TAG, "newState: " + newState);
switch (newState) {
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
removePendingAnimations();
if (mRequireConfirmation) {
mPositiveButton.setEnabled(false);
mPositiveButton.setVisibility(View.VISIBLE);
}
break;
case STATE_AUTHENTICATED:
if (mSize != AuthDialog.SIZE_SMALL) {
mPositiveButton.setVisibility(View.GONE);
mNegativeButton.setVisibility(View.GONE);
mErrorView.setVisibility(View.INVISIBLE);
}
mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_AUTHENTICATED),
getDelayAfterAuthenticatedDurationMs());
break;
case STATE_PENDING_CONFIRMATION:
removePendingAnimations();
mNegativeButton.setText(R.string.cancel);
mNegativeButton.setContentDescription(getResources().getString(R.string.cancel));
mPositiveButton.setEnabled(true);
mErrorView.setTextColor(mTextColorHint);
mErrorView.setText(R.string.biometric_dialog_tap_confirm);
mErrorView.setVisibility(View.VISIBLE);
break;
case STATE_ERROR:
if (mSize == AuthDialog.SIZE_SMALL) {
updateSize(AuthDialog.SIZE_MEDIUM);
}
break;
default:
Log.w(TAG, "Unhandled state: " + newState);
break;
}
mState = newState;
}
public void onDialogAnimatedIn() {
updateState(STATE_AUTHENTICATING);
}
public void onAuthenticationSucceeded() {
removePendingAnimations();
if (mRequireConfirmation) {
updateState(STATE_PENDING_CONFIRMATION);
} else {
updateState(STATE_AUTHENTICATED);
}
}
public void onAuthenticationFailed(String failureReason) {
showTemporaryMessage(failureReason, mResetErrorRunnable);
updateState(STATE_ERROR);
}
public void onHelp(String help) {
if (mSize != AuthDialog.SIZE_MEDIUM) {
return;
}
showTemporaryMessage(help, mResetHelpRunnable);
updateState(STATE_HELP);
}
private void setTextOrHide(TextView view, String string) {
if (TextUtils.isEmpty(string)) {
view.setVisibility(View.GONE);
} else {
view.setText(string);
}
}
private void setText(TextView view, String string) {
view.setText(string);
}
// Remove all pending icon and text animations
private void removePendingAnimations() {
mHandler.removeCallbacks(mResetHelpRunnable);
mHandler.removeCallbacks(mResetErrorRunnable);
}
private void showTemporaryMessage(String message, Runnable resetMessageRunnable) {
removePendingAnimations();
mErrorView.setText(message);
mErrorView.setTextColor(mTextColorError);
mErrorView.setVisibility(View.VISIBLE);
mHandler.postDelayed(resetMessageRunnable, BiometricPrompt.HIDE_DIALOG_DELAY);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
initializeViews();
}
@VisibleForTesting
void initializeViews() {
mTitleView = mInjector.getTitleView();
mSubtitleView = mInjector.getSubtitleView();
mDescriptionView = mInjector.getDescriptionView();
mIconView = mInjector.getIconView();
mErrorView = mInjector.getErrorView();
mNegativeButton = mInjector.getNegativeButton();
mPositiveButton = mInjector.getPositiveButton();
mTryAgainButton = mInjector.getTryAgainButton();
mNegativeButton.setOnClickListener((view) -> {
if (mState == STATE_PENDING_CONFIRMATION) {
mCallback.onAction(Callback.ACTION_USER_CANCELED);
} else {
mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
}
});
mPositiveButton.setOnClickListener((view) -> {
updateState(STATE_AUTHENTICATED);
});
mTryAgainButton.setOnClickListener((view) -> {
updateState(STATE_AUTHENTICATING);
mCallback.onAction(Callback.ACTION_BUTTON_TRY_AGAIN);
mTryAgainButton.setVisibility(View.GONE);
});
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setText(mTitleView, mBundle.getString(BiometricPrompt.KEY_TITLE));
setText(mNegativeButton, mBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT));
setTextOrHide(mSubtitleView, mBundle.getString(BiometricPrompt.KEY_SUBTITLE));
setTextOrHide(mDescriptionView, mBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
updateState(STATE_AUTHENTICATING_ANIMATING_IN);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int height = MeasureSpec.getSize(heightMeasureSpec);
final int newWidth = Math.min(width, height);
int totalHeight = 0;
final int numChildren = getChildCount();
for (int i = 0; i < numChildren; i++) {
final View child = getChildAt(i);
if (child.getId() == R.id.biometric_icon) {
child.measure(
MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
} else if (child.getId() == R.id.button_bar) {
child.measure(
MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
MeasureSpec.EXACTLY));
} else {
child.measure(
MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
}
totalHeight += child.getMeasuredHeight();
}
// Use the new width so it's centered horizontally
setMeasuredDimension(newWidth, totalHeight);
mMediumHeight = totalHeight;
mMediumWidth = getMeasuredWidth();
}
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// Start with initial size only once. Subsequent layout changes don't matter since we
// only care about the initial icon position.
if (mIconOriginalY == 0) {
mIconOriginalY = mIconView.getY();
updateSize(mRequireConfirmation ? AuthDialog.SIZE_MEDIUM
: AuthDialog.SIZE_SMALL);
}
}
}

View File

@@ -0,0 +1,400 @@
/*
* 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.annotation.IntDef;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Top level container/controller for the BiometricPrompt UI.
*/
public class AuthContainerView extends LinearLayout
implements AuthDialog, WakefulnessLifecycle.Observer {
private static final String TAG = "BiometricPrompt/AuthContainerView";
private static final int ANIMATION_DURATION_SHOW_MS = 250;
private static final int ANIMATION_DURATION_AWAY_MS = 350; // ms
private static final int STATE_UNKNOWN = 0;
private static final int STATE_ANIMATING_IN = 1;
private static final int STATE_PENDING_DISMISS = 2;
private static final int STATE_SHOWING = 3;
private static final int STATE_ANIMATING_OUT = 4;
private static final int STATE_GONE = 5;
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
STATE_ANIMATING_OUT, STATE_GONE})
@interface ContainerState {}
final Config mConfig;
private final IBinder mWindowToken = new Binder();
private final WindowManager mWindowManager;
private final AuthPanelController mPanelController;
private final Interpolator mLinearOutSlowIn;
@VisibleForTesting final BiometricCallback mBiometricCallback;
private final ViewGroup mContainerView;
private final AuthBiometricView mBiometricView;
private final ImageView mBackgroundView;
private final ScrollView mScrollView;
private final View mPanelView;
private final float mTranslationY;
@VisibleForTesting final WakefulnessLifecycle mWakefulnessLifecycle;
private @ContainerState int mContainerState = STATE_UNKNOWN;
static class Config {
Context mContext;
AuthDialogCallback mCallback;
Bundle mBiometricPromptBundle;
boolean mRequireConfirmation;
int mUserId;
String mOpPackageName;
int mModalityMask;
boolean mSkipIntro;
}
public static class Builder {
Config mConfig;
public Builder(Context context) {
mConfig = new Config();
mConfig.mContext = context;
}
public Builder setCallback(AuthDialogCallback callback) {
mConfig.mCallback = callback;
return this;
}
public Builder setBiometricPromptBundle(Bundle bundle) {
mConfig.mBiometricPromptBundle = bundle;
return this;
}
public Builder setRequireConfirmation(boolean requireConfirmation) {
mConfig.mRequireConfirmation = requireConfirmation;
return this;
}
public Builder setUserId(int userId) {
mConfig.mUserId = userId;
return this;
}
public Builder setOpPackageName(String opPackageName) {
mConfig.mOpPackageName = opPackageName;
return this;
}
public Builder setSkipIntro(boolean skip) {
mConfig.mSkipIntro = skip;
return this;
}
public AuthContainerView build(int modalityMask) { // TODO
return new AuthContainerView(mConfig);
}
}
@VisibleForTesting
final class BiometricCallback implements AuthBiometricView.Callback {
@Override
public void onAction(int action) {
switch (action) {
case AuthBiometricView.Callback.ACTION_AUTHENTICATED:
animateAway(AuthDialogCallback.DISMISSED_AUTHENTICATED);
break;
case AuthBiometricView.Callback.ACTION_USER_CANCELED:
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
break;
case AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE:
animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
break;
case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN:
mConfig.mCallback.onTryAgainPressed();
break;
default:
Log.e(TAG, "Unhandled action: " + action);
}
}
}
@VisibleForTesting
AuthContainerView(Config config) {
super(config.mContext);
mConfig = config;
mWindowManager = mContext.getSystemService(WindowManager.class);
mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
mTranslationY = getResources()
.getDimension(R.dimen.biometric_dialog_animation_translation_offset);
mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
mBiometricCallback = new BiometricCallback();
final LayoutInflater factory = LayoutInflater.from(mContext);
mContainerView = (ViewGroup) factory.inflate(
R.layout.auth_container_view, this, false /* attachToRoot */);
// TODO: Depends on modality
mBiometricView = (AuthBiometricFaceView)
factory.inflate(R.layout.auth_biometric_face_view, null, false);
mBackgroundView = mContainerView.findViewById(R.id.background);
mPanelView = mContainerView.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
mBiometricView.setPanelController(mPanelController);
mBiometricView.setBiometricPromptBundle(config.mBiometricPromptBundle);
mBiometricView.setCallback(mBiometricCallback);
mBiometricView.setBackgroundView(mBackgroundView);
mScrollView = mContainerView.findViewById(R.id.scrollview);
mScrollView.addView(mBiometricView);
addView(mContainerView);
setOnKeyListener((v, keyCode, event) -> {
if (keyCode != KeyEvent.KEYCODE_BACK) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_UP) {
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
}
return true;
});
setFocusableInTouchMode(true);
requestFocus();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mPanelController.setContainerDimensions(getMeasuredWidth(), getMeasuredHeight());
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mWakefulnessLifecycle.addObserver(this);
if (mConfig.mSkipIntro) {
mContainerState = STATE_SHOWING;
} else {
mContainerState = STATE_ANIMATING_IN;
// 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);
setAlpha(0f);
postOnAnimation(() -> {
mPanelView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.withEndAction(this::onDialogAnimatedIn)
.start();
mScrollView.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
animate()
.alpha(1f)
.setDuration(ANIMATION_DURATION_SHOW_MS)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
});
}
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mWakefulnessLifecycle.removeObserver(this);
}
@Override
public void onStartedGoingToSleep() {
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
}
@Override
public void show(WindowManager wm) {
wm.addView(this, getLayoutParams(mWindowToken));
}
@Override
public void dismissWithoutCallback(boolean animate) {
if (animate) {
animateAway(false /* sendReason */, 0 /* reason */);
} else {
mWindowManager.removeView(this);
}
}
@Override
public void dismissFromSystemServer() {
mWindowManager.removeView(this);
}
@Override
public void onAuthenticationSucceeded() {
mBiometricView.onAuthenticationSucceeded();
}
@Override
public void onAuthenticationFailed(String failureReason) {
mBiometricView.onAuthenticationFailed(failureReason);
}
@Override
public void onHelp(String help) {
mBiometricView.onHelp(help);
}
@Override
public void onError(String error) {
}
@Override
public void onSaveState(Bundle outState) {
}
@Override
public void restoreState(Bundle savedState) {
}
@Override
public String getOpPackageName() {
return mConfig.mOpPackageName;
}
@VisibleForTesting
void animateAway(int reason) {
animateAway(true /* sendReason */, reason);
}
private void animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason) {
if (mContainerState == STATE_ANIMATING_IN) {
Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
mContainerState = STATE_PENDING_DISMISS;
return;
}
if (mContainerState == STATE_ANIMATING_OUT) {
Log.w(TAG, "Already dismissing, sendReason: " + sendReason + " reason: " + reason);
return;
}
mContainerState = STATE_ANIMATING_OUT;
final Runnable endActionRunnable = () -> {
setVisibility(View.INVISIBLE);
mWindowManager.removeView(this);
if (sendReason) {
mConfig.mCallback.onDismissed(reason);
}
};
postOnAnimation(() -> {
mPanelView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.withEndAction(endActionRunnable)
.start();
mScrollView.animate()
.translationY(mTranslationY)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
animate()
.alpha(0f)
.setDuration(ANIMATION_DURATION_AWAY_MS)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.start();
});
}
private void onDialogAnimatedIn() {
if (mContainerState == STATE_PENDING_DISMISS) {
Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
animateAway(false /* sendReason */, 0);
return;
}
mContainerState = STATE_SHOWING;
mBiometricView.onDialogAnimatedIn();
}
/**
* @param windowToken token for the window
* @return
*/
public static WindowManager.LayoutParams getLayoutParams(IBinder windowToken) {
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
PixelFormat.TRANSLUCENT);
lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
lp.setTitle("BiometricPrompt");
lp.token = windowToken;
return lp;
}
}

View File

@@ -11,7 +11,7 @@
* 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
* limitations under the License.
*/
package com.android.systemui.biometrics;
@@ -29,13 +29,13 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
import android.view.WindowManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.systemui.SystemUI;
import com.android.systemui.biometrics.ui.BiometricDialogView;
import com.android.systemui.statusbar.CommandQueue;
import java.util.List;
@@ -44,9 +44,12 @@ import java.util.List;
* Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
* appropriate biometric UI (e.g. BiometricDialogView).
*/
public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks,
DialogViewCallback {
private static final String TAG = "BiometricDialogImpl";
public class AuthController extends SystemUI implements CommandQueue.Callbacks,
AuthDialogCallback {
private static final String USE_NEW_DIALOG =
"com.android.systemui.biometrics.AuthController.USE_NEW_DIALOG";
private static final String TAG = "BiometricPrompt/AuthController";
private static final boolean DEBUG = true;
private final Injector mInjector;
@@ -54,7 +57,7 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
// TODO: These should just be saved from onSaveState
private SomeArgs mCurrentDialogArgs;
@VisibleForTesting
BiometricDialog mCurrentDialog;
AuthDialog mCurrentDialog;
private Handler mHandler = new Handler(Looper.getMainLooper());
private WindowManager mWindowManager;
@@ -107,27 +110,27 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
@Override
public void onDismissed(@DismissedReason int reason) {
switch (reason) {
case DialogViewCallback.DISMISSED_USER_CANCELED:
case AuthDialogCallback.DISMISSED_USER_CANCELED:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
break;
case DialogViewCallback.DISMISSED_BUTTON_NEGATIVE:
case AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
break;
case DialogViewCallback.DISMISSED_BUTTON_POSITIVE:
case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
break;
case DialogViewCallback.DISMISSED_AUTHENTICATED:
case AuthDialogCallback.DISMISSED_AUTHENTICATED:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
break;
case DialogViewCallback.DISMISSED_ERROR:
case AuthDialogCallback.DISMISSED_ERROR:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR);
break;
case DialogViewCallback.DISMISSED_BY_SYSTEM_SERVER:
case AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER:
sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED);
break;
@@ -156,12 +159,12 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
}
}
public BiometricDialogImpl() {
public AuthController() {
this(new Injector());
}
@VisibleForTesting
BiometricDialogImpl(Injector injector) {
AuthController(Injector injector) {
mInjector = injector;
}
@@ -248,12 +251,13 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
final String opPackageName = (String) args.arg4;
// Create a new dialog but do not replace the current one yet.
final BiometricDialog newDialog = buildDialog(
final AuthDialog newDialog = buildDialog(
biometricPromptBundle,
requireConfirmation,
userId,
type,
opPackageName);
opPackageName,
skipAnimation);
if (newDialog == null) {
Log.e(TAG, "Unsupported type: " + type);
@@ -282,7 +286,7 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
mReceiver = (IBiometricServiceReceiverInternal) args.arg2;
mCurrentDialog = newDialog;
mCurrentDialog.show(mWindowManager, skipAnimation);
mCurrentDialog.show(mWindowManager);
}
private void onDialogDismissed(@DismissedReason int reason) {
@@ -309,14 +313,27 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
}
}
protected BiometricDialog buildDialog(Bundle biometricPromptBundle,
boolean requireConfirmation, int userId, int type, String opPackageName) {
return new BiometricDialogView.Builder(mContext)
.setCallback(this)
.setBiometricPromptBundle(biometricPromptBundle)
.setRequireConfirmation(requireConfirmation)
.setUserId(userId)
.setOpPackageName(opPackageName)
.build(type);
protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation,
int userId, int type, String opPackageName, boolean skipIntro) {
if (Settings.Secure.getIntForUser(
mContext.getContentResolver(), USE_NEW_DIALOG, userId, 0) != 0) {
return new AuthContainerView.Builder(mContext)
.setCallback(this)
.setBiometricPromptBundle(biometricPromptBundle)
.setRequireConfirmation(requireConfirmation)
.setUserId(userId)
.setOpPackageName(opPackageName)
.setSkipIntro(skipIntro)
.build(type);
} else {
return new BiometricDialogView.Builder(mContext)
.setCallback(this)
.setBiometricPromptBundle(biometricPromptBundle)
.setRequireConfirmation(requireConfirmation)
.setUserId(userId)
.setOpPackageName(opPackageName)
.setSkipIntro(skipIntro)
.build(type);
}
}
}

View File

@@ -16,16 +16,18 @@
package com.android.systemui.biometrics;
import android.annotation.IntDef;
import android.hardware.biometrics.BiometricPrompt;
import android.os.Bundle;
import android.view.WindowManager;
import com.android.systemui.biometrics.ui.BiometricDialogView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Interface for the biometric dialog UI.
*/
public interface BiometricDialog {
public interface AuthDialog {
// TODO: Clean up save/restore state
String[] KEYS_TO_BACKUP = {
@@ -49,12 +51,24 @@ public interface BiometricDialog {
BiometricDialogView.KEY_ERROR_TEXT_COLOR,
};
int SIZE_UNKNOWN = 0;
int SIZE_SMALL = 1;
int SIZE_MEDIUM = 2;
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.
*/
int ANIMATE_DURATION_MS = 150;
/**
* Show the dialog.
* @param wm
* @param skipIntroAnimation
*/
void show(WindowManager wm, boolean skipIntroAnimation);
void show(WindowManager wm);
/**
* Dismiss the dialog without sending a callback.

View File

@@ -11,7 +11,7 @@
* 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
* limitations under the License.
*/
package com.android.systemui.biometrics;
@@ -22,7 +22,7 @@ import android.annotation.IntDef;
* Callback interface for dialog views. These should be implemented by the controller (e.g.
* FingerprintDialogImpl) and passed into their views (e.g. FingerprintDialogView).
*/
public interface DialogViewCallback {
public interface AuthDialogCallback {
int DISMISSED_USER_CANCELED = 1;
int DISMISSED_BUTTON_NEGATIVE = 2;

View File

@@ -0,0 +1,105 @@
/*
* 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.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Outline;
import android.util.Log;
import android.view.View;
import android.view.ViewOutlineProvider;
import com.android.systemui.R;
import com.android.systemui.biometrics.AuthDialog;
/**
* Controls the back panel and its animations for the BiometricPrompt UI.
*/
public class AuthPanelController extends ViewOutlineProvider {
private static final String TAG = "BiometricPrompt/AuthPanelController";
private static final boolean DEBUG = false;
private final Context mContext;
private final View mPanelView;
private final float mCornerRadius;
private final int mBiometricMargin;
private int mContainerWidth;
private int mContainerHeight;
private int mContentWidth;
private int mContentHeight;
@Override
public void getOutline(View view, Outline outline) {
final int left = (mContainerWidth - mContentWidth) / 2;
final int right = mContainerWidth - left;
final int top = mContentHeight < mContainerHeight
? mContainerHeight - mContentHeight - mBiometricMargin
: mBiometricMargin;
final int bottom = mContainerHeight - mBiometricMargin;
outline.setRoundRect(left, top, right, bottom, mCornerRadius);
}
public void setContainerDimensions(int containerWidth, int containerHeight) {
if (DEBUG) {
Log.v(TAG, "Container Width: " + containerWidth + " Height: " + containerHeight);
}
mContainerWidth = containerWidth;
mContainerHeight = containerHeight;
}
public void updateForContentDimensions(int contentWidth, int contentHeight, boolean animate) {
if (DEBUG) {
Log.v(TAG, "Content Width: " + contentWidth
+ " Height: " + contentHeight
+ " Animate: " + animate);
}
if (mContainerWidth == 0 || mContainerHeight == 0) {
Log.w(TAG, "Not done measuring yet");
return;
}
if (animate) {
ValueAnimator heightAnimator = ValueAnimator.ofInt(mContentHeight, contentHeight);
heightAnimator.setDuration(AuthDialog.ANIMATE_DURATION_MS);
heightAnimator.addUpdateListener((animation) -> {
mContentHeight = (int) animation.getAnimatedValue();
mPanelView.invalidateOutline();
});
heightAnimator.start();
} else {
mContentWidth = contentWidth;
mContentHeight = contentHeight;
mPanelView.invalidateOutline();
}
}
AuthPanelController(Context context, View panelView) {
mContext = context;
mPanelView = panelView;
mCornerRadius = context.getResources()
.getDimension(R.dimen.biometric_dialog_corner_size);
mBiometricMargin = (int) context.getResources()
.getDimension(R.dimen.biometric_dialog_border_padding);
mPanelView.setOutlineProvider(this);
mPanelView.setClipToOutline(true);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.biometrics.ui;
package com.android.systemui.biometrics;
import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
@@ -58,17 +58,15 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.biometrics.BiometricDialog;
import com.android.systemui.biometrics.DialogViewCallback;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.util.leak.RotationUtils;
/**
* Abstract base class. Shows a dialog for BiometricPrompt.
*/
public abstract class BiometricDialogView extends LinearLayout implements BiometricDialog {
public abstract class BiometricDialogView extends LinearLayout implements AuthDialog {
private static final String TAG = "BiometricDialogView";
private static final String TAG = "BiometricPrompt/DialogView";
public static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility";
public static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility";
@@ -112,7 +110,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
private final float mAnimationTranslationOffset;
private final int mErrorColor;
private final float mDialogWidth;
protected final DialogViewCallback mCallback;
protected final AuthDialogCallback mCallback;
private final DialogOutlineProvider mOutlineProvider = new DialogOutlineProvider();
protected final ViewGroup mLayout;
@@ -176,7 +174,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
new WakefulnessLifecycle.Observer() {
@Override
public void onStartedGoingToSleep() {
animateAway(DialogViewCallback.DISMISSED_USER_CANCELED);
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
}
};
@@ -226,17 +224,18 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
public static final int TYPE_FACE = BiometricAuthenticator.TYPE_FACE;
private Context mContext;
private DialogViewCallback mCallback;
private AuthDialogCallback mCallback;
private Bundle mBundle;
private boolean mRequireConfirmation;
private int mUserId;
private String mOpPackageName;
private boolean mSkipIntro;
public Builder(Context context) {
mContext = context;
}
public Builder setCallback(DialogViewCallback callback) {
public Builder setCallback(AuthDialogCallback callback) {
mCallback = callback;
return this;
}
@@ -261,6 +260,11 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
return this;
}
public Builder setSkipIntro(boolean skipIntro) {
mSkipIntro = skipIntro;
return this;
}
public BiometricDialogView build(int type) {
return build(type, new Injector());
}
@@ -278,6 +282,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
dialog.setRequireConfirmation(mRequireConfirmation);
dialog.setUserId(mUserId);
dialog.setOpPackageName(mOpPackageName);
dialog.setSkipIntro(mSkipIntro);
return dialog;
}
}
@@ -288,7 +293,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
}
}
protected BiometricDialogView(Context context, DialogViewCallback callback, Injector injector) {
protected BiometricDialogView(Context context, AuthDialogCallback callback, Injector injector) {
super(context);
mWakefulnessLifecycle = injector.getWakefulnessLifecycle();
@@ -319,7 +324,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
return false;
}
if (event.getAction() == KeyEvent.ACTION_UP) {
animateAway(DialogViewCallback.DISMISSED_USER_CANCELED);
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
}
return true;
}
@@ -348,16 +353,16 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
mNegativeButton.setOnClickListener((View v) -> {
if (mState == STATE_PENDING_CONFIRMATION || mState == STATE_AUTHENTICATED) {
animateAway(DialogViewCallback.DISMISSED_USER_CANCELED);
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
} else {
animateAway(DialogViewCallback.DISMISSED_BUTTON_NEGATIVE);
animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
}
});
mPositiveButton.setOnClickListener((View v) -> {
updateState(STATE_AUTHENTICATED);
mHandler.postDelayed(() -> {
animateAway(DialogViewCallback.DISMISSED_BUTTON_POSITIVE);
animateAway(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE);
}, getDelayAfterAuthenticatedDurationMs());
});
@@ -639,20 +644,20 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
v.setClickable(true);
v.setOnClickListener(v1 -> {
if (mState != STATE_AUTHENTICATED && shouldGrayAreaDismissDialog()) {
animateAway(DialogViewCallback.DISMISSED_USER_CANCELED);
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
}
});
}
private void animateAway(@DialogViewCallback.DismissedReason int reason) {
private void animateAway(@AuthDialogCallback.DismissedReason int reason) {
animateAway(true /* sendReason */, reason);
}
/**
* Animate the dialog away
* @param reason one of the {@link DialogViewCallback} codes
* @param reason one of the {@link AuthDialogCallback} codes
*/
private void animateAway(boolean sendReason, @DialogViewCallback.DismissedReason int reason) {
private void animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason) {
if (!mCompletedAnimatingIn) {
Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
mPendingDismissDialog = true;
@@ -733,8 +738,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
}
@Override
public void show(WindowManager wm, boolean skipIntroAnimation) {
setSkipIntro(skipIntroAnimation);
public void show(WindowManager wm) {
wm.addView(this, getLayoutParams(mWindowToken));
}
@@ -757,7 +761,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
@Override
public void dismissFromSystemServer() {
animateAway(DialogViewCallback.DISMISSED_BY_SYSTEM_SERVER);
animateAway(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER);
}
@Override
@@ -768,7 +772,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
updateState(STATE_PENDING_CONFIRMATION);
} else {
mHandler.postDelayed(() -> {
animateAway(DialogViewCallback.DISMISSED_AUTHENTICATED);
animateAway(AuthDialogCallback.DISMISSED_AUTHENTICATED);
}, getDelayAfterAuthenticatedDurationMs());
updateState(STATE_AUTHENTICATED);
@@ -810,7 +814,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
showTryAgainButton(false /* show */);
mHandler.postDelayed(() -> {
animateAway(DialogViewCallback.DISMISSED_ERROR);
animateAway(AuthDialogCallback.DISMISSED_ERROR);
}, BiometricPrompt.HIDE_DIALOG_DELAY);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.biometrics.ui;
package com.android.systemui.biometrics;
import android.content.Context;
import android.graphics.drawable.Animatable2;
@@ -26,7 +26,6 @@ import android.util.Log;
import android.view.View;
import com.android.systemui.R;
import com.android.systemui.biometrics.DialogViewCallback;
/**
* This class loads the view for the system-provided dialog. The view consists of:
@@ -35,7 +34,7 @@ import com.android.systemui.biometrics.DialogViewCallback;
*/
public class FaceDialogView extends BiometricDialogView {
private static final String TAG = "FaceDialogView";
private static final String TAG = "BiometricPrompt/FaceDialogView";
private static final String KEY_DIALOG_ANIMATED_IN = "key_dialog_animated_in";
@@ -110,7 +109,7 @@ public class FaceDialogView extends BiometricDialogView {
announceAccessibilityEvent();
};
protected FaceDialogView(Context context, DialogViewCallback callback, Injector injector) {
protected FaceDialogView(Context context, AuthDialogCallback callback, Injector injector) {
super(context, callback, injector);
mIconController = new IconController();
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.biometrics.ui;
package com.android.systemui.biometrics;
import android.content.Context;
import android.graphics.drawable.AnimatedVectorDrawable;
@@ -22,7 +22,6 @@ import android.graphics.drawable.Drawable;
import android.util.Log;
import com.android.systemui.R;
import com.android.systemui.biometrics.DialogViewCallback;
/**
* This class loads the view for the system-provided dialog. The view consists of:
@@ -31,9 +30,9 @@ import com.android.systemui.biometrics.DialogViewCallback;
*/
public class FingerprintDialogView extends BiometricDialogView {
private static final String TAG = "FingerprintDialogView";
private static final String TAG = "BiometricPrompt/FingerprintDialogView";
protected FingerprintDialogView(Context context, DialogViewCallback callback,
protected FingerprintDialogView(Context context, AuthDialogCallback callback,
Injector injector) {
super(context, callback, injector);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.biometrics.ui;
package com.android.systemui.biometrics;
import android.content.Context;
import android.util.DisplayMetrics;

View File

@@ -0,0 +1,110 @@
/*
* 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 static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.android.systemui.R;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class AuthBiometricFaceViewTest extends SysuiTestCase {
@Mock
AuthBiometricView.Callback mCallback;
private TestableFaceView mFaceView;
@Mock private Button mNegativeButton;
@Mock private Button mPositiveButton;
@Mock private Button mTryAgainButton;
@Mock private TextView mErrorView;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mFaceView = new TestableFaceView(mContext);
mFaceView.mIconController = mock(TestableFaceView.TestableIconController.class);
mFaceView.setCallback(mCallback);
mFaceView.mNegativeButton = mNegativeButton;
mFaceView.mPositiveButton = mPositiveButton;
mFaceView.mTryAgainButton = mTryAgainButton;
mFaceView.mErrorView = mErrorView;
}
@Test
public void testStateUpdated_whenDialogAnimatedIn() {
mFaceView.onDialogAnimatedIn();
verify(mFaceView.mIconController)
.updateState(anyInt(), eq(AuthBiometricFaceView.STATE_AUTHENTICATING));
}
@Test
public void testIconUpdatesState_whenDialogStateUpdated() {
mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATING);
verify(mFaceView.mIconController)
.updateState(anyInt(), eq(AuthBiometricFaceView.STATE_AUTHENTICATING));
mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATED);
verify(mFaceView.mIconController).updateState(
eq(AuthBiometricFaceView.STATE_AUTHENTICATING),
eq(AuthBiometricFaceView.STATE_AUTHENTICATED));
}
public class TestableFaceView extends AuthBiometricFaceView {
public class TestableIconController extends IconController {
TestableIconController(Context context, ImageView iconView) {
super(context, iconView, mock(TextView.class));
}
public void startPulsing() {
// Stub for testing
}
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return 0; // Keep this at 0 for tests to invoke callback immediately.
}
public TestableFaceView(Context context) {
super(context);
}
}
}

View File

@@ -0,0 +1,271 @@
/*
* 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 static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class AuthBiometricViewTest extends SysuiTestCase {
@Mock private AuthBiometricView.Callback mCallback;
@Mock private AuthPanelController mPanelController;
@Mock private Button mNegativeButton;
@Mock private Button mPositiveButton;
@Mock private Button mTryAgainButton;
@Mock private TextView mTitleView;
@Mock private TextView mSubtitleView;
@Mock private TextView mDescriptionView;
@Mock private TextView mErrorView;
@Mock private ImageView mIconView;
TestableBiometricView mBiometricView;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() {
initDialog(mContext, mCallback, new MockInjector());
// The onAuthenticated runnable is posted when authentication succeeds.
mBiometricView.onAuthenticationSucceeded();
waitForIdleSync();
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED);
}
@Test
public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() {
initDialog(mContext, mCallback, new MockInjector());
mBiometricView.setRequireConfirmation(true);
mBiometricView.onAuthenticationSucceeded();
waitForIdleSync();
assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState);
verify(mCallback, never()).onAction(anyInt());
verify(mBiometricView.mNegativeButton).setText(eq(R.string.cancel));
verify(mBiometricView.mPositiveButton).setEnabled(eq(true));
verify(mErrorView).setText(eq(R.string.biometric_dialog_tap_confirm));
verify(mErrorView).setVisibility(eq(View.VISIBLE));
}
@Test
public void testPositiveButton_sendsActionAuthenticated() {
Button button = new Button(mContext);
initDialog(mContext, mCallback, new MockInjector() {
@Override
public Button getPositiveButton() {
return button;
}
});
button.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED);
assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState);
}
@Test
public void testNegativeButton_beforeAuthentication_sendsActionButtonNegative() {
Button button = new Button(mContext);
initDialog(mContext, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return button;
}
});
mBiometricView.onDialogAnimatedIn();
button.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE);
}
@Test
public void testNegativeButton_whenPendingConfirmation_sendsActionUserCanceled() {
Button button = new Button(mContext);
initDialog(mContext, mCallback, new MockInjector() {
@Override
public Button getNegativeButton() {
return button;
}
});
mBiometricView.setRequireConfirmation(true);
mBiometricView.onAuthenticationSucceeded();
button.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED);
}
@Test
public void testTryAgainButton_sendsActionTryAgain() {
Button button = new Button(mContext);
initDialog(mContext, mCallback, new MockInjector() {
@Override
public Button getTryAgainButton() {
return button;
}
});
button.performClick();
waitForIdleSync();
verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState);
}
@Test
public void testBackgroundClicked_sendsActionUserCanceled() {
initDialog(mContext, mCallback, new MockInjector());
View view = new View(mContext);
mBiometricView.setBackgroundView(view);
view.performClick();
verify(mCallback).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
}
@Test
public void testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() {
initDialog(mContext, mCallback, new MockInjector());
View view = new View(mContext);
mBiometricView.setBackgroundView(view);
mBiometricView.onAuthenticationSucceeded();
view.performClick();
verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
}
@Test
public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() {
initDialog(mContext, mCallback, new MockInjector());
mBiometricView.setPanelController(mPanelController);
mBiometricView.updateSize(AuthDialog.SIZE_SMALL);
View view = new View(mContext);
mBiometricView.setBackgroundView(view);
view.performClick();
verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
}
private void initDialog(Context context, AuthBiometricView.Callback callback,
MockInjector injector) {
mBiometricView = new TestableBiometricView(context, null, injector);
mBiometricView.setCallback(callback);
mBiometricView.initializeViews();
}
private class MockInjector extends AuthBiometricView.Injector {
@Override
public Button getNegativeButton() {
return mNegativeButton;
}
@Override
public Button getPositiveButton() {
return mPositiveButton;
}
@Override
public Button getTryAgainButton() {
return mTryAgainButton;
}
@Override
public TextView getTitleView() {
return mTitleView;
}
@Override
public TextView getSubtitleView() {
return mSubtitleView;
}
@Override
public TextView getDescriptionView() {
return mDescriptionView;
}
@Override
public TextView getErrorView() {
return mErrorView;
}
@Override
public ImageView getIconView() {
return mIconView;
}
}
private class TestableBiometricView extends AuthBiometricView {
TestableBiometricView(Context context, AttributeSet attrs,
Injector injector) {
super(context, attrs, injector);
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return 0; // Keep this at 0 for tests to invoke callback immediately.
}
@Override
protected int getStateForAfterError() {
return 0;
}
@Override
protected void handleResetAfterError() {
}
@Override
protected void handleResetAfterHelp() {
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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 static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class AuthContainerViewTest extends SysuiTestCase {
private TestableAuthContainer mAuthContainer;
private @Mock AuthDialogCallback mCallback;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
AuthContainerView.Config config = new AuthContainerView.Config();
config.mContext = mContext;
config.mCallback = mCallback;
mAuthContainer = new TestableAuthContainer(config);
}
@Test
public void testActionAuthenticated_sendsDismissedAuthenticated() {
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_AUTHENTICATED);
verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_AUTHENTICATED));
}
@Test
public void testActionUserCanceled_sendsDismissedUserCanceled() {
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_USER_CANCELED);
verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_USER_CANCELED));
}
@Test
public void testActionButtonNegative_sendsDismissedButtonNegative() {
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE);
verify(mCallback).onDismissed(eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE));
}
@Test
public void testActionTryAgain_sendsTryAgain() {
mAuthContainer.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
verify(mCallback).onTryAgainPressed();
}
private class TestableAuthContainer extends AuthContainerView {
TestableAuthContainer(AuthContainerView.Config config) {
super(config);
}
@Override
public void animateAway(int reason) {
mConfig.mCallback.onDismissed(reason);
}
}
}

View File

@@ -58,16 +58,16 @@ import java.util.List;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class BiometricDialogImplTest extends SysuiTestCase {
public class AuthControllerTest extends SysuiTestCase {
@Mock
private PackageManager mPackageManager;
@Mock
private IBiometricServiceReceiverInternal mReceiver;
@Mock
private BiometricDialog mDialog1;
private AuthDialog mDialog1;
@Mock
private BiometricDialog mDialog2;
private AuthDialog mDialog2;
private TestableBiometricDialogImpl mBiometricDialogImpl;
@@ -102,42 +102,42 @@ public class BiometricDialogImplTest extends SysuiTestCase {
@Test
public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_USER_CANCELED);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
}
@Test
public void testSendsReasonNegative_whenDismissedByButtonNegative() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_BUTTON_NEGATIVE);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
}
@Test
public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_BUTTON_POSITIVE);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
}
@Test
public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_AUTHENTICATED);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_AUTHENTICATED);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
}
@Test
public void testSendsReasonError_whenDismissedByError() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_ERROR);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_ERROR);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_ERROR);
}
@Test
public void testSendsReasonDismissedBySystemServer_whenDismissedByServer() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
mBiometricDialogImpl.onDismissed(DialogViewCallback.DISMISSED_BY_SYSTEM_SERVER);
mBiometricDialogImpl.onDismissed(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER);
verify(mReceiver).onDialogDismissed(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED);
}
@@ -147,7 +147,7 @@ public class BiometricDialogImplTest extends SysuiTestCase {
public void testShowInvoked_whenSystemRequested()
throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
verify(mDialog1).show(any(), eq(false) /* skipIntro */);
verify(mDialog1).show(any());
}
@Test
@@ -215,7 +215,7 @@ public class BiometricDialogImplTest extends SysuiTestCase {
@Test
public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
verify(mDialog1).show(any(), eq(false) /* skipIntro */);
verify(mDialog1).show(any());
showDialog(BiometricPrompt.TYPE_FACE);
@@ -223,13 +223,13 @@ public class BiometricDialogImplTest extends SysuiTestCase {
verify(mDialog1).dismissWithoutCallback(eq(false) /* animate */);
// Second dialog should be shown without animation
verify(mDialog2).show(any(), eq(true)) /* skipIntro */;
verify(mDialog2).show(any());
}
@Test
public void testConfigurationPersists_whenOnConfigurationChanged() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
verify(mDialog1).show(any(), eq(false) /* skipIntro */);
verify(mDialog1).show(any());
mBiometricDialogImpl.onConfigurationChanged(new Configuration());
@@ -244,7 +244,7 @@ public class BiometricDialogImplTest extends SysuiTestCase {
verify(mDialog2).restoreState(captor2.capture());
// Dialog for new configuration skips intro
verify(mDialog2).show(any(), eq(true) /* skipIntro */);
verify(mDialog2).show(any());
// TODO: This should check all values we want to save/restore
assertEquals(captor.getValue(), captor2.getValue());
@@ -296,7 +296,7 @@ public class BiometricDialogImplTest extends SysuiTestCase {
return bundle;
}
private final class TestableBiometricDialogImpl extends BiometricDialogImpl {
private final class TestableBiometricDialogImpl extends AuthController {
private int mBuildCount = 0;
public TestableBiometricDialogImpl(Injector injector) {
@@ -304,9 +304,10 @@ public class BiometricDialogImplTest extends SysuiTestCase {
}
@Override
protected BiometricDialog buildDialog(Bundle biometricPromptBundle,
boolean requireConfirmation, int userId, int type, String opPackageName) {
BiometricDialog dialog;
protected AuthDialog buildDialog(Bundle biometricPromptBundle,
boolean requireConfirmation, int userId, int type, String opPackageName,
boolean skipIntro) {
AuthDialog dialog;
if (mBuildCount == 0) {
dialog = mDialog1;
} else if (mBuildCount == 1) {
@@ -319,7 +320,7 @@ public class BiometricDialogImplTest extends SysuiTestCase {
}
}
private final class MockInjector extends BiometricDialogImpl.Injector {
private final class MockInjector extends AuthController.Injector {
@Override
IActivityTaskManager getActivityTaskManager() {
return mock(IActivityTaskManager.class);

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.biometrics.ui;
package com.android.systemui.biometrics;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotSame;
@@ -32,12 +32,9 @@ import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.DialogViewCallback;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import org.junit.Before;
@@ -62,7 +59,7 @@ public class BiometricDialogViewTest extends SysuiTestCase {
TestableContext mTestableContext;
@Mock
private DialogViewCallback mCallback;
private AuthDialogCallback mCallback;
@Mock
private UserManager mUserManager;
@Mock
@@ -176,7 +173,7 @@ public class BiometricDialogViewTest extends SysuiTestCase {
assertEquals(View.VISIBLE, mFaceDialogView.mTryAgainButton.getVisibility());
}
private FaceDialogView buildFaceDialogView(Context context, DialogViewCallback callback,
private FaceDialogView buildFaceDialogView(Context context, AuthDialogCallback callback,
boolean requireConfirmation) {
return (FaceDialogView) new BiometricDialogView.Builder(context)
.setCallback(callback)