1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.biometrics;
18 
19 import android.app.AlertDialog;
20 import android.app.admin.DevicePolicyManager;
21 import android.content.Context;
22 import android.content.pm.UserInfo;
23 import android.graphics.drawable.Drawable;
24 import android.hardware.biometrics.BiometricPrompt;
25 import android.os.AsyncTask;
26 import android.os.Bundle;
27 import android.os.CountDownTimer;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.os.SystemClock;
31 import android.os.UserManager;
32 import android.text.TextUtils;
33 import android.util.AttributeSet;
34 import android.view.View;
35 import android.view.WindowManager;
36 import android.view.accessibility.AccessibilityManager;
37 import android.widget.ImageView;
38 import android.widget.LinearLayout;
39 import android.widget.TextView;
40 
41 import androidx.annotation.IntDef;
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.StringRes;
45 
46 import com.android.internal.widget.LockPatternUtils;
47 import com.android.systemui.Interpolators;
48 import com.android.systemui.R;
49 
50 import java.lang.annotation.Retention;
51 import java.lang.annotation.RetentionPolicy;
52 
53 /**
54  * Abstract base class for Pin, Pattern, or Password authentication, for
55  * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}}
56  */
57 public abstract class AuthCredentialView extends LinearLayout {
58     private static final String TAG = "BiometricPrompt/AuthCredentialView";
59     private static final int ERROR_DURATION_MS = 3000;
60 
61     static final int USER_TYPE_PRIMARY = 1;
62     static final int USER_TYPE_MANAGED_PROFILE = 2;
63     static final int USER_TYPE_SECONDARY = 3;
64     @Retention(RetentionPolicy.SOURCE)
65     @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY})
66     private @interface UserType {}
67 
68     protected final Handler mHandler;
69     protected final LockPatternUtils mLockPatternUtils;
70 
71     private final AccessibilityManager mAccessibilityManager;
72     private final UserManager mUserManager;
73     private final DevicePolicyManager mDevicePolicyManager;
74 
75     private Bundle mBiometricPromptBundle;
76     private AuthPanelController mPanelController;
77     private boolean mShouldAnimatePanel;
78     private boolean mShouldAnimateContents;
79 
80     private TextView mTitleView;
81     private TextView mSubtitleView;
82     private TextView mDescriptionView;
83     private ImageView mIconView;
84     protected TextView mErrorView;
85 
86     protected @Utils.CredentialType int mCredentialType;
87     protected AuthContainerView mContainerView;
88     protected Callback mCallback;
89     protected AsyncTask<?, ?, ?> mPendingLockCheck;
90     protected int mUserId;
91     protected long mOperationId;
92     protected int mEffectiveUserId;
93     protected ErrorTimer mErrorTimer;
94 
95     interface Callback {
onCredentialMatched(byte[] attestation)96         void onCredentialMatched(byte[] attestation);
97     }
98 
99     protected static class ErrorTimer extends CountDownTimer {
100         private final TextView mErrorView;
101         private final Context mContext;
102 
103         /**
104          * @param millisInFuture    The number of millis in the future from the call
105          *                          to {@link #start()} until the countdown is done and {@link
106          *                          #onFinish()}
107          *                          is called.
108          * @param countDownInterval The interval along the way to receive
109          *                          {@link #onTick(long)} callbacks.
110          */
ErrorTimer(Context context, long millisInFuture, long countDownInterval, TextView errorView)111         public ErrorTimer(Context context, long millisInFuture, long countDownInterval,
112                 TextView errorView) {
113             super(millisInFuture, countDownInterval);
114             mErrorView = errorView;
115             mContext = context;
116         }
117 
118         @Override
onTick(long millisUntilFinished)119         public void onTick(long millisUntilFinished) {
120             final int secondsCountdown = (int) (millisUntilFinished / 1000);
121             mErrorView.setText(mContext.getString(
122                     R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown));
123         }
124 
125         @Override
onFinish()126         public void onFinish() {
127             if (mErrorView != null) {
128                 mErrorView.setText("");
129             }
130         }
131     }
132 
133     protected final Runnable mClearErrorRunnable = new Runnable() {
134         @Override
135         public void run() {
136             if (mErrorView != null) {
137                 mErrorView.setText("");
138             }
139         }
140     };
141 
AuthCredentialView(Context context, AttributeSet attrs)142     public AuthCredentialView(Context context, AttributeSet attrs) {
143         super(context, attrs);
144 
145         mLockPatternUtils = new LockPatternUtils(mContext);
146         mHandler = new Handler(Looper.getMainLooper());
147         mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
148         mUserManager = mContext.getSystemService(UserManager.class);
149         mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
150     }
151 
showError(String error)152     protected void showError(String error) {
153         if (mHandler != null) {
154             mHandler.removeCallbacks(mClearErrorRunnable);
155             mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
156         }
157         if (mErrorView != null) {
158             mErrorView.setText(error);
159         }
160     }
161 
setTextOrHide(TextView view, CharSequence text)162     private void setTextOrHide(TextView view, CharSequence text) {
163         if (TextUtils.isEmpty(text)) {
164             view.setVisibility(View.GONE);
165         } else {
166             view.setText(text);
167         }
168 
169         Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
170     }
171 
setText(TextView view, CharSequence text)172     private void setText(TextView view, CharSequence text) {
173         view.setText(text);
174     }
175 
setUserId(int userId)176     void setUserId(int userId) {
177         mUserId = userId;
178     }
179 
setOperationId(long operationId)180     void setOperationId(long operationId) {
181         mOperationId = operationId;
182     }
183 
setEffectiveUserId(int effectiveUserId)184     void setEffectiveUserId(int effectiveUserId) {
185         mEffectiveUserId = effectiveUserId;
186     }
187 
setCredentialType(@tils.CredentialType int credentialType)188     void setCredentialType(@Utils.CredentialType int credentialType) {
189         mCredentialType = credentialType;
190     }
191 
setCallback(Callback callback)192     void setCallback(Callback callback) {
193         mCallback = callback;
194     }
195 
setBiometricPromptBundle(Bundle bundle)196     void setBiometricPromptBundle(Bundle bundle) {
197         mBiometricPromptBundle = bundle;
198     }
199 
setPanelController(AuthPanelController panelController, boolean animatePanel)200     void setPanelController(AuthPanelController panelController, boolean animatePanel) {
201         mPanelController = panelController;
202         mShouldAnimatePanel = animatePanel;
203     }
204 
setShouldAnimateContents(boolean animateContents)205     void setShouldAnimateContents(boolean animateContents) {
206         mShouldAnimateContents = animateContents;
207     }
208 
setContainerView(AuthContainerView containerView)209     void setContainerView(AuthContainerView containerView) {
210         mContainerView = containerView;
211     }
212 
213     @Override
onAttachedToWindow()214     protected void onAttachedToWindow() {
215         super.onAttachedToWindow();
216 
217         final CharSequence title = getTitle(mBiometricPromptBundle);
218         setText(mTitleView, title);
219         setTextOrHide(mSubtitleView, getSubtitle(mBiometricPromptBundle));
220         setTextOrHide(mDescriptionView, getDescription(mBiometricPromptBundle));
221         announceForAccessibility(title);
222 
223         if (mIconView != null) {
224             final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId);
225             final Drawable image;
226             if (isManagedProfile) {
227                 image = getResources().getDrawable(R.drawable.auth_dialog_enterprise,
228                         mContext.getTheme());
229             } else {
230                 image = getResources().getDrawable(R.drawable.auth_dialog_lock,
231                         mContext.getTheme());
232             }
233             mIconView.setImageDrawable(image);
234         }
235 
236         // Only animate this if we're transitioning from a biometric view.
237         if (mShouldAnimateContents) {
238             setTranslationY(getResources()
239                     .getDimension(R.dimen.biometric_dialog_credential_translation_offset));
240             setAlpha(0);
241 
242             postOnAnimation(() -> {
243                 animate().translationY(0)
244                         .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS)
245                         .alpha(1.f)
246                         .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
247                         .withLayer()
248                         .start();
249             });
250         }
251     }
252 
253     @Override
onDetachedFromWindow()254     protected void onDetachedFromWindow() {
255         super.onDetachedFromWindow();
256         if (mErrorTimer != null) {
257             mErrorTimer.cancel();
258         }
259     }
260 
261     @Override
onFinishInflate()262     protected void onFinishInflate() {
263         super.onFinishInflate();
264         mTitleView = findViewById(R.id.title);
265         mSubtitleView = findViewById(R.id.subtitle);
266         mDescriptionView = findViewById(R.id.description);
267         mIconView = findViewById(R.id.icon);
268         mErrorView = findViewById(R.id.error);
269     }
270 
271     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)272     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
273         super.onLayout(changed, left, top, right, bottom);
274 
275         if (mShouldAnimatePanel) {
276             // Credential view is always full screen.
277             mPanelController.setUseFullScreen(true);
278             mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(),
279                     mPanelController.getContainerHeight(), 0 /* animateDurationMs */);
280             mShouldAnimatePanel = false;
281         }
282     }
283 
onErrorTimeoutFinish()284     protected void onErrorTimeoutFinish() {}
285 
onCredentialVerified(byte[] attestation, int timeoutMs)286     protected void onCredentialVerified(byte[] attestation, int timeoutMs) {
287 
288         final boolean matched = attestation != null;
289 
290         if (matched) {
291             mClearErrorRunnable.run();
292             mLockPatternUtils.userPresent(mEffectiveUserId);
293             mCallback.onCredentialMatched(attestation);
294         } else {
295             if (timeoutMs > 0) {
296                 mHandler.removeCallbacks(mClearErrorRunnable);
297                 long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
298                         mEffectiveUserId, timeoutMs);
299                 mErrorTimer = new ErrorTimer(mContext,
300                         deadline - SystemClock.elapsedRealtime(),
301                         LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
302                         mErrorView) {
303                     @Override
304                     public void onFinish() {
305                         onErrorTimeoutFinish();
306                         mClearErrorRunnable.run();
307                     }
308                 };
309                 mErrorTimer.start();
310             } else {
311                 final boolean didUpdateErrorText = reportFailedAttempt();
312                 if (!didUpdateErrorText) {
313                     final @StringRes int errorRes;
314                     switch (mCredentialType) {
315                         case Utils.CREDENTIAL_PIN:
316                             errorRes = R.string.biometric_dialog_wrong_pin;
317                             break;
318                         case Utils.CREDENTIAL_PATTERN:
319                             errorRes = R.string.biometric_dialog_wrong_pattern;
320                             break;
321                         case Utils.CREDENTIAL_PASSWORD:
322                         default:
323                             errorRes = R.string.biometric_dialog_wrong_password;
324                             break;
325                     }
326                     showError(getResources().getString(errorRes));
327                 }
328             }
329         }
330     }
331 
reportFailedAttempt()332     private boolean reportFailedAttempt() {
333         boolean result = updateErrorMessage(
334                 mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1);
335         mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId);
336         return result;
337     }
338 
updateErrorMessage(int numAttempts)339     private boolean updateErrorMessage(int numAttempts) {
340         // Don't show any message if there's no maximum number of attempts.
341         final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe(
342                 mEffectiveUserId);
343         if (maxAttempts <= 0 || numAttempts <= 0) {
344             return false;
345         }
346 
347         // Update the on-screen error string.
348         if (mErrorView != null) {
349             final String message = getResources().getString(
350                     R.string.biometric_dialog_credential_attempts_before_wipe,
351                     numAttempts,
352                     maxAttempts);
353             showError(message);
354         }
355 
356         // Only show dialog if <=1 attempts are left before wiping.
357         final int remainingAttempts = maxAttempts - numAttempts;
358         if (remainingAttempts == 1) {
359             showLastAttemptBeforeWipeDialog();
360         } else if (remainingAttempts <= 0) {
361             showNowWipingDialog();
362         }
363         return true;
364     }
365 
showLastAttemptBeforeWipeDialog()366     private void showLastAttemptBeforeWipeDialog() {
367         final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
368                 .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title)
369                 .setMessage(
370                         getLastAttemptBeforeWipeMessageRes(getUserTypeForWipe(), mCredentialType))
371                 .setPositiveButton(android.R.string.ok, null)
372                 .create();
373         alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
374         alertDialog.show();
375     }
376 
showNowWipingDialog()377     private void showNowWipingDialog() {
378         final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
379                 .setMessage(getNowWipingMessageRes(getUserTypeForWipe()))
380                 .setPositiveButton(R.string.biometric_dialog_now_wiping_dialog_dismiss, null)
381                 .setOnDismissListener(
382                         dialog -> mContainerView.animateAway(AuthDialogCallback.DISMISSED_ERROR))
383                 .create();
384         alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
385         alertDialog.show();
386     }
387 
getUserTypeForWipe()388     private @UserType int getUserTypeForWipe() {
389         final UserInfo userToBeWiped = mUserManager.getUserInfo(
390                 mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId));
391         if (userToBeWiped == null || userToBeWiped.isPrimary()) {
392             return USER_TYPE_PRIMARY;
393         } else if (userToBeWiped.isManagedProfile()) {
394             return USER_TYPE_MANAGED_PROFILE;
395         } else {
396             return USER_TYPE_SECONDARY;
397         }
398     }
399 
getLastAttemptBeforeWipeMessageRes( @serType int userType, @Utils.CredentialType int credentialType)400     private static @StringRes int getLastAttemptBeforeWipeMessageRes(
401             @UserType int userType, @Utils.CredentialType int credentialType) {
402         switch (userType) {
403             case USER_TYPE_PRIMARY:
404                 return getLastAttemptBeforeWipeDeviceMessageRes(credentialType);
405             case USER_TYPE_MANAGED_PROFILE:
406                 return getLastAttemptBeforeWipeProfileMessageRes(credentialType);
407             case USER_TYPE_SECONDARY:
408                 return getLastAttemptBeforeWipeUserMessageRes(credentialType);
409             default:
410                 throw new IllegalArgumentException("Unrecognized user type:" + userType);
411         }
412     }
413 
getLastAttemptBeforeWipeDeviceMessageRes( @tils.CredentialType int credentialType)414     private static @StringRes int getLastAttemptBeforeWipeDeviceMessageRes(
415             @Utils.CredentialType int credentialType) {
416         switch (credentialType) {
417             case Utils.CREDENTIAL_PIN:
418                 return R.string.biometric_dialog_last_pin_attempt_before_wipe_device;
419             case Utils.CREDENTIAL_PATTERN:
420                 return R.string.biometric_dialog_last_pattern_attempt_before_wipe_device;
421             case Utils.CREDENTIAL_PASSWORD:
422             default:
423                 return R.string.biometric_dialog_last_password_attempt_before_wipe_device;
424         }
425     }
426 
getLastAttemptBeforeWipeProfileMessageRes( @tils.CredentialType int credentialType)427     private static @StringRes int getLastAttemptBeforeWipeProfileMessageRes(
428             @Utils.CredentialType int credentialType) {
429         switch (credentialType) {
430             case Utils.CREDENTIAL_PIN:
431                 return R.string.biometric_dialog_last_pin_attempt_before_wipe_profile;
432             case Utils.CREDENTIAL_PATTERN:
433                 return R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile;
434             case Utils.CREDENTIAL_PASSWORD:
435             default:
436                 return R.string.biometric_dialog_last_password_attempt_before_wipe_profile;
437         }
438     }
439 
getLastAttemptBeforeWipeUserMessageRes( @tils.CredentialType int credentialType)440     private static @StringRes int getLastAttemptBeforeWipeUserMessageRes(
441             @Utils.CredentialType int credentialType) {
442         switch (credentialType) {
443             case Utils.CREDENTIAL_PIN:
444                 return R.string.biometric_dialog_last_pin_attempt_before_wipe_user;
445             case Utils.CREDENTIAL_PATTERN:
446                 return R.string.biometric_dialog_last_pattern_attempt_before_wipe_user;
447             case Utils.CREDENTIAL_PASSWORD:
448             default:
449                 return R.string.biometric_dialog_last_password_attempt_before_wipe_user;
450         }
451     }
452 
getNowWipingMessageRes(@serType int userType)453     private static @StringRes int getNowWipingMessageRes(@UserType int userType) {
454         switch (userType) {
455             case USER_TYPE_PRIMARY:
456                 return R.string.biometric_dialog_failed_attempts_now_wiping_device;
457             case USER_TYPE_MANAGED_PROFILE:
458                 return R.string.biometric_dialog_failed_attempts_now_wiping_profile;
459             case USER_TYPE_SECONDARY:
460                 return R.string.biometric_dialog_failed_attempts_now_wiping_user;
461             default:
462                 throw new IllegalArgumentException("Unrecognized user type:" + userType);
463         }
464     }
465 
466     @Nullable
getTitle(@onNull Bundle bundle)467     private static CharSequence getTitle(@NonNull Bundle bundle) {
468         final CharSequence credentialTitle =
469                 bundle.getCharSequence(BiometricPrompt.KEY_DEVICE_CREDENTIAL_TITLE);
470         return credentialTitle != null ? credentialTitle
471                 : bundle.getCharSequence(BiometricPrompt.KEY_TITLE);
472     }
473 
474     @Nullable
getSubtitle(@onNull Bundle bundle)475     private static CharSequence getSubtitle(@NonNull Bundle bundle) {
476         final CharSequence credentialSubtitle =
477                 bundle.getCharSequence(BiometricPrompt.KEY_DEVICE_CREDENTIAL_SUBTITLE);
478         return credentialSubtitle != null ? credentialSubtitle
479                 : bundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
480     }
481 
482     @Nullable
getDescription(@onNull Bundle bundle)483     private static CharSequence getDescription(@NonNull Bundle bundle) {
484         final CharSequence credentialDescription =
485                 bundle.getCharSequence(BiometricPrompt.KEY_DEVICE_CREDENTIAL_DESCRIPTION);
486         return credentialDescription != null ? credentialDescription
487                 : bundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
488     }
489 }
490