diff options
| author | 2019-08-20 17:17:11 -0700 | |
|---|---|---|
| committer | 2019-08-30 17:43:27 -0700 | |
| commit | fc46826f0a80cc89d05cce8aa737149148ac3203 (patch) | |
| tree | 40c8ff585fc3837cc8afef7f88d5583576f87486 | |
| parent | 4077d36bfce8d5ded6b40f5f5ed842c84903f2d3 (diff) | |
1/n: Refactor BiometricPrompt UI hierarchy
The UI is split into a few components now
1) BiometricPromptContainerView - top level, contains the work profile
background view, the panel (rounded background)
2) BiometricPromptBiometricView - nested within, displays contents for
biometric auth
The panel must be one level higher (in hierarchy) than the biometric
dialog to allow future non-biometric views to be added cleanly, and to
allow separate animations for the background/foreground.
Bug: 123378871
Test: Demo app with text that requires scrolling; dialog bounds are correct,
view elements are contained within the dialog bounds
Test: atest BiometricDialogImplTest
Test: atest BiometricDialogViewTest
Test: atest AuthBiometricViewTest
Test: atest AuthBiometricFaceViewTest
Change-Id: Ie4e5a8641a10229154a1011afefacb823aadf565
14 files changed, 1254 insertions, 21 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..ce6b7cca225e --- /dev/null +++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml @@ -0,0 +1,119 @@ +<!-- + ~ 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" + android:text="ERROR"/> + + <LinearLayout + 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" + android:text="NEGATIVE"/> + <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..4fd98d66bbe3 --- /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.ui.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.ui.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..491169c7a171 --- /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/dimens.xml b/packages/SystemUI/res/values/dimens.xml index be815e13e68e..c25f631272b6 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/BiometricDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java index d4baefd64512..8a4f479c8df9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java @@ -16,12 +16,16 @@ 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. */ @@ -49,12 +53,19 @@ 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 {} + /** * 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/BiometricDialogImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java index a8e572216315..6aff3f7010f2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java @@ -29,6 +29,7 @@ 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; @@ -36,6 +37,7 @@ 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.biometrics.ui.AuthContainerView; import com.android.systemui.statusbar.CommandQueue; import java.util.List; @@ -46,6 +48,9 @@ import java.util.List; */ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks, DialogViewCallback { + private static final String USE_NEW_DIALOG = + "com.android.systemui.biometrics.BiometricDialogImpl.USE_NEW_DIALOG"; + private static final String TAG = "BiometricDialogImpl"; private static final boolean DEBUG = true; @@ -253,7 +258,8 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba requireConfirmation, userId, type, - opPackageName); + opPackageName, + skipAnimation); if (newDialog == null) { Log.e(TAG, "Unsupported type: " + type); @@ -282,7 +288,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 +315,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 BiometricDialog 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/ui/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthBiometricFaceView.java new file mode 100644 index 000000000000..d7b41b7b2a77 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthBiometricFaceView.java @@ -0,0 +1,120 @@ +/* + * 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.ui; + +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.widget.ImageView; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.R; + +public class AuthBiometricFaceView extends AuthBiometricView { + + // 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; + Handler mHandler; + boolean mLastPulseLightToDark; // false = dark to light, true = light to dark + @State int mState; + + IconController(Context context, ImageView iconView) { + mContext = context; + mIconView = iconView; + mHandler = new Handler(Looper.getMainLooper()); + showIcon(R.drawable.face_dialog_pulse_dark_to_light); + } + + void showIcon(int iconRes) { + final Drawable drawable = mContext.getDrawable(iconRes); + mIconView.setImageDrawable(drawable); + } + + 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) { + pulseInNextDirection(); + } + } + + public void updateState(int newState) { + if (newState == STATE_AUTHENTICATING) { + startPulsing(); + } + 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 void onFinishInflate() { + super.onFinishInflate(); + mIconController = new IconController(mContext, mIconView); + } + + @Override + public void updateState(@State int newState) { + super.updateState(newState); + mIconController.updateState(newState); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthBiometricView.java new file mode 100644 index 000000000000..f596f4ecd66b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthBiometricView.java @@ -0,0 +1,257 @@ +/* + * 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.ui; + +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.systemui.R; +import com.android.systemui.biometrics.BiometricDialog; + +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; + /** + * Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle. + */ + protected static final int STATE_ERROR = 3; + /** + * Authenticated, waiting for user confirmation. Authentication hardware idle. + */ + protected static final int STATE_PENDING_CONFIRMATION = 4; + /** + * Authenticated, dialog animating away soon. + */ + protected static final int STATE_AUTHENTICATED = 5; + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_IDLE, STATE_AUTHENTICATING_ANIMATING_IN, STATE_AUTHENTICATING, STATE_ERROR, + STATE_PENDING_CONFIRMATION, STATE_AUTHENTICATED}) + @interface State {} + + /** + * Callback to the parent when a user action has occurred. + */ + interface Callback { + int ACTION_AUTHENTICATED = 1; + + /** + * 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); + } + + private final Handler mHandler; + + private AuthPanelController mPanelController; + private Bundle mBundle; + private boolean mRequireConfirmation; + private @BiometricDialog.DialogSize int mSize = BiometricDialog.SIZE_UNKNOWN; + + private TextView mTitleView; + private TextView mSubtitleView; + private TextView mDescriptionView; + protected ImageView mIconView; + private TextView mErrorView; + private Button mNegativeButton; + private Button mPositiveButton; + private Button mTryAgainButton; + + private int mCurrentHeight; + private int mCurrentWidth; + private Callback mCallback; + protected @State int mState; + + protected abstract int getDelayAfterAuthenticatedDurationMs(); + + public AuthBiometricView(Context context) { + this(context, null); + } + + public AuthBiometricView(Context context, AttributeSet attrs) { + super(context, attrs); + mHandler = new Handler(Looper.getMainLooper()); + + addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + updateSize(mRequireConfirmation ? BiometricDialog.SIZE_MEDIUM + : BiometricDialog.SIZE_SMALL); + mPanelController.updateForContentDimensions(mCurrentWidth, mCurrentHeight); + } + }); + } + + public void setPanelController(AuthPanelController panelController) { + mPanelController = panelController; + } + + public void setBiometricPromptBundle(Bundle bundle) { + mBundle = bundle; + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public void setRequireConfirmation(boolean requireConfirmation) { + mRequireConfirmation = requireConfirmation; + } + + public void updateSize(@BiometricDialog.DialogSize int newSize) { + if (mSize == newSize) { + Log.w(TAG, "Skipping updating size: " + mSize); + return; + } + + if (newSize == BiometricDialog.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); + + mCurrentHeight = mIconView.getHeight() + 2 * (int) iconPadding; + } + + mSize = newSize; + } + + public void updateState(@State int newState) { + Log.v(TAG, "newState: " + newState); + if (newState == STATE_AUTHENTICATED) { + if (mRequireConfirmation) { + + } else { + mHandler.postDelayed(() -> { + mCallback.onAction(Callback.ACTION_AUTHENTICATED); + }, getDelayAfterAuthenticatedDurationMs()); + } + } + mState = newState; + } + + public void onDialogAnimatedIn() { + updateState(STATE_AUTHENTICATING); + } + + public void onAuthenticationSucceeded() { + if (mRequireConfirmation) { + updateState(STATE_PENDING_CONFIRMATION); + } else { + updateState(STATE_AUTHENTICATED); + } + } + + private void setTextOrHide(TextView view, String string) { + if (TextUtils.isEmpty(string)) { + view.setVisibility(View.GONE); + } else { + view.setText(string); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTitleView = findViewById(R.id.title); + mSubtitleView = findViewById(R.id.subtitle); + mDescriptionView = findViewById(R.id.description); + mIconView = findViewById(R.id.biometric_icon); + mErrorView = findViewById(R.id.error); + mNegativeButton = findViewById(R.id.button_negative); + mPositiveButton = findViewById(R.id.button_positive); + mTryAgainButton = findViewById(R.id.button_try_again); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + setTextOrHide(mTitleView, mBundle.getString(BiometricPrompt.KEY_TITLE)); + 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 { + 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); + + mCurrentHeight = getMeasuredHeight(); + mCurrentWidth = getMeasuredWidth(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthContainerView.java new file mode 100644 index 000000000000..cadc73d926ba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthContainerView.java @@ -0,0 +1,363 @@ +/* + * 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.ui; + +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.biometrics.BiometricDialog; +import com.android.systemui.biometrics.DialogViewCallback; +import com.android.systemui.keyguard.WakefulnessLifecycle; + +/** + * Top level container/controller for the BiometricPrompt UI. + */ +public class AuthContainerView extends LinearLayout + implements BiometricDialog, 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 final Config mConfig; + private final Handler mHandler = new Handler(); + private final IBinder mWindowToken = new Binder(); + private final WindowManager mWindowManager; + private final AuthPanelController mPanelController; + private final Interpolator mLinearOutSlowIn; + + 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 boolean mCompletedAnimatingIn; + private boolean mPendingDismissDialog; + + private static class Config { + Context mContext; + DialogViewCallback 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(DialogViewCallback 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); + } + } + + private final AuthBiometricView.Callback mBiometricCallback = action -> { + switch (action) { + case AuthBiometricView.Callback.ACTION_AUTHENTICATED: + animateAway(DialogViewCallback.DISMISSED_AUTHENTICATED); + break; + default: + Log.e(TAG, "Unhandled action: " + action); + } + }; + + private 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; + + 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); + + 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(DialogViewCallback.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) { + mCompletedAnimatingIn = true; + } else { + // 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(DialogViewCallback.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) { + + } + + @Override + public void onHelp(String 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; + } + + private void animateAway(int reason) { + animateAway(true /* sendReason */, reason); + } + + private void animateAway(boolean sendReason, @DialogViewCallback.DismissedReason int reason) { + if (!mCompletedAnimatingIn) { + Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn"); + mPendingDismissDialog = true; + return; + } + + 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() { + mCompletedAnimatingIn = true; + if (mPendingDismissDialog) { + Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); + animateAway(false /* sendReason */, 0); + mPendingDismissDialog = false; + return; + } + 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/ui/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthPanelController.java new file mode 100644 index 000000000000..95d1df6533e1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthPanelController.java @@ -0,0 +1,92 @@ +/* + * 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.ui; + +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; + +/** + * 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) { + if (DEBUG) { + Log.v(TAG, "Content Width: " + contentWidth + " Height: " + contentHeight); + } + + mContentWidth = contentWidth; + mContentHeight = contentHeight; + + if (mContainerWidth == 0 || mContainerHeight == 0) { + Log.w(TAG, "Not done measuring yet"); + return; + } + + 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/ui/BiometricDialogView.java index 290475562893..58fb3d869e74 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java @@ -231,6 +231,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet private boolean mRequireConfirmation; private int mUserId; private String mOpPackageName; + private boolean mSkipIntro; public Builder(Context context) { mContext = context; @@ -261,6 +262,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 +284,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet dialog.setRequireConfirmation(mRequireConfirmation); dialog.setUserId(mUserId); dialog.setOpPackageName(mOpPackageName); + dialog.setSkipIntro(mSkipIntro); return dialog; } } @@ -508,6 +515,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); } + @VisibleForTesting void updateSize(@DialogSize int newSize) { final float padding = Utils.dpToPixels(mContext, IMPLICIT_Y_PADDING); @@ -733,8 +741,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)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java index 8f2f8b1c0e63..6af8956fccc8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java @@ -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()); @@ -305,7 +305,8 @@ public class BiometricDialogImplTest extends SysuiTestCase { @Override protected BiometricDialog buildDialog(Bundle biometricPromptBundle, - boolean requireConfirmation, int userId, int type, String opPackageName) { + boolean requireConfirmation, int userId, int type, String opPackageName, + boolean skipIntro) { BiometricDialog dialog; if (mBuildCount == 0) { dialog = mDialog1; diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/AuthBiometricFaceViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/AuthBiometricFaceViewTest.java new file mode 100644 index 000000000000..ea8d66d714a3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/AuthBiometricFaceViewTest.java @@ -0,0 +1,97 @@ +/* + * 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.ui; + +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.ImageView; + +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; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mFaceView = new TestableFaceView(mContext); + mFaceView.mIconController = mock(TestableFaceView.TestableIconController.class); + mFaceView.setCallback(mCallback); + } + + @Test + public void testStateUpdated_whenDialogAnimatedIn() { + mFaceView.onDialogAnimatedIn(); + verify(mFaceView.mIconController) + .updateState(eq(AuthBiometricFaceView.STATE_AUTHENTICATING)); + } + + @Test + public void testIconUpdatesState_whenDialogStateUpdated() { + mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATING); + verify(mFaceView.mIconController) + .updateState(eq(AuthBiometricFaceView.STATE_AUTHENTICATING)); + + mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATED); + verify(mFaceView.mIconController) + .updateState(eq(AuthBiometricFaceView.STATE_AUTHENTICATED)); + } + + public class TestableFaceView extends AuthBiometricFaceView { + + public class TestableIconController extends IconController { + TestableIconController(Context context, ImageView iconView) { + super(context, iconView); + } + + 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/ui/AuthBiometricViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/AuthBiometricViewTest.java new file mode 100644 index 000000000000..ab138b3182e1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/AuthBiometricViewTest.java @@ -0,0 +1,76 @@ +/* + * 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.ui; + +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 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 + AuthBiometricView.Callback mCallback; + + TestableBiometricView mBiometricView; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mBiometricView = new TestableBiometricView(mContext); + mBiometricView.setCallback(mCallback); + } + + @Test + public void testOnAuthenticationSucceeded_noConfirmationRequired() { + // The onAuthenticated runnable is posted when authentication succeeds. + mBiometricView.onAuthenticationSucceeded(); + waitForIdleSync(); + verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED); + } + + @Test + public void testOnAuthenticationSucceeded_confirmationRequired() { + mBiometricView.setRequireConfirmation(true); + + // TODO: Update when code path is complete + } + + public class TestableBiometricView extends AuthBiometricView { + public TestableBiometricView(Context context) { + super(context); + } + + @Override + protected int getDelayAfterAuthenticatedDurationMs() { + return 0; // Keep this at 0 for tests to invoke callback immediately. + } + } +} |