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

Commit 65b5769c authored by Dave Mankoff's avatar Dave Mankoff
Browse files

Add DistanceClassifier to the BrightLineFalsingManager

This requires swipes to travel a minimum distance and/or
fling a minimum distance.

Bug: 111394067
Test: atest SystemUITests
Change-Id: Iec90bb73b4108ce803f9247ebc30046e8c1a6a2d
parent d8efd0d4
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -67,6 +67,7 @@ public class BrightLineFalsingManager implements FalsingManager {
        mClassifiers.add(new PointerCountClassifier(mDataProvider));
        mClassifiers.add(new TypeClassifier(mDataProvider));
        mClassifiers.add(new DiagonalClassifier(mDataProvider));
        mClassifiers.add(new DistanceClassifier(mDataProvider));
    }

    private void registerSensors() {
+158 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.systemui.classifier.brightline;

import android.view.MotionEvent;
import android.view.VelocityTracker;

import java.util.List;

/**
 * Ensure that the swipe + momentum covers a minimum distance.
 */
class DistanceClassifier extends FalsingClassifier {

    private static final float HORIZONTAL_FLING_THRESHOLD_DISTANCE_IN = 1;
    private static final float VERTICAL_FLING_THRESHOLD_DISTANCE_IN = 1;
    private static final float HORIZONTAL_SWIPE_THRESHOLD_DISTANCE_IN = 3;
    private static final float VERTICAL_SWIPE_THRESHOLD_DISTANCE_IN = 3;
    private static final float VELOCITY_TO_DISTANCE = 80f;
    private static final float SCREEN_FRACTION_MIN_DISTANCE = 0.8f;

    private final float mVerticalFlingThresholdPx;
    private final float mHorizontalFlingThresholdPx;
    private final float mVerticalSwipeThresholdPx;
    private final float mHorizontalSwipeThresholdPx;

    private boolean mDistanceDirty;
    private DistanceVectors mCachedDistance;

    DistanceClassifier(FalsingDataProvider dataProvider) {
        super(dataProvider);

        mHorizontalFlingThresholdPx = Math
                .min(getWidthPixels() * SCREEN_FRACTION_MIN_DISTANCE,
                        HORIZONTAL_FLING_THRESHOLD_DISTANCE_IN * getXdpi());
        mVerticalFlingThresholdPx = Math
                .min(getHeightPixels() * SCREEN_FRACTION_MIN_DISTANCE,
                        VERTICAL_FLING_THRESHOLD_DISTANCE_IN * getYdpi());
        mHorizontalSwipeThresholdPx = Math
                .min(getWidthPixels() * SCREEN_FRACTION_MIN_DISTANCE,
                        HORIZONTAL_SWIPE_THRESHOLD_DISTANCE_IN * getXdpi());
        mVerticalSwipeThresholdPx = Math
                .min(getHeightPixels() * SCREEN_FRACTION_MIN_DISTANCE,
                        VERTICAL_SWIPE_THRESHOLD_DISTANCE_IN * getYdpi());
        mDistanceDirty = true;
    }

    private DistanceVectors getDistances() {
        if (mDistanceDirty) {
            mCachedDistance = calculateDistances();
            mDistanceDirty = false;
        }

        return mCachedDistance;
    }

    private DistanceVectors calculateDistances() {
        // This code assumes that there will be no missed DOWN or UP events.
        VelocityTracker velocityTracker = VelocityTracker.obtain();
        List<MotionEvent> motionEvents = getRecentMotionEvents();

        if (motionEvents.size() < 3) {
            logDebug("Only " + motionEvents.size() + " motion events recorded.");
            return new DistanceVectors(0, 0, 0, 0);
        }

        for (MotionEvent motionEvent : motionEvents) {
            velocityTracker.addMovement(motionEvent);
        }
        velocityTracker.computeCurrentVelocity(1);

        float vX = velocityTracker.getXVelocity();
        float vY = velocityTracker.getYVelocity();

        velocityTracker.recycle();

        float dX = getLastMotionEvent().getX() - getFirstMotionEvent().getX();
        float dY = getLastMotionEvent().getY() - getFirstMotionEvent().getY();

        logInfo("dX: " + dX + " dY: " + dY + " xV: " + vX + " yV: " + vY);

        return new DistanceVectors(dX, dY, vX, vY);
    }

    @Override
    public void onTouchEvent(MotionEvent motionEvent) {
        mDistanceDirty = true;
    }

    @Override
    public boolean isFalseTouch() {
        return !getDistances().getPassedFlingThreshold();
    }

    boolean isLongSwipe() {
        boolean longSwipe = getDistances().getPassedDistanceThreshold();
        logDebug("Is longSwipe? " + longSwipe);
        return longSwipe;
    }

    private class DistanceVectors {
        final float mDx;
        final float mDy;
        private final float mVx;
        private final float mVy;

        DistanceVectors(float dX, float dY, float vX, float vY) {
            this.mDx = dX;
            this.mDy = dY;
            this.mVx = vX;
            this.mVy = vY;
        }

        boolean getPassedDistanceThreshold() {
            if (isHorizontal()) {
                logDebug("Horizontal swipe distance: " + Math.abs(mDx));
                logDebug("Threshold: " + mHorizontalSwipeThresholdPx);

                return Math.abs(mDx) >= mHorizontalSwipeThresholdPx;
            }

            logDebug("Vertical swipe distance: " + Math.abs(mDy));
            logDebug("Threshold: " + mVerticalSwipeThresholdPx);
            return Math.abs(mDy) >= mVerticalSwipeThresholdPx;
        }

        boolean getPassedFlingThreshold() {
            float dX = this.mDx + this.mVx * VELOCITY_TO_DISTANCE;
            float dY = this.mDy + this.mVy * VELOCITY_TO_DISTANCE;

            if (isHorizontal()) {
                logDebug("Horizontal swipe and fling distance: " + this.mDx + ", "
                        + this.mVx * VELOCITY_TO_DISTANCE);
                logDebug("Threshold: " + mHorizontalFlingThresholdPx);
                return Math.abs(dX) >= mHorizontalFlingThresholdPx;
            }

            logDebug("Vertical swipe and fling distance: " + this.mDy + ", "
                    + this.mVy * VELOCITY_TO_DISTANCE);
            logDebug("Threshold: " + mVerticalFlingThresholdPx);
            return Math.abs(dY) >= mVerticalFlingThresholdPx;
        }
    }
}
+4 −4
Original line number Diff line number Diff line
@@ -66,19 +66,19 @@ abstract class FalsingClassifier {
    }

    int getWidthPixels() {
        return mDataProvider.mWidthPixels;
        return mDataProvider.getWidthPixels();
    }

    int getHeightPixels() {
        return mDataProvider.mHeightPixels;
        return mDataProvider.getHeightPixels();
    }

    float getXdpi() {
        return mDataProvider.mXdpi;
        return mDataProvider.getXdpi();
    }

    float getYdpi() {
        return mDataProvider.mYdpi;
        return mDataProvider.getYdpi();
    }

    final @Classifier.InteractionType int getInteractionType() {
+24 −6
Original line number Diff line number Diff line
@@ -35,10 +35,10 @@ class FalsingDataProvider {
    private static final long MOTION_EVENT_AGE_MS = 1000;
    private static final float THREE_HUNDRED_SIXTY_DEG = (float) (2 * Math.PI);

    final int mWidthPixels;
    final int mHeightPixels;
    final float mXdpi;
    final float mYdpi;
    private final int mWidthPixels;
    private final int mHeightPixels;
    private final float mXdpi;
    private final float mYdpi;

    private @Classifier.InteractionType int mInteractionType;
    private final TimeLimitedMotionEventBuffer mRecentMotionEvents =
@@ -58,8 +58,8 @@ class FalsingDataProvider {
        mWidthPixels = displayMetrics.widthPixels;
        mHeightPixels = displayMetrics.heightPixels;

        FalsingClassifier.logInfo("xdpi, ydpi: " + mXdpi + ", " + mYdpi);
        FalsingClassifier.logInfo("width, height: " + mWidthPixels + ", " + mHeightPixels);
        FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi());
        FalsingClassifier.logInfo("width, height: " + getWidthPixels() + ", " + getHeightPixels());
    }

    void onMotionEvent(MotionEvent motionEvent) {
@@ -86,6 +86,24 @@ class FalsingDataProvider {
        mDirty = true;
    }

    /** Returns screen width in pixels. */
    int getWidthPixels() {
        return mWidthPixels;
    }

    /** Returns screen height in pixels. */
    int getHeightPixels() {
        return mHeightPixels;
    }

    float getXdpi() {
        return mXdpi;
    }

    float getYdpi() {
        return mYdpi;
    }

    List<MotionEvent> getRecentMotionEvents() {
        return mRecentMotionEvents;
    }
+170 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.systemui.classifier.brightline;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;

import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;

import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.List;

@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class DistanceClassifierTest extends SysuiTestCase {

    @Mock
    private FalsingDataProvider mDataProvider;
    private FalsingClassifier mClassifier;
    private List<MotionEvent> mMotionEvents = new ArrayList<>();

    private static final float DPI = 100;
    private static final int SCREEN_SIZE = (int) (DPI * 10);

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        when(mDataProvider.getHeightPixels()).thenReturn(SCREEN_SIZE);
        when(mDataProvider.getWidthPixels()).thenReturn(SCREEN_SIZE);
        when(mDataProvider.getXdpi()).thenReturn(DPI);
        when(mDataProvider.getYdpi()).thenReturn(DPI);
        mClassifier = new DistanceClassifier(mDataProvider);
    }

    @Test
    public void testPass_noPointer() {
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void testPass_fling() {
        MotionEvent motionEventA = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 1, 1, 0);
        MotionEvent motionEventB = MotionEvent.obtain(1, 2, MotionEvent.ACTION_MOVE, 1, 2, 0);
        MotionEvent motionEventC = MotionEvent.obtain(1, 3, MotionEvent.ACTION_UP, 1, 40, 0);

        appendMotionEvent(motionEventA);
        assertThat(mClassifier.isFalseTouch(), is(true));

        appendMotionEvent(motionEventB);
        assertThat(mClassifier.isFalseTouch(), is(true));

        appendMotionEvent(motionEventC);
        assertThat(mClassifier.isFalseTouch(), is(false));

        motionEventA.recycle();
        motionEventB.recycle();
        motionEventC.recycle();
    }

    @Test
    public void testFail_flingShort() {
        MotionEvent motionEventA = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 1, 1, 0);
        MotionEvent motionEventB = MotionEvent.obtain(1, 2, MotionEvent.ACTION_MOVE, 1, 2, 0);
        MotionEvent motionEventC = MotionEvent.obtain(1, 3, MotionEvent.ACTION_UP, 1, 10, 0);

        appendMotionEvent(motionEventA);
        assertThat(mClassifier.isFalseTouch(), is(true));

        appendMotionEvent(motionEventB);
        assertThat(mClassifier.isFalseTouch(), is(true));

        appendMotionEvent(motionEventC);
        assertThat(mClassifier.isFalseTouch(), is(true));

        motionEventA.recycle();
        motionEventB.recycle();
        motionEventC.recycle();
    }

    @Test
    public void testFail_flingSlowly() {
        // These events, in testing, result in a fling that falls just short of the threshold.
        MotionEvent motionEventA = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 1, 1, 0);
        MotionEvent motionEventB = MotionEvent.obtain(1, 2, MotionEvent.ACTION_MOVE, 1, 15, 0);
        MotionEvent motionEventC = MotionEvent.obtain(1, 3, MotionEvent.ACTION_MOVE, 1, 16, 0);
        MotionEvent motionEventD = MotionEvent.obtain(1, 300, MotionEvent.ACTION_MOVE, 1, 17, 0);
        MotionEvent motionEventE = MotionEvent.obtain(1, 301, MotionEvent.ACTION_MOVE, 1, 18, 0);
        MotionEvent motionEventF = MotionEvent.obtain(1, 500, MotionEvent.ACTION_UP, 1, 19, 0);

        appendMotionEvent(motionEventA);
        assertThat(mClassifier.isFalseTouch(), is(true));

        appendMotionEvent(motionEventB);
        assertThat(mClassifier.isFalseTouch(), is(true));

        appendMotionEvent(motionEventC);
        appendMotionEvent(motionEventD);
        appendMotionEvent(motionEventE);
        appendMotionEvent(motionEventF);
        assertThat(mClassifier.isFalseTouch(), is(true));

        motionEventA.recycle();
        motionEventB.recycle();
        motionEventC.recycle();
        motionEventD.recycle();
        motionEventE.recycle();
        motionEventF.recycle();
    }

    @Test
    public void testPass_swipe() {
        MotionEvent motionEventA = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 1, 1, 0);
        MotionEvent motionEventB = MotionEvent.obtain(1, 3, MotionEvent.ACTION_MOVE, 1, DPI * 3, 0);
        MotionEvent motionEventC = MotionEvent.obtain(1, 1000, MotionEvent.ACTION_UP, 1, DPI * 3,
                0);

        appendMotionEvent(motionEventA);
        assertThat(mClassifier.isFalseTouch(), is(true));


        appendMotionEvent(motionEventB);
        appendMotionEvent(motionEventC);
        assertThat(mClassifier.isFalseTouch(), is(false));

        motionEventA.recycle();
        motionEventB.recycle();
        motionEventC.recycle();
    }

    private void appendMotionEvent(MotionEvent motionEvent) {
        if (mMotionEvents.isEmpty()) {
            when(mDataProvider.getFirstRecentMotionEvent()).thenReturn(motionEvent);
        }

        mMotionEvents.add(motionEvent);
        when(mDataProvider.getRecentMotionEvents()).thenReturn(mMotionEvents);

        when(mDataProvider.getLastMotionEvent()).thenReturn(motionEvent);

        mClassifier.onTouchEvent(motionEvent);
    }
}