summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Kevin Chyn <kchyn@google.com> 2019-08-20 17:17:11 -0700
committer Kevin Chyn <kchyn@google.com> 2019-08-30 17:43:27 -0700
commitfc46826f0a80cc89d05cce8aa737149148ac3203 (patch)
tree40c8ff585fc3837cc8afef7f88d5583576f87486
parent4077d36bfce8d5ded6b40f5f5ed842c84903f2d3 (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
-rw-r--r--packages/SystemUI/res/layout/auth_biometric_contents.xml119
-rw-r--r--packages/SystemUI/res/layout/auth_biometric_face_view.xml26
-rw-r--r--packages/SystemUI/res/layout/auth_container_view.xml43
-rw-r--r--packages/SystemUI/res/values/dimens.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialog.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogImpl.java41
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthBiometricFaceView.java120
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthBiometricView.java257
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthContainerView.java363
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/AuthPanelController.java92
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricDialogView.java11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricDialogImplTest.java13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/AuthBiometricFaceViewTest.java97
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/AuthBiometricViewTest.java76
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.
+ }
+ }
+}