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

Commit 89ad2468 authored by Dave Mankoff's avatar Dave Mankoff
Browse files

Add ZigZagClassifier to the BrightLineFalsingManager.

This rejects swipes that wiggle around too much. Swipes
should be mostly straight.

Bug: 111394067
Test: atest SystemUITests
Change-Id: I43aa1cc62abb47ce43423c3c7c8e58c14dc0db03
parent 8bfbe334
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -72,6 +72,7 @@ public class BrightLineFalsingManager implements FalsingManager {
        mClassifiers.add(new DiagonalClassifier(mDataProvider));
        mClassifiers.add(distanceClassifier);
        mClassifiers.add(proximityClassifier);
        mClassifiers.add(new ZigZagClassifier(mDataProvider));
    }

    private void registerSensors() {
+168 −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.graphics.Point;
import android.view.MotionEvent;

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

/**
 * Penalizes gestures that change direction in either the x or y too much.
 */
class ZigZagClassifier extends FalsingClassifier {

    // Define how far one can move back and forth over one inch of travel before being falsed.
    // `PRIMARY` defines how far one can deviate in the primary direction of travel. I.e. if you're
    // swiping vertically, you shouldn't have a lot of zig zag in the vertical direction. Since
    // most swipes will follow somewhat of a 'C' or 'S' shape, we allow more deviance along the
    // `SECONDARY` axis.
    private static final float MAX_X_PRIMARY_DEVIANCE = .05f;
    private static final float MAX_Y_PRIMARY_DEVIANCE = .05f;
    private static final float MAX_X_SECONDARY_DEVIANCE = .3f;
    private static final float MAX_Y_SECONDARY_DEVIANCE = .3f;

    ZigZagClassifier(FalsingDataProvider dataProvider) {
        super(dataProvider);
    }

    @Override
    boolean isFalseTouch() {
        List<MotionEvent> motionEvents = getRecentMotionEvents();
        // Rotate horizontal gestures to be horizontal between their first and last point.
        // Rotate vertical gestures to be vertical between their first and last point.
        // Sum the absolute value of every dx and dy along the gesture. Compare this with the dx
        // and dy
        // between the first and last point.
        // For horizontal lines, the difference in the x direction should be small.
        // For vertical lines, the difference in the y direction should be small.

        if (motionEvents.size() < 3) {
            return false;
        }

        List<Point> rotatedPoints;
        if (isHorizontal()) {
            rotatedPoints = rotateHorizontal();
        } else {
            rotatedPoints = rotateVertical();
        }

        float actualDx = Math
                .abs(rotatedPoints.get(0).x - rotatedPoints.get(rotatedPoints.size() - 1).x);
        float actualDy = Math
                .abs(rotatedPoints.get(0).y - rotatedPoints.get(rotatedPoints.size() - 1).y);
        logDebug("Actual: (" + actualDx + "," + actualDy + ")");
        float runningAbsDx = 0;
        float runningAbsDy = 0;
        float pX = 0;
        float pY = 0;
        boolean firstLoop = true;
        for (Point point : rotatedPoints) {
            if (firstLoop) {
                pX = point.x;
                pY = point.y;
                firstLoop = false;
                continue;
            }
            runningAbsDx += Math.abs(point.x - pX);
            runningAbsDy += Math.abs(point.y - pY);
            pX = point.x;
            pY = point.y;
            logDebug("(x, y, runningAbsDx, runningAbsDy) - (" + pX + ", " + pY + ", " + runningAbsDx
                    + ", " + runningAbsDy + ")");
        }

        float devianceX = runningAbsDx - actualDx;
        float devianceY = runningAbsDy - actualDy;
        float distanceXIn = actualDx / getXdpi();
        float distanceYIn = actualDy / getYdpi();
        float totalDistanceIn = (float) Math
                .sqrt(distanceXIn * distanceXIn + distanceYIn * distanceYIn);

        float maxXDeviance;
        float maxYDeviance;
        if (actualDx > actualDy) {
            maxXDeviance = MAX_X_PRIMARY_DEVIANCE * totalDistanceIn * getXdpi();
            maxYDeviance = MAX_Y_SECONDARY_DEVIANCE * totalDistanceIn * getYdpi();
        } else {
            maxXDeviance = MAX_X_SECONDARY_DEVIANCE * totalDistanceIn * getXdpi();
            maxYDeviance = MAX_Y_PRIMARY_DEVIANCE * totalDistanceIn * getYdpi();
        }

        logDebug("Straightness Deviance: (" + devianceX + "," + devianceY + ") vs "
                + "(" + maxXDeviance + "," + maxYDeviance + ")");
        return devianceX > maxXDeviance || devianceY > maxYDeviance;
    }

    private float getAtan2LastPoint() {
        MotionEvent firstEvent = getFirstMotionEvent();
        MotionEvent lastEvent = getLastMotionEvent();
        float offsetX = firstEvent.getX();
        float offsetY = firstEvent.getY();
        float lastX = lastEvent.getX() - offsetX;
        float lastY = lastEvent.getY() - offsetY;

        return (float) Math.atan2(lastY, lastX);
    }

    private List<Point> rotateVertical() {
        // Calculate the angle relative to the y axis.
        double angle = Math.PI / 2 - getAtan2LastPoint();
        logDebug("Rotating to vertical by: " + angle);
        return rotateMotionEvents(getRecentMotionEvents(), -angle);
    }

    private List<Point> rotateHorizontal() {
        // Calculate the angle relative to the x axis.
        double angle = getAtan2LastPoint();
        logDebug("Rotating to horizontal by: " + angle);
        return rotateMotionEvents(getRecentMotionEvents(), angle);
    }

    private List<Point> rotateMotionEvents(List<MotionEvent> motionEvents, double angle) {
        List<Point> points = new ArrayList<>();
        double cosAngle = Math.cos(angle);
        double sinAngle = Math.sin(angle);
        MotionEvent firstEvent = motionEvents.get(0);
        float offsetX = firstEvent.getX();
        float offsetY = firstEvent.getY();
        for (MotionEvent motionEvent : motionEvents) {
            float x = motionEvent.getX() - offsetX;
            float y = motionEvent.getY() - offsetY;
            double rotatedX = cosAngle * x + sinAngle * y + offsetX;
            double rotatedY = -sinAngle * x + cosAngle * y + offsetY;
            points.add(new Point((int) rotatedX, (int) rotatedY));
        }

        MotionEvent lastEvent = motionEvents.get(motionEvents.size() - 1);
        Point firstPoint = points.get(0);
        Point lastPoint = points.get(points.size() - 1);
        logDebug(
                "Before: (" + firstEvent.getX() + "," + firstEvent.getY() + "), ("
                        + lastEvent.getX() + ","
                        + lastEvent.getY() + ")");
        logDebug(
                "After: (" + firstPoint.x + "," + firstPoint.y + "), (" + lastPoint.x + ","
                        + lastPoint.y
                        + ")");

        return points;
    }

}
+467 −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.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;

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

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

    private static final long NS_PER_MS = 1000000;

    @Mock
    private FalsingDataProvider mDataProvider;
    private FalsingClassifier mClassifier;
    private List<MotionEvent> mMotionEvents = new ArrayList<>();
    private float mOffsetX = 0;
    private float mOffsetY = 0;
    private float mDx;
    private float mDy;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        when(mDataProvider.getXdpi()).thenReturn(100f);
        when(mDataProvider.getYdpi()).thenReturn(100f);
        when(mDataProvider.getRecentMotionEvents()).thenReturn(mMotionEvents);
        mClassifier = new ZigZagClassifier(mDataProvider);


        // Calculate the response to these calls on the fly, otherwise Mockito gets bogged down
        // everytime we call appendMotionEvent.
        when(mDataProvider.getFirstRecentMotionEvent()).thenAnswer(
                (Answer<MotionEvent>) invocation -> mMotionEvents.get(0));
        when(mDataProvider.getLastMotionEvent()).thenAnswer(
                (Answer<MotionEvent>) invocation -> mMotionEvents.get(mMotionEvents.size() - 1));
        when(mDataProvider.isHorizontal()).thenAnswer(
                (Answer<Boolean>) invocation -> Math.abs(mDy) < Math.abs(mDx));
        when(mDataProvider.isVertical()).thenAnswer(
                (Answer<Boolean>) invocation -> Math.abs(mDy) > Math.abs(mDx));
        when(mDataProvider.isRight()).thenAnswer((Answer<Boolean>) invocation -> mDx > 0);
        when(mDataProvider.isUp()).thenAnswer((Answer<Boolean>) invocation -> mDy < 0);
    }

    @After
    public void tearDown() {
        for (MotionEvent motionEvent : mMotionEvents) {
            motionEvent.recycle();
        }
        mMotionEvents.clear();
    }

    @Test
    public void testPass_fewTouchesVertical() {
        assertThat(mClassifier.isFalseTouch(), is(false));
        appendMotionEvent(0, 0);
        assertThat(mClassifier.isFalseTouch(), is(false));
        appendMotionEvent(0, 100);
        assertThat(mClassifier.isFalseTouch(), is(false));
    }

    @Test
    public void testPass_vertical() {
        appendMotionEvent(0, 0);
        appendMotionEvent(0, 100);
        appendMotionEvent(0, 200);
        assertThat(mClassifier.isFalseTouch(), is(false));
    }

    @Test
    public void testPass_fewTouchesHorizontal() {
        assertThat(mClassifier.isFalseTouch(), is(false));
        appendMotionEvent(0, 0);
        assertThat(mClassifier.isFalseTouch(), is(false));
        appendMotionEvent(100, 0);
        assertThat(mClassifier.isFalseTouch(), is(false));
    }

    @Test
    public void testPass_horizontal() {
        appendMotionEvent(0, 0);
        appendMotionEvent(100, 0);
        appendMotionEvent(200, 0);
        assertThat(mClassifier.isFalseTouch(), is(false));
    }


    @Test
    public void testFail_minimumTouchesVertical() {
        appendMotionEvent(0, 0);
        appendMotionEvent(0, 100);
        appendMotionEvent(0, 1);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void testFail_minimumTouchesHorizontal() {
        appendMotionEvent(0, 0);
        appendMotionEvent(100, 0);
        appendMotionEvent(1, 0);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void testPass_fortyFiveDegreesStraight() {
        appendMotionEvent(0, 0);
        appendMotionEvent(10, 10);
        appendMotionEvent(20, 20);
        assertThat(mClassifier.isFalseTouch(), is(false));
    }

    @Test
    public void testPass_horizontalZigZagVerticalStraight() {
        // This test looks just like testFail_horizontalZigZagVerticalStraight but with
        // a longer y range, making it look straighter.
        appendMotionEvent(0, 0);
        appendMotionEvent(5, 100);
        appendMotionEvent(-5, 200);
        assertThat(mClassifier.isFalseTouch(), is(false));
    }

    @Test
    public void testPass_horizontalStraightVerticalZigZag() {
        // This test looks just like testFail_horizontalStraightVerticalZigZag but with
        // a longer x range, making it look straighter.
        appendMotionEvent(0, 0);
        appendMotionEvent(100, 5);
        appendMotionEvent(200, -5);
        assertThat(mClassifier.isFalseTouch(), is(false));
    }

    @Test
    public void testFail_horizontalZigZagVerticalStraight() {
        // This test looks just like testPass_horizontalZigZagVerticalStraight but with
        // a shorter y range, making it look more crooked.
        appendMotionEvent(0, 0);
        appendMotionEvent(5, 10);
        appendMotionEvent(-5, 20);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void testFail_horizontalStraightVerticalZigZag() {
        // This test looks just like testPass_horizontalStraightVerticalZigZag but with
        // a shorter x range, making it look more crooked.
        appendMotionEvent(0, 0);
        appendMotionEvent(10, 5);
        appendMotionEvent(20, -5);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void test_between0And45() {
        appendMotionEvent(0, 0);
        appendMotionEvent(100, 5);
        appendMotionEvent(200, 10);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(100, 0);
        appendMotionEvent(200, 10);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(100, -10);
        appendMotionEvent(200, 10);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(100, -10);
        appendMotionEvent(200, 50);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void test_between45And90() {
        appendMotionEvent(0, 0);
        appendMotionEvent(10, 50);
        appendMotionEvent(8, 100);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(1, 800);
        appendMotionEvent(2, 900);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-10, 600);
        appendMotionEvent(30, 700);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(40, 100);
        appendMotionEvent(0, 101);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void test_between90And135() {
        appendMotionEvent(0, 0);
        appendMotionEvent(-10, 50);
        appendMotionEvent(-24, 100);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-20, 800);
        appendMotionEvent(-20, 900);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(30, 600);
        appendMotionEvent(-10, 700);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-80, 100);
        appendMotionEvent(-10, 101);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void test_between135And180() {
        appendMotionEvent(0, 0);
        appendMotionEvent(-120, 10);
        appendMotionEvent(-200, 20);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-20, 8);
        appendMotionEvent(-40, 2);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-500, -2);
        appendMotionEvent(-600, 70);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-80, 100);
        appendMotionEvent(-100, 1);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void test_between180And225() {
        appendMotionEvent(0, 0);
        appendMotionEvent(-120, -10);
        appendMotionEvent(-200, -20);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-20, -8);
        appendMotionEvent(-40, -2);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-500, 2);
        appendMotionEvent(-600, -70);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-80, -100);
        appendMotionEvent(-100, -1);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void test_between225And270() {
        appendMotionEvent(0, 0);
        appendMotionEvent(-12, -20);
        appendMotionEvent(-20, -40);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-20, -130);
        appendMotionEvent(-40, -260);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(1, -100);
        appendMotionEvent(-6, -200);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-80, -100);
        appendMotionEvent(-10, -110);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void test_between270And315() {
        appendMotionEvent(0, 0);
        appendMotionEvent(12, -20);
        appendMotionEvent(20, -40);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(20, -130);
        appendMotionEvent(40, -260);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(-1, -100);
        appendMotionEvent(6, -200);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(80, -100);
        appendMotionEvent(10, -110);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void test_between315And360() {
        appendMotionEvent(0, 0);
        appendMotionEvent(120, -20);
        appendMotionEvent(200, -40);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(200, -13);
        appendMotionEvent(400, -30);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(100, 10);
        appendMotionEvent(600, -20);
        assertThat(mClassifier.isFalseTouch(), is(false));

        mMotionEvents.clear();
        appendMotionEvent(0, 0);
        appendMotionEvent(80, -100);
        appendMotionEvent(100, -1);
        assertThat(mClassifier.isFalseTouch(), is(true));
    }

    @Test
    public void test_randomOrigins() {
        // The purpose of this test is to try all the other tests from different starting points.
        // We use a pre-determined seed to make this test repeatable.
        Random rand = new Random(23);
        for (int i = 0; i < 100; i++) {
            mOffsetX = rand.nextInt(2000) - 1000;
            mOffsetY = rand.nextInt(2000) - 1000;
            try {
                mMotionEvents.clear();
                testPass_fewTouchesVertical();
                mMotionEvents.clear();
                testPass_vertical();
                mMotionEvents.clear();
                testFail_horizontalStraightVerticalZigZag();
                mMotionEvents.clear();
                testFail_horizontalZigZagVerticalStraight();
                mMotionEvents.clear();
                testFail_minimumTouchesHorizontal();
                mMotionEvents.clear();
                testFail_minimumTouchesVertical();
                mMotionEvents.clear();
                testPass_fewTouchesHorizontal();
                mMotionEvents.clear();
                testPass_fortyFiveDegreesStraight();
                mMotionEvents.clear();
                testPass_horizontal();
                mMotionEvents.clear();
                testPass_horizontalStraightVerticalZigZag();
                mMotionEvents.clear();
                testPass_horizontalZigZagVerticalStraight();
                mMotionEvents.clear();
                test_between0And45();
                mMotionEvents.clear();
                test_between45And90();
                mMotionEvents.clear();
                test_between90And135();
                mMotionEvents.clear();
                test_between135And180();
                mMotionEvents.clear();
                test_between180And225();
                mMotionEvents.clear();
                test_between225And270();
                mMotionEvents.clear();
                test_between270And315();
                mMotionEvents.clear();
                test_between315And360();
            } catch (AssertionError e) {
                throw new AssertionError("Random origin failure in iteration " + i, e);
            }
        }
    }


    private void appendMotionEvent(float x, float y) {
        x += mOffsetX;
        y += mOffsetY;

        long eventTime = mMotionEvents.size() + 1;
        MotionEvent motionEvent = MotionEvent.obtain(1, eventTime, MotionEvent.ACTION_DOWN, x, y,
                0);
        mMotionEvents.add(motionEvent);

        mDx = mDataProvider.getFirstRecentMotionEvent().getX()
                - mDataProvider.getLastMotionEvent().getX();
        mDy = mDataProvider.getFirstRecentMotionEvent().getY()
                - mDataProvider.getLastMotionEvent().getY();

        mClassifier.onTouchEvent(motionEvent);
    }
}