Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e9ed5d6c authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Handle case where ClientMonitors should not be started"

parents 67199f70 336ad77e
Loading
Loading
Loading
Loading
+79 −17
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;

import java.io.PrintWriter;
@@ -37,9 +38,9 @@ import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Date;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Queue;

/**
 * A scheduler for biometric HAL operations. Maintains a queue of {@link ClientMonitor} operations,
@@ -53,7 +54,8 @@ public class BiometricScheduler {
    /**
     * Contains all the necessary information for a HAL operation.
     */
    private static final class Operation {
    @VisibleForTesting
    static final class Operation {

        /**
         * The operation is added to the list of pending operations and waiting for its turn.
@@ -176,8 +178,8 @@ public class BiometricScheduler {
    @NonNull private final IBiometricService mBiometricService;
    @NonNull private final Handler mHandler = new Handler(Looper.getMainLooper());
    @NonNull private final InternalCallback mInternalCallback;
    @NonNull private final Queue<Operation> mPendingOperations;
    @Nullable private Operation mCurrentOperation;
    @VisibleForTesting @NonNull final Deque<Operation> mPendingOperations;
    @VisibleForTesting @Nullable Operation mCurrentOperation;
    @NonNull private final ArrayDeque<CrashState> mCrashStates;

    // Internal callback, notified when an operation is complete. Notifies the requester
@@ -226,6 +228,18 @@ public class BiometricScheduler {
        }
    }

    @VisibleForTesting
    BiometricScheduler(@NonNull String tag,
            @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
            @NonNull IBiometricService biometricService) {
        mBiometricTag = tag;
        mInternalCallback = new InternalCallback();
        mGestureAvailabilityDispatcher = gestureAvailabilityDispatcher;
        mPendingOperations = new ArrayDeque<>();
        mBiometricService = biometricService;
        mCrashStates = new ArrayDeque<>();
    }

    /**
     * Creates a new scheduler.
     * @param tag for the specific instance of the scheduler. Should be unique.
@@ -234,13 +248,8 @@ public class BiometricScheduler {
     */
    public BiometricScheduler(@NonNull String tag,
            @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
        mBiometricTag = tag;
        mInternalCallback = new InternalCallback();
        mGestureAvailabilityDispatcher = gestureAvailabilityDispatcher;
        mPendingOperations = new ArrayDeque<>();
        mBiometricService = IBiometricService.Stub.asInterface(
                ServiceManager.getService(Context.BIOMETRIC_SERVICE));
        mCrashStates = new ArrayDeque<>();
        this(tag, gestureAvailabilityDispatcher, IBiometricService.Stub.asInterface(
                ServiceManager.getService(Context.BIOMETRIC_SERVICE)));
    }

    /**
@@ -295,9 +304,50 @@ public class BiometricScheduler {
        // to arrive at the head of the queue, before pinging it to start.
        final boolean shouldStartNow = currentClient.getCookie() == 0;
        if (shouldStartNow) {
            if (mCurrentOperation.clientMonitor.getFreshDaemon() == null) {
                // Note down current length of queue
                final int pendingOperationsLength = mPendingOperations.size();
                final Operation lastOperation = mPendingOperations.peekLast();
                Slog.e(getTag(), "[Unable To Start] " + mCurrentOperation
                        + ". Last pending operation: " + lastOperation);

                // For current operations, 1) unableToStart, which notifies the caller-side, then
                // 2) notify operation's callback, to notify applicable system service that the
                // operation failed.
                mCurrentOperation.clientMonitor.unableToStart();
                if (mCurrentOperation.mClientCallback != null) {
                    mCurrentOperation.mClientCallback
                            .onClientFinished(mCurrentOperation.clientMonitor, false /* success */);
                }

                // Then for each operation currently in the pending queue at the time of this
                // failure, do the same as above. Otherwise, it's possible that something like
                // setActiveUser fails, but then authenticate (for the wrong user) is invoked.
                for (int i = 0; i < pendingOperationsLength; i++) {
                    final Operation operation = mPendingOperations.pollFirst();
                    if (operation == null) {
                        Slog.e(getTag(), "Null operation, index: " + i
                                + ", expected length: " + pendingOperationsLength);
                        break;
                    }
                    operation.clientMonitor.unableToStart();
                    if (operation.mClientCallback != null) {
                        operation.mClientCallback.onClientFinished(operation.clientMonitor,
                                false /* success */);
                    }
                    Slog.w(getTag(), "[Aborted Operation] " + operation);
                }

                // It's possible that during cleanup a new set of operations came in. We can try to
                // run these. A single request from the manager layer to the service layer may
                // actually be multiple operations (i.e. updateActiveUser + authenticate).
                mCurrentOperation = null;
                startNextOperationIfIdle();
            } else {
                Slog.d(getTag(), "[Starting] " + mCurrentOperation);
                currentClient.start(getInternalCallback());
                mCurrentOperation.state = Operation.STATE_STARTED;
            }
        } else {
            try {
                mBiometricService.onReadyForAuthentication(currentClient.getCookie());
@@ -338,10 +388,22 @@ public class BiometricScheduler {
            return;
        }

        if (mCurrentOperation.clientMonitor.getFreshDaemon() == null) {
            Slog.e(getTag(), "[Unable To Start] Prepared client: " + mCurrentOperation);
            // This is BiometricPrompt trying to auth but something's wrong with the HAL.
            mCurrentOperation.clientMonitor.unableToStart();
            if (mCurrentOperation.mClientCallback != null) {
                mCurrentOperation.mClientCallback.onClientFinished(mCurrentOperation.clientMonitor,
                        false /* success */);
            }
            mCurrentOperation = null;
            startNextOperationIfIdle();
        } else {
            Slog.d(getTag(), "[Starting] Prepared client: " + mCurrentOperation);
            mCurrentOperation.state = Operation.STATE_STARTED;
            mCurrentOperation.clientMonitor.start(getInternalCallback());
        }
    }

    /**
     * Adds a {@link ClientMonitor} to the pending queue
+132 −5
Original line number Diff line number Diff line
@@ -16,11 +16,21 @@

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.mockito.ArgumentMatchers.any;
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 android.content.Context;
import android.hardware.biometrics.IBiometricService;
import android.platform.test.annotations.Presubmit;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;

@@ -40,18 +50,21 @@ public class BiometricSchedulerTest {
    @Mock
    private Context mContext;
    @Mock
    private ClientMonitor.LazyDaemon<Object> mLazyDaemon;
    private IBiometricService mBiometricService;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mScheduler = new BiometricScheduler(TAG, null /* gestureAvailabilityTracker */);
        mScheduler = new BiometricScheduler(TAG, null /* gestureAvailabilityTracker */,
                mBiometricService);
    }

    @Test
    public void testClientDuplicateFinish_ignoredBySchedulerAndDoesNotCrash() {
        final ClientMonitor<Object> client1 = new TestClientMonitor(mContext, mLazyDaemon);
        final ClientMonitor<Object> client2 = new TestClientMonitor(mContext, mLazyDaemon);
        final ClientMonitor.LazyDaemon<Object> nonNullDaemon = () -> mock(Object.class);

        final ClientMonitor<Object> client1 = new TestClientMonitor(mContext, nonNullDaemon);
        final ClientMonitor<Object> client2 = new TestClientMonitor(mContext, nonNullDaemon);
        mScheduler.scheduleClientMonitor(client1);
        mScheduler.scheduleClientMonitor(client2);

@@ -59,7 +72,93 @@ public class BiometricSchedulerTest {
        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 ClientMonitor.LazyDaemon<Object> lazyDaemon1 = () -> null;
        final ClientMonitor.LazyDaemon<Object> lazyDaemon2 = () -> daemon2;

        final TestClientMonitor client1 = new TestClientMonitor(mContext, lazyDaemon1);
        final TestClientMonitor client2 = new TestClientMonitor(mContext, lazyDaemon2);

        final ClientMonitor.Callback callback1 = mock(ClientMonitor.Callback.class);
        final ClientMonitor.Callback callback2 = mock(ClientMonitor.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(ClientMonitor.class), mock(ClientMonitor.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() {
        // Second non-BiometricPrompt client has a valid daemon
        final Object daemon2 = mock(Object.class);

        final ClientMonitor.LazyDaemon<Object> lazyDaemon1 = () -> null;
        final ClientMonitor.LazyDaemon<Object> lazyDaemon2 = () -> daemon2;

        final TestClientMonitor client1 =
                new TestBiometricPromptClientMonitor(mContext, lazyDaemon1);
        final TestClientMonitor client2 = new TestClientMonitor(mContext, lazyDaemon2);

        final ClientMonitor.Callback callback1 = mock(ClientMonitor.Callback.class);
        final ClientMonitor.Callback callback2 = mock(ClientMonitor.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(ClientMonitor.class), mock(ClientMonitor.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());

        assertTrue(client1.wasUnableToStart());
        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));
    }

    private static class TestBiometricPromptClientMonitor extends TestClientMonitor {
        public TestBiometricPromptClientMonitor(@NonNull Context context,
                @NonNull LazyDaemon<Object> lazyDaemon) {
            super(context, lazyDaemon, 1 /* cookie */);
        }
    }

    private static class TestClientMonitor extends ClientMonitor<Object> {
        private boolean mUnableToStart;
        private boolean mStarted;

        public TestClientMonitor(@NonNull Context context, @NonNull LazyDaemon<Object> lazyDaemon) {
            super(context, lazyDaemon, null /* token */, null /* listener */, 0 /* userId */,
@@ -67,14 +166,42 @@ public class BiometricSchedulerTest {
                    0 /* statsAction */, 0 /* statsClient */);
        }

        public TestClientMonitor(@NonNull Context context, @NonNull LazyDaemon<Object> lazyDaemon,
                int cookie) {
            super(context, lazyDaemon, null /* token */, null /* listener */, 0 /* userId */,
                    TAG, cookie, 0 /* sensorId */, 0 /* statsModality */,
                    0 /* statsAction */, 0 /* statsClient */);
        }


        @Override
        public void unableToStart() {
            assertFalse(mUnableToStart);
            mUnableToStart = true;
        }

        @Override
        public void start(@NonNull Callback callback) {
            super.start(callback);
            assertFalse(mStarted);
            mStarted = true;
        }

        @Override
        protected void startHalOperation() {

        }

        public boolean wasUnableToStart() {
            return mUnableToStart;
        }

        public boolean hasStarted() {
            return mStarted;
        }
    }

    private static void waitForIdle() {
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
    }
}