• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS;
20 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT;
21 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT;
22 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT;
23 import static android.app.admin.DevicePolicyResources.UNDEFINED;
24 
25 import android.annotation.IntDef;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.app.AlertDialog;
29 import android.app.admin.DevicePolicyManager;
30 import android.content.Context;
31 import android.content.pm.UserInfo;
32 import android.graphics.drawable.Drawable;
33 import android.hardware.biometrics.PromptInfo;
34 import android.os.AsyncTask;
35 import android.os.CountDownTimer;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.os.SystemClock;
39 import android.os.UserManager;
40 import android.text.TextUtils;
41 import android.util.AttributeSet;
42 import android.view.View;
43 import android.view.WindowManager;
44 import android.view.accessibility.AccessibilityManager;
45 import android.widget.ImageView;
46 import android.widget.LinearLayout;
47 import android.widget.TextView;
48 
49 import androidx.annotation.StringRes;
50 import androidx.annotation.VisibleForTesting;
51 
52 import com.android.internal.widget.LockPatternUtils;
53 import com.android.internal.widget.VerifyCredentialResponse;
54 import com.android.systemui.R;
55 import com.android.systemui.animation.Interpolators;
56 import com.android.systemui.dagger.qualifiers.Background;
57 import com.android.systemui.util.concurrency.DelayableExecutor;
58 
59 import java.lang.annotation.Retention;
60 import java.lang.annotation.RetentionPolicy;
61 
62 /**
63  * Abstract base class for Pin, Pattern, or Password authentication, for
64  * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}}
65  */
66 public abstract class AuthCredentialView extends LinearLayout {
67     private static final String TAG = "BiometricPrompt/AuthCredentialView";
68     private static final int ERROR_DURATION_MS = 3000;
69 
70     static final int USER_TYPE_PRIMARY = 1;
71     static final int USER_TYPE_MANAGED_PROFILE = 2;
72     static final int USER_TYPE_SECONDARY = 3;
73     @Retention(RetentionPolicy.SOURCE)
74     @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY})
75     private @interface UserType {}
76 
77     protected final Handler mHandler;
78     protected final LockPatternUtils mLockPatternUtils;
79 
80     protected final AccessibilityManager mAccessibilityManager;
81     private final UserManager mUserManager;
82     private final DevicePolicyManager mDevicePolicyManager;
83 
84     private PromptInfo mPromptInfo;
85     private AuthPanelController mPanelController;
86     private boolean mShouldAnimatePanel;
87     private boolean mShouldAnimateContents;
88 
89     protected TextView mTitleView;
90     protected TextView mSubtitleView;
91     protected TextView mDescriptionView;
92     protected ImageView mIconView;
93     protected TextView mErrorView;
94 
95     protected @Utils.CredentialType int mCredentialType;
96     protected AuthContainerView mContainerView;
97     protected Callback mCallback;
98     protected AsyncTask<?, ?, ?> mPendingLockCheck;
99     protected int mUserId;
100     protected long mOperationId;
101     protected int mEffectiveUserId;
102     @VisibleForTesting ErrorTimer mErrorTimer;
103 
104     protected @Background DelayableExecutor mBackgroundExecutor;
105 
106     interface Callback {
onCredentialMatched(byte[] attestation)107         void onCredentialMatched(byte[] attestation);
108     }
109 
110     protected static class ErrorTimer extends CountDownTimer {
111         private final TextView mErrorView;
112         private final Context mContext;
113 
114         /**
115          * @param millisInFuture    The number of millis in the future from the call
116          *                          to {@link #start()} until the countdown is done and {@link
117          *                          #onFinish()}
118          *                          is called.
119          * @param countDownInterval The interval along the way to receive
120          *                          {@link #onTick(long)} callbacks.
121          */
ErrorTimer(Context context, long millisInFuture, long countDownInterval, TextView errorView)122         public ErrorTimer(Context context, long millisInFuture, long countDownInterval,
123                 TextView errorView) {
124             super(millisInFuture, countDownInterval);
125             mErrorView = errorView;
126             mContext = context;
127         }
128 
129         @Override
onTick(long millisUntilFinished)130         public void onTick(long millisUntilFinished) {
131             final int secondsCountdown = (int) (millisUntilFinished / 1000);
132             mErrorView.setText(mContext.getString(
133                     R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown));
134         }
135 
136         @Override
onFinish()137         public void onFinish() {
138             if (mErrorView != null) {
139                 mErrorView.setText("");
140             }
141         }
142     }
143 
144     protected final Runnable mClearErrorRunnable = new Runnable() {
145         @Override
146         public void run() {
147             if (mErrorView != null) {
148                 mErrorView.setText("");
149             }
150         }
151     };
152 
AuthCredentialView(Context context, AttributeSet attrs)153     public AuthCredentialView(Context context, AttributeSet attrs) {
154         super(context, attrs);
155 
156         mLockPatternUtils = new LockPatternUtils(mContext);
157         mHandler = new Handler(Looper.getMainLooper());
158         mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
159         mUserManager = mContext.getSystemService(UserManager.class);
160         mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
161     }
162 
showError(String error)163     protected void showError(String error) {
164         if (mHandler != null) {
165             mHandler.removeCallbacks(mClearErrorRunnable);
166             mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
167         }
168         if (mErrorView != null) {
169             mErrorView.setText(error);
170         }
171     }
172 
setTextOrHide(TextView view, CharSequence text)173     private void setTextOrHide(TextView view, CharSequence text) {
174         if (TextUtils.isEmpty(text)) {
175             view.setVisibility(View.GONE);
176         } else {
177             view.setText(text);
178         }
179 
180         Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
181     }
182 
setText(TextView view, CharSequence text)183     private void setText(TextView view, CharSequence text) {
184         view.setText(text);
185     }
186 
setUserId(int userId)187     void setUserId(int userId) {
188         mUserId = userId;
189     }
190 
setOperationId(long operationId)191     void setOperationId(long operationId) {
192         mOperationId = operationId;
193     }
194 
setEffectiveUserId(int effectiveUserId)195     void setEffectiveUserId(int effectiveUserId) {
196         mEffectiveUserId = effectiveUserId;
197     }
198 
setCredentialType(@tils.CredentialType int credentialType)199     void setCredentialType(@Utils.CredentialType int credentialType) {
200         mCredentialType = credentialType;
201     }
202 
setCallback(Callback callback)203     void setCallback(Callback callback) {
204         mCallback = callback;
205     }
206 
setPromptInfo(PromptInfo promptInfo)207     void setPromptInfo(PromptInfo promptInfo) {
208         mPromptInfo = promptInfo;
209     }
210 
setPanelController(AuthPanelController panelController, boolean animatePanel)211     void setPanelController(AuthPanelController panelController, boolean animatePanel) {
212         mPanelController = panelController;
213         mShouldAnimatePanel = animatePanel;
214     }
215 
setShouldAnimateContents(boolean animateContents)216     void setShouldAnimateContents(boolean animateContents) {
217         mShouldAnimateContents = animateContents;
218     }
219 
setContainerView(AuthContainerView containerView)220     void setContainerView(AuthContainerView containerView) {
221         mContainerView = containerView;
222     }
223 
setBackgroundExecutor(@ackground DelayableExecutor bgExecutor)224     void setBackgroundExecutor(@Background DelayableExecutor bgExecutor) {
225         mBackgroundExecutor = bgExecutor;
226     }
227 
228     @Override
onAttachedToWindow()229     protected void onAttachedToWindow() {
230         super.onAttachedToWindow();
231 
232         final CharSequence title = getTitle(mPromptInfo);
233         setText(mTitleView, title);
234         setTextOrHide(mSubtitleView, getSubtitle(mPromptInfo));
235         setTextOrHide(mDescriptionView, getDescription(mPromptInfo));
236         announceForAccessibility(title);
237 
238         if (mIconView != null) {
239             final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId);
240             final Drawable image;
241             if (isManagedProfile) {
242                 image = getResources().getDrawable(R.drawable.auth_dialog_enterprise,
243                         mContext.getTheme());
244             } else {
245                 image = getResources().getDrawable(R.drawable.auth_dialog_lock,
246                         mContext.getTheme());
247             }
248             mIconView.setImageDrawable(image);
249         }
250 
251         // Only animate this if we're transitioning from a biometric view.
252         if (mShouldAnimateContents) {
253             setTranslationY(getResources()
254                     .getDimension(R.dimen.biometric_dialog_credential_translation_offset));
255             setAlpha(0);
256 
257             postOnAnimation(() -> {
258                 animate().translationY(0)
259                         .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS)
260                         .alpha(1.f)
261                         .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
262                         .withLayer()
263                         .start();
264             });
265         }
266     }
267 
268     @Override
onDetachedFromWindow()269     protected void onDetachedFromWindow() {
270         super.onDetachedFromWindow();
271         if (mErrorTimer != null) {
272             mErrorTimer.cancel();
273         }
274     }
275 
276     @Override
onFinishInflate()277     protected void onFinishInflate() {
278         super.onFinishInflate();
279         mTitleView = findViewById(R.id.title);
280         mSubtitleView = findViewById(R.id.subtitle);
281         mDescriptionView = findViewById(R.id.description);
282         mIconView = findViewById(R.id.icon);
283         mErrorView = findViewById(R.id.error);
284     }
285 
286     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)287     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
288         super.onLayout(changed, left, top, right, bottom);
289 
290         if (mShouldAnimatePanel) {
291             // Credential view is always full screen.
292             mPanelController.setUseFullScreen(true);
293             mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(),
294                     mPanelController.getContainerHeight(), 0 /* animateDurationMs */);
295             mShouldAnimatePanel = false;
296         }
297     }
298 
onErrorTimeoutFinish()299     protected void onErrorTimeoutFinish() {}
300 
onCredentialVerified(@onNull VerifyCredentialResponse response, int timeoutMs)301     protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) {
302         if (response.isMatched()) {
303             mClearErrorRunnable.run();
304             mLockPatternUtils.userPresent(mEffectiveUserId);
305 
306             // The response passed into this method contains the Gatekeeper Password. We still
307             // have to request Gatekeeper to create a Hardware Auth Token with the
308             // Gatekeeper Password and Challenge (keystore operationId in this case)
309             final long pwHandle = response.getGatekeeperPasswordHandle();
310             final VerifyCredentialResponse gkResponse = mLockPatternUtils
311                     .verifyGatekeeperPasswordHandle(pwHandle, mOperationId, mEffectiveUserId);
312 
313             mCallback.onCredentialMatched(gkResponse.getGatekeeperHAT());
314             mLockPatternUtils.removeGatekeeperPasswordHandle(pwHandle);
315         } else {
316             if (timeoutMs > 0) {
317                 mHandler.removeCallbacks(mClearErrorRunnable);
318                 long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
319                         mEffectiveUserId, timeoutMs);
320                 mErrorTimer = new ErrorTimer(mContext,
321                         deadline - SystemClock.elapsedRealtime(),
322                         LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
323                         mErrorView) {
324                     @Override
325                     public void onFinish() {
326                         onErrorTimeoutFinish();
327                         mClearErrorRunnable.run();
328                     }
329                 };
330                 mErrorTimer.start();
331             } else {
332                 final boolean didUpdateErrorText = reportFailedAttempt();
333                 if (!didUpdateErrorText) {
334                     final @StringRes int errorRes;
335                     switch (mCredentialType) {
336                         case Utils.CREDENTIAL_PIN:
337                             errorRes = R.string.biometric_dialog_wrong_pin;
338                             break;
339                         case Utils.CREDENTIAL_PATTERN:
340                             errorRes = R.string.biometric_dialog_wrong_pattern;
341                             break;
342                         case Utils.CREDENTIAL_PASSWORD:
343                         default:
344                             errorRes = R.string.biometric_dialog_wrong_password;
345                             break;
346                     }
347                     showError(getResources().getString(errorRes));
348                 }
349             }
350         }
351     }
352 
reportFailedAttempt()353     private boolean reportFailedAttempt() {
354         boolean result = updateErrorMessage(
355                 mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1);
356         mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId);
357         return result;
358     }
359 
updateErrorMessage(int numAttempts)360     private boolean updateErrorMessage(int numAttempts) {
361         // Don't show any message if there's no maximum number of attempts.
362         final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe(
363                 mEffectiveUserId);
364         if (maxAttempts <= 0 || numAttempts <= 0) {
365             return false;
366         }
367 
368         // Update the on-screen error string.
369         if (mErrorView != null) {
370             final String message = getResources().getString(
371                     R.string.biometric_dialog_credential_attempts_before_wipe,
372                     numAttempts,
373                     maxAttempts);
374             showError(message);
375         }
376 
377         // Only show dialog if <=1 attempts are left before wiping.
378         final int remainingAttempts = maxAttempts - numAttempts;
379         if (remainingAttempts == 1) {
380             showLastAttemptBeforeWipeDialog();
381         } else if (remainingAttempts <= 0) {
382             showNowWipingDialog();
383         }
384         return true;
385     }
386 
showLastAttemptBeforeWipeDialog()387     private void showLastAttemptBeforeWipeDialog() {
388         mBackgroundExecutor.execute(() -> {
389             final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
390                     .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title)
391                     .setMessage(
392                             getLastAttemptBeforeWipeMessage(getUserTypeForWipe(), mCredentialType))
393                     .setPositiveButton(android.R.string.ok, null)
394                     .create();
395             alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
396             mHandler.post(alertDialog::show);
397         });
398     }
399 
showNowWipingDialog()400     private void showNowWipingDialog() {
401         mBackgroundExecutor.execute(() -> {
402             String nowWipingMessage = getNowWipingMessage(getUserTypeForWipe());
403             final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
404                     .setMessage(nowWipingMessage)
405                     .setPositiveButton(
406                             com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss,
407                             null /* OnClickListener */)
408                     .setOnDismissListener(
409                             dialog -> mContainerView.animateAway(
410                                     AuthDialogCallback.DISMISSED_ERROR))
411                     .create();
412             alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
413             mHandler.post(alertDialog::show);
414         });
415     }
416 
getUserTypeForWipe()417     private @UserType int getUserTypeForWipe() {
418         final UserInfo userToBeWiped = mUserManager.getUserInfo(
419                 mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId));
420         if (userToBeWiped == null || userToBeWiped.isPrimary()) {
421             return USER_TYPE_PRIMARY;
422         } else if (userToBeWiped.isManagedProfile()) {
423             return USER_TYPE_MANAGED_PROFILE;
424         } else {
425             return USER_TYPE_SECONDARY;
426         }
427     }
428 
429     // This should not be called on the main thread to avoid making an IPC.
getLastAttemptBeforeWipeMessage( @serType int userType, @Utils.CredentialType int credentialType)430     private String getLastAttemptBeforeWipeMessage(
431             @UserType int userType, @Utils.CredentialType int credentialType) {
432         switch (userType) {
433             case USER_TYPE_PRIMARY:
434                 return getLastAttemptBeforeWipeDeviceMessage(credentialType);
435             case USER_TYPE_MANAGED_PROFILE:
436                 return getLastAttemptBeforeWipeProfileMessage(credentialType);
437             case USER_TYPE_SECONDARY:
438                 return getLastAttemptBeforeWipeUserMessage(credentialType);
439             default:
440                 throw new IllegalArgumentException("Unrecognized user type:" + userType);
441         }
442     }
443 
getLastAttemptBeforeWipeDeviceMessage( @tils.CredentialType int credentialType)444     private String getLastAttemptBeforeWipeDeviceMessage(
445             @Utils.CredentialType int credentialType) {
446         switch (credentialType) {
447             case Utils.CREDENTIAL_PIN:
448                 return mContext.getString(
449                         R.string.biometric_dialog_last_pin_attempt_before_wipe_device);
450             case Utils.CREDENTIAL_PATTERN:
451                 return mContext.getString(
452                         R.string.biometric_dialog_last_pattern_attempt_before_wipe_device);
453             case Utils.CREDENTIAL_PASSWORD:
454             default:
455                 return mContext.getString(
456                         R.string.biometric_dialog_last_password_attempt_before_wipe_device);
457         }
458     }
459 
460     // This should not be called on the main thread to avoid making an IPC.
getLastAttemptBeforeWipeProfileMessage( @tils.CredentialType int credentialType)461     private String getLastAttemptBeforeWipeProfileMessage(
462             @Utils.CredentialType int credentialType) {
463         return mDevicePolicyManager.getResources().getString(
464                 getLastAttemptBeforeWipeProfileUpdatableStringId(credentialType),
465                 () -> getLastAttemptBeforeWipeProfileDefaultMessage(credentialType));
466     }
467 
getLastAttemptBeforeWipeProfileUpdatableStringId( @tils.CredentialType int credentialType)468     private static String getLastAttemptBeforeWipeProfileUpdatableStringId(
469             @Utils.CredentialType int credentialType) {
470         switch (credentialType) {
471             case Utils.CREDENTIAL_PIN:
472                 return BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT;
473             case Utils.CREDENTIAL_PATTERN:
474                 return BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT;
475             case Utils.CREDENTIAL_PASSWORD:
476             default:
477                 return BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT;
478         }
479     }
480 
getLastAttemptBeforeWipeProfileDefaultMessage( @tils.CredentialType int credentialType)481     private String getLastAttemptBeforeWipeProfileDefaultMessage(
482             @Utils.CredentialType int credentialType) {
483         int resId;
484         switch (credentialType) {
485             case Utils.CREDENTIAL_PIN:
486                 resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_profile;
487                 break;
488             case Utils.CREDENTIAL_PATTERN:
489                 resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile;
490                 break;
491             case Utils.CREDENTIAL_PASSWORD:
492             default:
493                 resId = R.string.biometric_dialog_last_password_attempt_before_wipe_profile;
494         }
495         return mContext.getString(resId);
496     }
497 
getLastAttemptBeforeWipeUserMessage( @tils.CredentialType int credentialType)498     private String getLastAttemptBeforeWipeUserMessage(
499             @Utils.CredentialType int credentialType) {
500         int resId;
501         switch (credentialType) {
502             case Utils.CREDENTIAL_PIN:
503                 resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_user;
504                 break;
505             case Utils.CREDENTIAL_PATTERN:
506                 resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_user;
507                 break;
508             case Utils.CREDENTIAL_PASSWORD:
509             default:
510                 resId = R.string.biometric_dialog_last_password_attempt_before_wipe_user;
511         }
512         return mContext.getString(resId);
513     }
514 
getNowWipingMessage(@serType int userType)515     private String getNowWipingMessage(@UserType int userType) {
516         return mDevicePolicyManager.getResources().getString(
517                 getNowWipingUpdatableStringId(userType),
518                 () -> getNowWipingDefaultMessage(userType));
519     }
520 
getNowWipingUpdatableStringId(@serType int userType)521     private String getNowWipingUpdatableStringId(@UserType int userType) {
522         switch (userType) {
523             case USER_TYPE_MANAGED_PROFILE:
524                 return BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS;
525             default:
526                 return UNDEFINED;
527         }
528     }
529 
getNowWipingDefaultMessage(@serType int userType)530     private String getNowWipingDefaultMessage(@UserType int userType) {
531         int resId;
532         switch (userType) {
533             case USER_TYPE_PRIMARY:
534                 resId = com.android.settingslib.R.string.failed_attempts_now_wiping_device;
535                 break;
536             case USER_TYPE_MANAGED_PROFILE:
537                 resId = com.android.settingslib.R.string.failed_attempts_now_wiping_profile;
538                 break;
539             case USER_TYPE_SECONDARY:
540                 resId = com.android.settingslib.R.string.failed_attempts_now_wiping_user;
541                 break;
542             default:
543                 throw new IllegalArgumentException("Unrecognized user type:" + userType);
544         }
545         return mContext.getString(resId);
546     }
547 
548     @Nullable
getTitle(@onNull PromptInfo promptInfo)549     private static CharSequence getTitle(@NonNull PromptInfo promptInfo) {
550         final CharSequence credentialTitle = promptInfo.getDeviceCredentialTitle();
551         return credentialTitle != null ? credentialTitle : promptInfo.getTitle();
552     }
553 
554     @Nullable
getSubtitle(@onNull PromptInfo promptInfo)555     private static CharSequence getSubtitle(@NonNull PromptInfo promptInfo) {
556         final CharSequence credentialSubtitle = promptInfo.getDeviceCredentialSubtitle();
557         return credentialSubtitle != null ? credentialSubtitle : promptInfo.getSubtitle();
558     }
559 
560     @Nullable
getDescription(@onNull PromptInfo promptInfo)561     private static CharSequence getDescription(@NonNull PromptInfo promptInfo) {
562         final CharSequence credentialDescription = promptInfo.getDeviceCredentialDescription();
563         return credentialDescription != null ? credentialDescription : promptInfo.getDescription();
564     }
565 }
566