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

Commit 6ae23ed9 authored by Alex Stetson's avatar Alex Stetson
Browse files

Support biometric prompt for visible background users

Allow biometric prompt to appear on secondary displays when being
launched for visible background users.

Bug: 365998136
Test: atest com.android.systemui.biometrics.AuthControllerTest
Flag: NONE bugfix
Change-Id: I3a8052c13e810a4301e1e90968664d22a6d38372
parent 6c9e8045
Loading
Loading
Loading
Loading
+9 −1
Original line number Diff line number Diff line
@@ -138,6 +138,13 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
     */
    public static final int DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS = 8;

    /**
     * Dialog dismissal due to the system being unable to retrieve a WindowManager instance required
     * to show the dialog.
     * @hide
     */
    public static final int DISMISSED_REASON_ERROR_NO_WM = 9;

    /**
     * @hide
     */
@@ -148,7 +155,8 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
            DISMISSED_REASON_ERROR,
            DISMISSED_REASON_SERVER_REQUESTED,
            DISMISSED_REASON_CREDENTIAL_CONFIRMED,
            DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS})
            DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS,
            DISMISSED_REASON_ERROR_NO_WM})
    @Retention(RetentionPolicy.SOURCE)
    public @interface DismissedReason {}

+127 −5
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.biometrics;

import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.view.Display.INVALID_DISPLAY;

import static com.google.common.truth.Truth.assertThat;

@@ -68,10 +69,12 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
import android.view.WindowManager;
@@ -210,6 +213,7 @@ public class AuthControllerTest extends SysuiTestCase {
                .thenReturn(true);
        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
                .thenReturn(true);
        when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(false);

        when(mDialog1.getOpPackageName()).thenReturn("Dialog1");
        when(mDialog2.getOpPackageName()).thenReturn("Dialog2");
@@ -462,7 +466,7 @@ public class AuthControllerTest extends SysuiTestCase {
    @Test
    public void testShowInvoked_whenSystemRequested() {
        showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
        verify(mDialog1).show(any());
        verify(mDialog1).show(mWindowManager);
    }

    @Test
@@ -679,7 +683,7 @@ public class AuthControllerTest extends SysuiTestCase {
        // 2) Client cancels authentication

        showDialog(new int[0] /* sensorIds */, true /* credentialAllowed */);
        verify(mDialog1).show(any());
        verify(mDialog1).show(mWindowManager);

        final byte[] credentialAttestation = generateRandomHAT();

@@ -695,7 +699,7 @@ public class AuthControllerTest extends SysuiTestCase {
    @Test
    public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() {
        showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
        verify(mDialog1).show(any());
        verify(mDialog1).show(mWindowManager);

        showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);

@@ -703,7 +707,7 @@ public class AuthControllerTest extends SysuiTestCase {
        verify(mDialog1).dismissWithoutCallback(eq(false) /* animate */);

        // Second dialog should be shown without animation
        verify(mDialog2).show(any());
        verify(mDialog2).show(mWindowManager);
    }

    @Test
@@ -990,13 +994,97 @@ public class AuthControllerTest extends SysuiTestCase {
        verify(mDialog1, never()).show(any());
    }

    @Test
    public void testShowDialog_visibleBackgroundUser() {
        int backgroundUserId = 1001;
        int backgroundDisplayId = 1001;
        when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
        WindowManager wm = mockBackgroundUser(backgroundUserId, backgroundDisplayId,
                true /* isVisible */, true /* hasUserManager */, true /* hasDisplay */);

        showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
                false /* credentialAllowed */);

        verify(mDialog1).show(wm);
    }

    @Test
    public void testShowDialog_invisibleBackgroundUser_defaultWM() {
        int backgroundUserId = 1001;
        when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
        mockBackgroundUser(backgroundUserId, INVALID_DISPLAY,
                false /* isVisible */, true /* hasUserManager */, true /* hasDisplay */);

        showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
                false /* credentialAllowed */);

        verify(mDialog1).show(mWindowManager);
    }

    @Test
    public void testShowDialog_visibleBackgroundUser_noUserManager_dismissError()
            throws RemoteException {
        int backgroundUserId = 1001;
        int backgroundDisplayId = 1001;
        when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
        mockBackgroundUser(backgroundUserId, backgroundDisplayId,
                true /* isVisible */, false /* hasUserManager */, true /* hasDisplay */);

        showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
                false /* credentialAllowed */);

        verify(mDialog1, never()).show(any());
        verify(mReceiver).onDialogDismissed(
                eq(BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM),
                eq(null) /* credentialAttestation */);
    }

    @Test
    public void testShowDialog_visibleBackgroundUser_invalidDisplayId_dismissError()
            throws RemoteException {
        int backgroundUserId = 1001;
        when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
        mockBackgroundUser(backgroundUserId, INVALID_DISPLAY,
                true /* isVisible */, true /* hasUserManager */, false /* hasDisplay */);

        showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
                false /* credentialAllowed */);

        verify(mDialog1, never()).show(any());
        verify(mReceiver).onDialogDismissed(
                eq(BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM),
                eq(null) /* credentialAttestation */);
    }

    @Test
    public void testShowDialog_visibleBackgroundUser_invalidDisplay_dismissError()
            throws RemoteException {
        int backgroundUserId = 1001;
        int backgroundDisplayId = 1001;
        when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
        mockBackgroundUser(backgroundUserId, backgroundDisplayId,
                true /* isVisible */, true /* hasUserManager */, false /* hasDisplay */);

        showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
                false /* credentialAllowed */);

        verify(mDialog1, never()).show(any());
        verify(mReceiver).onDialogDismissed(
                eq(BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM),
                eq(null) /* credentialAttestation */);
    }

    private void showDialog(int[] sensorIds, boolean credentialAllowed) {
        showDialog(sensorIds, 0 /* userId */, credentialAllowed);
    }

    private void showDialog(int[] sensorIds, int userId, boolean credentialAllowed) {
        mAuthController.showAuthenticationDialog(createTestPromptInfo(),
                mReceiver /* receiver */,
                sensorIds,
                credentialAllowed,
                true /* requireConfirmation */,
                0 /* userId */,
                userId /* userId */,
                0 /* operationId */,
                "testPackage",
                REQUEST_ID);
@@ -1059,6 +1147,40 @@ public class AuthControllerTest extends SysuiTestCase {
        assertTrue(mAuthController.isFaceAuthEnrolled(userId));
    }

    /**
     * Create mocks related to visible background users.
     *
     * @param userId the user id of the background user to mock
     * @param displayId display id of the background user
     * @param isVisible whether the background user is a visible background user or not
     * @param hasUserManager simulate whether the background user's context will return a mock
     *                       UserManager instance or null
     * @param hasDisplay simulate whether the background user's context will return a mock Display
     *                   instance or null
     * @return mock WindowManager instance associated with the background user's display context
     */
    private WindowManager mockBackgroundUser(int userId, int displayId, boolean isVisible,
            boolean hasUserManager, boolean hasDisplay) {
        Context mockUserContext = mock(Context.class);
        Context mockDisplayContext = mock(Context.class);
        UserManager mockUserManager = mock(UserManager.class);
        Display mockDisplay = mock(Display.class);
        WindowManager mockDisplayWM = mock(WindowManager.class);
        doReturn(mockUserContext).when(mContextSpy).createContextAsUser(eq(UserHandle.of(userId)),
                anyInt());
        if (hasUserManager) {
            when(mockUserContext.getSystemService(UserManager.class)).thenReturn(mockUserManager);
        }
        when(mockUserManager.isUserVisible()).thenReturn(isVisible);
        when(mockUserManager.getMainDisplayIdAssignedToUser()).thenReturn(displayId);
        if (hasDisplay) {
            when(mDisplayManager.getDisplay(displayId)).thenReturn(mockDisplay);
        }
        doReturn(mockDisplayContext).when(mContextSpy).createDisplayContext(mockDisplay);
        when(mockDisplayContext.getSystemService(WindowManager.class)).thenReturn(mockDisplayWM);
        return mockDisplayWM;
    }

    private final class TestableAuthController extends AuthController {
        private int mBuildCount = 0;

+45 −24
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
import static android.view.Display.INVALID_DISPLAY;

import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy;

@@ -54,6 +55,7 @@ import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import android.util.RotationUtils;
@@ -211,9 +213,13 @@ public class AuthController implements
        }
    };

    private void closeDialog(String reason) {
    private void closeDialog(String reasonString) {
        closeDialog(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, reasonString);
    }

    private void closeDialog(@DismissedReason int reason, String reasonString) {
        if (isShowing()) {
            Log.i(TAG, "Close BP, reason :" + reason);
            Log.i(TAG, "Close BP, reason :" + reasonString);
            mCurrentDialog.dismissWithoutCallback(true /* animate */);
            mCurrentDialog = null;

@@ -223,8 +229,7 @@ public class AuthController implements

            try {
                if (mReceiver != null) {
                    mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
                            null /* credentialAttestation */);
                    mReceiver.onDialogDismissed(reason, null /* credentialAttestation */);
                    mReceiver = null;
                }
            } catch (RemoteException e) {
@@ -251,25 +256,7 @@ public class AuthController implements

    private void cancelIfOwnerIsNotInForeground() {
        mExecution.assertIsMainThread();
        if (mCurrentDialog != null) {
            try {
                mCurrentDialog.dismissWithoutCallback(true /* animate */);
                mCurrentDialog = null;

                for (Callback cb : mCallbacks) {
                    cb.onBiometricPromptDismissed();
                }

                if (mReceiver != null) {
                    mReceiver.onDialogDismissed(
                            BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
                            null /* credentialAttestation */);
                    mReceiver = null;
                }
            } catch (RemoteException e) {
                Log.e(TAG, "Remote exception", e);
            }
        }
        closeDialog("owner not in foreground");
    }

    /**
@@ -1271,8 +1258,42 @@ public class AuthController implements
        if (!promptInfo.isAllowBackgroundAuthentication() && isOwnerInBackground()) {
            cancelIfOwnerIsNotInForeground();
        } else {
            mCurrentDialog.show(mWindowManager);
            WindowManager wm = getWindowManagerForUser(userId);
            if (wm != null) {
                mCurrentDialog.show(wm);
            } else {
                closeDialog(BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM,
                        "unable to get WM instance for user");
            }
        }
    }

    @Nullable
    private WindowManager getWindowManagerForUser(int userId) {
        if (!mUserManager.isVisibleBackgroundUsersSupported()) {
            return mWindowManager;
        }
        UserManager um = mContext.createContextAsUser(UserHandle.of(userId),
                0 /* flags */).getSystemService(UserManager.class);
        if (um == null) {
            Log.e(TAG, "unable to get UserManager for user=" + userId);
            return null;
        }
        if (!um.isUserVisible()) {
            // not visible user - use default window manager
            return mWindowManager;
        }
        int displayId = um.getMainDisplayIdAssignedToUser();
        if (displayId == INVALID_DISPLAY) {
            Log.e(TAG, "unable to get display assigned to user=" + userId);
            return null;
        }
        Display display = mDisplayManager.getDisplay(displayId);
        if (display == null) {
            Log.e(TAG, "unable to get Display for user=" + userId);
            return null;
        }
        return mContext.createDisplayContext(display).getSystemService(WindowManager.class);
    }

    private void onDialogDismissed(@DismissedReason int reason) {
+8 −0
Original line number Diff line number Diff line
@@ -879,6 +879,14 @@ public final class AuthSession implements IBinder.DeathRecipient {
                    );
                    break;

                case BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM:
                    mClientReceiver.onError(
                            getEligibleModalities(),
                            BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE,
                            0 /* vendorCode */
                    );
                    break;

                default:
                    Slog.w(TAG, "Unhandled reason: " + reason);
                    break;