16/n: Add PIN/Password

Bug: 140127687

Make AuthCredentialView abstract, and have the following subclasses
 1) AuthCredentialPatternView
 2) AuthCredentialPasswordView

Back button cancels password authentication

Test: manual test with pattern, pin, password
Test: atest com.android.systemui.biometrics

Change-Id: I95e42144616a59827da25d10d063990452714f76
This commit is contained in:
Kevin Chyn
2019-10-02 11:16:31 -07:00
parent 86f1b8e335
commit 8110ebb57c
9 changed files with 451 additions and 106 deletions

View File

@@ -14,7 +14,7 @@
~ limitations under the License.
-->
<com.android.systemui.biometrics.AuthCredentialView
<com.android.systemui.biometrics.AuthCredentialPatternView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -110,4 +110,4 @@
</LinearLayout>
</com.android.systemui.biometrics.AuthCredentialView>
</com.android.systemui.biometrics.AuthCredentialPatternView>

View File

@@ -0,0 +1,101 @@
<!--
~ 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.AuthCredentialPasswordView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:elevation="@dimen/biometric_dialog_elevation">
<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"/>
<EditText
android:id="@+id/lockPassword"
android:layout_marginBottom="20dp"
android:layout_marginLeft="100dp"
android:layout_marginRight="100dp"
android:layout_width="208dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:inputType="textPassword"
android:maxLength="500"
android:textSize="16sp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:imeOptions="flagForceAscii"
style="@style/LockPatternStyleBiometricPrompt"/>
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="5"/>
</com.android.systemui.biometrics.AuthCredentialPasswordView>

View File

@@ -14,7 +14,7 @@
~ limitations under the License.
-->
<com.android.systemui.biometrics.AuthCredentialView
<com.android.systemui.biometrics.AuthCredentialPatternView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -94,4 +94,4 @@
android:layout_height="0dp"
android:layout_weight="1"/>
</com.android.systemui.biometrics.AuthCredentialView>
</com.android.systemui.biometrics.AuthCredentialPatternView>

View File

@@ -629,19 +629,17 @@ public abstract class AuthBiometricView extends LinearLayout {
final String negativeText;
if (isDeviceCredentialAllowed()) {
final LockPatternUtils lpu = new LockPatternUtils(mContext);
switch (lpu.getKeyguardStoredPasswordQuality(mUserId)) {
case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING:
negativeText = getResources().getString(R.string.biometric_dialog_use_pattern);
break;
case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
final @Utils.CredentialType int credentialType =
Utils.getCredentialType(mContext, mUserId);
switch(credentialType) {
case Utils.CREDENTIAL_PIN:
negativeText = getResources().getString(R.string.biometric_dialog_use_pin);
break;
case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC:
case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC:
case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX:
case DevicePolicyManager.PASSWORD_QUALITY_MANAGED:
case Utils.CREDENTIAL_PATTERN:
negativeText = getResources().getString(R.string.biometric_dialog_use_pattern);
break;
case Utils.CREDENTIAL_PASSWORD:
negativeText = getResources().getString(R.string.biometric_dialog_use_password);
break;
default:

View File

@@ -307,8 +307,22 @@ public class AuthContainerView extends LinearLayout
*/
private void addCredentialView(boolean animatePanel, boolean animateContents) {
final LayoutInflater factory = LayoutInflater.from(mContext);
mCredentialView = (AuthCredentialView) factory.inflate(
R.layout.auth_credential_view, null, false);
final int credentialType = Utils.getCredentialType(mContext, mConfig.mUserId);
switch (credentialType) {
case Utils.CREDENTIAL_PATTERN:
mCredentialView = (AuthCredentialView) factory.inflate(
R.layout.auth_credential_pattern_view, null, false);
break;
case Utils.CREDENTIAL_PIN:
case Utils.CREDENTIAL_PASSWORD:
mCredentialView = (AuthCredentialView) factory.inflate(
R.layout.auth_credential_password_view, null, false);
break;
default:
throw new IllegalStateException("Unknown credential type: " + credentialType);
}
mCredentialView.setContainerView(this);
mCredentialView.setUser(mConfig.mUserId);
mCredentialView.setCallback(mCredentialCallback);
mCredentialView.setBiometricPromptBundle(mConfig.mBiometricPromptBundle);

View 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.
*/
package com.android.systemui.biometrics;
import android.content.Context;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import com.android.internal.widget.LockPatternChecker;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
/**
* Pin and Password UI
*/
public class AuthCredentialPasswordView extends AuthCredentialView
implements TextView.OnEditorActionListener {
private static final String TAG = "BiometricPrompt/AuthCredentialPasswordView";
private final InputMethodManager mImm;
private EditText mPasswordField;
public AuthCredentialPasswordView(Context context,
AttributeSet attrs) {
super(context, attrs);
mImm = mContext.getSystemService(InputMethodManager.class);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mPasswordField = findViewById(R.id.lockPassword);
mPasswordField.setOnEditorActionListener(this);
if (mCredentialType == Utils.CREDENTIAL_PIN) {
mPasswordField.setInputType(
InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
}
mPasswordField.setOnKeyListener((v, keyCode, event) -> {
if (keyCode != KeyEvent.KEYCODE_BACK) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_UP) {
mContainerView.animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
}
return true;
});
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// Wait a bit to focus the field so the focusable flag on the window is already set then.
post(() -> {
mPasswordField.requestFocus();
mImm.showSoftInput(mPasswordField, InputMethodManager.SHOW_IMPLICIT);
});
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
// Check if this was the result of hitting the enter key
final boolean isSoftImeEvent = event == null
&& (actionId == EditorInfo.IME_NULL
|| actionId == EditorInfo.IME_ACTION_DONE
|| actionId == EditorInfo.IME_ACTION_NEXT);
final boolean isKeyboardEnterKey = event != null
&& KeyEvent.isConfirmKey(event.getKeyCode())
&& event.getAction() == KeyEvent.ACTION_DOWN;
if (isSoftImeEvent || isKeyboardEnterKey) {
checkPasswordAndUnlock();
return true;
}
return false;
}
private void checkPasswordAndUnlock() {
final byte[] password = LockPatternUtils.charSequenceToByteArray(mPasswordField.getText());
if (password == null || password.length == 0) {
return;
}
mPendingLockCheck = LockPatternChecker.checkPassword(mLockPatternUtils,
password, mUserId, this::onCredentialChecked);
}
@Override
protected void onCredentialChecked(boolean matched, int timeoutMs) {
super.onCredentialChecked(matched, timeoutMs);
if (matched) {
mImm.hideSoftInputFromWindow(getWindowToken(), 0 /* flags */);
} else {
mPasswordField.setText("");
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics;
import android.content.Context;
import android.util.AttributeSet;
import com.android.internal.widget.LockPatternChecker;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockPatternView;
import com.android.systemui.R;
import java.util.List;
/**
* Pattern UI
*/
public class AuthCredentialPatternView extends AuthCredentialView {
private LockPatternView mLockPatternView;
private class UnlockPatternListener implements LockPatternView.OnPatternListener {
@Override
public void onPatternStart() {
}
@Override
public void onPatternCleared() {
}
@Override
public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
}
@Override
public void onPatternDetected(List<LockPatternView.Cell> pattern) {
if (mPendingLockCheck != null) {
mPendingLockCheck.cancel(false);
}
mLockPatternView.setEnabled(false);
if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
// Pattern size is less than the minimum, do not count it as a failed attempt.
onPatternChecked(false /* matched */, 0 /* timeoutMs */);
return;
}
mPendingLockCheck = LockPatternChecker.checkPattern(
mLockPatternUtils,
pattern,
mUserId,
this::onPatternChecked);
}
private void onPatternChecked(boolean matched, int timeoutMs) {
AuthCredentialPatternView.this.onCredentialChecked(matched, timeoutMs);
if (timeoutMs > 0) {
mLockPatternView.setEnabled(false);
} else {
mLockPatternView.setEnabled(true);
}
}
}
@Override
protected void onErrorTimeoutFinish() {
super.onErrorTimeoutFinish();
mLockPatternView.setEnabled(true);
}
public AuthCredentialPatternView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mLockPatternView = findViewById(R.id.lockPattern);
mLockPatternView.setOnPatternListener(new UnlockPatternListener());
mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(mUserId));
mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
}
}

View File

@@ -31,31 +31,23 @@ import android.view.accessibility.AccessibilityManager;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.internal.widget.LockPatternChecker;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockPatternView;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import java.util.List;
/**
* Shows Pin, Pattern, or Password for
* Abstract base class for Pin, Pattern, or Password authentication, for
* {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)}
*/
public class AuthCredentialView extends LinearLayout {
public abstract class AuthCredentialView extends LinearLayout {
private static final String TAG = "BiometricPrompt/AuthCredentialView";
private static final int ERROR_DURATION_MS = 3000;
private final AccessibilityManager mAccessibilityManager;
private final LockPatternUtils mLockPatternUtils;
private final Handler mHandler;
private LockPatternView mLockPatternView;
private int mUserId;
private AsyncTask<?, ?, ?> mPendingLockCheck;
private Callback mCallback;
private ErrorTimer mErrorTimer;
protected final Handler mHandler;
private Bundle mBiometricPromptBundle;
private AuthPanelController mPanelController;
private boolean mShouldAnimatePanel;
@@ -64,13 +56,21 @@ public class AuthCredentialView extends LinearLayout {
private TextView mTitleView;
private TextView mSubtitleView;
private TextView mDescriptionView;
private TextView mErrorView;
protected TextView mErrorView;
protected @Utils.CredentialType int mCredentialType;
protected final LockPatternUtils mLockPatternUtils;
protected AuthContainerView mContainerView;
protected Callback mCallback;
protected AsyncTask<?, ?, ?> mPendingLockCheck;
protected int mUserId;
protected ErrorTimer mErrorTimer;
interface Callback {
void onCredentialMatched();
}
private static class ErrorTimer extends CountDownTimer {
protected static class ErrorTimer extends CountDownTimer {
private final TextView mErrorView;
private final Context mContext;
@@ -102,74 +102,7 @@ public class AuthCredentialView extends LinearLayout {
}
}
private class UnlockPatternListener implements LockPatternView.OnPatternListener {
@Override
public void onPatternStart() {
}
@Override
public void onPatternCleared() {
}
@Override
public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
}
@Override
public void onPatternDetected(List<LockPatternView.Cell> pattern) {
if (mPendingLockCheck != null) {
mPendingLockCheck.cancel(false);
}
mLockPatternView.setEnabled(false);
if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
// Pattern size is less than the minimum, do not count it as a failed attempt.
onPatternChecked(false /* matched */, 0 /* timeoutMs */);
return;
}
mPendingLockCheck = LockPatternChecker.checkPattern(
mLockPatternUtils,
pattern,
mUserId,
this::onPatternChecked);
}
private void onPatternChecked(boolean matched, int timeoutMs) {
mLockPatternView.setEnabled(true);
if (matched) {
mClearErrorRunnable.run();
mCallback.onCredentialMatched();
} else {
if (timeoutMs > 0) {
mHandler.removeCallbacks(mClearErrorRunnable);
mLockPatternView.setEnabled(false);
long deadline = mLockPatternUtils.setLockoutAttemptDeadline(mUserId, timeoutMs);
mErrorTimer = new ErrorTimer(mContext,
deadline - SystemClock.elapsedRealtime(),
LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
mErrorView) {
@Override
public void onFinish() {
mClearErrorRunnable.run();
mLockPatternView.setEnabled(true);
}
};
mErrorTimer.start();
} else {
showError(getResources().getString(R.string.biometric_dialog_wrong_pattern));
}
}
}
}
private final Runnable mClearErrorRunnable = new Runnable() {
protected final Runnable mClearErrorRunnable = new Runnable() {
@Override
public void run() {
mErrorView.setText("");
@@ -179,12 +112,12 @@ public class AuthCredentialView extends LinearLayout {
public AuthCredentialView(Context context, AttributeSet attrs) {
super(context, attrs);
mHandler = new Handler(Looper.getMainLooper());
mLockPatternUtils = new LockPatternUtils(mContext);
mHandler = new Handler(Looper.getMainLooper());
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
}
private void showError(String error) {
protected void showError(String error) {
mHandler.removeCallbacks(mClearErrorRunnable);
mErrorView.setText(error);
mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
@@ -225,6 +158,10 @@ public class AuthCredentialView extends LinearLayout {
mShouldAnimateContents = animateContents;
}
void setContainerView(AuthContainerView containerView) {
mContainerView = containerView;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -263,14 +200,11 @@ public class AuthCredentialView extends LinearLayout {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mCredentialType = Utils.getCredentialType(mContext, mUserId);
mTitleView = findViewById(R.id.title);
mSubtitleView = findViewById(R.id.subtitle);
mDescriptionView = findViewById(R.id.description);
mErrorView = findViewById(R.id.error);
mLockPatternView = findViewById(R.id.lockPattern);
mLockPatternView.setOnPatternListener(new UnlockPatternListener());
mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(mUserId));
mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
}
@Override
@@ -286,4 +220,45 @@ public class AuthCredentialView extends LinearLayout {
}
}
protected void onErrorTimeoutFinish() {}
protected void onCredentialChecked(boolean matched, int timeoutMs) {
if (matched) {
mClearErrorRunnable.run();
mCallback.onCredentialMatched();
} else {
if (timeoutMs > 0) {
mHandler.removeCallbacks(mClearErrorRunnable);
long deadline = mLockPatternUtils.setLockoutAttemptDeadline(mUserId, timeoutMs);
mErrorTimer = new ErrorTimer(mContext,
deadline - SystemClock.elapsedRealtime(),
LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
mErrorView) {
@Override
public void onFinish() {
onErrorTimeoutFinish();
mClearErrorRunnable.run();
}
};
mErrorTimer.start();
} else {
final int error;
switch (mCredentialType) {
case Utils.CREDENTIAL_PIN:
error = R.string.biometric_dialog_wrong_pin;
break;
case Utils.CREDENTIAL_PATTERN:
error = R.string.biometric_dialog_wrong_pattern;
break;
case Utils.CREDENTIAL_PASSWORD:
error = R.string.biometric_dialog_wrong_password;
break;
default:
error = R.string.biometric_dialog_wrong_password;
break;
}
showError(getResources().getString(error));
}
}
}
}

View File

@@ -18,6 +18,8 @@ package com.android.systemui.biometrics;
import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
import android.annotation.IntDef;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.hardware.biometrics.Authenticator;
import android.hardware.biometrics.BiometricPrompt;
@@ -28,7 +30,23 @@ import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class Utils {
public static final int CREDENTIAL_PIN = 1;
public static final int CREDENTIAL_PATTERN = 2;
public static final int CREDENTIAL_PASSWORD = 3;
@Retention(RetentionPolicy.SOURCE)
@IntDef({CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD})
@interface CredentialType {}
static float dpToPixels(Context context, float dp) {
return dp * ((float) context.getResources().getDisplayMetrics().densityDpi
/ DisplayMetrics.DENSITY_DEFAULT);
@@ -63,4 +81,22 @@ public class Utils {
static int getAuthenticators(Bundle biometricPromptBundle) {
return biometricPromptBundle.getInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED);
}
static @CredentialType int getCredentialType(Context context, int userId) {
final LockPatternUtils lpu = new LockPatternUtils(context);
switch (lpu.getKeyguardStoredPasswordQuality(userId)) {
case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING:
return CREDENTIAL_PATTERN;
case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
return CREDENTIAL_PIN;
case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC:
case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC:
case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX:
case DevicePolicyManager.PASSWORD_QUALITY_MANAGED:
return CREDENTIAL_PASSWORD;
default:
return CREDENTIAL_PASSWORD;
}
}
}