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.content.Context;
     20 import android.content.pm.PackageManager;
     21 import android.content.res.Configuration;
     22 import android.hardware.biometrics.BiometricAuthenticator;
     23 import android.hardware.biometrics.BiometricPrompt;
     24 import android.hardware.biometrics.IBiometricServiceReceiverInternal;
     25 import android.os.Bundle;
     26 import android.os.Handler;
     27 import android.os.Looper;
     28 import android.os.Message;
     29 import android.os.RemoteException;
     30 import android.util.Log;
     31 import android.view.WindowManager;
     32 
     33 import com.android.internal.os.SomeArgs;
     34 import com.android.systemui.Dependency;
     35 import com.android.systemui.SystemUI;
     36 import com.android.systemui.keyguard.WakefulnessLifecycle;
     37 import com.android.systemui.statusbar.CommandQueue;
     38 
     39 /**
     40  * Receives messages sent from AuthenticationClient and shows the appropriate biometric UI (e.g.
     41  * BiometricDialogView).
     42  */
     43 public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks {
     44     private static final String TAG = "BiometricDialogImpl";
     45     private static final boolean DEBUG = true;
     46 
     47     private static final int MSG_SHOW_DIALOG = 1;
     48     private static final int MSG_BIOMETRIC_AUTHENTICATED = 2;
     49     private static final int MSG_BIOMETRIC_HELP = 3;
     50     private static final int MSG_BIOMETRIC_ERROR = 4;
     51     private static final int MSG_HIDE_DIALOG = 5;
     52     private static final int MSG_BUTTON_NEGATIVE = 6;
     53     private static final int MSG_USER_CANCELED = 7;
     54     private static final int MSG_BUTTON_POSITIVE = 8;
     55     private static final int MSG_TRY_AGAIN_PRESSED = 9;
     56 
     57     private SomeArgs mCurrentDialogArgs;
     58     private BiometricDialogView mCurrentDialog;
     59     private WindowManager mWindowManager;
     60     private IBiometricServiceReceiverInternal mReceiver;
     61     private boolean mDialogShowing;
     62     private Callback mCallback = new Callback();
     63     private WakefulnessLifecycle mWakefulnessLifecycle;
     64 
     65     private Handler mHandler = new Handler(Looper.getMainLooper()) {
     66         @Override
     67         public void handleMessage(Message msg) {
     68             switch(msg.what) {
     69                 case MSG_SHOW_DIALOG:
     70                     handleShowDialog((SomeArgs) msg.obj, false /* skipAnimation */,
     71                             null /* savedState */);
     72                     break;
     73                 case MSG_BIOMETRIC_AUTHENTICATED: {
     74                     SomeArgs args = (SomeArgs) msg.obj;
     75                     handleBiometricAuthenticated((boolean) args.arg1 /* authenticated */,
     76                             (String) args.arg2 /* failureReason */);
     77                     args.recycle();
     78                     break;
     79                 }
     80                 case MSG_BIOMETRIC_HELP: {
     81                     SomeArgs args = (SomeArgs) msg.obj;
     82                     handleBiometricHelp((String) args.arg1 /* message */);
     83                     args.recycle();
     84                     break;
     85                 }
     86                 case MSG_BIOMETRIC_ERROR:
     87                     handleBiometricError((String) msg.obj);
     88                     break;
     89                 case MSG_HIDE_DIALOG:
     90                     handleHideDialog((Boolean) msg.obj);
     91                     break;
     92                 case MSG_BUTTON_NEGATIVE:
     93                     handleButtonNegative();
     94                     break;
     95                 case MSG_USER_CANCELED:
     96                     handleUserCanceled();
     97                     break;
     98                 case MSG_BUTTON_POSITIVE:
     99                     handleButtonPositive();
    100                     break;
    101                 case MSG_TRY_AGAIN_PRESSED:
    102                     handleTryAgainPressed();
    103                     break;
    104                 default:
    105                     Log.w(TAG, "Unknown message: " + msg.what);
    106                     break;
    107             }
    108         }
    109     };
    110 
    111     private class Callback implements DialogViewCallback {
    112         @Override
    113         public void onUserCanceled() {
    114             mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget();
    115         }
    116 
    117         @Override
    118         public void onErrorShown() {
    119             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_HIDE_DIALOG,
    120                     false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY);
    121         }
    122 
    123         @Override
    124         public void onNegativePressed() {
    125             mHandler.obtainMessage(MSG_BUTTON_NEGATIVE).sendToTarget();
    126         }
    127 
    128         @Override
    129         public void onPositivePressed() {
    130             mHandler.obtainMessage(MSG_BUTTON_POSITIVE).sendToTarget();
    131         }
    132 
    133         @Override
    134         public void onTryAgainPressed() {
    135             mHandler.obtainMessage(MSG_TRY_AGAIN_PRESSED).sendToTarget();
    136         }
    137     }
    138 
    139     final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
    140         @Override
    141         public void onStartedGoingToSleep() {
    142             if (mDialogShowing) {
    143                 if (DEBUG) Log.d(TAG, "User canceled due to screen off");
    144                 mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget();
    145             }
    146         }
    147     };
    148 
    149     @Override
    150     public void start() {
    151         final PackageManager pm = mContext.getPackageManager();
    152         if (pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
    153                 || pm.hasSystemFeature(PackageManager.FEATURE_FACE)
    154                 || pm.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
    155             getComponent(CommandQueue.class).addCallback(this);
    156             mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    157             mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
    158             mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
    159         }
    160     }
    161 
    162     @Override
    163     public void showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
    164             int type, boolean requireConfirmation, int userId) {
    165         if (DEBUG) {
    166             Log.d(TAG, "showBiometricDialog, type: " + type
    167                     + ", requireConfirmation: " + requireConfirmation);
    168         }
    169         // Remove these messages as they are part of the previous client
    170         mHandler.removeMessages(MSG_BIOMETRIC_ERROR);
    171         mHandler.removeMessages(MSG_BIOMETRIC_HELP);
    172         mHandler.removeMessages(MSG_BIOMETRIC_AUTHENTICATED);
    173         mHandler.removeMessages(MSG_HIDE_DIALOG);
    174         SomeArgs args = SomeArgs.obtain();
    175         args.arg1 = bundle;
    176         args.arg2 = receiver;
    177         args.argi1 = type;
    178         args.arg3 = requireConfirmation;
    179         args.argi2 = userId;
    180         mHandler.obtainMessage(MSG_SHOW_DIALOG, args).sendToTarget();
    181     }
    182 
    183     @Override
    184     public void onBiometricAuthenticated(boolean authenticated, String failureReason) {
    185         if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: " + authenticated
    186                 + " reason: " + failureReason);
    187 
    188         SomeArgs args = SomeArgs.obtain();
    189         args.arg1 = authenticated;
    190         args.arg2 = failureReason;
    191         mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget();
    192     }
    193 
    194     @Override
    195     public void onBiometricHelp(String message) {
    196         if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message);
    197         SomeArgs args = SomeArgs.obtain();
    198         args.arg1 = message;
    199         mHandler.obtainMessage(MSG_BIOMETRIC_HELP, args).sendToTarget();
    200     }
    201 
    202     @Override
    203     public void onBiometricError(String error) {
    204         if (DEBUG) Log.d(TAG, "onBiometricError: " + error);
    205         mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, error).sendToTarget();
    206     }
    207 
    208     @Override
    209     public void hideBiometricDialog() {
    210         if (DEBUG) Log.d(TAG, "hideBiometricDialog");
    211         mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget();
    212     }
    213 
    214     private void handleShowDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
    215         mCurrentDialogArgs = args;
    216         final int type = args.argi1;
    217 
    218         // Create a new dialog but do not replace the current one yet.
    219         BiometricDialogView newDialog;
    220         if (type == BiometricAuthenticator.TYPE_FINGERPRINT) {
    221             newDialog = new FingerprintDialogView(mContext, mCallback);
    222         } else if (type == BiometricAuthenticator.TYPE_FACE) {
    223             newDialog = new FaceDialogView(mContext, mCallback);
    224         } else {
    225             Log.e(TAG, "Unsupported type: " + type);
    226             return;
    227         }
    228 
    229         if (DEBUG) Log.d(TAG, "handleShowDialog, "
    230                 + " savedState: " + savedState
    231                 + " mCurrentDialog: " + mCurrentDialog
    232                 + " newDialog: " + newDialog
    233                 + " type: " + type);
    234 
    235         if (savedState != null) {
    236             // SavedState is only non-null if it's from onConfigurationChanged. Restore the state
    237             // even though it may be removed / re-created again
    238             newDialog.restoreState(savedState);
    239         } else if (mCurrentDialog != null && mDialogShowing) {
    240             // If somehow we're asked to show a dialog, the old one doesn't need to be animated
    241             // away. This can happen if the app cancels and re-starts auth during configuration
    242             // change. This is ugly because we also have to do things on onConfigurationChanged
    243             // here.
    244             mCurrentDialog.forceRemove();
    245         }
    246 
    247         mReceiver = (IBiometricServiceReceiverInternal) args.arg2;
    248         newDialog.setBundle((Bundle) args.arg1);
    249         newDialog.setRequireConfirmation((boolean) args.arg3);
    250         newDialog.setUserId(args.argi2);
    251         newDialog.setSkipIntro(skipAnimation);
    252         mCurrentDialog = newDialog;
    253         mWindowManager.addView(mCurrentDialog, mCurrentDialog.getLayoutParams());
    254         mDialogShowing = true;
    255     }
    256 
    257     private void handleBiometricAuthenticated(boolean authenticated, String failureReason) {
    258         if (DEBUG) Log.d(TAG, "handleBiometricAuthenticated: " + authenticated);
    259 
    260         if (authenticated) {
    261             mCurrentDialog.announceForAccessibility(
    262                     mContext.getResources()
    263                             .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId()));
    264             if (mCurrentDialog.requiresConfirmation()) {
    265                 mCurrentDialog.updateState(BiometricDialogView.STATE_PENDING_CONFIRMATION);
    266             } else {
    267                 mCurrentDialog.updateState(BiometricDialogView.STATE_AUTHENTICATED);
    268                 mHandler.postDelayed(() -> {
    269                     handleHideDialog(false /* userCanceled */);
    270                 }, mCurrentDialog.getDelayAfterAuthenticatedDurationMs());
    271             }
    272         } else {
    273             mCurrentDialog.onAuthenticationFailed(failureReason);
    274         }
    275     }
    276 
    277     private void handleBiometricHelp(String message) {
    278         if (DEBUG) Log.d(TAG, "handleBiometricHelp: " + message);
    279         mCurrentDialog.onHelpReceived(message);
    280     }
    281 
    282     private void handleBiometricError(String error) {
    283         if (DEBUG) Log.d(TAG, "handleBiometricError: " + error);
    284         if (!mDialogShowing) {
    285             if (DEBUG) Log.d(TAG, "Dialog already dismissed");
    286             return;
    287         }
    288         mCurrentDialog.onErrorReceived(error);
    289     }
    290 
    291     private void handleHideDialog(boolean userCanceled) {
    292         if (DEBUG) Log.d(TAG, "handleHideDialog, userCanceled: " + userCanceled);
    293         if (!mDialogShowing) {
    294             // This can happen if there's a race and we get called from both
    295             // onAuthenticated and onError, etc.
    296             Log.w(TAG, "Dialog already dismissed, userCanceled: " + userCanceled);
    297             return;
    298         }
    299         if (userCanceled) {
    300             try {
    301                 mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
    302             } catch (RemoteException e) {
    303                 Log.e(TAG, "RemoteException when hiding dialog", e);
    304             }
    305         }
    306         mReceiver = null;
    307         mDialogShowing = false;
    308         mCurrentDialog.startDismiss();
    309     }
    310 
    311     private void handleButtonNegative() {
    312         if (mReceiver == null) {
    313             Log.e(TAG, "Receiver is null");
    314             return;
    315         }
    316         try {
    317             mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
    318         } catch (RemoteException e) {
    319             Log.e(TAG, "Remote exception when handling negative button", e);
    320         }
    321         handleHideDialog(false /* userCanceled */);
    322     }
    323 
    324     private void handleButtonPositive() {
    325         if (mReceiver == null) {
    326             Log.e(TAG, "Receiver is null");
    327             return;
    328         }
    329         try {
    330             mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_POSITIVE);
    331         } catch (RemoteException e) {
    332             Log.e(TAG, "Remote exception when handling positive button", e);
    333         }
    334         handleHideDialog(false /* userCanceled */);
    335     }
    336 
    337     private void handleUserCanceled() {
    338         handleHideDialog(true /* userCanceled */);
    339     }
    340 
    341     private void handleTryAgainPressed() {
    342         try {
    343             mReceiver.onTryAgainPressed();
    344         } catch (RemoteException e) {
    345             Log.e(TAG, "RemoteException when handling try again", e);
    346         }
    347     }
    348 
    349     @Override
    350     protected void onConfigurationChanged(Configuration newConfig) {
    351         super.onConfigurationChanged(newConfig);
    352         final boolean wasShowing = mDialogShowing;
    353 
    354         // Save the state of the current dialog (buttons showing, etc)
    355         final Bundle savedState = new Bundle();
    356         if (mCurrentDialog != null) {
    357             mCurrentDialog.onSaveState(savedState);
    358         }
    359 
    360         if (mDialogShowing) {
    361             mCurrentDialog.forceRemove();
    362             mDialogShowing = false;
    363         }
    364 
    365         if (wasShowing) {
    366             handleShowDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
    367         }
    368     }
    369 }
    370