summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Alex Stetson <alexstetson@google.com> 2024-09-24 10:53:32 -0700
committer Alex Stetson <alexstetson@google.com> 2024-10-02 15:22:43 -0700
commit6ae23ed901cb49ccace53bcd0293ffd5d99c86d9 (patch)
tree00d5e21eac501a283c1da7c7bd4c4d8d0459920c
parent6c9e8045e11a065fffaba7a3671dbf2c9b025e7c (diff)
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
-rw-r--r--core/java/android/hardware/biometrics/BiometricPrompt.java10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java132
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java69
-rw-r--r--services/core/java/com/android/server/biometrics/AuthSession.java8
4 files changed, 189 insertions, 30 deletions
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index b11961cc2b21..e3fdd267623e 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -139,6 +139,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
*/
@IntDef({DISMISSED_REASON_BIOMETRIC_CONFIRMED,
@@ -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 {}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 65825b2444af..2dcbdc80f695 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -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;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index b39aae94ed4e..a5bd559dcbf2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -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) {
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index abfbddc18e24..3afecf1d8bbf 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -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;