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 static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
20 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
21 import static android.hardware.biometrics.BiometricManager.Authenticators;
22 
23 import android.annotation.Nullable;
24 import android.app.ActivityManager;
25 import android.app.ActivityTaskManager;
26 import android.app.IActivityTaskManager;
27 import android.app.TaskStackListener;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.content.res.Configuration;
33 import android.hardware.biometrics.BiometricConstants;
34 import android.hardware.biometrics.BiometricPrompt;
35 import android.hardware.biometrics.IBiometricServiceReceiverInternal;
36 import android.hardware.face.FaceManager;
37 import android.hardware.fingerprint.FingerprintManager;
38 import android.os.Bundle;
39 import android.os.Handler;
40 import android.os.Looper;
41 import android.os.RemoteException;
42 import android.util.Log;
43 import android.view.WindowManager;
44 
45 import com.android.internal.R;
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.os.SomeArgs;
48 import com.android.systemui.SystemUI;
49 import com.android.systemui.statusbar.CommandQueue;
50 
51 import java.util.List;
52 
53 import javax.inject.Inject;
54 import javax.inject.Singleton;
55 
56 /**
57  * Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
58  * appropriate biometric UI (e.g. BiometricDialogView).
59  */
60 @Singleton
61 public class AuthController extends SystemUI implements CommandQueue.Callbacks,
62         AuthDialogCallback {
63 
64     private static final String TAG = "BiometricPrompt/AuthController";
65     private static final boolean DEBUG = true;
66 
67     private final CommandQueue mCommandQueue;
68     private final Injector mInjector;
69 
70     // TODO: These should just be saved from onSaveState
71     private SomeArgs mCurrentDialogArgs;
72     @VisibleForTesting
73     AuthDialog mCurrentDialog;
74 
75     private Handler mHandler = new Handler(Looper.getMainLooper());
76     private WindowManager mWindowManager;
77     @VisibleForTesting
78     IActivityTaskManager mActivityTaskManager;
79     @VisibleForTesting
80     BiometricTaskStackListener mTaskStackListener;
81     @VisibleForTesting
82     IBiometricServiceReceiverInternal mReceiver;
83 
84     public class BiometricTaskStackListener extends TaskStackListener {
85         @Override
onTaskStackChanged()86         public void onTaskStackChanged() {
87             mHandler.post(mTaskStackChangedRunnable);
88         }
89     }
90 
91     @VisibleForTesting
92     final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
93         @Override
94         public void onReceive(Context context, Intent intent) {
95             if (mCurrentDialog != null
96                     && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
97                 Log.w(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received");
98                 mCurrentDialog.dismissWithoutCallback(true /* animate */);
99                 mCurrentDialog = null;
100 
101                 try {
102                     if (mReceiver != null) {
103                         mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
104                                 null /* credentialAttestation */);
105                         mReceiver = null;
106                     }
107                 } catch (RemoteException e) {
108                     Log.e(TAG, "Remote exception", e);
109                 }
110             }
111         }
112     };
113 
114     private final Runnable mTaskStackChangedRunnable = () -> {
115         if (mCurrentDialog != null) {
116             try {
117                 final String clientPackage = mCurrentDialog.getOpPackageName();
118                 Log.w(TAG, "Task stack changed, current client: " + clientPackage);
119                 final List<ActivityManager.RunningTaskInfo> runningTasks =
120                         mActivityTaskManager.getTasks(1);
121                 if (!runningTasks.isEmpty()) {
122                     final String topPackage = runningTasks.get(0).topActivity.getPackageName();
123                     if (!topPackage.contentEquals(clientPackage)) {
124                         Log.w(TAG, "Evicting client due to: " + topPackage);
125                         mCurrentDialog.dismissWithoutCallback(true /* animate */);
126                         mCurrentDialog = null;
127                         if (mReceiver != null) {
128                             mReceiver.onDialogDismissed(
129                                     BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
130                                     null /* credentialAttestation */);
131                             mReceiver = null;
132                         }
133                     }
134                 }
135             } catch (RemoteException e) {
136                 Log.e(TAG, "Remote exception", e);
137             }
138         }
139     };
140 
141     @Override
onTryAgainPressed()142     public void onTryAgainPressed() {
143         if (mReceiver == null) {
144             Log.e(TAG, "onTryAgainPressed: Receiver is null");
145             return;
146         }
147         try {
148             mReceiver.onTryAgainPressed();
149         } catch (RemoteException e) {
150             Log.e(TAG, "RemoteException when handling try again", e);
151         }
152     }
153 
154     @Override
onDeviceCredentialPressed()155     public void onDeviceCredentialPressed() {
156         if (mReceiver == null) {
157             Log.e(TAG, "onDeviceCredentialPressed: Receiver is null");
158             return;
159         }
160         try {
161             mReceiver.onDeviceCredentialPressed();
162         } catch (RemoteException e) {
163             Log.e(TAG, "RemoteException when handling credential button", e);
164         }
165     }
166 
167     @Override
onSystemEvent(int event)168     public void onSystemEvent(int event) {
169         if (mReceiver == null) {
170             Log.e(TAG, "onSystemEvent(" + event + "): Receiver is null");
171             return;
172         }
173         try {
174             mReceiver.onSystemEvent(event);
175         } catch (RemoteException e) {
176             Log.e(TAG, "RemoteException when sending system event", e);
177         }
178     }
179 
180     @Override
onDismissed(@ismissedReason int reason, @Nullable byte[] credentialAttestation)181     public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) {
182         switch (reason) {
183             case AuthDialogCallback.DISMISSED_USER_CANCELED:
184                 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
185                         credentialAttestation);
186                 break;
187 
188             case AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE:
189                 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_NEGATIVE,
190                         credentialAttestation);
191                 break;
192 
193             case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE:
194                 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED,
195                         credentialAttestation);
196                 break;
197 
198             case AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED:
199                 sendResultAndCleanUp(
200                         BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED,
201                         credentialAttestation);
202                 break;
203 
204             case AuthDialogCallback.DISMISSED_ERROR:
205                 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR,
206                         credentialAttestation);
207                 break;
208 
209             case AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER:
210                 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED,
211                         credentialAttestation);
212                 break;
213 
214             case AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED:
215                 sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED,
216                         credentialAttestation);
217                 break;
218 
219             default:
220                 Log.e(TAG, "Unhandled reason: " + reason);
221                 break;
222         }
223     }
224 
sendResultAndCleanUp(@ismissedReason int reason, @Nullable byte[] credentialAttestation)225     private void sendResultAndCleanUp(@DismissedReason int reason,
226             @Nullable byte[] credentialAttestation) {
227         if (mReceiver == null) {
228             Log.e(TAG, "sendResultAndCleanUp: Receiver is null");
229             return;
230         }
231         try {
232             mReceiver.onDialogDismissed(reason, credentialAttestation);
233         } catch (RemoteException e) {
234             Log.w(TAG, "Remote exception", e);
235         }
236         onDialogDismissed(reason);
237     }
238 
239     public static class Injector {
getActivityTaskManager()240         IActivityTaskManager getActivityTaskManager() {
241             return ActivityTaskManager.getService();
242         }
243     }
244 
245     @Inject
AuthController(Context context, CommandQueue commandQueue)246     public AuthController(Context context, CommandQueue commandQueue) {
247         this(context, commandQueue, new Injector());
248     }
249 
250     @VisibleForTesting
AuthController(Context context, CommandQueue commandQueue, Injector injector)251     AuthController(Context context, CommandQueue commandQueue, Injector injector) {
252         super(context);
253         mCommandQueue = commandQueue;
254         mInjector = injector;
255 
256         IntentFilter filter = new IntentFilter();
257         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
258 
259         context.registerReceiver(mBroadcastReceiver, filter);
260     }
261 
262     @Override
start()263     public void start() {
264         mCommandQueue.addCallback(this);
265         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
266         mActivityTaskManager = mInjector.getActivityTaskManager();
267 
268         try {
269             mTaskStackListener = new BiometricTaskStackListener();
270             mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
271         } catch (RemoteException e) {
272             Log.w(TAG, "Unable to register task stack listener", e);
273         }
274     }
275 
276     @Override
showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int biometricModality, boolean requireConfirmation, int userId, String opPackageName, long operationId, int sysUiSessionId)277     public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver,
278             int biometricModality, boolean requireConfirmation, int userId, String opPackageName,
279             long operationId, int sysUiSessionId) {
280         final int authenticators = Utils.getAuthenticators(bundle);
281 
282         if (DEBUG) {
283             Log.d(TAG, "showAuthenticationDialog, authenticators: " + authenticators
284                     + ", biometricModality: " + biometricModality
285                     + ", requireConfirmation: " + requireConfirmation
286                     + ", operationId: " + operationId
287                     + ", sysUiSessionId: " + sysUiSessionId);
288         }
289         SomeArgs args = SomeArgs.obtain();
290         args.arg1 = bundle;
291         args.arg2 = receiver;
292         args.argi1 = biometricModality;
293         args.arg3 = requireConfirmation;
294         args.argi2 = userId;
295         args.arg4 = opPackageName;
296         args.arg5 = operationId;
297         args.argi3 = sysUiSessionId;
298 
299         boolean skipAnimation = false;
300         if (mCurrentDialog != null) {
301             Log.w(TAG, "mCurrentDialog: " + mCurrentDialog);
302             skipAnimation = true;
303         }
304 
305         showDialog(args, skipAnimation, null /* savedState */);
306     }
307 
308     @Override
onBiometricAuthenticated()309     public void onBiometricAuthenticated() {
310         mCurrentDialog.onAuthenticationSucceeded();
311     }
312 
313     @Override
onBiometricHelp(String message)314     public void onBiometricHelp(String message) {
315         if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message);
316 
317         mCurrentDialog.onHelp(message);
318     }
319 
getErrorString(int modality, int error, int vendorCode)320     private String getErrorString(int modality, int error, int vendorCode) {
321         switch (modality) {
322             case TYPE_FACE:
323                 return FaceManager.getErrorString(mContext, error, vendorCode);
324 
325             case TYPE_FINGERPRINT:
326                 return FingerprintManager.getErrorString(mContext, error, vendorCode);
327 
328             default:
329                 return "";
330         }
331     }
332 
333     @Override
onBiometricError(int modality, int error, int vendorCode)334     public void onBiometricError(int modality, int error, int vendorCode) {
335         if (DEBUG) {
336             Log.d(TAG, String.format("onBiometricError(%d, %d, %d)", modality, error, vendorCode));
337         }
338 
339         final boolean isLockout = (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT)
340                 || (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT);
341 
342         // TODO(b/141025588): Create separate methods for handling hard and soft errors.
343         final boolean isSoftError = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED
344                 || error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT);
345 
346         if (mCurrentDialog.isAllowDeviceCredentials() && isLockout) {
347             if (DEBUG) Log.d(TAG, "onBiometricError, lockout");
348             mCurrentDialog.animateToCredentialUI();
349         } else if (isSoftError) {
350             final String errorMessage = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED)
351                     ? mContext.getString(R.string.biometric_not_recognized)
352                     : getErrorString(modality, error, vendorCode);
353             if (DEBUG) Log.d(TAG, "onBiometricError, soft error: " + errorMessage);
354             mCurrentDialog.onAuthenticationFailed(errorMessage);
355         } else {
356             final String errorMessage = getErrorString(modality, error, vendorCode);
357             if (DEBUG) Log.d(TAG, "onBiometricError, hard error: " + errorMessage);
358             mCurrentDialog.onError(errorMessage);
359         }
360     }
361 
362     @Override
hideAuthenticationDialog()363     public void hideAuthenticationDialog() {
364         if (DEBUG) Log.d(TAG, "hideAuthenticationDialog: " + mCurrentDialog);
365 
366         if (mCurrentDialog == null) {
367             // Could be possible if the caller canceled authentication after credential success
368             // but before the client was notified.
369             return;
370         }
371 
372         mCurrentDialog.dismissFromSystemServer();
373 
374         // BiometricService will have already sent the callback to the client in this case.
375         // This avoids a round trip to SystemUI. So, just dismiss the dialog and we're done.
376         mCurrentDialog = null;
377     }
378 
showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState)379     private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
380         mCurrentDialogArgs = args;
381         final int type = args.argi1;
382         final Bundle biometricPromptBundle = (Bundle) args.arg1;
383         final boolean requireConfirmation = (boolean) args.arg3;
384         final int userId = args.argi2;
385         final String opPackageName = (String) args.arg4;
386         final long operationId = (long) args.arg5;
387         final int sysUiSessionId = args.argi3;
388 
389         // Create a new dialog but do not replace the current one yet.
390         final AuthDialog newDialog = buildDialog(
391                 biometricPromptBundle,
392                 requireConfirmation,
393                 userId,
394                 type,
395                 opPackageName,
396                 skipAnimation,
397                 operationId,
398                 sysUiSessionId);
399 
400         if (newDialog == null) {
401             Log.e(TAG, "Unsupported type: " + type);
402             return;
403         }
404 
405         if (DEBUG) {
406             Log.d(TAG, "userId: " + userId
407                     + " savedState: " + savedState
408                     + " mCurrentDialog: " + mCurrentDialog
409                     + " newDialog: " + newDialog
410                     + " type: " + type
411                     + " sysUiSessionId: " + sysUiSessionId);
412         }
413 
414         if (mCurrentDialog != null) {
415             // If somehow we're asked to show a dialog, the old one doesn't need to be animated
416             // away. This can happen if the app cancels and re-starts auth during configuration
417             // change. This is ugly because we also have to do things on onConfigurationChanged
418             // here.
419             mCurrentDialog.dismissWithoutCallback(false /* animate */);
420         }
421 
422         mReceiver = (IBiometricServiceReceiverInternal) args.arg2;
423         mCurrentDialog = newDialog;
424         mCurrentDialog.show(mWindowManager, savedState);
425     }
426 
onDialogDismissed(@ismissedReason int reason)427     private void onDialogDismissed(@DismissedReason int reason) {
428         if (DEBUG) Log.d(TAG, "onDialogDismissed: " + reason);
429         if (mCurrentDialog == null) {
430             Log.w(TAG, "Dialog already dismissed");
431         }
432         mReceiver = null;
433         mCurrentDialog = null;
434     }
435 
436     @Override
onConfigurationChanged(Configuration newConfig)437     protected void onConfigurationChanged(Configuration newConfig) {
438         super.onConfigurationChanged(newConfig);
439 
440         // Save the state of the current dialog (buttons showing, etc)
441         if (mCurrentDialog != null) {
442             final Bundle savedState = new Bundle();
443             mCurrentDialog.onSaveState(savedState);
444             mCurrentDialog.dismissWithoutCallback(false /* animate */);
445             mCurrentDialog = null;
446 
447             // Only show the dialog if necessary. If it was animating out, the dialog is supposed
448             // to send its pending callback immediately.
449             if (savedState.getInt(AuthDialog.KEY_CONTAINER_STATE)
450                     != AuthContainerView.STATE_ANIMATING_OUT) {
451                 final boolean credentialShowing =
452                         savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING);
453                 if (credentialShowing) {
454                     // TODO: Clean this up
455                     Bundle bundle = (Bundle) mCurrentDialogArgs.arg1;
456                     bundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED,
457                             Authenticators.DEVICE_CREDENTIAL);
458                 }
459 
460                 showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
461             }
462         }
463     }
464 
buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation, int userId, int type, String opPackageName, boolean skipIntro, long operationId, int sysUiSessionId)465     protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation,
466             int userId, int type, String opPackageName, boolean skipIntro, long operationId,
467             int sysUiSessionId) {
468         return new AuthContainerView.Builder(mContext)
469                 .setCallback(this)
470                 .setBiometricPromptBundle(biometricPromptBundle)
471                 .setRequireConfirmation(requireConfirmation)
472                 .setUserId(userId)
473                 .setOpPackageName(opPackageName)
474                 .setSkipIntro(skipIntro)
475                 .setOperationId(operationId)
476                 .setSysUiSessionId(sysUiSessionId)
477                 .build(type);
478     }
479 }
480