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
This commit is contained in:
119
packages/SystemUI/res/layout/auth_biometric_contents.xml
Normal file
119
packages/SystemUI/res/layout/auth_biometric_contents.xml
Normal file
@@ -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>
|
||||
26
packages/SystemUI/res/layout/auth_biometric_face_view.xml
Normal file
26
packages/SystemUI/res/layout/auth_biometric_face_view.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
~ Copyright (C) 2019 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<com.android.systemui.biometrics.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>
|
||||
43
packages/SystemUI/res/layout/auth_container_view.xml
Normal file
43
packages/SystemUI/res/layout/auth_container_view.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<!--
|
||||
~ Copyright (C) 2019 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/biometric_dialog_dim_color"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackgroundFloating"
|
||||
android:elevation="@dimen/biometric_dialog_elevation"/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:layout_margin="@dimen/biometric_dialog_border_padding"
|
||||
android:elevation="@dimen/biometric_dialog_elevation"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user