blob: e3e3900c47e0847e300b1d2d93513f1ebcad11c0 [file] [log] [blame]
/*
* Copyright (C) 2020 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 junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
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 static org.mockito.Mockito.withSettings;
import android.content.Context;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.IBiometricService;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.testing.TestableContext;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.android.server.biometrics.nano.BiometricSchedulerProto;
import com.android.server.biometrics.nano.BiometricsProto;
import com.android.server.biometrics.sensors.BiometricScheduler.Operation;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@Presubmit
@SmallTest
public class BiometricSchedulerTest {
private static final String TAG = "BiometricSchedulerTest";
private static final int TEST_SENSOR_ID = 1;
private static final int LOG_NUM_RECENT_OPERATIONS = 2;
private BiometricScheduler mScheduler;
private IBinder mToken;
@Mock
private IBiometricService mBiometricService;
@Rule
public final TestableContext mContext =
new TestableContext(InstrumentationRegistry.getContext(), null);
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mToken = new Binder();
mScheduler = new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_UNKNOWN,
null /* gestureAvailabilityTracker */, mBiometricService, LOG_NUM_RECENT_OPERATIONS,
CoexCoordinator.getInstance());
}
@Test
public void testClientDuplicateFinish_ignoredBySchedulerAndDoesNotCrash() {
final HalClientMonitor.LazyDaemon<Object> nonNullDaemon = () -> mock(Object.class);
final HalClientMonitor<Object> client1 =
new TestClientMonitor(mContext, mToken, nonNullDaemon);
final HalClientMonitor<Object> client2 =
new TestClientMonitor(mContext, mToken, nonNullDaemon);
mScheduler.scheduleClientMonitor(client1);
mScheduler.scheduleClientMonitor(client2);
client1.mCallback.onClientFinished(client1, true /* success */);
client1.mCallback.onClientFinished(client1, true /* success */);
}
@Test
public void testRemovesPendingOperations_whenNullHal_andNotBiometricPrompt() {
// Even if second client has a non-null daemon, it needs to be canceled.
Object daemon2 = mock(Object.class);
final HalClientMonitor.LazyDaemon<Object> lazyDaemon1 = () -> null;
final HalClientMonitor.LazyDaemon<Object> lazyDaemon2 = () -> daemon2;
final TestClientMonitor client1 = new TestClientMonitor(mContext, mToken, lazyDaemon1);
final TestClientMonitor client2 = new TestClientMonitor(mContext, mToken, lazyDaemon2);
final BaseClientMonitor.Callback callback1 = mock(BaseClientMonitor.Callback.class);
final BaseClientMonitor.Callback callback2 = mock(BaseClientMonitor.Callback.class);
// Pretend the scheduler is busy so the first operation doesn't start right away. We want
// to pretend like there are two operations in the queue before kicking things off
mScheduler.mCurrentOperation = new BiometricScheduler.Operation(
mock(BaseClientMonitor.class), mock(BaseClientMonitor.Callback.class));
mScheduler.scheduleClientMonitor(client1, callback1);
assertEquals(1, mScheduler.mPendingOperations.size());
// client1 is pending. Allow the scheduler to start once second client is added.
mScheduler.mCurrentOperation = null;
mScheduler.scheduleClientMonitor(client2, callback2);
waitForIdle();
assertTrue(client1.wasUnableToStart());
verify(callback1).onClientFinished(eq(client1), eq(false) /* success */);
verify(callback1, never()).onClientStarted(any());
assertTrue(client2.wasUnableToStart());
verify(callback2).onClientFinished(eq(client2), eq(false) /* success */);
verify(callback2, never()).onClientStarted(any());
assertTrue(mScheduler.mPendingOperations.isEmpty());
}
@Test
public void testRemovesOnlyBiometricPromptOperation_whenNullHal() throws Exception {
// Second non-BiometricPrompt client has a valid daemon
final Object daemon2 = mock(Object.class);
final HalClientMonitor.LazyDaemon<Object> lazyDaemon1 = () -> null;
final HalClientMonitor.LazyDaemon<Object> lazyDaemon2 = () -> daemon2;
final ClientMonitorCallbackConverter listener1 = mock(ClientMonitorCallbackConverter.class);
final BiometricPromptClientMonitor client1 =
new BiometricPromptClientMonitor(mContext, mToken, lazyDaemon1, listener1);
final TestClientMonitor client2 = new TestClientMonitor(mContext, mToken, lazyDaemon2);
final BaseClientMonitor.Callback callback1 = mock(BaseClientMonitor.Callback.class);
final BaseClientMonitor.Callback callback2 = mock(BaseClientMonitor.Callback.class);
// Pretend the scheduler is busy so the first operation doesn't start right away. We want
// to pretend like there are two operations in the queue before kicking things off
mScheduler.mCurrentOperation = new BiometricScheduler.Operation(
mock(BaseClientMonitor.class), mock(BaseClientMonitor.Callback.class));
mScheduler.scheduleClientMonitor(client1, callback1);
assertEquals(1, mScheduler.mPendingOperations.size());
// client1 is pending. Allow the scheduler to start once second client is added.
mScheduler.mCurrentOperation = null;
mScheduler.scheduleClientMonitor(client2, callback2);
waitForIdle();
// Simulate that the BiometricPrompt client's sensor is ready
mScheduler.startPreparedClient(client1.getCookie());
// Client 1 cleans up properly
verify(listener1).onError(eq(TEST_SENSOR_ID), anyInt(),
eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(0));
verify(callback1).onClientFinished(eq(client1), eq(false) /* success */);
verify(callback1, never()).onClientStarted(any());
// Client 2 was able to start
assertFalse(client2.wasUnableToStart());
assertTrue(client2.hasStarted());
verify(callback2).onClientStarted(eq(client2));
}
@Test
public void testCancelNotInvoked_whenOperationWaitingForCookie() {
final HalClientMonitor.LazyDaemon<Object> lazyDaemon1 = () -> mock(Object.class);
final BiometricPromptClientMonitor client1 = new BiometricPromptClientMonitor(mContext,
mToken, lazyDaemon1, mock(ClientMonitorCallbackConverter.class));
final BaseClientMonitor.Callback callback1 = mock(BaseClientMonitor.Callback.class);
// Schedule a BiometricPrompt authentication request
mScheduler.scheduleClientMonitor(client1, callback1);
assertEquals(Operation.STATE_WAITING_FOR_COOKIE, mScheduler.mCurrentOperation.mState);
assertEquals(client1, mScheduler.mCurrentOperation.mClientMonitor);
assertEquals(0, mScheduler.mPendingOperations.size());
// Request it to be canceled. The operation can be canceled immediately, and the scheduler
// should go back to idle, since in this case the framework has not even requested the HAL
// to authenticate yet.
mScheduler.cancelAuthenticationOrDetection(mToken, 1 /* requestId */);
assertNull(mScheduler.mCurrentOperation);
}
@Test
public void testProtoDump_singleCurrentOperation() throws Exception {
// Nothing so far
BiometricSchedulerProto bsp = getDump(true /* clearSchedulerBuffer */);
assertEquals(BiometricsProto.CM_NONE, bsp.currentOperation);
assertEquals(0, bsp.totalOperations);
// TODO:(b/178828362) See bug and/or commit message :/
// assertEquals(0, bsp.recentOperations.length);
// Pretend the scheduler is busy enrolling, and check the proto dump again.
final TestClientMonitor2 client = new TestClientMonitor2(mContext, mToken,
() -> mock(Object.class), BiometricsProto.CM_ENROLL);
mScheduler.scheduleClientMonitor(client);
waitForIdle();
bsp = getDump(true /* clearSchedulerBuffer */);
assertEquals(BiometricsProto.CM_ENROLL, bsp.currentOperation);
// No operations have completed yet
assertEquals(0, bsp.totalOperations);
// TODO:(b/178828362) See bug and/or commit message :/
assertEquals(1, bsp.recentOperations.length);
assertEquals(BiometricsProto.CM_NONE, bsp.recentOperations[0]);
// Finish this operation, so the next scheduled one can start
client.getCallback().onClientFinished(client, true);
}
@Test
public void testProtoDump_fifo() throws Exception {
// Add the first operation
final TestClientMonitor2 client = new TestClientMonitor2(mContext, mToken,
() -> mock(Object.class), BiometricsProto.CM_ENROLL);
mScheduler.scheduleClientMonitor(client);
waitForIdle();
BiometricSchedulerProto bsp = getDump(false /* clearSchedulerBuffer */);
assertEquals(BiometricsProto.CM_ENROLL, bsp.currentOperation);
// No operations have completed yet
assertEquals(0, bsp.totalOperations);
// TODO:(b/178828362) See bug and/or commit message :/
// assertEquals(0, bsp.recentOperations.length);
// Finish this operation, so the next scheduled one can start
client.getCallback().onClientFinished(client, true);
// Add another operation
final TestClientMonitor2 client2 = new TestClientMonitor2(mContext, mToken,
() -> mock(Object.class), BiometricsProto.CM_REMOVE);
mScheduler.scheduleClientMonitor(client2);
waitForIdle();
bsp = getDump(false /* clearSchedulerBuffer */);
assertEquals(BiometricsProto.CM_REMOVE, bsp.currentOperation);
assertEquals(1, bsp.totalOperations); // Enroll finished
assertEquals(1, bsp.recentOperations.length);
assertEquals(BiometricsProto.CM_ENROLL, bsp.recentOperations[0]);
client2.getCallback().onClientFinished(client2, true);
// And another operation
final TestClientMonitor2 client3 = new TestClientMonitor2(mContext, mToken,
() -> mock(Object.class), BiometricsProto.CM_AUTHENTICATE);
mScheduler.scheduleClientMonitor(client3);
waitForIdle();
bsp = getDump(false /* clearSchedulerBuffer */);
assertEquals(BiometricsProto.CM_AUTHENTICATE, bsp.currentOperation);
assertEquals(2, bsp.totalOperations);
assertEquals(2, bsp.recentOperations.length);
assertEquals(BiometricsProto.CM_ENROLL, bsp.recentOperations[0]);
assertEquals(BiometricsProto.CM_REMOVE, bsp.recentOperations[1]);
// Finish the last operation, and check that the first operation is removed from the FIFO.
// The test initializes the scheduler with "LOG_NUM_RECENT_OPERATIONS = 2" :)
client3.getCallback().onClientFinished(client3, true);
waitForIdle();
bsp = getDump(true /* clearSchedulerBuffer */);
assertEquals(3, bsp.totalOperations);
assertEquals(2, bsp.recentOperations.length);
assertEquals(BiometricsProto.CM_REMOVE, bsp.recentOperations[0]);
assertEquals(BiometricsProto.CM_AUTHENTICATE, bsp.recentOperations[1]);
// Nothing is currently running anymore
assertEquals(BiometricsProto.CM_NONE, bsp.currentOperation);
// RecentOperations queue is cleared (by the previous dump)
bsp = getDump(true /* clearSchedulerBuffer */);
// TODO:(b/178828362) See bug and/or commit message :/
assertEquals(1, bsp.recentOperations.length);
assertEquals(BiometricsProto.CM_NONE, bsp.recentOperations[0]);
}
@Test
public void testCancelPendingAuth() throws RemoteException {
final HalClientMonitor.LazyDaemon<Object> lazyDaemon = () -> mock(Object.class);
final TestClientMonitor client1 = new TestClientMonitor(mContext, mToken, lazyDaemon);
final ClientMonitorCallbackConverter callback = mock(ClientMonitorCallbackConverter.class);
final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext, lazyDaemon,
mToken, callback);
// Add a non-cancellable client, then add the auth client
mScheduler.scheduleClientMonitor(client1);
mScheduler.scheduleClientMonitor(client2);
waitForIdle();
assertEquals(mScheduler.getCurrentClient(), client1);
assertEquals(Operation.STATE_WAITING_IN_QUEUE,
mScheduler.mPendingOperations.getFirst().mState);
// Request cancel before the authentication client has started
mScheduler.cancelAuthenticationOrDetection(mToken, 1 /* requestId */);
waitForIdle();
assertEquals(Operation.STATE_WAITING_IN_QUEUE_CANCELING,
mScheduler.mPendingOperations.getFirst().mState);
// Finish the blocking client. The authentication client should send ERROR_CANCELED
client1.getCallback().onClientFinished(client1, true /* success */);
waitForIdle();
verify(callback).onError(anyInt(), anyInt(),
eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
eq(0) /* vendorCode */);
assertNull(mScheduler.getCurrentClient());
}
@Test
public void testCancels_whenAuthRequestIdNotSet() {
testCancelsWhenRequestId(null /* requestId */, 2, true /* started */);
}
@Test
public void testCancels_whenAuthRequestIdNotSet_notStarted() {
testCancelsWhenRequestId(null /* requestId */, 2, false /* started */);
}
@Test
public void testCancels_whenAuthRequestIdMatches() {
testCancelsWhenRequestId(200L, 200, true /* started */);
}
@Test
public void testCancels_whenAuthRequestIdMatches_noStarted() {
testCancelsWhenRequestId(200L, 200, false /* started */);
}
@Test
public void testDoesNotCancel_whenAuthRequestIdMismatched() {
testCancelsWhenRequestId(10L, 20, true /* started */);
}
@Test
public void testDoesNotCancel_whenAuthRequestIdMismatched_notStarted() {
testCancelsWhenRequestId(10L, 20, false /* started */);
}
private void testCancelsWhenRequestId(@Nullable Long requestId, long cancelRequestId,
boolean started) {
final boolean matches = requestId == null || requestId == cancelRequestId;
final HalClientMonitor.LazyDaemon<Object> lazyDaemon = () -> mock(Object.class);
final ClientMonitorCallbackConverter callback = mock(ClientMonitorCallbackConverter.class);
final TestAuthenticationClient client = new TestAuthenticationClient(
mContext, lazyDaemon, mToken, callback);
if (requestId != null) {
client.setRequestId(requestId);
}
mScheduler.scheduleClientMonitor(client);
if (started) {
mScheduler.startPreparedClient(client.getCookie());
}
waitForIdle();
mScheduler.cancelAuthenticationOrDetection(mToken, cancelRequestId);
waitForIdle();
assertEquals(matches && started ? 1 : 0, client.mNumCancels);
if (matches) {
if (started) {
assertEquals(Operation.STATE_STARTED_CANCELING,
mScheduler.mCurrentOperation.mState);
}
} else {
if (started) {
assertEquals(Operation.STATE_STARTED,
mScheduler.mCurrentOperation.mState);
} else {
assertEquals(Operation.STATE_WAITING_FOR_COOKIE,
mScheduler.mCurrentOperation.mState);
}
}
}
@Test
public void testCancelsPending_whenAuthRequestIdsSet() {
final long requestId1 = 10;
final long requestId2 = 20;
final HalClientMonitor.LazyDaemon<Object> lazyDaemon = () -> mock(Object.class);
final ClientMonitorCallbackConverter callback = mock(ClientMonitorCallbackConverter.class);
final TestAuthenticationClient client1 = new TestAuthenticationClient(
mContext, lazyDaemon, mToken, callback);
client1.setRequestId(requestId1);
final TestAuthenticationClient client2 = new TestAuthenticationClient(
mContext, lazyDaemon, mToken, callback);
client2.setRequestId(requestId2);
mScheduler.scheduleClientMonitor(client1);
mScheduler.scheduleClientMonitor(client2);
mScheduler.startPreparedClient(client1.getCookie());
waitForIdle();
mScheduler.cancelAuthenticationOrDetection(mToken, 9999);
waitForIdle();
assertEquals(Operation.STATE_STARTED,
mScheduler.mCurrentOperation.mState);
assertEquals(Operation.STATE_WAITING_IN_QUEUE,
mScheduler.mPendingOperations.getFirst().mState);
mScheduler.cancelAuthenticationOrDetection(mToken, requestId2);
waitForIdle();
assertEquals(Operation.STATE_STARTED,
mScheduler.mCurrentOperation.mState);
assertEquals(Operation.STATE_WAITING_IN_QUEUE_CANCELING,
mScheduler.mPendingOperations.getFirst().mState);
}
@Test
public void testInterruptPrecedingClients_whenExpected() {
final BaseClientMonitor interruptableMonitor = mock(BaseClientMonitor.class,
withSettings().extraInterfaces(Interruptable.class));
final BaseClientMonitor interrupter = mock(BaseClientMonitor.class);
when(interrupter.interruptsPrecedingClients()).thenReturn(true);
mScheduler.scheduleClientMonitor(interruptableMonitor);
mScheduler.scheduleClientMonitor(interrupter);
waitForIdle();
verify((Interruptable) interruptableMonitor).cancel();
mScheduler.getInternalCallback().onClientFinished(interruptableMonitor, true /* success */);
}
@Test
public void testDoesNotInterruptPrecedingClients_whenNotExpected() {
final BaseClientMonitor interruptableMonitor = mock(BaseClientMonitor.class,
withSettings().extraInterfaces(Interruptable.class));
final BaseClientMonitor interrupter = mock(BaseClientMonitor.class);
when(interrupter.interruptsPrecedingClients()).thenReturn(false);
mScheduler.scheduleClientMonitor(interruptableMonitor);
mScheduler.scheduleClientMonitor(interrupter);
waitForIdle();
verify((Interruptable) interruptableMonitor, never()).cancel();
}
@Test
public void testClientDestroyed_afterFinish() {
final HalClientMonitor.LazyDaemon<Object> nonNullDaemon = () -> mock(Object.class);
final TestClientMonitor client =
new TestClientMonitor(mContext, mToken, nonNullDaemon);
mScheduler.scheduleClientMonitor(client);
client.mCallback.onClientFinished(client, true /* success */);
waitForIdle();
assertTrue(client.wasDestroyed());
}
private BiometricSchedulerProto getDump(boolean clearSchedulerBuffer) throws Exception {
return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer));
}
private static class BiometricPromptClientMonitor extends AuthenticationClient<Object> {
public BiometricPromptClientMonitor(@NonNull Context context, @NonNull IBinder token,
@NonNull LazyDaemon<Object> lazyDaemon, ClientMonitorCallbackConverter listener) {
super(context, lazyDaemon, token, listener, 0 /* targetUserId */, 0 /* operationId */,
false /* restricted */, TAG, 1 /* cookie */, false /* requireConfirmation */,
TEST_SENSOR_ID, true /* isStrongBiometric */, 0 /* statsModality */,
0 /* statsClient */, null /* taskStackListener */, mock(LockoutTracker.class),
false /* isKeyguard */, true /* shouldVibrate */,
false /* isKeyguardBypassEnabled */);
}
@Override
protected void stopHalOperation() {
}
@Override
protected void startHalOperation() {
}
@Override
protected void handleLifecycleAfterAuth(boolean authenticated) {
}
@Override
public boolean wasUserDetected() {
return false;
}
}
private static class TestAuthenticationClient extends AuthenticationClient<Object> {
int mNumCancels = 0;
public TestAuthenticationClient(@NonNull Context context,
@NonNull LazyDaemon<Object> lazyDaemon, @NonNull IBinder token,
@NonNull ClientMonitorCallbackConverter listener) {
super(context, lazyDaemon, token, listener, 0 /* targetUserId */, 0 /* operationId */,
false /* restricted */, TAG, 1 /* cookie */, false /* requireConfirmation */,
TEST_SENSOR_ID, true /* isStrongBiometric */, 0 /* statsModality */,
0 /* statsClient */, null /* taskStackListener */, mock(LockoutTracker.class),
false /* isKeyguard */, true /* shouldVibrate */,
false /* isKeyguardBypassEnabled */);
}
@Override
protected void stopHalOperation() {
}
@Override
protected void startHalOperation() {
}
@Override
protected void handleLifecycleAfterAuth(boolean authenticated) {
}
@Override
public boolean wasUserDetected() {
return false;
}
public void cancel() {
mNumCancels++;
super.cancel();
}
}
private static class TestClientMonitor2 extends TestClientMonitor {
private final int mProtoEnum;
public TestClientMonitor2(@NonNull Context context, @NonNull IBinder token,
@NonNull LazyDaemon<Object> lazyDaemon, int protoEnum) {
super(context, token, lazyDaemon);
mProtoEnum = protoEnum;
}
@Override
public int getProtoEnum() {
return mProtoEnum;
}
}
private static class TestClientMonitor extends HalClientMonitor<Object> {
private boolean mUnableToStart;
private boolean mStarted;
private boolean mDestroyed;
public TestClientMonitor(@NonNull Context context, @NonNull IBinder token,
@NonNull LazyDaemon<Object> lazyDaemon) {
this(context, token, lazyDaemon, 0 /* cookie */);
}
public TestClientMonitor(@NonNull Context context, @NonNull IBinder token,
@NonNull LazyDaemon<Object> lazyDaemon, int cookie) {
super(context, lazyDaemon, token /* token */, null /* listener */, 0 /* userId */,
TAG, cookie, TEST_SENSOR_ID, 0 /* statsModality */,
0 /* statsAction */, 0 /* statsClient */);
}
@Override
public void unableToStart() {
assertFalse(mUnableToStart);
mUnableToStart = true;
}
@Override
public int getProtoEnum() {
// Anything other than CM_NONE, which is used to represent "idle". Tests that need
// real proto enums should use TestClientMonitor2
return BiometricsProto.CM_UPDATE_ACTIVE_USER;
}
@Override
public void start(@NonNull Callback callback) {
super.start(callback);
assertFalse(mStarted);
mStarted = true;
}
@Override
protected void startHalOperation() {
}
@Override
public void destroy() {
mDestroyed = true;
}
public boolean wasUnableToStart() {
return mUnableToStart;
}
public boolean hasStarted() {
return mStarted;
}
public boolean wasDestroyed() {
return mDestroyed;
}
}
private static void waitForIdle() {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
}