Added sensor privacy message to biometric prompt

Bug: 201045056
Test: Verified that BiometricPrompt shows an error message if the user
is using face authentication and camera privacy is enabled.

Change-Id: Idc2140062c4aa87e0bcffb14e07336dc6cd7d955
This commit is contained in:
Joshua Mccloskey
2021-11-29 15:56:59 -08:00
parent 9d3f1be63c
commit 27db3c0af9
8 changed files with 181 additions and 25 deletions

View File

@@ -150,6 +150,12 @@ public interface BiometricConstants {
*/
int BIOMETRIC_ERROR_RE_ENROLL = 16;
/**
* The privacy setting has been enabled and will block use of the sensor.
* @hide
*/
int BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED = 18;
/**
* This constant is only used by SystemUI. It notifies SystemUI that authentication was paused
* because the authentication attempt was unsuccessful.

View File

@@ -1666,6 +1666,8 @@
<string name="face_setup_notification_title">Set up Face Unlock</string>
<!-- Contents of a notification that directs the user to set up face unlock by enrolling their face. [CHAR LIMIT=NONE] -->
<string name="face_setup_notification_content">Unlock your phone by looking at it</string>
<!-- Error message indicating that the camera privacy sensor has been turned on [CHAR LIMIT=NONE] -->
<string name="face_sensor_privacy_enabled">To use Face Unlock, turn on <b>Camera access</b> in Settings > Privacy</string>
<!-- Title of a notification that directs the user to enroll a fingerprint. [CHAR LIMIT=NONE] -->
<string name="fingerprint_setup_notification_title">Set up more ways to unlock</string>
<!-- Contents of a notification that directs the user to enroll a fingerprint. [CHAR LIMIT=NONE] -->

View File

@@ -2570,6 +2570,7 @@
<java-symbol type="string" name="face_recalibrate_notification_name" />
<java-symbol type="string" name="face_recalibrate_notification_title" />
<java-symbol type="string" name="face_recalibrate_notification_content" />
<java-symbol type="string" name="face_sensor_privacy_enabled" />
<java-symbol type="string" name="face_error_unable_to_process" />
<java-symbol type="string" name="face_error_hw_not_available" />
<java-symbol type="string" name="face_error_no_space" />

View File

@@ -31,6 +31,7 @@ import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.PointF;
import android.hardware.SensorPrivacyManager;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager.Authenticators;
@@ -89,6 +90,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
private static final String TAG = "AuthController";
private static final boolean DEBUG = true;
private static final int SENSOR_PRIVACY_DELAY = 500;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final CommandQueue mCommandQueue;
@@ -122,6 +124,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
@Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps;
@NonNull private final SparseBooleanArray mUdfpsEnrolledForUser;
private SensorPrivacyManager mSensorPrivacyManager;
private class BiometricTaskStackListener extends TaskStackListener {
@Override
@@ -492,6 +495,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
context.registerReceiver(mBroadcastReceiver, filter);
mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
}
private void updateFingerprintLocation() {
@@ -642,10 +646,16 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
final boolean isLockout = (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT)
|| (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT);
boolean isCameraPrivacyEnabled = false;
if (error == BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE
&& mSensorPrivacyManager.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA,
mCurrentDialogArgs.argi1 /* userId */)) {
isCameraPrivacyEnabled = true;
}
// TODO(b/141025588): Create separate methods for handling hard and soft errors.
final boolean isSoftError = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED
|| error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT);
|| error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT
|| isCameraPrivacyEnabled);
if (mCurrentDialog != null) {
if (mCurrentDialog.isAllowDeviceCredentials() && isLockout) {
if (DEBUG) Log.d(TAG, "onBiometricError, lockout");
@@ -655,12 +665,23 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
? mContext.getString(R.string.biometric_not_recognized)
: getErrorString(modality, error, vendorCode);
if (DEBUG) Log.d(TAG, "onBiometricError, soft error: " + errorMessage);
mCurrentDialog.onAuthenticationFailed(modality, errorMessage);
// The camera privacy error can return before the prompt initializes its state,
// causing the prompt to appear to endlessly authenticate. Add a small delay
// to stop this.
if (isCameraPrivacyEnabled) {
mHandler.postDelayed(() -> {
mCurrentDialog.onAuthenticationFailed(modality,
mContext.getString(R.string.face_sensor_privacy_enabled));
}, SENSOR_PRIVACY_DELAY);
} else {
mCurrentDialog.onAuthenticationFailed(modality, errorMessage);
}
} else {
final String errorMessage = getErrorString(modality, error, vendorCode);
if (DEBUG) Log.d(TAG, "onBiometricError, hard error: " + errorMessage);
mCurrentDialog.onError(modality, errorMessage);
}
} else {
Log.w(TAG, "onBiometricError callback but dialog is gone");
}

View File

@@ -1036,7 +1036,8 @@ public class BiometricService extends SystemService {
promptInfo.setAuthenticators(authenticators);
return PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors,
userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */);
userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */,
getContext());
}
/**
@@ -1375,7 +1376,8 @@ public class BiometricService extends SystemService {
try {
final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager,
mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo,
opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists());
opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(),
getContext());
final Pair<Integer, Integer> preAuthStatus = preAuthInfo.getPreAuthenticateStatus();
@@ -1383,8 +1385,11 @@ public class BiometricService extends SystemService {
+ "), status(" + preAuthStatus.second + "), preAuthInfo: " + preAuthInfo
+ " requestId: " + requestId + " promptInfo.isIgnoreEnrollmentState: "
+ promptInfo.isIgnoreEnrollmentState());
if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS) {
// BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED is added so that BiometricPrompt can
// be shown for this case.
if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS
|| preAuthStatus.second
== BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED) {
// If BIOMETRIC_WEAK or BIOMETRIC_STRONG are allowed, but not enrolled, but
// CREDENTIAL is requested and available, set the bundle to only request
// CREDENTIAL.

View File

@@ -26,6 +26,8 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.admin.DevicePolicyManager;
import android.app.trust.ITrustManager;
import android.content.Context;
import android.hardware.SensorPrivacyManager;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.PromptInfo;
@@ -59,6 +61,7 @@ class PreAuthInfo {
static final int CREDENTIAL_NOT_ENROLLED = 9;
static final int BIOMETRIC_LOCKOUT_TIMED = 10;
static final int BIOMETRIC_LOCKOUT_PERMANENT = 11;
static final int BIOMETRIC_SENSOR_PRIVACY_ENABLED = 12;
@IntDef({AUTHENTICATOR_OK,
BIOMETRIC_NO_HARDWARE,
BIOMETRIC_DISABLED_BY_DEVICE_POLICY,
@@ -69,7 +72,8 @@ class PreAuthInfo {
BIOMETRIC_NOT_ENABLED_FOR_APPS,
CREDENTIAL_NOT_ENROLLED,
BIOMETRIC_LOCKOUT_TIMED,
BIOMETRIC_LOCKOUT_PERMANENT})
BIOMETRIC_LOCKOUT_PERMANENT,
BIOMETRIC_SENSOR_PRIVACY_ENABLED})
@Retention(RetentionPolicy.SOURCE)
@interface AuthenticatorStatus {}
@@ -84,13 +88,15 @@ class PreAuthInfo {
final boolean credentialAvailable;
final boolean confirmationRequested;
final boolean ignoreEnrollmentState;
final int userId;
final Context context;
static PreAuthInfo create(ITrustManager trustManager,
DevicePolicyManager devicePolicyManager,
BiometricService.SettingObserver settingObserver,
List<BiometricSensor> sensors,
int userId, PromptInfo promptInfo, String opPackageName,
boolean checkDevicePolicyManager)
boolean checkDevicePolicyManager, Context context)
throws RemoteException {
final boolean confirmationRequested = promptInfo.isConfirmationRequested();
@@ -116,14 +122,22 @@ class PreAuthInfo {
devicePolicyManager, settingObserver, sensor, userId, opPackageName,
checkDevicePolicyManager, requestedStrength,
promptInfo.getAllowedSensorIds(),
promptInfo.isIgnoreEnrollmentState());
promptInfo.isIgnoreEnrollmentState(),
context);
Slog.d(TAG, "Package: " + opPackageName
+ " Sensor ID: " + sensor.id
+ " Modality: " + sensor.modality
+ " Status: " + status);
if (status == AUTHENTICATOR_OK) {
// A sensor with privacy enabled will still be eligible to
// authenticate with biometric prompt. This is so the framework can display
// a sensor privacy error message to users after briefly showing the
// Biometric Prompt.
//
// Note: if only a certain sensor is required and the privacy is enabled,
// canAuthenticate() will return false.
if (status == AUTHENTICATOR_OK || status == BIOMETRIC_SENSOR_PRIVACY_ENABLED) {
eligibleSensors.add(sensor);
} else {
ineligibleSensors.add(new Pair<>(sensor, status));
@@ -133,7 +147,7 @@ class PreAuthInfo {
return new PreAuthInfo(biometricRequested, requestedStrength, credentialRequested,
eligibleSensors, ineligibleSensors, credentialAvailable, confirmationRequested,
promptInfo.isIgnoreEnrollmentState());
promptInfo.isIgnoreEnrollmentState(), userId, context);
}
/**
@@ -149,7 +163,7 @@ class PreAuthInfo {
BiometricSensor sensor, int userId, String opPackageName,
boolean checkDevicePolicyManager, int requestedStrength,
@NonNull List<Integer> requestedSensorIds,
boolean ignoreEnrollmentState) {
boolean ignoreEnrollmentState, Context context) {
if (!requestedSensorIds.isEmpty() && !requestedSensorIds.contains(sensor.id)) {
return BIOMETRIC_NO_HARDWARE;
@@ -175,6 +189,16 @@ class PreAuthInfo {
&& !ignoreEnrollmentState) {
return BIOMETRIC_NOT_ENROLLED;
}
final SensorPrivacyManager sensorPrivacyManager = context
.getSystemService(SensorPrivacyManager.class);
if (sensorPrivacyManager != null && sensor.modality == TYPE_FACE) {
if (sensorPrivacyManager
.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId)) {
return BIOMETRIC_SENSOR_PRIVACY_ENABLED;
}
}
final @LockoutTracker.LockoutMode int lockoutMode =
sensor.impl.getLockoutModeForUser(userId);
@@ -243,7 +267,8 @@ class PreAuthInfo {
private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested,
boolean credentialRequested, List<BiometricSensor> eligibleSensors,
List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable,
boolean confirmationRequested, boolean ignoreEnrollmentState) {
boolean confirmationRequested, boolean ignoreEnrollmentState, int userId,
Context context) {
mBiometricRequested = biometricRequested;
mBiometricStrengthRequested = biometricStrengthRequested;
this.credentialRequested = credentialRequested;
@@ -253,6 +278,8 @@ class PreAuthInfo {
this.credentialAvailable = credentialAvailable;
this.confirmationRequested = confirmationRequested;
this.ignoreEnrollmentState = ignoreEnrollmentState;
this.userId = userId;
this.context = context;
}
private Pair<BiometricSensor, Integer> calculateErrorByPriority() {
@@ -280,15 +307,35 @@ class PreAuthInfo {
private Pair<Integer, Integer> getInternalStatus() {
@AuthenticatorStatus final int status;
@BiometricAuthenticator.Modality int modality = TYPE_NONE;
final SensorPrivacyManager sensorPrivacyManager = context
.getSystemService(SensorPrivacyManager.class);
boolean cameraPrivacyEnabled = false;
if (sensorPrivacyManager != null) {
cameraPrivacyEnabled = sensorPrivacyManager
.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId);
}
if (mBiometricRequested && credentialRequested) {
if (credentialAvailable || !eligibleSensors.isEmpty()) {
status = AUTHENTICATOR_OK;
if (credentialAvailable) {
modality |= TYPE_CREDENTIAL;
}
for (BiometricSensor sensor : eligibleSensors) {
modality |= sensor.modality;
}
if (credentialAvailable) {
modality |= TYPE_CREDENTIAL;
status = AUTHENTICATOR_OK;
} else if (modality == TYPE_FACE && cameraPrivacyEnabled) {
// If the only modality requested is face, credential is unavailable,
// and the face sensor privacy is enabled then return
// BIOMETRIC_SENSOR_PRIVACY_ENABLED.
//
// Note: This sensor will still be eligible for calls to authenticate.
status = BIOMETRIC_SENSOR_PRIVACY_ENABLED;
} else {
status = AUTHENTICATOR_OK;
}
} else {
// Pick the first sensor error if it exists
if (!ineligibleSensors.isEmpty()) {
@@ -302,10 +349,18 @@ class PreAuthInfo {
}
} else if (mBiometricRequested) {
if (!eligibleSensors.isEmpty()) {
status = AUTHENTICATOR_OK;
for (BiometricSensor sensor : eligibleSensors) {
modality |= sensor.modality;
}
for (BiometricSensor sensor : eligibleSensors) {
modality |= sensor.modality;
}
if (modality == TYPE_FACE && cameraPrivacyEnabled) {
// If the only modality requested is face and the privacy is enabled
// then return BIOMETRIC_SENSOR_PRIVACY_ENABLED.
//
// Note: This sensor will still be eligible for calls to authenticate.
status = BIOMETRIC_SENSOR_PRIVACY_ENABLED;
} else {
status = AUTHENTICATOR_OK;
}
} else {
// Pick the first sensor error if it exists
if (!ineligibleSensors.isEmpty()) {
@@ -326,9 +381,9 @@ class PreAuthInfo {
Slog.e(TAG, "No authenticators requested");
status = BIOMETRIC_NO_HARDWARE;
}
Slog.d(TAG, "getCanAuthenticateInternal Modality: " + modality
+ " AuthenticatorStatus: " + status);
return new Pair<>(modality, status);
}
@@ -362,6 +417,7 @@ class PreAuthInfo {
case CREDENTIAL_NOT_ENROLLED:
case BIOMETRIC_LOCKOUT_TIMED:
case BIOMETRIC_LOCKOUT_PERMANENT:
case BIOMETRIC_SENSOR_PRIVACY_ENABLED:
break;
case BIOMETRIC_DISABLED_BY_DEVICE_POLICY:

View File

@@ -33,6 +33,7 @@ import static com.android.server.biometrics.PreAuthInfo.BIOMETRIC_LOCKOUT_TIMED;
import static com.android.server.biometrics.PreAuthInfo.BIOMETRIC_NOT_ENABLED_FOR_APPS;
import static com.android.server.biometrics.PreAuthInfo.BIOMETRIC_NOT_ENROLLED;
import static com.android.server.biometrics.PreAuthInfo.BIOMETRIC_NO_HARDWARE;
import static com.android.server.biometrics.PreAuthInfo.BIOMETRIC_SENSOR_PRIVACY_ENABLED;
import static com.android.server.biometrics.PreAuthInfo.CREDENTIAL_NOT_ENROLLED;
import android.annotation.NonNull;
@@ -278,6 +279,9 @@ public class Utils {
case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT:
biometricManagerCode = BiometricManager.BIOMETRIC_SUCCESS;
break;
case BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED:
biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
break;
default:
Slog.e(BiometricService.TAG, "Unhandled result code: " + biometricConstantsCode);
biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
@@ -337,7 +341,8 @@ public class Utils {
case BIOMETRIC_LOCKOUT_PERMANENT:
return BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
case BIOMETRIC_SENSOR_PRIVACY_ENABLED:
return BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED;
case BIOMETRIC_DISABLED_BY_DEVICE_POLICY:
case BIOMETRIC_HARDWARE_NOT_DETECTED:
case BIOMETRIC_NOT_ENABLED_FOR_APPS:

View File

@@ -302,6 +302,65 @@ public class AuthSessionTest {
testInvokesCancel(session -> session.onDialogDismissed(DISMISSED_REASON_NEGATIVE, null));
}
// TODO (b/208484275) : Enable these tests
// @Test
// public void testPreAuth_canAuthAndPrivacyDisabled() throws Exception {
// SensorPrivacyManager manager = ExtendedMockito.mock(SensorPrivacyManager.class);
// when(manager
// .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, anyInt()))
// .thenReturn(false);
// when(mContext.getSystemService(SensorPrivacyManager.class))
// .thenReturn(manager);
// setupFace(1 /* id */, false /* confirmationAlwaysRequired */,
// mock(IBiometricAuthenticator.class));
// final PromptInfo promptInfo = createPromptInfo(Authenticators.BIOMETRIC_STRONG);
// final PreAuthInfo preAuthInfo = createPreAuthInfo(mSensors, 0, promptInfo, false);
// assertEquals(BiometricManager.BIOMETRIC_SUCCESS, preAuthInfo.getCanAuthenticateResult());
// for (BiometricSensor sensor : preAuthInfo.eligibleSensors) {
// assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState());
// }
// }
// @Test
// public void testPreAuth_cannotAuthAndPrivacyEnabled() throws Exception {
// SensorPrivacyManager manager = ExtendedMockito.mock(SensorPrivacyManager.class);
// when(manager
// .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, anyInt()))
// .thenReturn(true);
// when(mContext.getSystemService(SensorPrivacyManager.class))
// .thenReturn(manager);
// setupFace(1 /* id */, false /* confirmationAlwaysRequired */,
// mock(IBiometricAuthenticator.class));
// final PromptInfo promptInfo = createPromptInfo(Authenticators.BIOMETRIC_STRONG);
// final PreAuthInfo preAuthInfo = createPreAuthInfo(mSensors, 0, promptInfo, false);
// assertEquals(BiometricManager.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED,
// preAuthInfo.getCanAuthenticateResult());
// // Even though canAuth returns privacy enabled, we should still be able to authenticate.
// for (BiometricSensor sensor : preAuthInfo.eligibleSensors) {
// assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState());
// }
// }
// @Test
// public void testPreAuth_canAuthAndPrivacyEnabledCredentialEnabled() throws Exception {
// SensorPrivacyManager manager = ExtendedMockito.mock(SensorPrivacyManager.class);
// when(manager
// .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, anyInt()))
// .thenReturn(true);
// when(mContext.getSystemService(SensorPrivacyManager.class))
// .thenReturn(manager);
// setupFace(1 /* id */, false /* confirmationAlwaysRequired */,
// mock(IBiometricAuthenticator.class));
// final PromptInfo promptInfo =
// createPromptInfo(Authenticators.BIOMETRIC_STRONG
// | Authenticators. DEVICE_CREDENTIAL);
// final PreAuthInfo preAuthInfo = createPreAuthInfo(mSensors, 0, promptInfo, false);
// assertEquals(BiometricManager.BIOMETRIC_SUCCESS, preAuthInfo.getCanAuthenticateResult());
// for (BiometricSensor sensor : preAuthInfo.eligibleSensors) {
// assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState());
// }
// }
private void testInvokesCancel(Consumer<AuthSession> sessionConsumer) throws RemoteException {
final IBiometricAuthenticator faceAuthenticator = mock(IBiometricAuthenticator.class);
@@ -331,7 +390,8 @@ public class AuthSessionTest {
userId,
promptInfo,
TEST_PACKAGE,
checkDevicePolicyManager);
checkDevicePolicyManager,
mContext);
}
private AuthSession createAuthSession(List<BiometricSensor> sensors,