diff --git a/core/proto/android/server/biometrics.proto b/core/proto/android/server/biometrics.proto index bbb0edd4fd59c..4f3ae28bec7d8 100644 --- a/core/proto/android/server/biometrics.proto +++ b/core/proto/android/server/biometrics.proto @@ -178,4 +178,6 @@ enum ClientMonitorEnum { CM_DETECT_INTERACTION = 13; CM_INVALIDATION_REQUESTER = 14; CM_INVALIDATE = 15; + CM_STOP_USER = 16; + CM_START_USER = 17; } \ No newline at end of file diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java index 81ce2d5352370..3cfaaf7c23e0d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java +++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java @@ -197,7 +197,7 @@ public abstract class BaseClientMonitor extends LoggableMonitor return mListener; } - public final int getTargetUserId() { + public int getTargetUserId() { return mTargetUserId; } diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index 20c25c35535a9..6c480f1342792 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -55,7 +55,7 @@ public class BiometricScheduler { private static final String BASE_TAG = "BiometricScheduler"; // Number of recent operations to keep in our logs for dumpsys - private static final int LOG_NUM_RECENT_OPERATIONS = 50; + protected static final int LOG_NUM_RECENT_OPERATIONS = 50; /** * Contains all the necessary information for a HAL operation. @@ -196,10 +196,10 @@ public class BiometricScheduler { } } - @NonNull private final String mBiometricTag; + @NonNull protected final String mBiometricTag; @Nullable private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher; @NonNull private final IBiometricService mBiometricService; - @NonNull private final Handler mHandler = new Handler(Looper.getMainLooper()); + @NonNull protected final Handler mHandler = new Handler(Looper.getMainLooper()); @NonNull private final InternalCallback mInternalCallback; @VisibleForTesting @NonNull final Deque mPendingOperations; @VisibleForTesting @Nullable Operation mCurrentOperation; @@ -294,11 +294,11 @@ public class BiometricScheduler { return mInternalCallback; } - private String getTag() { + protected String getTag() { return BASE_TAG + "/" + mBiometricTag; } - private void startNextOperationIfIdle() { + protected void startNextOperationIfIdle() { if (mCurrentOperation != null) { Slog.v(getTag(), "Not idle, current operation: " + mCurrentOperation); return; @@ -310,6 +310,7 @@ public class BiometricScheduler { mCurrentOperation = mPendingOperations.poll(); final BaseClientMonitor currentClient = mCurrentOperation.mClientMonitor; + Slog.d(getTag(), "[Polled] " + mCurrentOperation); // If the operation at the front of the queue has been marked for cancellation, send // ERROR_CANCELED. No need to start this client. diff --git a/services/core/java/com/android/server/biometrics/sensors/StartUserClient.java b/services/core/java/com/android/server/biometrics/sensors/StartUserClient.java new file mode 100644 index 0000000000000..8b9be83bc8c36 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/StartUserClient.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2021 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.os.IBinder; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.BiometricsProto; + +public abstract class StartUserClient extends HalClientMonitor { + + public interface UserStartedCallback { + void onUserStarted(int newUserId); + } + + @NonNull @VisibleForTesting + protected final UserStartedCallback mUserStartedCallback; + + public StartUserClient(@NonNull Context context, @NonNull LazyDaemon lazyDaemon, + @Nullable IBinder token, int userId, int sensorId, + @NonNull UserStartedCallback callback) { + super(context, lazyDaemon, token, null /* listener */, userId, context.getOpPackageName(), + 0 /* cookie */, sensorId, BiometricsProtoEnums.MODALITY_UNKNOWN, + BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN); + mUserStartedCallback = callback; + } + + @Override + public int getProtoEnum() { + return BiometricsProto.CM_START_USER; + } +} \ No newline at end of file diff --git a/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java b/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java new file mode 100644 index 0000000000000..62cd673babac7 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2021 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.os.IBinder; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.BiometricsProto; + +public abstract class StopUserClient extends HalClientMonitor { + + public interface UserStoppedCallback { + void onUserStopped(); + } + + @NonNull @VisibleForTesting + protected final UserStoppedCallback mUserStoppedCallback; + + public StopUserClient(@NonNull Context context, @NonNull LazyDaemon lazyDaemon, + @Nullable IBinder token, int userId, int sensorId, + @NonNull UserStoppedCallback callback) { + super(context, lazyDaemon, token, null /* listener */, userId, context.getOpPackageName(), + 0 /* cookie */, sensorId, BiometricsProtoEnums.MODALITY_UNKNOWN, + BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN); + mUserStoppedCallback = callback; + } + + @Override + public int getProtoEnum() { + return BiometricsProto.CM_STOP_USER; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java new file mode 100644 index 0000000000000..c0ea2b3f8b933 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.biometrics.IBiometricService; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; + +/** + * A user-aware scheduler that requests user-switches based on scheduled operation's targetUserId. + */ +public class UserAwareBiometricScheduler extends BiometricScheduler { + + private static final String BASE_TAG = "UaBiometricScheduler"; + + /** + * Interface to retrieve the owner's notion of the current userId. Note that even though + * the scheduler can determine this based on its history of processed clients, we should still + * query the owner since it may be cleared due to things like HAL death, etc. + */ + public interface CurrentUserRetriever { + int getCurrentUserId(); + } + + public interface UserSwitchCallback { + @NonNull StopUserClient getStopUserClient(int userId); + @NonNull StartUserClient getStartUserClient(int newUserId); + } + + @NonNull private final CurrentUserRetriever mCurrentUserRetriever; + @NonNull private final UserSwitchCallback mUserSwitchCallback; + @NonNull @VisibleForTesting final ClientFinishedCallback mClientFinishedCallback; + + @VisibleForTesting + class ClientFinishedCallback implements BaseClientMonitor.Callback { + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { + mHandler.post(() -> { + Slog.d(getTag(), "[Client finished] " + clientMonitor + ", success: " + success); + + startNextOperationIfIdle(); + }); + } + } + + @VisibleForTesting + UserAwareBiometricScheduler(@NonNull String tag, + @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull IBiometricService biometricService, + @NonNull CurrentUserRetriever currentUserRetriever, + @NonNull UserSwitchCallback userSwitchCallback) { + super(tag, gestureAvailabilityDispatcher, biometricService, LOG_NUM_RECENT_OPERATIONS); + + mCurrentUserRetriever = currentUserRetriever; + mUserSwitchCallback = userSwitchCallback; + mClientFinishedCallback = new ClientFinishedCallback(); + } + + public UserAwareBiometricScheduler(@NonNull String tag, + @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull CurrentUserRetriever currentUserRetriever, + @NonNull UserSwitchCallback userSwitchCallback) { + this(tag, gestureAvailabilityDispatcher, IBiometricService.Stub.asInterface( + ServiceManager.getService(Context.BIOMETRIC_SERVICE)), currentUserRetriever, + userSwitchCallback); + } + + @Override + protected String getTag() { + return BASE_TAG + "/" + mBiometricTag; + } + + @Override + protected void startNextOperationIfIdle() { + if (mCurrentOperation != null) { + Slog.v(getTag(), "Not idle, current operation: " + mCurrentOperation); + return; + } + if (mPendingOperations.isEmpty()) { + Slog.d(getTag(), "No operations, returning to idle"); + return; + } + + final int currentUserId = mCurrentUserRetriever.getCurrentUserId(); + final int nextUserId = mPendingOperations.getFirst().mClientMonitor.getTargetUserId(); + + if (nextUserId == currentUserId) { + super.startNextOperationIfIdle(); + } else if (currentUserId == UserHandle.USER_NULL) { + Slog.d(getTag(), "User switch required, current user null, next: " + nextUserId); + final BaseClientMonitor startClient = + mUserSwitchCallback.getStartUserClient(nextUserId); + startClient.start(mClientFinishedCallback); + } else { + final BaseClientMonitor stopClient = mUserSwitchCallback + .getStopUserClient(currentUserId); + Slog.d(getTag(), "User switch required, current: " + currentUserId + + ", next: " + nextUserId + ". " + stopClient); + stopClient.start(mClientFinishedCallback); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/UserAwareBiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/UserAwareBiometricSchedulerTest.java new file mode 100644 index 0000000000000..6cdac1af87eb4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/UserAwareBiometricSchedulerTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2021 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; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.hardware.biometrics.IBiometricService; +import android.os.Binder; +import android.os.IBinder; +import android.os.UserHandle; +import android.platform.test.annotations.Presubmit; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@Presubmit +@SmallTest +public class UserAwareBiometricSchedulerTest { + + private static final String TAG = "BiometricSchedulerTest"; + private static final int TEST_SENSOR_ID = 0; + + private UserAwareBiometricScheduler mScheduler; + private IBinder mToken; + + @Mock + private Context mContext; + @Mock + private IBiometricService mBiometricService; + + private TestUserStartedCallback mUserStartedCallback; + private TestUserStoppedCallback mUserStoppedCallback; + private int mCurrentUserId = UserHandle.USER_NULL; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mToken = new Binder(); + mUserStartedCallback = new TestUserStartedCallback(); + mUserStoppedCallback = new TestUserStoppedCallback(); + + mScheduler = new UserAwareBiometricScheduler(TAG, + null /* gestureAvailabilityDispatcher */, + mBiometricService, + () -> mCurrentUserId, + new UserAwareBiometricScheduler.UserSwitchCallback() { + @NonNull + @Override + public StopUserClient getStopUserClient(int userId) { + return new TestStopUserClient(mContext, Object::new, mToken, userId, + TEST_SENSOR_ID, mUserStoppedCallback); + } + + @NonNull + @Override + public StartUserClient getStartUserClient(int newUserId) { + return new TestStartUserClient(mContext, Object::new, mToken, newUserId, + TEST_SENSOR_ID, mUserStartedCallback); + } + }); + } + + @Test + public void testScheduleOperation_whenNoUser() { + mCurrentUserId = UserHandle.USER_NULL; + + final int nextUserId = 0; + + BaseClientMonitor nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(nextUserId); + + mScheduler.scheduleClientMonitor(nextClient); + verify(nextClient, never()).start(any()); + assertEquals(0, mUserStoppedCallback.numInvocations); + assertEquals(1, mUserStartedCallback.numInvocations); + + waitForIdle(); + verify(nextClient).start(any()); + } + + @Test + public void testScheduleOperation_whenSameUser() { + mCurrentUserId = 10; + + BaseClientMonitor nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(mCurrentUserId); + + mScheduler.scheduleClientMonitor(nextClient); + + waitForIdle(); + + verify(nextClient).start(any()); + assertEquals(0, mUserStoppedCallback.numInvocations); + assertEquals(0, mUserStartedCallback.numInvocations); + } + + @Test + public void testScheduleOperation_whenDifferentUser() { + mCurrentUserId = 10; + + final int nextUserId = 11; + BaseClientMonitor nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(nextUserId); + + mScheduler.scheduleClientMonitor(nextClient); + + waitForIdle(); + assertEquals(1, mUserStoppedCallback.numInvocations); + + waitForIdle(); + assertEquals(1, mUserStartedCallback.numInvocations); + + waitForIdle(); + verify(nextClient).start(any()); + } + + private static void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private class TestUserStoppedCallback implements StopUserClient.UserStoppedCallback { + + int numInvocations; + + @Override + public void onUserStopped() { + numInvocations++; + mCurrentUserId = UserHandle.USER_NULL; + } + } + + private class TestUserStartedCallback implements StartUserClient.UserStartedCallback { + + int numInvocations; + + @Override + public void onUserStarted(int newUserId) { + numInvocations++; + mCurrentUserId = newUserId; + } + } + + private static class TestStopUserClient extends StopUserClient { + public TestStopUserClient(@NonNull Context context, + @NonNull LazyDaemon lazyDaemon, @Nullable IBinder token, int userId, + int sensorId, @NonNull UserStoppedCallback callback) { + super(context, lazyDaemon, token, userId, sensorId, callback); + } + + @Override + protected void startHalOperation() { + + } + + @Override + public void start(@NonNull Callback callback) { + super.start(callback); + mUserStoppedCallback.onUserStopped(); + callback.onClientFinished(this, true /* success */); + } + + @Override + public void unableToStart() { + + } + } + + private static class TestStartUserClient extends StartUserClient { + public TestStartUserClient(@NonNull Context context, + @NonNull LazyDaemon lazyDaemon, @Nullable IBinder token, int userId, + int sensorId, @NonNull UserStartedCallback callback) { + super(context, lazyDaemon, token, userId, sensorId, callback); + } + + @Override + protected void startHalOperation() { + + } + + @Override + public void start(@NonNull Callback callback) { + super.start(callback); + mUserStartedCallback.onUserStarted(getTargetUserId()); + callback.onClientFinished(this, true /* success */); + } + + @Override + public void unableToStart() { + + } + } +}