• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onUserCanceled()113         public void onUserCanceled() {
114             mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget();
115         }
116 
117         @Override
onErrorShown()118         public void onErrorShown() {
119             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_HIDE_DIALOG,
120                     false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY);
121         }
122 
123         @Override
onNegativePressed()124         public void onNegativePressed() {
125             mHandler.obtainMessage(MSG_BUTTON_NEGATIVE).sendToTarget();
126         }
127 
128         @Override
onPositivePressed()129         public void onPositivePressed() {
130             mHandler.obtainMessage(MSG_BUTTON_POSITIVE).sendToTarget();
131         }
132 
133         @Override
onTryAgainPressed()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
start()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
showBiometricDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int type, boolean requireConfirmation, int userId)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
onBiometricAuthenticated(boolean authenticated, String failureReason)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
onBiometricHelp(String message)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
onBiometricError(String error)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
hideBiometricDialog()209     public void hideBiometricDialog() {
210         if (DEBUG) Log.d(TAG, "hideBiometricDialog");
211         mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget();
212     }
213 
handleShowDialog(SomeArgs args, boolean skipAnimation, Bundle savedState)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 
handleBiometricAuthenticated(boolean authenticated, String failureReason)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 
handleBiometricHelp(String message)277     private void handleBiometricHelp(String message) {
278         if (DEBUG) Log.d(TAG, "handleBiometricHelp: " + message);
279         mCurrentDialog.onHelpReceived(message);
280     }
281 
handleBiometricError(String error)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 
handleHideDialog(boolean userCanceled)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 
handleButtonNegative()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 
handleButtonPositive()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 
handleUserCanceled()337     private void handleUserCanceled() {
338         handleHideDialog(true /* userCanceled */);
339     }
340 
handleTryAgainPressed()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
onConfigurationChanged(Configuration newConfig)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