Add enrollment sync command for virtual sensors.

Bug: 228638448
Test: atest  FingerprintInternalCleanupClientTest
Test: manual (with virtual HAL)
Change-Id: I0d011b3485e4a95425f232c24c0864cdcfad125c
This commit is contained in:
Joe Bolinger
2022-04-12 16:14:37 -07:00
parent c864aa7dfd
commit af3b1b800e
12 changed files with 352 additions and 3 deletions

View File

@@ -9658,6 +9658,13 @@ public final class Settings {
public static final String BIOMETRIC_DEBUG_ENABLED =
"biometric_debug_enabled";
/**
* Whether or not virtual sensors are enabled.
* @hide
*/
@Readable
public static final String BIOMETRIC_VIRTUAL_ENABLED = "biometric_virtual_enabled";
/**
* Whether or not biometric is allowed on Keyguard.
* @hide

View File

@@ -53,6 +53,7 @@ import android.hardware.biometrics.SensorProperties;
import android.hardware.biometrics.SensorPropertiesInternal;
import android.os.Binder;
import android.os.Build;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
@@ -87,6 +88,13 @@ public class Utils {
return true;
}
/** If virtualized biometrics are supported (requires debug build). */
public static boolean isVirtualEnabled(Context context) {
return Build.isDebuggable()
&& Settings.Secure.getIntForUser(context.getContentResolver(),
Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 0, UserHandle.USER_CURRENT) == 1;
}
/**
* Combines {@link PromptInfo#setDeviceCredentialAllowed(boolean)} with
* {@link PromptInfo#setAuthenticators(int)}, as the former is not flexible enough.
@@ -374,6 +382,15 @@ public class Utils {
return false;
}
/** Same as checkPermission but also allows shell. */
public static void checkPermissionOrShell(Context context, String permission) {
if (Binder.getCallingUid() == Process.SHELL_UID) {
return;
}
checkPermission(context, permission);
}
public static void checkPermission(Context context, String permission) {
context.enforceCallingOrSelfPermission(permission,
"Must have " + permission + " permission.");

View File

@@ -19,9 +19,11 @@ package com.android.server.biometrics.sensors;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator;
import android.os.Build;
import android.os.IBinder;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.BiometricsProto;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
@@ -65,6 +67,7 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide
private final List<S> mEnrolledList;
private final boolean mHasEnrollmentsBeforeStarting;
private BaseClientMonitor mCurrentTask;
private boolean mFavorHalEnrollments = false;
private final ClientMonitorCallback mEnumerateCallback = new ClientMonitorCallback() {
@Override
@@ -87,7 +90,21 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide
// InternalEnumerateClient. Finish this client.
mCallback.onClientFinished(InternalCleanupClient.this, success);
} else {
startCleanupUnknownHalTemplates();
if (mFavorHalEnrollments && Build.isDebuggable()) {
// on debug builds, optionally allow the HAL be the source of
// truth for enrollments
try {
for (UserTemplate template : mUnknownHALTemplates) {
Slog.i(TAG, "Adding unknown HAL template: "
+ template.mIdentifier.getBiometricId());
onAddUnknownTemplate(template.mUserId, template.mIdentifier);
}
} finally {
mCallback.onClientFinished(InternalCleanupClient.this, success);
}
} else {
startCleanupUnknownHalTemplates();
}
}
}
};
@@ -197,8 +214,27 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide
((EnumerateConsumer) mCurrentTask).onEnumerationResult(identifier, remaining);
}
/** When set unknown templates in the HAL will be added instead of deleted. */
public void setFavorHalEnrollments() {
mFavorHalEnrollments = true;
}
/** Called when an unknown template is found and setFavorHalEnrollments was requested. */
protected void onAddUnknownTemplate(int userId,
@NonNull BiometricAuthenticator.Identifier identifier) {}
@Override
public int getProtoEnum() {
return BiometricsProto.CM_INTERNAL_CLEANUP;
}
@VisibleForTesting
public InternalEnumerateClient<T> getCurrentEnumerateClient() {
return (InternalEnumerateClient<T>) mCurrentTask;
}
@VisibleForTesting
public RemovalClient<S, T> getCurrentRemoveClient() {
return (RemovalClient<S, T>) mCurrentTask;
}
}

View File

@@ -50,8 +50,6 @@ public abstract class RemovalClient<S extends BiometricAuthenticator.Identifier,
@NonNull Map<Integer, Long> authenticatorIds) {
super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId,
logger, biometricContext);
//, BiometricsProtoEnums.ACTION_REMOVE,
// BiometricsProtoEnums.CLIENT_UNKNOWN);
mBiometricUtils = utils;
mAuthenticatorIds = authenticatorIds;
mHasEnrollmentsBeforeStarting = !utils.getBiometricsForUser(context, userId).isEmpty();

View File

@@ -18,6 +18,7 @@ package com.android.server.biometrics.sensors.face.aidl;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.face.IFace;
import android.hardware.face.Face;
import android.os.IBinder;
@@ -28,6 +29,7 @@ import com.android.server.biometrics.sensors.BiometricUtils;
import com.android.server.biometrics.sensors.InternalCleanupClient;
import com.android.server.biometrics.sensors.InternalEnumerateClient;
import com.android.server.biometrics.sensors.RemovalClient;
import com.android.server.biometrics.sensors.face.FaceUtils;
import java.util.List;
import java.util.Map;
@@ -68,4 +70,11 @@ class FaceInternalCleanupClient extends InternalCleanupClient<Face, AidlSession>
null /* ClientMonitorCallbackConverter */, new int[] {biometricId}, userId, owner,
utils, sensorId, logger, biometricContext, authenticatorIds);
}
@Override
protected void onAddUnknownTemplate(int userId,
@NonNull BiometricAuthenticator.Identifier identifier) {
FaceUtils.getInstance(getSensorId()).addBiometricForUser(
getContext(), getTargetUserId(), (Face) identifier);
}
}

View File

@@ -32,6 +32,7 @@ import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -67,7 +68,9 @@ import android.os.Looper;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
@@ -620,6 +623,15 @@ public class FingerprintService extends SystemService {
mLockoutResetDispatcher.addCallback(callback, opPackageName);
}
@Override // Binder call
public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
@Nullable FileDescriptor err, @NonNull String[] args,
@Nullable ShellCallback callback, @NonNull ResultReceiver resultReceiver)
throws RemoteException {
(new FingerprintShellCommand(getContext(), FingerprintService.this))
.exec(this, in, out, err, args, callback, resultReceiver);
}
@Override // Binder call
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
@@ -1172,4 +1184,18 @@ public class FingerprintService extends SystemService {
}
return appOpsOk;
}
void syncEnrollmentsNow() {
Utils.checkPermissionOrShell(getContext(), MANAGE_FINGERPRINT);
if (Utils.isVirtualEnabled(getContext())) {
Slog.i(TAG, "Sync virtual enrollments");
final int userId = ActivityManager.getCurrentUser();
for (ServiceProvider provider : mServiceProviders) {
for (FingerprintSensorPropertiesInternal props : provider.getSensorProperties()) {
provider.scheduleInternalCleanup(props.sensorId, userId, null /* callback */,
true /* favorHalEnrollments */);
}
}
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2022 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.server.biometrics.sensors.fingerprint;
import android.content.Context;
import android.os.ShellCommand;
import java.io.PrintWriter;
/** Handles shell commands for {@link FingerprintService}. */
public class FingerprintShellCommand extends ShellCommand {
private final Context mContext;
private final FingerprintService mService;
public FingerprintShellCommand(Context context, FingerprintService service) {
mContext = context;
mService = service;
}
@Override
public int onCommand(String cmd) {
if (cmd == null) {
onHelp();
return 1;
}
try {
switch (cmd) {
case "help":
return doHelp();
case "sync":
return doSync();
default:
getOutPrintWriter().println("Unrecognized command: " + cmd);
}
} catch (Exception e) {
getOutPrintWriter().println("Exception: " + e);
}
return -1;
}
@Override
public void onHelp() {
PrintWriter pw = getOutPrintWriter();
pw.println("Fingerprint Service commands:");
pw.println(" help");
pw.println(" Print this help text.");
pw.println(" sync");
pw.println(" Sync enrollments now (virtualized sensors only).");
}
private int doHelp() {
onHelp();
return 0;
}
private int doSync() {
mService.syncEnrollmentsNow();
return 0;
}
}

View File

@@ -123,6 +123,9 @@ public interface ServiceProvider {
void scheduleInternalCleanup(int sensorId, int userId,
@Nullable ClientMonitorCallback callback);
void scheduleInternalCleanup(int sensorId, int userId,
@Nullable ClientMonitorCallback callback, boolean favorHalEnrollments);
boolean isHardwareDetected(int sensorId);
void rename(int sensorId, int fingerId, int userId, @NonNull String name);

View File

@@ -18,6 +18,7 @@ package com.android.server.biometrics.sensors.fingerprint.aidl;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.fingerprint.Fingerprint;
import android.os.IBinder;
@@ -72,4 +73,11 @@ class FingerprintInternalCleanupClient extends InternalCleanupClient<Fingerprint
utils, sensorId, logger.swapAction(context, BiometricsProtoEnums.ACTION_REMOVE),
biometricContext, authenticatorIds);
}
@Override
protected void onAddUnknownTemplate(int userId,
@NonNull BiometricAuthenticator.Identifier identifier) {
FingerprintUtils.getInstance(getSensorId()).addBiometricForUser(
getContext(), getTargetUserId(), (Fingerprint) identifier);
}
}

View File

@@ -516,6 +516,12 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
@Override
public void scheduleInternalCleanup(int sensorId, int userId,
@Nullable ClientMonitorCallback callback) {
scheduleInternalCleanup(sensorId, userId, callback, false /* favorHalEnrollments */);
}
@Override
public void scheduleInternalCleanup(int sensorId, int userId,
@Nullable ClientMonitorCallback callback, boolean favorHalEnrollments) {
mHandler.post(() -> {
final List<Fingerprint> enrolledList = getEnrolledFingerprints(sensorId, userId);
final FingerprintInternalCleanupClient client =
@@ -527,6 +533,9 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
mBiometricContext,
enrolledList, FingerprintUtils.getInstance(sensorId),
mSensors.get(sensorId).getAuthenticatorIds());
if (favorHalEnrollments) {
client.setFavorHalEnrollments();
}
scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(callback,
mFingerprintStateCallback));
});

View File

@@ -757,6 +757,13 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
mFingerprintStateCallback));
}
@Override
public void scheduleInternalCleanup(int sensorId, int userId,
@Nullable ClientMonitorCallback callback, boolean favorHalEnrollments) {
scheduleInternalCleanup(userId, new ClientMonitorCompositeCallback(callback,
mFingerprintStateCallback));
}
private BiometricLogger createLogger(int statsAction, int statsClient) {
return new BiometricLogger(mContext, BiometricsProtoEnums.MODALITY_FINGERPRINT,
statsAction, statsClient);

View File

@@ -0,0 +1,153 @@
/*
* Copyright (C) 2022 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.server.biometrics.sensors.fingerprint.aidl;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.fingerprint.ISession;
import android.hardware.fingerprint.Fingerprint;
import android.platform.test.annotations.Presubmit;
import android.testing.TestableContext;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Presubmit
@SmallTest
public class FingerprintInternalCleanupClientTest {
private static final int SENSOR_ID = 22;
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@Rule
public final TestableContext mContext = new TestableContext(
InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
@Mock
private AidlSession mAidlSession;
@Mock
private ISession mSession;
@Mock
private BiometricLogger mLogger;
@Mock
private BiometricContext mBiometricContext;
@Mock
private FingerprintUtils mFingerprintUtils;
@Mock
private ClientMonitorCallback mCallback;
private FingerprintInternalCleanupClient mClient;
private List<Integer> mAddedIds;
@Before
public void setup() {
when(mAidlSession.getSession()).thenReturn(mSession);
mAddedIds = new ArrayList<>();
}
@Ignore("TODO(b/229015801): verify cleanup behavior")
@Test
public void removesUnknownTemplate() throws Exception {
mClient = createClient();
final List<Fingerprint> templates = List.of(
new Fingerprint("one", 1, 1),
new Fingerprint("two", 2, 1)
);
mClient.start(mCallback);
for (int i = templates.size() - 1; i >= 0; i--) {
mClient.getCurrentEnumerateClient().onEnumerationResult(templates.get(i), i);
}
for (int i = templates.size() - 1; i >= 0; i--) {
mClient.getCurrentRemoveClient().onRemoved(templates.get(i), 0);
}
assertThat(mAddedIds).isEmpty();
final ArgumentCaptor<int[]> captor = ArgumentCaptor.forClass(int[].class);
verify(mSession, times(2)).removeEnrollments(captor.capture());
assertThat(captor.getAllValues().stream()
.flatMap(x -> Arrays.stream(x).boxed())
.collect(Collectors.toList()))
.containsExactly(1, 2);
verify(mCallback).onClientFinished(eq(mClient), eq(true));
}
@Test
public void addsUnknownTemplateWhenVirtualIsEnabled() throws Exception {
mClient = createClient();
mClient.setFavorHalEnrollments();
final List<Fingerprint> templates = List.of(
new Fingerprint("one", 1, 1),
new Fingerprint("two", 2, 1)
);
mClient.start(mCallback);
for (int i = templates.size() - 1; i >= 0; i--) {
mClient.getCurrentEnumerateClient().onEnumerationResult(templates.get(i), i);
}
assertThat(mAddedIds).containsExactly(1, 2);
verify(mSession, never()).removeEnrollments(any());
verify(mCallback).onClientFinished(eq(mClient), eq(true));
}
protected FingerprintInternalCleanupClient createClient() {
final List<Fingerprint> enrollments = new ArrayList<>();
final Map<Integer, Long> authenticatorIds = new HashMap<>();
return new FingerprintInternalCleanupClient(mContext, () -> mAidlSession, 2 /* userId */,
"the.test.owner", SENSOR_ID, mLogger, mBiometricContext, enrollments,
mFingerprintUtils, authenticatorIds) {
@Override
protected void onAddUnknownTemplate(int userId,
@NonNull BiometricAuthenticator.Identifier identifier) {
mAddedIds.add(identifier.getBiometricId());
}
};
}
}