Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c53d981e authored by Kevin Chyn's avatar Kevin Chyn
Browse files

1/n: Update BiometricDialog (SystemUI side) with cleaner lifecycle

1) Clean up BiometricDialogImpl. As a side-effect of 2, which cleans up
   the dialog lifecycle, we no longer need to have a handler here. This
   greatly simplifies the code here.
2) Clean up interface between BiometricDialogImpl and the UI
   (BiometricDialogView).
3) Clean up interface between BimetricService and BiometricDialogImpl.
   SystemUI is now responsible for dismissing the dialog.

Test: atest BiometricDialogImplTest
Bug: 138628043

Change-Id: Ic1fea4c05c27dfc7eb6fc661f517f0380b9fff99
parent f165a18a
Loading
Loading
Loading
Loading
+12 −2
Original line number Diff line number Diff line
@@ -100,7 +100,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
    /**
     * @hide
     */
    public static final int DISMISSED_REASON_POSITIVE = 1;
    public static final int DISMISSED_REASON_CONFIRMED = 1;

    /**
     * @hide
@@ -112,6 +112,16 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
     */
    public static final int DISMISSED_REASON_USER_CANCEL = 3;

    /**
     * @hide
     */
    public static final int DISMISSED_REASON_CONFIRM_NOT_REQUIRED = 4;

    /**
     * @hide
     */
    public static final int DISMISSED_REASON_ERROR = 5;

    private static class ButtonInfo {
        Executor executor;
        DialogInterface.OnClickListener listener;
@@ -362,7 +372,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
        @Override
        public void onDialogDismissed(int reason) throws RemoteException {
            // Check the reason and invoke OnClickListener(s) if necessary
            if (reason == DISMISSED_REASON_POSITIVE) {
            if (reason == DISMISSED_REASON_CONFIRMED) {
                mPositiveButtonInfo.executor.execute(() -> {
                    mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE);
                });
+100 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.biometrics;

import android.hardware.biometrics.BiometricPrompt;
import android.os.Bundle;
import android.view.WindowManager;

import com.android.systemui.biometrics.ui.BiometricDialogView;

/**
 * Interface for the biometric dialog UI.
 */
public interface BiometricDialog {

    // TODO: Clean up save/restore state
    String[] KEYS_TO_BACKUP = {
            BiometricPrompt.KEY_TITLE,
            BiometricPrompt.KEY_USE_DEFAULT_TITLE,
            BiometricPrompt.KEY_SUBTITLE,
            BiometricPrompt.KEY_DESCRIPTION,
            BiometricPrompt.KEY_POSITIVE_TEXT,
            BiometricPrompt.KEY_NEGATIVE_TEXT,
            BiometricPrompt.KEY_REQUIRE_CONFIRMATION,
            BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL,
            BiometricPrompt.KEY_FROM_CONFIRM_DEVICE_CREDENTIAL,

            BiometricDialogView.KEY_TRY_AGAIN_VISIBILITY,
            BiometricDialogView.KEY_CONFIRM_VISIBILITY,
            BiometricDialogView.KEY_CONFIRM_ENABLED,
            BiometricDialogView.KEY_STATE,
            BiometricDialogView.KEY_ERROR_TEXT_VISIBILITY,
            BiometricDialogView.KEY_ERROR_TEXT_STRING,
            BiometricDialogView.KEY_ERROR_TEXT_IS_TEMPORARY,
            BiometricDialogView.KEY_ERROR_TEXT_COLOR,
    };

    /**
     * Show the dialog.
     * @param wm
     * @param skipIntroAnimation
     */
    void show(WindowManager wm, boolean skipIntroAnimation);

    /**
     * Dismiss the dialog without sending a callback. Only used when the system detects a case
     * where the error won't come from the UI (e.g. task stack changed).
     * @param animate
     */
    void dismissWithoutCallback(boolean animate);

    /**
     * Biometric authenticated. May be pending user confirmation, or completed.
     */
    void onAuthenticationSucceeded();

    /**
     * Authentication failed (reject, timeout). Dialog stays showing.
     * @param failureReason
     */
    void onAuthenticationFailed(String failureReason);

    /**
     * Authentication rejected, or help message received.
     * @param help
     */
    void onHelp(String help);

    /**
     * Authentication failed. Dialog going away.
     * @param error
     */
    void onError(String error);

    /**
     * Save the current state.
     * @param outState
     */
    void onSaveState(Bundle outState);

    /**
     * Restore a previous state.
     * @param savedState
     */
    void restoreState(Bundle savedState);
}
+111 −235
Original line number Diff line number Diff line
@@ -19,132 +19,88 @@ package com.android.systemui.biometrics;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.IBiometricServiceReceiverInternal;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.view.WindowManager;

import com.android.internal.os.SomeArgs;
import com.android.systemui.Dependency;
import com.android.systemui.SystemUI;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.biometrics.ui.BiometricDialogView;
import com.android.systemui.statusbar.CommandQueue;

/**
 * Receives messages sent from AuthenticationClient and shows the appropriate biometric UI (e.g.
 * BiometricDialogView).
 * Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
 * appropriate biometric UI (e.g. BiometricDialogView).
 */
public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks {
public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callbacks,
        DialogViewCallback {
    private static final String TAG = "BiometricDialogImpl";
    private static final boolean DEBUG = true;

    private static final int MSG_SHOW_DIALOG = 1;
    private static final int MSG_BIOMETRIC_AUTHENTICATED = 2;
    private static final int MSG_BIOMETRIC_HELP = 3;
    private static final int MSG_BIOMETRIC_ERROR = 4;
    private static final int MSG_HIDE_DIALOG = 5;
    private static final int MSG_BUTTON_NEGATIVE = 6;
    private static final int MSG_USER_CANCELED = 7;
    private static final int MSG_BUTTON_POSITIVE = 8;
    private static final int MSG_TRY_AGAIN_PRESSED = 9;

    // TODO: These should just be saved from onSaveState
    private SomeArgs mCurrentDialogArgs;
    private BiometricDialogView mCurrentDialog;
    private BiometricDialog mCurrentDialog;

    private WindowManager mWindowManager;
    private IBiometricServiceReceiverInternal mReceiver;
    private boolean mDialogShowing;
    private Callback mCallback = new Callback();
    private WakefulnessLifecycle mWakefulnessLifecycle;

    private Handler mHandler = new Handler(Looper.getMainLooper()) {
    @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case MSG_SHOW_DIALOG:
                    handleShowDialog((SomeArgs) msg.obj, false /* skipAnimation */,
                            null /* savedState */);
                    break;
                case MSG_BIOMETRIC_AUTHENTICATED: {
                    SomeArgs args = (SomeArgs) msg.obj;
                    handleBiometricAuthenticated((boolean) args.arg1 /* authenticated */,
                            (String) args.arg2 /* failureReason */);
                    args.recycle();
                    break;
    public void onTryAgainPressed() {
        try {
            mReceiver.onTryAgainPressed();
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException when handling try again", e);
        }
                case MSG_BIOMETRIC_HELP: {
                    SomeArgs args = (SomeArgs) msg.obj;
                    handleBiometricHelp((String) args.arg1 /* message */);
                    args.recycle();
                    break;
    }
                case MSG_BIOMETRIC_ERROR:
                    handleBiometricError((String) msg.obj);
                    break;
                case MSG_HIDE_DIALOG:
                    handleHideDialog((Boolean) msg.obj);

    @Override
    public void onDismissed(int reason) {
        switch (reason) {
            case DialogViewCallback.DISMISSED_USER_CANCELED:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
                break;
                case MSG_BUTTON_NEGATIVE:
                    handleButtonNegative();

            case DialogViewCallback.DISMISSED_BUTTON_NEGATIVE:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
                break;
                case MSG_USER_CANCELED:
                    handleUserCanceled();

            case DialogViewCallback.DISMISSED_BUTTON_POSITIVE:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRMED);
                break;
                case MSG_BUTTON_POSITIVE:
                    handleButtonPositive();

            case DialogViewCallback.DISMISSED_AUTHENTICATED:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CONFIRM_NOT_REQUIRED);
                // TODO: BiometricService currently sends the result immediately. This should
                // actually happen when the animation is completed.
                break;
                case MSG_TRY_AGAIN_PRESSED:
                    handleTryAgainPressed();

            case DialogViewCallback.DISMISSED_ERROR:
                sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR);
                // TODO: Make sure error isn't received until dialog is dismissed
                // TODO: Similarly, BiometricService currently sends the result immediately.
                // This should happen when the animation is completed.
                break;
            default:
                    Log.w(TAG, "Unknown message: " + msg.what);
                Log.e(TAG, "Unhandled reason: " + reason);
                break;
        }
    }
    };

    private class Callback implements DialogViewCallback {
        @Override
        public void onUserCanceled() {
            mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget();
        }

        @Override
        public void onErrorShown() {
            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_HIDE_DIALOG,
                    false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY);
        }

        @Override
        public void onNegativePressed() {
            mHandler.obtainMessage(MSG_BUTTON_NEGATIVE).sendToTarget();
        }

        @Override
        public void onPositivePressed() {
            mHandler.obtainMessage(MSG_BUTTON_POSITIVE).sendToTarget();
        }

        @Override
        public void onTryAgainPressed() {
            mHandler.obtainMessage(MSG_TRY_AGAIN_PRESSED).sendToTarget();
        }
    private void sendResultAndCleanUp(int result) {
        if (mReceiver == null) {
            Log.e(TAG, "Receiver is null");
            return;
        }

    final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
        @Override
        public void onStartedGoingToSleep() {
            if (mDialogShowing) {
                if (DEBUG) Log.d(TAG, "User canceled due to screen off");
                mHandler.obtainMessage(MSG_USER_CANCELED).sendToTarget();
        try {
            mReceiver.onDialogDismissed(result);
        } catch (RemoteException e) {
            Log.w(TAG, "Remote exception", e);
        }
        onDialogDismissed();
    }
    };

    @Override
    public void start() {
@@ -154,8 +110,6 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
                || pm.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
            getComponent(CommandQueue.class).addCallback(this);
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
            mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
            mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
        }
    }

@@ -166,18 +120,19 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
            Log.d(TAG, "showBiometricDialog, type: " + type
                    + ", requireConfirmation: " + requireConfirmation);
        }
        // Remove these messages as they are part of the previous client
        mHandler.removeMessages(MSG_BIOMETRIC_ERROR);
        mHandler.removeMessages(MSG_BIOMETRIC_HELP);
        mHandler.removeMessages(MSG_BIOMETRIC_AUTHENTICATED);
        mHandler.removeMessages(MSG_HIDE_DIALOG);
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = bundle;
        args.arg2 = receiver;
        args.argi1 = type;
        args.arg3 = requireConfirmation;
        args.argi2 = userId;
        mHandler.obtainMessage(MSG_SHOW_DIALOG, args).sendToTarget();

        boolean skipAnimation = false;
        if (mCurrentDialog != null) {
            Log.w(TAG, "mCurrentDialog: " + mCurrentDialog);
            skipAnimation = true;
        }
        showDialog(args, skipAnimation /* skipAnimation */, null /* savedState */);
    }

    @Override
@@ -185,185 +140,106 @@ public class BiometricDialogImpl extends SystemUI implements CommandQueue.Callba
        if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: " + authenticated
                + " reason: " + failureReason);

        SomeArgs args = SomeArgs.obtain();
        args.arg1 = authenticated;
        args.arg2 = failureReason;
        mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget();
        if (authenticated) {
            mCurrentDialog.onAuthenticationSucceeded();
        } else {
            mCurrentDialog.onAuthenticationFailed(failureReason);
        }
    }

    @Override
    public void onBiometricHelp(String message) {
        if (DEBUG) Log.d(TAG, "onBiometricHelp: " + message);
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = message;
        mHandler.obtainMessage(MSG_BIOMETRIC_HELP, args).sendToTarget();

        mCurrentDialog.onHelp(message);
    }

    @Override
    public void onBiometricError(String error) {
        if (DEBUG) Log.d(TAG, "onBiometricError: " + error);
        mHandler.obtainMessage(MSG_BIOMETRIC_ERROR, error).sendToTarget();
        mCurrentDialog.onError(error);
    }

    @Override
    public void hideBiometricDialog() {
        if (DEBUG) Log.d(TAG, "hideBiometricDialog");
        mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget();

        // TODO: I think we need to remove this interface
        mCurrentDialog.dismissWithoutCallback(true /* animate */);
    }

    private void handleShowDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
    private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
        mCurrentDialogArgs = args;
        final int type = args.argi1;

        // Create a new dialog but do not replace the current one yet.
        BiometricDialogView newDialog;
        if (type == BiometricAuthenticator.TYPE_FINGERPRINT) {
            newDialog = new FingerprintDialogView(mContext, mCallback);
        } else if (type == BiometricAuthenticator.TYPE_FACE) {
            newDialog = new FaceDialogView(mContext, mCallback);
        } else {
        final BiometricDialog newDialog = buildDialog(
                (Bundle) args.arg1 /* bundle */,
                (boolean) args.arg3 /* requireConfirmation */,
                args.argi2 /* userId */,
                type);

        if (newDialog == null) {
            Log.e(TAG, "Unsupported type: " + type);
            return;
        }

        if (DEBUG) Log.d(TAG, "handleShowDialog, "
        if (DEBUG) {
            Log.d(TAG, "showDialog, "
                    + " savedState: " + savedState
                    + " mCurrentDialog: " + mCurrentDialog
                    + " newDialog: " + newDialog
                    + " type: " + type);
        }

        if (savedState != null) {
            // SavedState is only non-null if it's from onConfigurationChanged. Restore the state
            // even though it may be removed / re-created again
            newDialog.restoreState(savedState);
        } else if (mCurrentDialog != null && mDialogShowing) {
        } else if (mCurrentDialog != null) {
            // If somehow we're asked to show a dialog, the old one doesn't need to be animated
            // away. This can happen if the app cancels and re-starts auth during configuration
            // change. This is ugly because we also have to do things on onConfigurationChanged
            // here.
            mCurrentDialog.forceRemove();
            mCurrentDialog.dismissWithoutCallback(false /* animate */);
        }

        mReceiver = (IBiometricServiceReceiverInternal) args.arg2;
        newDialog.setBundle((Bundle) args.arg1);
        newDialog.setRequireConfirmation((boolean) args.arg3);
        newDialog.setUserId(args.argi2);
        newDialog.setSkipIntro(skipAnimation);
        mCurrentDialog = newDialog;
        mWindowManager.addView(mCurrentDialog, mCurrentDialog.getLayoutParams());
        mDialogShowing = true;
    }

    private void handleBiometricAuthenticated(boolean authenticated, String failureReason) {
        if (DEBUG) Log.d(TAG, "handleBiometricAuthenticated: " + authenticated);

        if (authenticated) {
            mCurrentDialog.announceForAccessibility(
                    mContext.getResources()
                            .getText(mCurrentDialog.getAuthenticatedAccessibilityResourceId()));
            if (mCurrentDialog.requiresConfirmation()) {
                mCurrentDialog.updateState(BiometricDialogView.STATE_PENDING_CONFIRMATION);
            } else {
                mCurrentDialog.updateState(BiometricDialogView.STATE_AUTHENTICATED);
                mHandler.postDelayed(() -> {
                    handleHideDialog(false /* userCanceled */);
                }, mCurrentDialog.getDelayAfterAuthenticatedDurationMs());
            }
        } else {
            mCurrentDialog.onAuthenticationFailed(failureReason);
        }
        mCurrentDialog.show(mWindowManager, skipAnimation);
    }

    private void handleBiometricHelp(String message) {
        if (DEBUG) Log.d(TAG, "handleBiometricHelp: " + message);
        mCurrentDialog.onHelpReceived(message);
    }

    private void handleBiometricError(String error) {
        if (DEBUG) Log.d(TAG, "handleBiometricError: " + error);
        if (!mDialogShowing) {
            if (DEBUG) Log.d(TAG, "Dialog already dismissed");
            return;
        }
        mCurrentDialog.onErrorReceived(error);
    }

    private void handleHideDialog(boolean userCanceled) {
        if (DEBUG) Log.d(TAG, "handleHideDialog, userCanceled: " + userCanceled);
        if (!mDialogShowing) {
            // This can happen if there's a race and we get called from both
            // onAuthenticated and onError, etc.
            Log.w(TAG, "Dialog already dismissed, userCanceled: " + userCanceled);
            return;
        }
        if (userCanceled) {
            try {
                mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
            } catch (RemoteException e) {
                Log.e(TAG, "RemoteException when hiding dialog", e);
            }
    private void onDialogDismissed() {
        if (DEBUG) Log.d(TAG, "onDialogDismissed");
        if (mCurrentDialog == null) {
            Log.w(TAG, "Dialog already dismissed");
        }
        mReceiver = null;
        mDialogShowing = false;
        mCurrentDialog.startDismiss();
    }

    private void handleButtonNegative() {
        if (mReceiver == null) {
            Log.e(TAG, "Receiver is null");
            return;
        }
        try {
            mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception when handling negative button", e);
        }
        handleHideDialog(false /* userCanceled */);
    }

    private void handleButtonPositive() {
        if (mReceiver == null) {
            Log.e(TAG, "Receiver is null");
            return;
        }
        try {
            mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_POSITIVE);
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception when handling positive button", e);
        }
        handleHideDialog(false /* userCanceled */);
    }

    private void handleUserCanceled() {
        handleHideDialog(true /* userCanceled */);
    }

    private void handleTryAgainPressed() {
        try {
            mReceiver.onTryAgainPressed();
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException when handling try again", e);
        }
        mCurrentDialog = null;
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        final boolean wasShowing = mDialogShowing;

        // Save the state of the current dialog (buttons showing, etc)
        final Bundle savedState = new Bundle();
        if (mCurrentDialog != null) {
            final Bundle savedState = new Bundle();
            mCurrentDialog.onSaveState(savedState);
        }
            mCurrentDialog.dismissWithoutCallback(false /* animate */);
            mCurrentDialog = null;

        if (mDialogShowing) {
            mCurrentDialog.forceRemove();
            mDialogShowing = false;
            showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
        }

        if (wasShowing) {
            handleShowDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
    }

    protected BiometricDialog buildDialog(Bundle biometricPromptBundle,
            boolean requireConfirmation, int userId, int type) {
        return new BiometricDialogView.Builder(mContext)
                .setCallback(this)
                .setBiometricPromptBundle(biometricPromptBundle)
                .setRequireConfirmation(requireConfirmation)
                .setUserId(userId)
                .build(type);
    }
}
+9 −18
Original line number Diff line number Diff line
@@ -21,31 +21,22 @@ package com.android.systemui.biometrics;
 * FingerprintDialogImpl) and passed into their views (e.g. FingerprintDialogView).
 */
public interface DialogViewCallback {
    /**
     * Invoked when the user cancels authentication by tapping outside the prompt, etc. The dialog
     * should be dismissed.
     */
    void onUserCanceled();

    /**
     * Invoked when an error is shown. The dialog should be dismissed after a set amount of time.
     */
    void onErrorShown();
    int DISMISSED_USER_CANCELED = 1;
    int DISMISSED_BUTTON_NEGATIVE = 2;
    int DISMISSED_BUTTON_POSITIVE = 3;

    /**
     * Invoked when the negative button is pressed. The client should be notified and the dialog
     * should be dismissed.
     */
    void onNegativePressed();
    int DISMISSED_AUTHENTICATED = 4;
    int DISMISSED_ERROR = 5;

    /**
     * Invoked when the positive button is pressed. The client should be notified and the dialog
     * should be dismissed.
     * Invoked when the dialog is dismissed
     * @param reason
     */
    void onPositivePressed();
    void onDismissed(int reason);

    /**
     * Invoked when the "try again" button is pressed.
     * Invoked when the "try again" button is clicked
     */
    void onTryAgainPressed();
}
+218 −106

File changed and moved.

Preview size limit exceeded, changes collapsed.

Loading