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

Commit 4613aa95 authored by Joe Bolinger's avatar Joe Bolinger
Browse files

Add a final lux method to ALSProbe.

This address two remaining issues with with ALS logging since the pointer events were removed from the framework (starting at commit aa07cc3d).

1) After the pointer events were removed the logging was updated to use wakefullness events as an alternative trigger. However, these events are often too slow when authenticating from the AoD and the sensor may not have a chance to be enabled or respond before the operation completes. The new method allows the sensor to be enabled after the operation has completed to fetch a value.
2) Fixes a bug where the initial wakefullness status was not checked at the start of an auth operation.

Fix: 253318983
Test: atest ALSProbeTest FingerprintAuthenticationClientTest BiometricLoggerTest
Test: statsd_testdrive 88 (verify lux field is always greater than 0 from AoD and with BP test app)
Change-Id: I2b1572579f52dc9dad7238d47e27b0068c7c74c6
parent 26ed84b7
Loading
Loading
Loading
Loading
+95 −5
Original line number Diff line number Diff line
@@ -30,7 +30,10 @@ import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.sensors.BaseClientMonitor;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/** Probe for ambient light. */
final class ALSProbe implements Probe {
@@ -47,12 +50,18 @@ final class ALSProbe implements Probe {

    private boolean mEnabled = false;
    private boolean mDestroyed = false;
    private boolean mDestroyRequested = false;
    private boolean mDisableRequested = false;
    private volatile NextConsumer mNextConsumer = null;
    private volatile float mLastAmbientLux = -1;

    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            mLastAmbientLux = event.values[0];
            if (mNextConsumer != null) {
                completeNextConsumer(mLastAmbientLux);
            }
        }

        @Override
@@ -102,29 +111,84 @@ final class ALSProbe implements Probe {

    @Override
    public synchronized void enable() {
        if (!mDestroyed) {
        if (!mDestroyed && !mDestroyRequested) {
            mDisableRequested = false;
            enableLightSensorLoggingLocked();
        }
    }

    @Override
    public synchronized void disable() {
        if (!mDestroyed) {
        mDisableRequested = true;

        // if a final consumer is set it will call destroy/disable on the next value if requested
        if (!mDestroyed && mNextConsumer == null) {
            disableLightSensorLoggingLocked();
        }
    }

    @Override
    public synchronized void destroy() {
        mDestroyRequested = true;

        // if a final consumer is set it will call destroy/disable on the next value if requested
        if (!mDestroyed && mNextConsumer == null) {
            disable();
            mDestroyed = true;
        }
    }

    /** The most recent lux reading. */
    public float getCurrentLux() {
    public float getMostRecentLux() {
        return mLastAmbientLux;
    }

    /**
     * Register a listener for the next available ALS reading, which will be reported to the given
     * consumer even if this probe is {@link #disable()}'ed or {@link #destroy()}'ed before a value
     * is available.
     *
     * This method is intended to be used for event logs that occur when the screen may be
     * off and sampling may have been {@link #disable()}'ed. In these cases, this method will turn
     * on the sensor (if needed), fetch & report the first value, and then destroy or disable this
     * probe (if needed).
     *
     * @param consumer consumer to notify when the data is available
     * @param handler handler for notifying the consumer, or null
     */
    public synchronized void awaitNextLux(@NonNull Consumer<Float> consumer,
            @Nullable Handler handler) {
        final NextConsumer nextConsumer = new NextConsumer(consumer, handler);
        final float current = mLastAmbientLux;
        if (current > 0) {
            nextConsumer.consume(current);
        } else if (mDestroyed) {
            nextConsumer.consume(-1f);
        } else if (mNextConsumer != null) {
            mNextConsumer.add(nextConsumer);
        } else {
            mNextConsumer = nextConsumer;
            enableLightSensorLoggingLocked();
        }
    }

    private synchronized void completeNextConsumer(float value) {
        Slog.v(TAG, "Finishing next consumer");

        final NextConsumer consumer = mNextConsumer;
        mNextConsumer = null;

        if (mDestroyRequested) {
            destroy();
        } else if (mDisableRequested) {
            disable();
        }

        if (consumer != null) {
            consumer.consume(value);
        }
    }

    private void enableLightSensorLoggingLocked() {
        if (!mEnabled) {
            mEnabled = true;
@@ -160,4 +224,30 @@ final class ALSProbe implements Probe {
                + mLightSensorListener.hashCode());
        disable();
    }

    private static class NextConsumer {
        @NonNull private final Consumer<Float> mConsumer;
        @Nullable private final Handler mHandler;
        @NonNull private final List<NextConsumer> mOthers = new ArrayList<>();

        private NextConsumer(@NonNull Consumer<Float> consumer, @Nullable Handler handler) {
            mConsumer = consumer;
            mHandler = handler;
        }

        public void consume(float value) {
            if (mHandler != null) {
                mHandler.post(() -> mConsumer.accept(value));
            } else {
                mConsumer.accept(value);
            }
            for (NextConsumer c : mOthers) {
                c.consume(value);
            }
        }

        public void add(NextConsumer consumer) {
            mOthers.add(consumer);
        }
    }
}
+11 −2
Original line number Diff line number Diff line
@@ -62,8 +62,7 @@ public class BiometricFrameworkStatsLogger {
    /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */
    public void authenticate(OperationContext operationContext,
            int statsModality, int statsAction, int statsClient, boolean isDebug, long latency,
            int authState, boolean requireConfirmation,
            int targetUserId, float ambientLightLux) {
            int authState, boolean requireConfirmation, int targetUserId, float ambientLightLux) {
        FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_AUTHENTICATED,
                statsModality,
                targetUserId,
@@ -80,6 +79,16 @@ public class BiometricFrameworkStatsLogger {
                operationContext.isAod);
    }

    /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */
    public void authenticate(OperationContext operationContext,
            int statsModality, int statsAction, int statsClient, boolean isDebug, long latency,
            int authState, boolean requireConfirmation, int targetUserId, ALSProbe alsProbe) {
        alsProbe.awaitNextLux((ambientLightLux) -> {
            authenticate(operationContext, statsModality, statsAction, statsClient, isDebug,
                    latency, authState, requireConfirmation, targetUserId, ambientLightLux);
        }, null /* handler */);
    }

    /** {@see FrameworkStatsLog.BIOMETRIC_ENROLLED}. */
    public void enroll(int statsModality, int statsAction, int statsClient,
            int targetUserId, long latency, boolean enrollSuccessful, float ambientLightLux) {
+4 −4
Original line number Diff line number Diff line
@@ -220,7 +220,7 @@ public class BiometricLogger {
                    + ", RequireConfirmation: " + requireConfirmation
                    + ", State: " + authState
                    + ", Latency: " + latency
                    + ", Lux: " + mALSProbe.getCurrentLux());
                    + ", Lux: " + mALSProbe.getMostRecentLux());
        } else {
            Slog.v(TAG, "Authentication latency: " + latency);
        }
@@ -231,7 +231,7 @@ public class BiometricLogger {

        mSink.authenticate(operationContext, mStatsModality, mStatsAction, mStatsClient,
                Utils.isDebugEnabled(context, targetUserId),
                latency, authState, requireConfirmation, targetUserId, mALSProbe.getCurrentLux());
                latency, authState, requireConfirmation, targetUserId, mALSProbe);
    }

    /** Log enrollment outcome. */
@@ -245,7 +245,7 @@ public class BiometricLogger {
                    + ", User: " + targetUserId
                    + ", Client: " + mStatsClient
                    + ", Latency: " + latency
                    + ", Lux: " + mALSProbe.getCurrentLux()
                    + ", Lux: " + mALSProbe.getMostRecentLux()
                    + ", Success: " + enrollSuccessful);
        } else {
            Slog.v(TAG, "Enroll latency: " + latency);
@@ -256,7 +256,7 @@ public class BiometricLogger {
        }

        mSink.enroll(mStatsModality, mStatsAction, mStatsClient,
                targetUserId, latency, enrollSuccessful, mALSProbe.getCurrentLux());
                targetUserId, latency, enrollSuccessful, mALSProbe.getMostRecentLux());
    }

    /** Report unexpected enrollment reported by the HAL. */
+3 −0
Original line number Diff line number Diff line
@@ -333,6 +333,9 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession>
                mALSProbeCallback.getProbe().disable();
            }
        });
        if (getBiometricContext().isAwake()) {
            mALSProbeCallback.getProbe().enable();
        }

        if (session.hasContextMethods()) {
            return session.getSession().authenticateWithContext(mOperationId, opContext);
+154 −5
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -50,6 +51,9 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Presubmit
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -93,7 +97,7 @@ public class ALSProbeTest {
        mSensorEventListenerCaptor.getValue().onSensorChanged(
                new SensorEvent(mLightSensor, 1, 2, new float[]{value}));

        assertThat(mProbe.getCurrentLux()).isEqualTo(value);
        assertThat(mProbe.getMostRecentLux()).isEqualTo(value);
    }

    @Test
@@ -121,13 +125,17 @@ public class ALSProbeTest {
        mProbe.destroy();
        mProbe.enable();

        AtomicInteger lux = new AtomicInteger(10);
        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);

        verify(mSensorManager, never()).registerListener(any(), any(), anyInt());
        verifyNoMoreInteractions(mSensorManager);
        assertThat(lux.get()).isLessThan(0);
    }

    @Test
    public void testDisabledReportsNegativeValue() {
        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);

        mProbe.enable();
        verify(mSensorManager).registerListener(
@@ -136,7 +144,7 @@ public class ALSProbeTest {
                new SensorEvent(mLightSensor, 1, 1, new float[]{4.0f}));
        mProbe.disable();

        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
    }

    @Test
@@ -150,7 +158,7 @@ public class ALSProbeTest {

        verify(mSensorManager).unregisterListener(eq(mSensorEventListenerCaptor.getValue()));
        verifyNoMoreInteractions(mSensorManager);
        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
    }

    @Test
@@ -166,7 +174,148 @@ public class ALSProbeTest {

        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
        verifyNoMoreInteractions(mSensorManager);
        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
    }

    @Test
    public void testNextLuxWhenAlreadyEnabledAndNotAvailable() {
        testNextLuxWhenAlreadyEnabled(false /* dataIsAvailable */);
    }

    @Test
    public void testNextLuxWhenAlreadyEnabledAndAvailable() {
        testNextLuxWhenAlreadyEnabled(true /* dataIsAvailable */);
    }

    private void testNextLuxWhenAlreadyEnabled(boolean dataIsAvailable) {
        final List<Integer> values = List.of(1, 2, 3, 4, 6);
        mProbe.enable();

        verify(mSensorManager).registerListener(
                mSensorEventListenerCaptor.capture(), any(), anyInt());

        if (dataIsAvailable) {
            for (int v : values) {
                mSensorEventListenerCaptor.getValue().onSensorChanged(
                        new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
            }
        }
        AtomicInteger lux = new AtomicInteger(-1);
        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
        if (!dataIsAvailable) {
            for (int v : values) {
                mSensorEventListenerCaptor.getValue().onSensorChanged(
                        new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
            }
        }

        mSensorEventListenerCaptor.getValue().onSensorChanged(
                new SensorEvent(mLightSensor, 1, 1, new float[]{200f}));

        // should remain enabled
        assertThat(lux.get()).isEqualTo(values.get(dataIsAvailable ? values.size() - 1 : 0));
        verify(mSensorManager, never()).unregisterListener(any(SensorEventListener.class));
        verifyNoMoreInteractions(mSensorManager);

        final int anotherValue = 12;
        mSensorEventListenerCaptor.getValue().onSensorChanged(
                new SensorEvent(mLightSensor, 1, 1, new float[]{12}));
        assertThat(mProbe.getMostRecentLux()).isEqualTo(anotherValue);
    }

    @Test
    public void testNextLuxWhenNotEnabled() {
        testNextLuxWhenNotEnabled(false /* enableWhileWaiting */);
    }

    @Test
    public void testNextLuxWhenNotEnabledButEnabledLater() {
        testNextLuxWhenNotEnabled(true /* enableWhileWaiting */);
    }

    private void testNextLuxWhenNotEnabled(boolean enableWhileWaiting) {
        final List<Integer> values = List.of(1, 2, 3, 4, 6);
        mProbe.disable();

        AtomicInteger lux = new AtomicInteger(-1);
        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);

        if (enableWhileWaiting) {
            mProbe.enable();
        }

        verify(mSensorManager).registerListener(
                mSensorEventListenerCaptor.capture(), any(), anyInt());
        for (int v : values) {
            mSensorEventListenerCaptor.getValue().onSensorChanged(
                    new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
        }

        // should restore the disabled state
        assertThat(lux.get()).isEqualTo(values.get(0));
        verify(mSensorManager, enableWhileWaiting ? never() : times(1)).unregisterListener(
                any(SensorEventListener.class));
        verifyNoMoreInteractions(mSensorManager);
    }

    @Test
    public void testNextLuxIsNotCanceledByDisableOrDestroy() {
        final int value = 7;
        AtomicInteger lux = new AtomicInteger(-1);
        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);

        verify(mSensorManager).registerListener(
                mSensorEventListenerCaptor.capture(), any(), anyInt());

        mProbe.destroy();
        mProbe.disable();

        assertThat(lux.get()).isEqualTo(-1);

        mSensorEventListenerCaptor.getValue().onSensorChanged(
                new SensorEvent(mLightSensor, 1, 1, new float[]{value}));

        assertThat(lux.get()).isEqualTo(value);

        mSensorEventListenerCaptor.getValue().onSensorChanged(
                new SensorEvent(mLightSensor, 1, 1, new float[]{value + 1}));

        // should remain destroyed
        mProbe.enable();

        assertThat(lux.get()).isEqualTo(value);
        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
        verifyNoMoreInteractions(mSensorManager);
    }

    @Test
    public void testMultipleNextConsumers() {
        final int value = 7;
        AtomicInteger lux = new AtomicInteger(-1);
        AtomicInteger lux2 = new AtomicInteger(-1);
        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
        mProbe.awaitNextLux((v) -> lux2.set(Math.round(v)), null /* handler */);

        verify(mSensorManager).registerListener(
                mSensorEventListenerCaptor.capture(), any(), anyInt());
        mSensorEventListenerCaptor.getValue().onSensorChanged(
                new SensorEvent(mLightSensor, 1, 1, new float[]{value}));

        assertThat(lux.get()).isEqualTo(value);
        assertThat(lux2.get()).isEqualTo(value);
    }

    @Test
    public void testNoNextLuxWhenDestroyed() {
        mProbe.destroy();

        AtomicInteger lux = new AtomicInteger(-20);
        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);

        assertThat(lux.get()).isEqualTo(-1);
        verify(mSensorManager, never()).registerListener(
                mSensorEventListenerCaptor.capture(), any(), anyInt());
        verifyNoMoreInteractions(mSensorManager);
    }

    private void moveTimeBy(long millis) {
Loading