Home | History | Annotate | Download | only in biometrics
      1 /*
      2  * Copyright (C) 2018 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.admin.DevicePolicyManager;
     20 import android.content.Context;
     21 import android.graphics.PixelFormat;
     22 import android.graphics.PorterDuff;
     23 import android.graphics.drawable.Drawable;
     24 import android.hardware.biometrics.BiometricPrompt;
     25 import android.os.Binder;
     26 import android.os.Bundle;
     27 import android.os.Handler;
     28 import android.os.IBinder;
     29 import android.os.Message;
     30 import android.os.UserManager;
     31 import android.text.TextUtils;
     32 import android.util.DisplayMetrics;
     33 import android.util.Log;
     34 import android.view.KeyEvent;
     35 import android.view.LayoutInflater;
     36 import android.view.View;
     37 import android.view.ViewGroup;
     38 import android.view.WindowManager;
     39 import android.view.animation.Interpolator;
     40 import android.widget.Button;
     41 import android.widget.ImageView;
     42 import android.widget.LinearLayout;
     43 import android.widget.TextView;
     44 
     45 import com.android.systemui.Interpolators;
     46 import com.android.systemui.R;
     47 import com.android.systemui.util.leak.RotationUtils;
     48 
     49 /**
     50  * Abstract base class. Shows a dialog for BiometricPrompt.
     51  */
     52 public abstract class BiometricDialogView extends LinearLayout {
     53 
     54     private static final String TAG = "BiometricDialogView";
     55 
     56     private static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility";
     57     private static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility";
     58     private static final String KEY_STATE = "key_state";
     59     private static final String KEY_ERROR_TEXT_VISIBILITY = "key_error_text_visibility";
     60     private static final String KEY_ERROR_TEXT_STRING = "key_error_text_string";
     61     private static final String KEY_ERROR_TEXT_IS_TEMPORARY = "key_error_text_is_temporary";
     62     private static final String KEY_ERROR_TEXT_COLOR = "key_error_text_color";
     63 
     64     private static final int ANIMATION_DURATION_SHOW = 250; // ms
     65     private static final int ANIMATION_DURATION_AWAY = 350; // ms
     66 
     67     protected static final int MSG_RESET_MESSAGE = 1;
     68 
     69     protected static final int STATE_IDLE = 0;
     70     protected static final int STATE_AUTHENTICATING = 1;
     71     protected static final int STATE_ERROR = 2;
     72     protected static final int STATE_PENDING_CONFIRMATION = 3;
     73     protected static final int STATE_AUTHENTICATED = 4;
     74 
     75     private final IBinder mWindowToken = new Binder();
     76     private final Interpolator mLinearOutSlowIn;
     77     private final WindowManager mWindowManager;
     78     private final UserManager mUserManager;
     79     private final DevicePolicyManager mDevicePolicyManager;
     80     private final float mAnimationTranslationOffset;
     81     private final int mErrorColor;
     82     private final float mDialogWidth;
     83     protected final DialogViewCallback mCallback;
     84 
     85     protected final ViewGroup mLayout;
     86     protected final LinearLayout mDialog;
     87     protected final TextView mTitleText;
     88     protected final TextView mSubtitleText;
     89     protected final TextView mDescriptionText;
     90     protected final ImageView mBiometricIcon;
     91     protected final TextView mErrorText;
     92     protected final Button mPositiveButton;
     93     protected final Button mNegativeButton;
     94     protected final Button mTryAgainButton;
     95 
     96     protected final int mTextColor;
     97 
     98     private Bundle mBundle;
     99     private Bundle mRestoredState;
    100 
    101     private int mState = STATE_IDLE;
    102     private boolean mAnimatingAway;
    103     private boolean mWasForceRemoved;
    104     private boolean mSkipIntro;
    105     protected boolean mRequireConfirmation;
    106     private int mUserId; // used to determine if we should show work background
    107 
    108     protected abstract int getHintStringResourceId();
    109     protected abstract int getAuthenticatedAccessibilityResourceId();
    110     protected abstract int getIconDescriptionResourceId();
    111     protected abstract int getDelayAfterAuthenticatedDurationMs();
    112     protected abstract boolean shouldGrayAreaDismissDialog();
    113     protected abstract void handleResetMessage();
    114     protected abstract void updateIcon(int oldState, int newState);
    115 
    116     private final Runnable mShowAnimationRunnable = new Runnable() {
    117         @Override
    118         public void run() {
    119             mLayout.animate()
    120                     .alpha(1f)
    121                     .setDuration(ANIMATION_DURATION_SHOW)
    122                     .setInterpolator(mLinearOutSlowIn)
    123                     .withLayer()
    124                     .start();
    125             mDialog.animate()
    126                     .translationY(0)
    127                     .setDuration(ANIMATION_DURATION_SHOW)
    128                     .setInterpolator(mLinearOutSlowIn)
    129                     .withLayer()
    130                     .withEndAction(() -> onDialogAnimatedIn())
    131                     .start();
    132         }
    133     };
    134 
    135     protected Handler mHandler = new Handler() {
    136         @Override
    137         public void handleMessage(Message msg) {
    138             switch(msg.what) {
    139                 case MSG_RESET_MESSAGE:
    140                     handleResetMessage();
    141                     break;
    142                 default:
    143                     Log.e(TAG, "Unhandled message: " + msg.what);
    144                     break;
    145             }
    146         }
    147     };
    148 
    149     public BiometricDialogView(Context context, DialogViewCallback callback) {
    150         super(context);
    151         mCallback = callback;
    152         mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
    153         mWindowManager = mContext.getSystemService(WindowManager.class);
    154         mUserManager = mContext.getSystemService(UserManager.class);
    155         mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
    156         mAnimationTranslationOffset = getResources()
    157                 .getDimension(R.dimen.biometric_dialog_animation_translation_offset);
    158         mErrorColor = getResources().getColor(R.color.biometric_dialog_error);
    159         mTextColor = getResources().getColor(R.color.biometric_dialog_gray);
    160 
    161         DisplayMetrics metrics = new DisplayMetrics();
    162         mWindowManager.getDefaultDisplay().getMetrics(metrics);
    163         mDialogWidth = Math.min(metrics.widthPixels, metrics.heightPixels);
    164 
    165         // Create the dialog
    166         LayoutInflater factory = LayoutInflater.from(getContext());
    167         mLayout = (ViewGroup) factory.inflate(R.layout.biometric_dialog, this, false);
    168         addView(mLayout);
    169 
    170         mLayout.setOnKeyListener(new View.OnKeyListener() {
    171             boolean downPressed = false;
    172             @Override
    173             public boolean onKey(View v, int keyCode, KeyEvent event) {
    174                 if (keyCode != KeyEvent.KEYCODE_BACK) {
    175                     return false;
    176                 }
    177                 if (event.getAction() == KeyEvent.ACTION_DOWN && downPressed == false) {
    178                     downPressed = true;
    179                 } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
    180                     downPressed = false;
    181                 } else if (event.getAction() == KeyEvent.ACTION_UP && downPressed == true) {
    182                     downPressed = false;
    183                     mCallback.onUserCanceled();
    184                 }
    185                 return true;
    186             }
    187         });
    188 
    189         final View space = mLayout.findViewById(R.id.space);
    190         final View leftSpace = mLayout.findViewById(R.id.left_space);
    191         final View rightSpace = mLayout.findViewById(R.id.right_space);
    192 
    193         mDialog = mLayout.findViewById(R.id.dialog);
    194         mTitleText = mLayout.findViewById(R.id.title);
    195         mSubtitleText = mLayout.findViewById(R.id.subtitle);
    196         mDescriptionText = mLayout.findViewById(R.id.description);
    197         mBiometricIcon = mLayout.findViewById(R.id.biometric_icon);
    198         mErrorText = mLayout.findViewById(R.id.error);
    199         mNegativeButton = mLayout.findViewById(R.id.button2);
    200         mPositiveButton = mLayout.findViewById(R.id.button1);
    201         mTryAgainButton = mLayout.findViewById(R.id.button_try_again);
    202 
    203         mBiometricIcon.setContentDescription(
    204                 getResources().getString(getIconDescriptionResourceId()));
    205 
    206         setDismissesDialog(space);
    207         setDismissesDialog(leftSpace);
    208         setDismissesDialog(rightSpace);
    209 
    210         mNegativeButton.setOnClickListener((View v) -> {
    211             if (mState == STATE_PENDING_CONFIRMATION || mState == STATE_AUTHENTICATED) {
    212                 mCallback.onUserCanceled();
    213             } else {
    214                 mCallback.onNegativePressed();
    215             }
    216         });
    217 
    218         mPositiveButton.setOnClickListener((View v) -> {
    219             updateState(STATE_AUTHENTICATED);
    220             mHandler.postDelayed(() -> {
    221                 mCallback.onPositivePressed();
    222             }, getDelayAfterAuthenticatedDurationMs());
    223         });
    224 
    225         mTryAgainButton.setOnClickListener((View v) -> {
    226             handleResetMessage();
    227             updateState(STATE_AUTHENTICATING);
    228             showTryAgainButton(false /* show */);
    229             mCallback.onTryAgainPressed();
    230         });
    231 
    232         // Must set these in order for the back button events to be received.
    233         mLayout.setFocusableInTouchMode(true);
    234         mLayout.requestFocus();
    235     }
    236 
    237     public void onSaveState(Bundle bundle) {
    238         bundle.putInt(KEY_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility());
    239         bundle.putInt(KEY_CONFIRM_VISIBILITY, mPositiveButton.getVisibility());
    240         bundle.putInt(KEY_STATE, mState);
    241         bundle.putInt(KEY_ERROR_TEXT_VISIBILITY, mErrorText.getVisibility());
    242         bundle.putCharSequence(KEY_ERROR_TEXT_STRING, mErrorText.getText());
    243         bundle.putBoolean(KEY_ERROR_TEXT_IS_TEMPORARY, mHandler.hasMessages(MSG_RESET_MESSAGE));
    244         bundle.putInt(KEY_ERROR_TEXT_COLOR, mErrorText.getCurrentTextColor());
    245     }
    246 
    247     @Override
    248     public void onAttachedToWindow() {
    249         super.onAttachedToWindow();
    250 
    251         final ImageView backgroundView = mLayout.findViewById(R.id.background);
    252 
    253         if (mUserManager.isManagedProfile(mUserId)) {
    254             final Drawable image = getResources().getDrawable(R.drawable.work_challenge_background,
    255                     mContext.getTheme());
    256             image.setColorFilter(mDevicePolicyManager.getOrganizationColorForUser(mUserId),
    257                     PorterDuff.Mode.DARKEN);
    258             backgroundView.setImageDrawable(image);
    259         } else {
    260             backgroundView.setImageDrawable(null);
    261             backgroundView.setBackgroundColor(R.color.biometric_dialog_dim_color);
    262         }
    263 
    264         mNegativeButton.setVisibility(View.VISIBLE);
    265 
    266         if (RotationUtils.getRotation(mContext) != RotationUtils.ROTATION_NONE) {
    267             mDialog.getLayoutParams().width = (int) mDialogWidth;
    268         }
    269 
    270         if (mRestoredState == null) {
    271             updateState(STATE_AUTHENTICATING);
    272             mErrorText.setText(getHintStringResourceId());
    273             mErrorText.setContentDescription(mContext.getString(getHintStringResourceId()));
    274             mErrorText.setVisibility(View.VISIBLE);
    275         } else {
    276             updateState(mState);
    277         }
    278 
    279         CharSequence titleText = mBundle.getCharSequence(BiometricPrompt.KEY_TITLE);
    280 
    281         mTitleText.setVisibility(View.VISIBLE);
    282         mTitleText.setText(titleText);
    283 
    284         final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
    285         if (TextUtils.isEmpty(subtitleText)) {
    286             mSubtitleText.setVisibility(View.GONE);
    287         } else {
    288             mSubtitleText.setVisibility(View.VISIBLE);
    289             mSubtitleText.setText(subtitleText);
    290         }
    291 
    292         final CharSequence descriptionText =
    293                 mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
    294         if (TextUtils.isEmpty(descriptionText)) {
    295             mDescriptionText.setVisibility(View.GONE);
    296         } else {
    297             mDescriptionText.setVisibility(View.VISIBLE);
    298             mDescriptionText.setText(descriptionText);
    299         }
    300 
    301         mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));
    302 
    303         if (requiresConfirmation() && mRestoredState == null) {
    304             mPositiveButton.setVisibility(View.VISIBLE);
    305             mPositiveButton.setEnabled(false);
    306         }
    307 
    308         if (mWasForceRemoved || mSkipIntro) {
    309             // Show the dialog immediately
    310             mLayout.animate().cancel();
    311             mDialog.animate().cancel();
    312             mDialog.setAlpha(1.0f);
    313             mDialog.setTranslationY(0);
    314             mLayout.setAlpha(1.0f);
    315         } else {
    316             // Dim the background and slide the dialog up
    317             mDialog.setTranslationY(mAnimationTranslationOffset);
    318             mLayout.setAlpha(0f);
    319             postOnAnimation(mShowAnimationRunnable);
    320         }
    321         mWasForceRemoved = false;
    322         mSkipIntro = false;
    323     }
    324 
    325     private void setDismissesDialog(View v) {
    326         v.setClickable(true);
    327         v.setOnClickListener(v1 -> {
    328             if (mState != STATE_AUTHENTICATED && shouldGrayAreaDismissDialog()) {
    329                 mCallback.onUserCanceled();
    330             }
    331         });
    332     }
    333 
    334     public void startDismiss() {
    335         mAnimatingAway = true;
    336 
    337         // This is where final cleanup should occur.
    338         final Runnable endActionRunnable = new Runnable() {
    339             @Override
    340             public void run() {
    341                 mWindowManager.removeView(BiometricDialogView.this);
    342                 mAnimatingAway = false;
    343                 // Set the icons / text back to normal state
    344                 handleResetMessage();
    345                 showTryAgainButton(false /* show */);
    346                 updateState(STATE_IDLE);
    347             }
    348         };
    349 
    350         postOnAnimation(new Runnable() {
    351             @Override
    352             public void run() {
    353                 mLayout.animate()
    354                         .alpha(0f)
    355                         .setDuration(ANIMATION_DURATION_AWAY)
    356                         .setInterpolator(mLinearOutSlowIn)
    357                         .withLayer()
    358                         .start();
    359                 mDialog.animate()
    360                         .translationY(mAnimationTranslationOffset)
    361                         .setDuration(ANIMATION_DURATION_AWAY)
    362                         .setInterpolator(mLinearOutSlowIn)
    363                         .withLayer()
    364                         .withEndAction(endActionRunnable)
    365                         .start();
    366             }
    367         });
    368     }
    369 
    370     /**
    371      * Force remove the window, cancelling any animation that's happening. This should only be
    372      * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method
    373      * will cause the dialog to show without an animation the next time it's attached.
    374      */
    375     public void forceRemove() {
    376         mLayout.animate().cancel();
    377         mDialog.animate().cancel();
    378         mWindowManager.removeView(BiometricDialogView.this);
    379         mAnimatingAway = false;
    380         mWasForceRemoved = true;
    381     }
    382 
    383     /**
    384      * Skip the intro animation
    385      */
    386     public void setSkipIntro(boolean skip) {
    387         mSkipIntro = skip;
    388     }
    389 
    390     public boolean isAnimatingAway() {
    391         return mAnimatingAway;
    392     }
    393 
    394     public void setBundle(Bundle bundle) {
    395         mBundle = bundle;
    396     }
    397 
    398     public void setRequireConfirmation(boolean requireConfirmation) {
    399         mRequireConfirmation = requireConfirmation;
    400     }
    401 
    402     public boolean requiresConfirmation() {
    403         return mRequireConfirmation;
    404     }
    405 
    406     public void setUserId(int userId) {
    407         mUserId = userId;
    408     }
    409 
    410     public ViewGroup getLayout() {
    411         return mLayout;
    412     }
    413 
    414     // Shows an error/help message
    415     protected void showTemporaryMessage(String message) {
    416         mHandler.removeMessages(MSG_RESET_MESSAGE);
    417         mErrorText.setText(message);
    418         mErrorText.setTextColor(mErrorColor);
    419         mErrorText.setContentDescription(message);
    420         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE),
    421                 BiometricPrompt.HIDE_DIALOG_DELAY);
    422     }
    423 
    424     /**
    425      * Transient help message (acquire) is received, dialog stays showing. Sensor stays in
    426      * "authenticating" state.
    427      * @param message
    428      */
    429     public void onHelpReceived(String message) {
    430         updateState(STATE_ERROR);
    431         showTemporaryMessage(message);
    432     }
    433 
    434     public void onAuthenticationFailed(String message) {
    435         updateState(STATE_ERROR);
    436         showTemporaryMessage(message);
    437     }
    438 
    439     /**
    440      * Hard error is received, dialog will be dismissed soon.
    441      * @param error
    442      */
    443     public void onErrorReceived(String error) {
    444         updateState(STATE_ERROR);
    445         showTemporaryMessage(error);
    446         showTryAgainButton(false /* show */);
    447         mCallback.onErrorShown(); // TODO: Split between fp and face
    448     }
    449 
    450     public void updateState(int newState) {
    451         if (newState == STATE_PENDING_CONFIRMATION) {
    452             mHandler.removeMessages(MSG_RESET_MESSAGE);
    453             mErrorText.setVisibility(View.INVISIBLE);
    454             mPositiveButton.setVisibility(View.VISIBLE);
    455             mPositiveButton.setEnabled(true);
    456         } else if (newState == STATE_AUTHENTICATED) {
    457             mPositiveButton.setVisibility(View.GONE);
    458             mNegativeButton.setVisibility(View.GONE);
    459             mErrorText.setVisibility(View.INVISIBLE);
    460         }
    461 
    462         if (newState == STATE_PENDING_CONFIRMATION || newState == STATE_AUTHENTICATED) {
    463             mNegativeButton.setText(R.string.cancel);
    464         }
    465 
    466         updateIcon(mState, newState);
    467         mState = newState;
    468     }
    469 
    470     public void showTryAgainButton(boolean show) {
    471     }
    472 
    473     public void onDialogAnimatedIn() {
    474     }
    475 
    476     public void restoreState(Bundle bundle) {
    477         mRestoredState = bundle;
    478         mTryAgainButton.setVisibility(bundle.getInt(KEY_TRY_AGAIN_VISIBILITY));
    479         mPositiveButton.setVisibility(bundle.getInt(KEY_CONFIRM_VISIBILITY));
    480         mState = bundle.getInt(KEY_STATE);
    481         mErrorText.setText(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
    482         mErrorText.setContentDescription(bundle.getCharSequence(KEY_ERROR_TEXT_STRING));
    483         mErrorText.setVisibility(bundle.getInt(KEY_ERROR_TEXT_VISIBILITY));
    484         mErrorText.setTextColor(bundle.getInt(KEY_ERROR_TEXT_COLOR));
    485 
    486         if (bundle.getBoolean(KEY_ERROR_TEXT_IS_TEMPORARY)) {
    487             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE),
    488                     BiometricPrompt.HIDE_DIALOG_DELAY);
    489         }
    490     }
    491 
    492     protected int getState() {
    493         return mState;
    494     }
    495 
    496     public WindowManager.LayoutParams getLayoutParams() {
    497         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
    498                 ViewGroup.LayoutParams.MATCH_PARENT,
    499                 ViewGroup.LayoutParams.MATCH_PARENT,
    500                 WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
    501                 WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    502                 PixelFormat.TRANSLUCENT);
    503         lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
    504         lp.setTitle("BiometricDialogView");
    505         lp.token = mWindowToken;
    506         return lp;
    507     }
    508 }
    509