13/n: persist device credential across configuration changes
1) AuthContainerView can be started in either `biometric` or `credential`
views. This is to potentially support an API where only credential
is allowed/requested.
2) When onSaveState, AuthContainerView saves both the states of
`biometric` and `credential` visibility. In the case of configuration
change, AuthController is responsibile for checking the visibility
and creating a AuthContainerView with the correct initial view.
3) When AuthCredentialView is the initial view, it owns the panel
expansion.
4) Added landscape layout
Bug: 140127687
Test: atest com.android.systemui.biometrics
Test: BiometricPromptDemo, use pattern, rotate device. auth, cancel,
demo logs are correct.
Change-Id: I1f501cf13b924353f251a69757fdb9d7e0bf1d21
diff --git a/packages/SystemUI/res/layout-land/auth_credential_view.xml b/packages/SystemUI/res/layout-land/auth_credential_view.xml
new file mode 100644
index 0000000..4a34ede
--- /dev/null
+++ b/packages/SystemUI/res/layout-land/auth_credential_view.xml
@@ -0,0 +1,113 @@
+<!--
+ ~ 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.AuthCredentialView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:elevation="@dimen/biometric_dialog_elevation">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+
+ <ImageView
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:background="@drawable/auth_dialog_lock"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:layout_marginTop="12dp"
+ android:textSize="20sp"
+ android:gravity="center"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:layout_marginTop="8dp"
+ android:textSize="16sp"
+ android:gravity="center"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:layout_marginTop="8dp"
+ android:gravity="center"
+ android:textSize="16sp"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+
+ <TextView
+ android:id="@+id/error"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="24dp"
+ android:textSize="16sp"
+ android:gravity="center"
+ android:textColor="?android:attr/colorError"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <com.android.internal.widget.LockPatternView
+ android:id="@+id/lockPattern"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="40dp"
+ android:layout_marginRight="40dp"
+ android:layout_gravity="center"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ style="@style/LockPatternStyleBiometricPrompt"/>
+
+ </LinearLayout>
+
+</com.android.systemui.biometrics.AuthCredentialView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_credential_view.xml b/packages/SystemUI/res/layout/auth_credential_view.xml
index d30dd68..74f991c 100644
--- a/packages/SystemUI/res/layout/auth_credential_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_view.xml
@@ -16,7 +16,6 @@
<com.android.systemui.biometrics.AuthCredentialView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/contents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index db23e62..a0ce67d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -107,11 +107,25 @@
String mOpPackageName;
int mModalityMask;
boolean mSkipIntro;
+ @Builder.InitialView int mInitialView;
}
public static class Builder {
Config mConfig;
+ /**
+ * Start the prompt with biometric UI. May flow to credential view if
+ * {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)} is set to true.
+ */
+ public static final int INITIAL_VIEW_BIOMETRIC = 1;
+ /**
+ * Start the prompt with credential UI
+ */
+ public static final int INITIAL_VIEW_CREDENTIAL = 2;
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({INITIAL_VIEW_BIOMETRIC, INITIAL_VIEW_CREDENTIAL})
+ @interface InitialView {}
+
public Builder(Context context) {
mConfig = new Config();
mConfig.mContext = context;
@@ -147,6 +161,11 @@
return this;
}
+ public Builder setInitialView(@InitialView int initialView) {
+ mConfig.mInitialView = initialView;
+ return this;
+ }
+
public AuthContainerView build(int modalityMask) {
mConfig.mModalityMask = modalityMask;
return new AuthContainerView(mConfig);
@@ -175,7 +194,7 @@
break;
case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL:
mConfig.mCallback.onDeviceCredentialPressed();
- showCredentialView();
+ addCredentialView(false /* animatePanel */);
break;
default:
Log.e(TAG, "Unhandled action: " + action);
@@ -226,6 +245,7 @@
return;
}
+ mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
mBackgroundView = mFrameLayout.findViewById(R.id.background);
UserManager userManager = mContext.getSystemService(UserManager.class);
@@ -239,15 +259,6 @@
mBackgroundView.setImageDrawable(image);
}
- mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
- mBiometricView.setPanelController(mPanelController);
- mBiometricView.setBiometricPromptBundle(config.mBiometricPromptBundle);
- mBiometricView.setCallback(mBiometricCallback);
- mBiometricView.setBackgroundView(mBackgroundView);
- mBiometricView.setUserId(mConfig.mUserId);
-
- mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
- mBiometricScrollView.addView(mBiometricView);
addView(mFrameLayout);
setOnKeyListener((v, keyCode, event) -> {
@@ -264,13 +275,30 @@
requestFocus();
}
- private void showCredentialView() {
+ private void addBiometricView() {
+ mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
+ mBiometricView.setPanelController(mPanelController);
+ mBiometricView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle);
+ mBiometricView.setCallback(mBiometricCallback);
+ mBiometricView.setBackgroundView(mBackgroundView);
+ mBiometricView.setUserId(mConfig.mUserId);
+ mBiometricScrollView.addView(mBiometricView);
+ }
+
+ /**
+ * Adds the credential view. When going from biometric to credential view, the biometric
+ * view starts the panel expansion animation. If the credential view is being shown first,
+ * it should own the panel expansion.
+ * @param animatePanel if the credential view needs to own the panel expansion animation
+ */
+ private void addCredentialView(boolean animatePanel) {
final LayoutInflater factory = LayoutInflater.from(mContext);
mCredentialView = (AuthCredentialView) factory.inflate(
R.layout.auth_credential_view, null, false);
mCredentialView.setUser(mConfig.mUserId);
mCredentialView.setCallback(mCredentialCallback);
mCredentialView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle);
+ mCredentialView.setPanelController(mPanelController, animatePanel);
mFrameLayout.addView(mCredentialView);
}
@@ -285,6 +313,21 @@
super.onAttachedToWindow();
mWakefulnessLifecycle.addObserver(this);
+ Log.v(TAG, "Initial view: " + mConfig.mInitialView);
+
+ switch (mConfig.mInitialView) {
+ case Builder.INITIAL_VIEW_BIOMETRIC:
+ addBiometricView();
+ break;
+ case Builder.INITIAL_VIEW_CREDENTIAL:
+ addCredentialView(true /* animatePanel */);
+ break;
+ default:
+ Log.e(TAG, "Initial view not supported: " + mConfig.mInitialView);
+ break;
+ }
+
+
if (mConfig.mSkipIntro) {
mContainerState = STATE_SHOWING;
} else {
@@ -373,6 +416,11 @@
@Override
public void onSaveState(@NonNull Bundle outState) {
outState.putInt(AuthDialog.KEY_CONTAINER_STATE, mContainerState);
+ // In the case where biometric and credential are both allowed, we can assume that
+ // biometric isn't showing if credential is showing since biometric is shown first.
+ outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING,
+ mBiometricView != null && mCredentialView == null);
+ outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null);
mBiometricView.onSaveState(outState);
}
@@ -441,11 +489,6 @@
});
}
- private boolean isDeviceCredentialAllowed() {
- return mConfig.mBiometricPromptBundle.getBoolean(
- BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, false);
- }
-
private void sendPendingCallbackIfNotNull() {
Log.d(TAG, "pendingCallback: " + mPendingCallbackReason);
if (mPendingCallbackReason != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index eb87834..1e8c26d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -218,7 +218,8 @@
Log.w(TAG, "mCurrentDialog: " + mCurrentDialog);
skipAnimation = true;
}
- showDialog(args, skipAnimation, null /* savedState */);
+ showDialog(args, skipAnimation, null /* savedState */,
+ AuthContainerView.Builder.INITIAL_VIEW_BIOMETRIC);
}
@Override
@@ -253,7 +254,8 @@
mCurrentDialog.dismissFromSystemServer();
}
- private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
+ private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState,
+ @AuthContainerView.Builder.InitialView int initialView) {
mCurrentDialogArgs = args;
final int type = args.argi1;
final Bundle biometricPromptBundle = (Bundle) args.arg1;
@@ -268,7 +270,8 @@
userId,
type,
opPackageName,
- skipAnimation);
+ skipAnimation,
+ initialView);
if (newDialog == null) {
Log.e(TAG, "Unsupported type: " + type);
@@ -280,7 +283,8 @@
+ " savedState: " + savedState
+ " mCurrentDialog: " + mCurrentDialog
+ " newDialog: " + newDialog
- + " type: " + type);
+ + " type: " + type
+ + " initialView: " + initialView);
}
if (mCurrentDialog != null) {
@@ -320,13 +324,23 @@
// to send its pending callback immediately.
if (savedState.getInt(AuthDialog.KEY_CONTAINER_STATE)
!= AuthContainerView.STATE_ANIMATING_OUT) {
- showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
+ final boolean credentialShowing =
+ savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING);
+
+ // We can assume if credential is showing, then biometric doesn't need to be shown,
+ // since credential is always after biometric.
+ final int initialView = credentialShowing
+ ? AuthContainerView.Builder.INITIAL_VIEW_CREDENTIAL
+ : AuthContainerView.Builder.INITIAL_VIEW_BIOMETRIC;
+
+ showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState, initialView);
}
}
}
protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation,
- int userId, int type, String opPackageName, boolean skipIntro) {
+ int userId, int type, String opPackageName, boolean skipIntro,
+ @AuthContainerView.Builder.InitialView int initialView) {
return new AuthContainerView.Builder(mContext)
.setCallback(this)
.setBiometricPromptBundle(biometricPromptBundle)
@@ -334,6 +348,7 @@
.setUserId(userId)
.setOpPackageName(opPackageName)
.setSkipIntro(skipIntro)
+ .setInitialView(initialView)
.build(type);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
index dabe84b..8eac8f5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
@@ -57,6 +57,8 @@
private Callback mCallback;
private ErrorTimer mErrorTimer;
private Bundle mBiometricPromptBundle;
+ private AuthPanelController mPanelController;
+ private boolean mShouldAnimatePanel;
private TextView mTitleView;
private TextView mSubtitleView;
@@ -213,6 +215,11 @@
mBiometricPromptBundle = bundle;
}
+ void setPanelController(AuthPanelController panelController, boolean animatePanel) {
+ mPanelController = panelController;
+ mShouldAnimatePanel = animatePanel;
+ }
+
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -258,4 +265,17 @@
mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
}
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mShouldAnimatePanel) {
+ // Credential view is always full screen.
+ mPanelController.setUseFullScreen(true);
+ mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(),
+ mPanelController.getContainerHeight(), 0 /* animateDurationMs */);
+ mShouldAnimatePanel = false;
+ }
+ }
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
index 29b84bb..461f237 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
@@ -31,6 +31,8 @@
public interface AuthDialog {
String KEY_CONTAINER_STATE = "container_state";
+ String KEY_BIOMETRIC_SHOWING = "biometric_showing";
+ String KEY_CREDENTIAL_SHOWING = "credential_showing";
String KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY = "try_agian_visibility";
String KEY_BIOMETRIC_STATE = "state";
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index 27040f0..18c1b4b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -90,18 +90,19 @@
return;
}
+ final int margin = mUseFullScreen ? 0 : (int) mContext.getResources()
+ .getDimension(R.dimen.biometric_dialog_border_padding);
+ final float cornerRadius = mUseFullScreen ? 0 : mContext.getResources()
+ .getDimension(R.dimen.biometric_dialog_corner_size);
+
if (animateDurationMs > 0) {
// Animate margin
- final int margin = mUseFullScreen ? 0 : (int) mContext.getResources()
- .getDimension(R.dimen.biometric_dialog_border_padding);
ValueAnimator marginAnimator = ValueAnimator.ofInt(mMargin, margin);
marginAnimator.addUpdateListener((animation) -> {
mMargin = (int) animation.getAnimatedValue();
});
// Animate corners
- final float cornerRadius = mUseFullScreen ? 0 : mContext.getResources()
- .getDimension(R.dimen.biometric_dialog_corner_size);
ValueAnimator cornerAnimator = ValueAnimator.ofFloat(mCornerRadius, cornerRadius);
cornerAnimator.addUpdateListener((animation) -> {
mCornerRadius = (float) animation.getAnimatedValue();
@@ -127,6 +128,8 @@
as.playTogether(cornerAnimator, heightAnimator, widthAnimator, marginAnimator);
as.start();
} else {
+ mMargin = margin;
+ mCornerRadius = cornerRadius;
mContentWidth = contentWidth;
mContentHeight = contentHeight;
mPanelView.invalidateOutline();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 8ee96bb..360183d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -312,7 +312,7 @@
@Override
protected AuthDialog buildDialog(Bundle biometricPromptBundle,
boolean requireConfirmation, int userId, int type, String opPackageName,
- boolean skipIntro) {
+ boolean skipIntro, @AuthContainerView.Builder.InitialView int initialView) {
AuthDialog dialog;
if (mBuildCount == 0) {
dialog = mDialog1;