From aad5f295b0f29209ae13a9bb77e092aad5ea51f4 Mon Sep 17 00:00:00 2001 From: Jan Tomljanovic Date: Thu, 24 Feb 2022 12:42:21 +0000 Subject: [PATCH] Trigger SafetyCenter update on each Face setting change. Test: atest SettingsUnitTests Bug: 215518850 Change-Id: I7a55fd5368c9aad5329448732125d4e43688eced --- .../biometrics/face/FaceEnrollSidecar.java | 8 +- ...tingsRemoveButtonPreferenceController.java | 4 +- .../settings/biometrics/face/FaceUpdater.java | 137 +++++++++ .../contextualcards/FaceReEnrollDialog.java | 5 +- .../biometrics/face/FaceUpdaterTest.java | 277 ++++++++++++++++++ 5 files changed, 424 insertions(+), 7 deletions(-) create mode 100644 src/com/android/settings/biometrics/face/FaceUpdater.java create mode 100644 tests/unit/src/com/android/settings/biometrics/face/FaceUpdaterTest.java diff --git a/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java b/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java index 6d2c301a71f..de713dd3386 100644 --- a/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java +++ b/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java @@ -19,9 +19,7 @@ package com.android.settings.biometrics.face; import android.app.Activity; import android.app.settings.SettingsEnums; import android.hardware.face.FaceManager; -import android.os.UserHandle; -import com.android.settings.Utils; import com.android.settings.biometrics.BiometricEnrollSidecar; import java.util.Arrays; @@ -33,7 +31,7 @@ public class FaceEnrollSidecar extends BiometricEnrollSidecar { private final int[] mDisabledFeatures; - private FaceManager mFaceManager; + private FaceUpdater mFaceUpdater; public FaceEnrollSidecar(int[] disabledFeatures) { mDisabledFeatures = Arrays.copyOf(disabledFeatures, disabledFeatures.length); @@ -42,13 +40,13 @@ public class FaceEnrollSidecar extends BiometricEnrollSidecar { @Override public void onAttach(Activity activity) { super.onAttach(activity); - mFaceManager = Utils.getFaceManagerOrNull(activity); + mFaceUpdater = new FaceUpdater(activity); } @Override public void startEnrollment() { super.startEnrollment(); - mFaceManager.enroll(mUserId, mToken, mEnrollmentCancel, + mFaceUpdater.enroll(mUserId, mToken, mEnrollmentCancel, mEnrollmentCallback, mDisabledFeatures); } diff --git a/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java index 616b736b8d6..7db59584899 100644 --- a/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java +++ b/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java @@ -103,6 +103,7 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference private final MetricsFeatureProvider mMetricsFeatureProvider; private final Context mContext; private final FaceManager mFaceManager; + private final FaceUpdater mFaceUpdater; private final FaceManager.RemovalCallback mRemovalCallback = new FaceManager.RemovalCallback() { @Override public void onRemovalError(Face face, int errMsgId, CharSequence errString) { @@ -144,7 +145,7 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference } // Remove the first/only face - mFaceManager.remove(faces.get(0), mUserId, mRemovalCallback); + mFaceUpdater.remove(faces.get(0), mUserId, mRemovalCallback); } else { mButton.setEnabled(true); mRemoving = false; @@ -157,6 +158,7 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference mContext = context; mFaceManager = context.getSystemService(FaceManager.class); mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + mFaceUpdater = new FaceUpdater(context, mFaceManager); } public FaceSettingsRemoveButtonPreferenceController(Context context) { diff --git a/src/com/android/settings/biometrics/face/FaceUpdater.java b/src/com/android/settings/biometrics/face/FaceUpdater.java new file mode 100644 index 00000000000..bb77caefb27 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceUpdater.java @@ -0,0 +1,137 @@ +/* + * 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.settings.biometrics.face; + +import android.content.Context; +import android.hardware.face.Face; +import android.hardware.face.FaceEnrollCell; +import android.hardware.face.FaceManager; +import android.os.CancellationSignal; +import android.view.Surface; + +import androidx.annotation.Nullable; + +import com.android.settings.Utils; +import com.android.settings.safetycenter.BiometricsSafetySource; + +/** + * Responsible for making {@link FaceManager#enroll} and {@link FaceManager#remove} calls and thus + * updating the face setting. + */ +public class FaceUpdater { + + private final Context mContext; + private final FaceManager mFaceManager; + + public FaceUpdater(Context context) { + mContext = context; + mFaceManager = Utils.getFaceManagerOrNull(context); + } + + public FaceUpdater(Context context, FaceManager faceManager) { + mContext = context; + mFaceManager = faceManager; + } + + /** Wrapper around the {@link FaceManager#enroll} method. */ + public void enroll(int userId, byte[] hardwareAuthToken, CancellationSignal cancel, + FaceManager.EnrollmentCallback callback, int[] disabledFeatures) { + mFaceManager.enroll(userId, hardwareAuthToken, cancel, + new NotifyingEnrollmentCallback(mContext, callback), disabledFeatures); + } + + /** Wrapper around the {@link FaceManager#enroll} method. */ + public void enroll(int userId, byte[] hardwareAuthToken, CancellationSignal cancel, + FaceManager.EnrollmentCallback callback, int[] disabledFeatures, + @Nullable Surface previewSurface, boolean debugConsent) { + mFaceManager.enroll(userId, hardwareAuthToken, cancel, + new NotifyingEnrollmentCallback(mContext, callback), disabledFeatures, + previewSurface, debugConsent); + } + + /** Wrapper around the {@link FaceManager#remove} method. */ + public void remove(Face face, int userId, FaceManager.RemovalCallback callback) { + mFaceManager.remove(face, userId, new NotifyingRemovalCallback(mContext, callback)); + } + + /** + * Decorator of the {@link FaceManager.EnrollmentCallback} class that notifies other + * interested parties that a face setting has changed. + */ + private static class NotifyingEnrollmentCallback + extends FaceManager.EnrollmentCallback { + + private final Context mContext; + private final FaceManager.EnrollmentCallback mCallback; + + NotifyingEnrollmentCallback(Context context, + FaceManager.EnrollmentCallback callback) { + mContext = context; + mCallback = callback; + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + mCallback.onEnrollmentError(errMsgId, errString); + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + mCallback.onEnrollmentHelp(helpMsgId, helpString); + } + + @Override + public void onEnrollmentFrame(int helpCode, @Nullable CharSequence helpMessage, + @Nullable FaceEnrollCell cell, int stage, float pan, float tilt, float distance) { + mCallback.onEnrollmentFrame(helpCode, helpMessage, cell, stage, pan, tilt, distance); + } + + @Override + public void onEnrollmentProgress(int remaining) { + mCallback.onEnrollmentProgress(remaining); + if (remaining == 0) { + BiometricsSafetySource.sendSafetyData(mContext); // biometrics data changed + } + } + } + + /** + * Decorator of the {@link FaceManager.RemovalCallback} class that notifies other + * interested parties that a face setting has changed. + */ + private static class NotifyingRemovalCallback extends FaceManager.RemovalCallback { + + private final Context mContext; + private final FaceManager.RemovalCallback mCallback; + + NotifyingRemovalCallback(Context context, FaceManager.RemovalCallback callback) { + mContext = context; + mCallback = callback; + } + + @Override + public void onRemovalError(Face fp, int errMsgId, CharSequence errString) { + mCallback.onRemovalError(fp, errMsgId, errString); + } + + @Override + public void onRemovalSucceeded(@Nullable Face fp, int remaining) { + mCallback.onRemovalSucceeded(fp, remaining); + BiometricsSafetySource.sendSafetyData(mContext); // biometrics data changed + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/FaceReEnrollDialog.java b/src/com/android/settings/homepage/contextualcards/FaceReEnrollDialog.java index e778e8c7ca4..a8239241bf5 100644 --- a/src/com/android/settings/homepage/contextualcards/FaceReEnrollDialog.java +++ b/src/com/android/settings/homepage/contextualcards/FaceReEnrollDialog.java @@ -30,6 +30,7 @@ import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; import com.android.settings.R; import com.android.settings.Utils; +import com.android.settings.biometrics.face.FaceUpdater; import com.android.settings.homepage.contextualcards.slices.FaceSetupSlice; /** @@ -43,6 +44,7 @@ public class FaceReEnrollDialog extends AlertActivity implements private static final String BIOMETRIC_ENROLL_ACTION = "android.settings.BIOMETRIC_ENROLL"; private FaceManager mFaceManager; + private FaceUpdater mFaceUpdater; /** * The type of re-enrollment that has been requested, * see {@link Settings.Secure#FACE_UNLOCK_RE_ENROLL} for more details. @@ -67,6 +69,7 @@ public class FaceReEnrollDialog extends AlertActivity implements alertParams.mPositiveButtonListener = this; mFaceManager = Utils.getFaceManagerOrNull(getApplicationContext()); + mFaceUpdater = new FaceUpdater(getApplicationContext(), mFaceManager); final Context context = getApplicationContext(); mReEnrollType = FaceSetupSlice.getReEnrollSetting(context, getUserId()); @@ -96,7 +99,7 @@ public class FaceReEnrollDialog extends AlertActivity implements if (mFaceManager == null || !mFaceManager.hasEnrolledTemplates(userId)) { finish(); } - mFaceManager.remove(new Face("", 0, 0), userId, new FaceManager.RemovalCallback() { + mFaceUpdater.remove(new Face("", 0, 0), userId, new FaceManager.RemovalCallback() { @Override public void onRemovalError(Face face, int errMsgId, CharSequence errString) { super.onRemovalError(face, errMsgId, errString); diff --git a/tests/unit/src/com/android/settings/biometrics/face/FaceUpdaterTest.java b/tests/unit/src/com/android/settings/biometrics/face/FaceUpdaterTest.java new file mode 100644 index 00000000000..a49b4a6c170 --- /dev/null +++ b/tests/unit/src/com/android/settings/biometrics/face/FaceUpdaterTest.java @@ -0,0 +1,277 @@ +/* + * 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.settings.biometrics.face; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.hardware.face.Face; +import android.hardware.face.FaceEnrollCell; +import android.hardware.face.FaceEnrollStages; +import android.hardware.face.FaceManager; +import android.os.CancellationSignal; +import android.view.Surface; + +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.safetycenter.SafetyCenterManagerWrapper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class FaceUpdaterTest { + + private static final byte[] HARDWARE_AUTH_TOKEN = new byte[] {0}; + private static final CancellationSignal CANCELLATION_SIGNAL = new CancellationSignal(); + private static final int USER_ID = 0; + private static final int ERR_MSG_ID = 0; + private static final int HELP_MSG_ID = 0; + private static final String HELP_STRING = ""; + private static final String ERR_STRING = ""; + private static final Face FACE = + new Face(/* name= */"", /* faceId */ 0, /* deviceId= */ 0L); + private static final int[] DISABLED_FEATURES = new int[] {0}; + private static final boolean DEBUG_CONSENT = false; + private static final Surface PREVIEW_SURFACE = new Surface(); + private static final int HELP_CODE = 0; + private static final CharSequence HELP_MESSAGE = ""; + private static final FaceEnrollCell CELL = + new FaceEnrollCell(/* x= */ 0, /* y= */ 0, /* z= */ 0); + private static final int STAGE = FaceEnrollStages.UNKNOWN; + private static final float PAN = 0; + private static final float TILT = 0; + private static final float DISTANCE = 0; + + + @Mock private FaceManager mFaceManager; + @Mock private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + + private FaceUpdater mFaceUpdater; + private Context mContext; + private FaceManager.EnrollmentCallback mEnrollmentCallback; + private FaceManager.RemovalCallback mRemovalCallback; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = ApplicationProvider.getApplicationContext(); + mFaceUpdater = new FaceUpdater(mContext, mFaceManager); + mEnrollmentCallback = spy(new TestEnrollmentCallback()); + mRemovalCallback = spy(new TestRemovalCallback()); + SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper; + } + + @Test + public void enroll_firstVersion_onEnrollmentCallbacks_triggerGivenCallback() { + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(FaceManager.EnrollmentCallback.class); + mFaceUpdater.enroll(USER_ID, HARDWARE_AUTH_TOKEN, CANCELLATION_SIGNAL, mEnrollmentCallback, + DISABLED_FEATURES); + verify(mFaceManager).enroll( + eq(USER_ID), + same(HARDWARE_AUTH_TOKEN), + same(CANCELLATION_SIGNAL), + callbackCaptor.capture(), + same(DISABLED_FEATURES)); + FaceManager.EnrollmentCallback callback = callbackCaptor.getValue(); + + callback.onEnrollmentError(ERR_MSG_ID, ERR_STRING); + callback.onEnrollmentProgress(/* remaining= */ 2); + callback.onEnrollmentHelp(HELP_MSG_ID, HELP_STRING); + callback.onEnrollmentFrame(HELP_CODE, HELP_MESSAGE, CELL, STAGE, PAN, TILT, DISTANCE); + + verify(mEnrollmentCallback, atLeast(1)).onEnrollmentError(ERR_MSG_ID, ERR_STRING); + verify(mEnrollmentCallback, atLeast(1)).onEnrollmentProgress(/* remaining= */ 2); + verify(mEnrollmentCallback, atLeast(1)).onEnrollmentHelp(HELP_MSG_ID, HELP_STRING); + verify(mEnrollmentCallback, atLeast(1)) + .onEnrollmentFrame(HELP_CODE, HELP_MESSAGE, CELL, STAGE, PAN, TILT, DISTANCE); + } + + @Test + public void enroll_firstVersion_onEnrollmentSuccess_invokedInteractionWithSafetyCenter() { + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(FaceManager.EnrollmentCallback.class); + mFaceUpdater.enroll(USER_ID, HARDWARE_AUTH_TOKEN, CANCELLATION_SIGNAL, mEnrollmentCallback, + DISABLED_FEATURES); + verify(mFaceManager).enroll( + eq(USER_ID), + same(HARDWARE_AUTH_TOKEN), + same(CANCELLATION_SIGNAL), + callbackCaptor.capture(), + same(DISABLED_FEATURES)); + FaceManager.EnrollmentCallback callback = callbackCaptor.getValue(); + + callback.onEnrollmentProgress(/* remaining= */ 0); + + verify(mSafetyCenterManagerWrapper).isEnabled(mContext); + } + + @Test + public void enroll_firstVersion_onEnrollmentNotYetFinished_didntInvokeInteractionWithSafetyCenter() { + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(FaceManager.EnrollmentCallback.class); + mFaceUpdater.enroll(USER_ID, HARDWARE_AUTH_TOKEN, CANCELLATION_SIGNAL, mEnrollmentCallback, + DISABLED_FEATURES); + verify(mFaceManager).enroll( + eq(USER_ID), + same(HARDWARE_AUTH_TOKEN), + same(CANCELLATION_SIGNAL), + callbackCaptor.capture(), + same(DISABLED_FEATURES)); + FaceManager.EnrollmentCallback callback = callbackCaptor.getValue(); + + callback.onEnrollmentProgress(/* remaining= */ 1); + + verify(mSafetyCenterManagerWrapper, never()).isEnabled(any()); + } + + @Test + public void enroll_secondVersion_onEnrollmentCallbacks_triggerGivenCallback() { + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(FaceManager.EnrollmentCallback.class); + mFaceUpdater.enroll(USER_ID, HARDWARE_AUTH_TOKEN, CANCELLATION_SIGNAL, mEnrollmentCallback, + DISABLED_FEATURES, PREVIEW_SURFACE, DEBUG_CONSENT); + verify(mFaceManager).enroll( + eq(USER_ID), + same(HARDWARE_AUTH_TOKEN), + same(CANCELLATION_SIGNAL), + callbackCaptor.capture(), + same(DISABLED_FEATURES), + same(PREVIEW_SURFACE), + eq(DEBUG_CONSENT)); + FaceManager.EnrollmentCallback callback = callbackCaptor.getValue(); + + callback.onEnrollmentError(ERR_MSG_ID, ERR_STRING); + callback.onEnrollmentProgress(/* remaining= */ 2); + callback.onEnrollmentHelp(HELP_MSG_ID, HELP_STRING); + callback.onEnrollmentFrame(HELP_CODE, HELP_MESSAGE, CELL, STAGE, PAN, TILT, DISTANCE); + + verify(mEnrollmentCallback, atLeast(1)).onEnrollmentError(ERR_MSG_ID, ERR_STRING); + verify(mEnrollmentCallback, atLeast(1)).onEnrollmentProgress(/* remaining= */ 2); + verify(mEnrollmentCallback, atLeast(1)).onEnrollmentHelp(HELP_MSG_ID, HELP_STRING); + verify(mEnrollmentCallback, atLeast(1)) + .onEnrollmentFrame(HELP_CODE, HELP_MESSAGE, CELL, STAGE, PAN, TILT, DISTANCE); + } + + @Test + public void enroll_secondVersion_onEnrollmentSuccess_invokedInteractionWithSafetyCenter() { + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(FaceManager.EnrollmentCallback.class); + mFaceUpdater.enroll(USER_ID, HARDWARE_AUTH_TOKEN, CANCELLATION_SIGNAL, mEnrollmentCallback, + DISABLED_FEATURES, PREVIEW_SURFACE, DEBUG_CONSENT); + verify(mFaceManager).enroll( + eq(USER_ID), + same(HARDWARE_AUTH_TOKEN), + same(CANCELLATION_SIGNAL), + callbackCaptor.capture(), + same(DISABLED_FEATURES), + same(PREVIEW_SURFACE), + eq(DEBUG_CONSENT)); + FaceManager.EnrollmentCallback callback = callbackCaptor.getValue(); + + callback.onEnrollmentProgress(/* remaining= */ 0); + + verify(mSafetyCenterManagerWrapper).isEnabled(mContext); + } + + @Test + public void enroll_secondVersion_onEnrollmentNotYetFinished_didntInvokeInteractionWithSafetyCenter() { + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(FaceManager.EnrollmentCallback.class); + mFaceUpdater.enroll(USER_ID, HARDWARE_AUTH_TOKEN, CANCELLATION_SIGNAL, mEnrollmentCallback, + DISABLED_FEATURES, PREVIEW_SURFACE, DEBUG_CONSENT); + verify(mFaceManager).enroll( + eq(USER_ID), + same(HARDWARE_AUTH_TOKEN), + same(CANCELLATION_SIGNAL), + callbackCaptor.capture(), + same(DISABLED_FEATURES), + same(PREVIEW_SURFACE), + eq(DEBUG_CONSENT)); + FaceManager.EnrollmentCallback callback = callbackCaptor.getValue(); + + callback.onEnrollmentProgress(/* remaining= */ 1); + + verify(mSafetyCenterManagerWrapper, never()).isEnabled(any()); + } + + @Test + public void remove_onRemovalCallbacks_triggerGivenCallback() { + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(FaceManager.RemovalCallback.class); + mFaceUpdater.remove(FACE, USER_ID, mRemovalCallback); + verify(mFaceManager) + .remove(same(FACE), eq(USER_ID), callbackCaptor.capture()); + FaceManager.RemovalCallback callback = callbackCaptor.getValue(); + + callback.onRemovalSucceeded(FACE, /* remaining= */ 1); + callback.onRemovalError(FACE, ERR_MSG_ID, ERR_STRING); + + verify(mRemovalCallback).onRemovalSucceeded(any(), eq(1)); + verify(mRemovalCallback).onRemovalError(FACE, ERR_MSG_ID, ERR_STRING); + } + + @Test + public void remove_onRemovalSuccess_invokedInteractionWithSafetyCenter() { + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(FaceManager.RemovalCallback.class); + mFaceUpdater.remove(FACE, USER_ID, mRemovalCallback); + verify(mFaceManager) + .remove(same(FACE), eq(USER_ID), callbackCaptor.capture()); + FaceManager.RemovalCallback callback = callbackCaptor.getValue(); + + callback.onRemovalSucceeded(FACE, /* remaining= */ 0); + + verify(mSafetyCenterManagerWrapper).isEnabled(mContext); + } + + public static class TestEnrollmentCallback extends FaceManager.EnrollmentCallback { + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) {} + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {} + + @Override + public void onEnrollmentProgress(int remaining) {} + + @Override + public void onEnrollmentFrame(int helpCode, @Nullable CharSequence helpMessage, + @Nullable FaceEnrollCell cell, int stage, float pan, float tilt, float distance) {} + } + + public static class TestRemovalCallback extends FaceManager.RemovalCallback { + @Override + public void onRemovalError(Face fp, int errMsgId, CharSequence errString) {} + + @Override + public void onRemovalSucceeded(@Nullable Face fp, int remaining) {} + } +}