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:
118
packages/SystemUI/res/layout/auth_biometric_contents.xml
Normal file
118
packages/SystemUI/res/layout/auth_biometric_contents.xml
Normal 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>
|
||||
26
packages/SystemUI/res/layout/auth_biometric_face_view.xml
Normal file
26
packages/SystemUI/res/layout/auth_biometric_face_view.xml
Normal 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>
|
||||
43
packages/SystemUI/res/layout/auth_container_view.xml
Normal file
43
packages/SystemUI/res/layout/auth_container_view.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user