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
This commit is contained in:
Dave Mankoff
2019-06-14 14:59:05 -04:00
parent 8bfbe3348e
commit 89ad24688b
3 changed files with 636 additions and 0 deletions

View File

@@ -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() {

View File

@@ -0,0 +1,168 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,467 @@
/*
* 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);
}
}