diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index a51ee4865b1df..f790195f3b411 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -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 diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java index 0e2582c23b86d..e1ef277a2b8a0 100644 --- a/services/core/java/com/android/server/biometrics/Utils.java +++ b/services/core/java/com/android/server/biometrics/Utils.java @@ -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."); diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java index 57ea812dbb3a7..ded9c8de4dba1 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java @@ -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 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 getCurrentEnumerateClient() { + return (InternalEnumerateClient) mCurrentTask; + } + + @VisibleForTesting + public RemovalClient getCurrentRemoveClient() { + return (RemovalClient) mCurrentTask; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java index e0d519469e32d..45ffa23dc66a3 100644 --- a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java @@ -50,8 +50,6 @@ public abstract class RemovalClient 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(); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java index 54f2033b363ad..b0b23faa9aa56 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java @@ -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 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); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index 5727ffc468dfc..f39a5056f9e4d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -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 */); + } + } + } + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintShellCommand.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintShellCommand.java new file mode 100644 index 0000000000000..636413f75cf58 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintShellCommand.java @@ -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; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java index 9cdbdc9158fb6..4dfc73872014f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java @@ -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); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java index 09bdd6de49f04..c315ccf8dea21 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java @@ -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 { final List 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)); }); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java index 1d2a3655021c0..895fb4671337e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java @@ -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); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java new file mode 100644 index 0000000000000..282c782e752aa --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java @@ -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 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 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 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 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 enrollments = new ArrayList<>(); + final Map 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()); + } + }; + } +}