diff options
| author | 2019-09-03 17:49:46 +0000 | |
|---|---|---|
| committer | 2019-09-03 17:49:46 +0000 | |
| commit | b63ff32f531946a13d80f9340aaa8acf692cf145 (patch) | |
| tree | 64cef608d2a14285091108ff5587590a44d9af60 | |
| parent | 2a17568e0ab3dce15b674a339168d4722d846863 (diff) | |
| parent | f8688a0a1e66d185016332a35b915047d57c668e (diff) | |
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
21 files changed, 2021 insertions, 90 deletions
diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml new file mode 100644 index 000000000000..aed200f69bc3 --- /dev/null +++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml @@ -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>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_biometric_face_view.xml b/packages/SystemUI/res/layout/auth_biometric_face_view.xml new file mode 100644 index 000000000000..be30f21af536 --- /dev/null +++ b/packages/SystemUI/res/layout/auth_biometric_face_view.xml @@ -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>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_container_view.xml b/packages/SystemUI/res/layout/auth_container_view.xml new file mode 100644 index 000000000000..d83776b74fd1 --- /dev/null +++ b/packages/SystemUI/res/layout/auth_container_view.xml @@ -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>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 78318cb7e858..38293bf2defd 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -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> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2e1799168b5d..3a1f7a37729f 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -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> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java new file mode 100644 index 000000000000..74cc9c39741e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java @@ -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); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java new file mode 100644 index 000000000000..f26083998c76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -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); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java new file mode 100644 index 000000000000..9cb5fcf4de00 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -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; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index a8e572216315..dcd01c682726 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -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); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java index d4baefd64512..a6a857ca5945 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java @@ -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. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java index b65d1e823a9b..70752f5f860e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/DialogViewCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java new file mode 100644 index 000000000000..55ba0491dc1e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java @@ -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); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java index 290475562893..89d08d795128 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java @@ -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); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java index 9e4fe24aec40..d5dcbf126b63 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/FaceDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java @@ -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(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java index 292588024646..cda217619eed 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/FingerprintDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintDialogView.java @@ -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); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/Utils.java b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java index 028b1aa5b513..edd8089c35f9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/Utils.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java @@ -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; diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java new file mode 100644 index 000000000000..128e819ccedd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java @@ -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); + } + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java new file mode 100644 index 000000000000..ffcb293ba398 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java @@ -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() { + + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java new file mode 100644 index 000000000000..25e27ef6b781 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java @@ -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); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index 8f2f8b1c0e63..a5e468e0545d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -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); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/BiometricDialogViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogViewTest.java index bbdd837bb446..3ff1f383fdee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/BiometricDialogViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogViewTest.java @@ -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) |