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

Commit 7860852d authored by Dave Mankoff's avatar Dave Mankoff
Browse files

Connect FalsingManager to HistoryTracker.

With this change, the analysis of gestures actually gets added to our
HistoryTracker. Prior to this, HistoryTracker was only ever being
exercised in tests.

The one trick that this addresses is that invalid single-taps can't
immediately be added to the HistoryTracker, as they may become
_valid_ double taps. We don't want double taps to be penalized.

Bug: 172655679
Test: atest SystemUITests
Change-Id: I2e5ece6af82eb20f053b6b17298dcd9002236e39
parent 8848abd1
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -120,6 +120,12 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
        }
    }

    @Override
    protected void onViewDetached() {
        super.onViewDetached();
        mView.removeMotionEventListener(mGlobalTouchListener);
    }

    @Override
    public void onResume(int reason) {
        super.onResume(reason);
+93 −37
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.classifier;

import static com.android.systemui.classifier.FalsingManagerProxy.FALSING_SUCCESS;
import static com.android.systemui.classifier.FalsingModule.BRIGHT_LINE_GESTURE_CLASSIFERS;
import static com.android.systemui.classifier.FalsingModule.DOUBLE_TAP_TIMEOUT_MS;

import android.net.Uri;
import android.os.Build;
@@ -28,23 +29,26 @@ import androidx.annotation.NonNull;

import com.android.internal.logging.MetricsLogger;
import com.android.systemui.classifier.FalsingDataProvider.SessionListener;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.TestHarness;
import com.android.systemui.dock.DockManager;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.sensors.ThresholdSensor;
import com.android.systemui.util.time.SystemClock;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;
import javax.inject.Named;
@@ -65,7 +69,8 @@ public class BrightLineFalsingManager implements FalsingManager {
    private final SingleTapClassifier mSingleTapClassifier;
    private final DoubleTapClassifier mDoubleTapClassifier;
    private final HistoryTracker mHistoryTracker;
    private final SystemClock mSystemClock;
    private final DelayableExecutor mDelayableExecutor;
    private final long mDoubleTapTimeMs;
    private final boolean mTestHarness;
    private final MetricsLogger mMetricsLogger;
    private int mIsFalseTouchCalls;
@@ -91,22 +96,43 @@ public class BrightLineFalsingManager implements FalsingManager {
    private final FalsingDataProvider.GestureCompleteListener mGestureCompleteListener =
            new FalsingDataProvider.GestureCompleteListener() {
                @Override
        public void onGestureComplete() {
        public void onGestureComplete(long completionTimeMs) {
            if (mPriorResults != null) {
                // Single taps that may become double taps don't get added right away.
                if (mClassifyAsSingleTap) {
                    Collection<FalsingClassifier.Result> singleTapResults = mPriorResults;
                    mSingleTapHistoryCanceller = mDelayableExecutor.executeDelayed(
                            () -> {
                                mSingleTapHistoryCanceller = null;
                                mHistoryTracker.addResults(singleTapResults, completionTimeMs);
                            },
                            mDoubleTapTimeMs);
                    mClassifyAsSingleTap = false;  // Don't treat things as single taps by default.
                } else {
                    mHistoryTracker.addResults(mPriorResults, completionTimeMs);
                }
                mPriorResults = null;
            } else {
                // Gestures that were not classified get treated as a false.
                mHistoryTracker.addResults(
                    mClassifiers.stream().map(FalsingClassifier::classifyGesture)
                            .collect(Collectors.toCollection(ArrayList::new)),
                    mSystemClock.uptimeMillis());
                        Collections.singleton(
                                FalsingClassifier.Result.falsed(.8, "unclassified")),
                        completionTimeMs);
            }
        }
    };

    private boolean mPreviousResult = false;
    private Collection<FalsingClassifier.Result> mPriorResults;
    private boolean mClassifyAsSingleTap;
    private Runnable mSingleTapHistoryCanceller;

    @Inject
    public BrightLineFalsingManager(FalsingDataProvider falsingDataProvider,
            DockManager dockManager, MetricsLogger metricsLogger,
            @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers,
            SingleTapClassifier singleTapClassifier, DoubleTapClassifier doubleTapClassifier,
            HistoryTracker historyTracker, SystemClock systemClock,
            HistoryTracker historyTracker, @Main DelayableExecutor delayableExecutor,
            @Named(DOUBLE_TAP_TIMEOUT_MS) long doubleTapTimeMs,
            @TestHarness boolean testHarness) {
        mDataProvider = falsingDataProvider;
        mDockManager = dockManager;
@@ -115,7 +141,8 @@ public class BrightLineFalsingManager implements FalsingManager {
        mSingleTapClassifier = singleTapClassifier;
        mDoubleTapClassifier = doubleTapClassifier;
        mHistoryTracker = historyTracker;
        mSystemClock = systemClock;
        mDelayableExecutor = delayableExecutor;
        mDoubleTapTimeMs = doubleTapTimeMs;
        mTestHarness = testHarness;

        mDataProvider.addSessionListener(mSessionListener);
@@ -129,38 +156,51 @@ public class BrightLineFalsingManager implements FalsingManager {

    @Override
    public boolean isFalseTouch(@Classifier.InteractionType int interactionType) {
        boolean result;

        mClassifyAsSingleTap = false;
        mDataProvider.setInteractionType(interactionType);
        if (!mDataProvider.isDirty()) {
            return mPreviousResult;
        }

        mPreviousResult = !mTestHarness
                && !mDataProvider.isJustUnlockedWithFace() && !mDockManager.isDocked()
                && mClassifiers.stream().anyMatch(falsingClassifier -> {
                    FalsingClassifier.Result result = falsingClassifier.classifyGesture(
                            mHistoryTracker.falsePenalty(), mHistoryTracker.falseConfidence());
                    if (result.isFalse()) {
        if (!mTestHarness && !mDataProvider.isJustUnlockedWithFace() && !mDockManager.isDocked()) {
            Stream<FalsingClassifier.Result> results =
                    mClassifiers.stream().map(falsingClassifier -> {
                        FalsingClassifier.Result classifierResult =
                                falsingClassifier.classifyGesture(
                                        mHistoryTracker.falsePenalty(),
                                        mHistoryTracker.falseConfidence());
                        if (classifierResult.isFalse()) {
                            logInfo(String.format(
                                    (Locale) null,
                                    "{classifier=%s, interactionType=%d}",
                                    falsingClassifier.getClass().getName(),
                                    mDataProvider.getInteractionType()));
                        String reason = result.getReason();
                            String reason = classifierResult.getReason();
                            if (reason != null) {
                                logInfo(reason);
                            }
                        } else {
                            logDebug(falsingClassifier.getClass().getName() + ": false");
                        }
                    return result.isFalse();
                        return classifierResult;
                    });
            mPriorResults = new ArrayList<>();
            final boolean[] localResult = {false};
            results.forEach(classifierResult -> {
                localResult[0] |= classifierResult.isFalse();
                mPriorResults.add(classifierResult);
            });
            result = localResult[0];
        } else {
            result = false;
            mPriorResults = Collections.singleton(FalsingClassifier.Result.passed(1));
        }

        logDebug("Is false touch? " + mPreviousResult);
        logDebug("Is false touch? " + result);

        if (Build.IS_ENG || Build.IS_USERDEBUG) {
            // Copy motion events, as the passed in list gets emptied out elsewhere in the code.
            RECENT_SWIPES.add(new DebugSwipeRecord(
                    mPreviousResult,
                    result,
                    mDataProvider.getInteractionType(),
                    mDataProvider.getRecentMotionEvents().stream().map(
                            motionEvent -> new XYDt(
@@ -173,13 +213,16 @@ public class BrightLineFalsingManager implements FalsingManager {
            }
        }

        return mPreviousResult;
        return result;
    }

    @Override
    public boolean isFalseTap(boolean robustCheck) {
        mClassifyAsSingleTap = true;

        FalsingClassifier.Result singleTapResult =
                mSingleTapClassifier.isTap(mDataProvider.getRecentMotionEvents());
        mPriorResults = Collections.singleton(singleTapResult);
        if (singleTapResult.isFalse()) {
            logInfo(String.format(
                    (Locale) null, "{classifier=%s}", mSingleTapClassifier.getClass().getName()));
@@ -192,7 +235,12 @@ public class BrightLineFalsingManager implements FalsingManager {

        // TODO(b/172655679): More heuristics to come. For now, allow touches through if face-authed
        if (robustCheck) {
            return !mDataProvider.isJustUnlockedWithFace();
            boolean result = !mDataProvider.isJustUnlockedWithFace();
            mPriorResults = Collections.singleton(
                    result ? FalsingClassifier.Result.falsed(0.1, "no face detected")
                            : FalsingClassifier.Result.passed(1));

            return result;
        }

        return false;
@@ -200,7 +248,9 @@ public class BrightLineFalsingManager implements FalsingManager {

    @Override
    public boolean isFalseDoubleTap() {
        mClassifyAsSingleTap = false;
        FalsingClassifier.Result result = mDoubleTapClassifier.classifyGesture();
        mPriorResults = Collections.singleton(result);
        if (result.isFalse()) {
            logInfo(String.format(
                    (Locale) null, "{classifier=%s}", mDoubleTapClassifier.getClass().getName()));
@@ -208,6 +258,12 @@ public class BrightLineFalsingManager implements FalsingManager {
            if (reason != null) {
                logInfo(reason);
            }
        } else {
            // A valid double tap prevents an invalid single tap from going into history.
            if (mSingleTapHistoryCanceller != null) {
                mSingleTapHistoryCanceller.run();
                mSingleTapHistoryCanceller = null;
            }
        }
        return result.isFalse();
    }
+19 −12
Original line number Diff line number Diff line
@@ -90,20 +90,34 @@ public class FalsingDataProvider {
        }

        if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
            if (!mRecentMotionEvents.isEmpty()) {
                mExtendedMotionEvents.addFirst(mRecentMotionEvents);
            completePriorGesture();
            mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS);
        }
        }
        mRecentMotionEvents.addAll(motionEvents);

        FalsingClassifier.logDebug("Size: " + mRecentMotionEvents.size());

        mMotionEventListeners.forEach(listener -> listener.onMotionEvent(motionEvent));

        // We explicitly do not complete a gesture on UP or CANCEL events.
        // We wait for the next gesture to start before marking the prior gesture as complete.  This
        // has multiple benefits. First, it makes it trivial to track the "current" or "recent"
        // gesture, as it will always be found in mRecentMotionEvents. Second, and most importantly,
        // it ensures that the current gesture doesn't get added to this HistoryTracker before it
        // is analyzed.

        mDirty = true;
    }

    private void completePriorGesture() {
        if (!mRecentMotionEvents.isEmpty()) {
            mGestuerCompleteListeners.forEach(listener -> listener.onGestureComplete(
                    mRecentMotionEvents.get(mRecentMotionEvents.size() - 1).getEventTime()));

            mExtendedMotionEvents.addFirst(mRecentMotionEvents);
        }
    }

    /** Returns screen width in pixels. */
    public int getWidthPixels() {
        return mWidthPixels;
@@ -146,13 +160,6 @@ public class FalsingDataProvider {
        }
    }

    /**
     * Returns true if new data has been supplied since the last time this class has been accessed.
     */
    public boolean isDirty() {
        return mDirty;
    }

    /** Return the interaction type that is being compared against for falsing. */
    public  final int getInteractionType() {
        return mInteractionType;
@@ -387,6 +394,6 @@ public class FalsingDataProvider {
    /** Callback to be alerted when the current gesture ends. */
    public interface GestureCompleteListener {
        /** */
        void onGestureComplete();
        void onGestureComplete(long completionTimeMs);
    }
}
+72 −14
Original line number Diff line number Diff line
@@ -16,12 +16,15 @@

package com.android.systemui.classifier;

import static com.android.systemui.util.mockito.KotlinMockitoHelpersKt.any;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@@ -35,6 +38,7 @@ import com.android.internal.logging.testing.FakeMetricsLogger;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.classifier.FalsingDataProvider.GestureCompleteListener;
import com.android.systemui.dock.DockManagerFake;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;

import org.junit.Before;
@@ -45,7 +49,6 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -53,6 +56,8 @@ import java.util.Set;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class BrightLineClassifierTest extends SysuiTestCase {
    private static final long DOUBLE_TAP_TIMEOUT_MS = 1000;

    private BrightLineFalsingManager mBrightLineFalsingManager;
    @Mock
    private FalsingDataProvider mFalsingDataProvider;
@@ -69,24 +74,35 @@ public class BrightLineClassifierTest extends SysuiTestCase {
    private FalsingClassifier mClassifierB;
    private final List<MotionEvent> mMotionEventList = new ArrayList<>();
    @Mock
    private HistoryTracker mHistoryTracker;
    private FakeSystemClock mSystemClock = new FakeSystemClock();
    private HistoryTracker mHistoryTracker;;
    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());

    private final FalsingClassifier.Result mFalsedResult = FalsingClassifier.Result.falsed(1, "");
    private final FalsingClassifier.Result mPassedResult = FalsingClassifier.Result.passed(1);
    private GestureCompleteListener mGestureCompleteListener;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        when(mClassifierA.classifyGesture(anyDouble(), anyDouble())).thenReturn(mPassedResult);
        when(mClassifierB.classifyGesture(anyDouble(), anyDouble())).thenReturn(mPassedResult);
        when(mSingleTapClassfier.isTap(any(List.class))).thenReturn(mPassedResult);
        when(mDoubleTapClassifier.classifyGesture()).thenReturn(mPassedResult);
        mClassifiers.add(mClassifierA);
        mClassifiers.add(mClassifierB);
        when(mFalsingDataProvider.isDirty()).thenReturn(true);
        when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList);
        mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mDockManager,
                mMetricsLogger, mClassifiers, mSingleTapClassfier, mDoubleTapClassifier,
                mHistoryTracker, mSystemClock, false);
                mHistoryTracker, mFakeExecutor, DOUBLE_TAP_TIMEOUT_MS, false);


        ArgumentCaptor<GestureCompleteListener> gestureCompleteListenerCaptor =
                ArgumentCaptor.forClass(GestureCompleteListener.class);

        verify(mFalsingDataProvider).addGestureCompleteListener(
                gestureCompleteListenerCaptor.capture());

        mGestureCompleteListener = gestureCompleteListenerCaptor.getValue();
    }

    @Test
@@ -179,15 +195,57 @@ public class BrightLineClassifierTest extends SysuiTestCase {

    @Test
    public void testHistory() {
        ArgumentCaptor<GestureCompleteListener> gestureCompleteListenerCaptor =
                ArgumentCaptor.forClass(GestureCompleteListener.class);
        mGestureCompleteListener.onGestureComplete(1000);

        verify(mFalsingDataProvider).addGestureCompleteListener(
                gestureCompleteListenerCaptor.capture());
        verify(mHistoryTracker).addResults(anyCollection(), eq(1000L));
    }

    @Test
    public void testHistory_singleTap() {
        // When trying to classify single taps, we don't immediately add results to history.
        mBrightLineFalsingManager.isFalseTap(false);
        mGestureCompleteListener.onGestureComplete(1000);

        verify(mHistoryTracker, never()).addResults(any(), anyLong());

        GestureCompleteListener gestureCompleteListener = gestureCompleteListenerCaptor.getValue();
        gestureCompleteListener.onGestureComplete();
        mFakeExecutor.advanceClockToNext();
        mFakeExecutor.runAllReady();

        verify(mHistoryTracker).addResults(any(Collection.class), eq(mSystemClock.uptimeMillis()));
        verify(mHistoryTracker).addResults(anyCollection(), eq(1000L));
    }

    @Test
    public void testHistory_multipleSingleTaps() {
        // When trying to classify single taps, we don't immediately add results to history.
        mBrightLineFalsingManager.isFalseTap(false);
        mGestureCompleteListener.onGestureComplete(1000);
        mBrightLineFalsingManager.isFalseTap(false);
        mGestureCompleteListener.onGestureComplete(2000);

        verify(mHistoryTracker, never()).addResults(any(), anyLong());

        mFakeExecutor.advanceClockToNext();
        mFakeExecutor.runNextReady();
        verify(mHistoryTracker).addResults(anyCollection(), eq(1000L));
        reset(mHistoryTracker);
        mFakeExecutor.advanceClockToNext();
        mFakeExecutor.runNextReady();
        verify(mHistoryTracker).addResults(anyCollection(), eq(2000L));
    }

    @Test
    public void testHistory_doubleTap() {
        // When trying to classify single taps, we don't immediately add results to history.
        mBrightLineFalsingManager.isFalseTap(false);
        mGestureCompleteListener.onGestureComplete(1000);
        // Before checking for double tap, we may check for single-tap on the second gesture.
        mBrightLineFalsingManager.isFalseTap(false);
        mBrightLineFalsingManager.isFalseDoubleTap();
        mGestureCompleteListener.onGestureComplete(2000);

        // Double tap is immediately added to history. Single tap is never added.
        verify(mHistoryTracker).addResults(anyCollection(), eq(2000L));

        assertThat(mFakeExecutor.numPending()).isEqualTo(0);
    }
}
+1 −1

File changed.

Contains only whitespace changes.