diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java index 57be77e9960ca..8c39e9e9016b0 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java @@ -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() { diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/ZigZagClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/ZigZagClassifier.java new file mode 100644 index 0000000000000..a62574f263999 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/ZigZagClassifier.java @@ -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 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 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 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 rotateHorizontal() { + // Calculate the angle relative to the x axis. + double angle = getAtan2LastPoint(); + logDebug("Rotating to horizontal by: " + angle); + return rotateMotionEvents(getRecentMotionEvents(), angle); + } + + private List rotateMotionEvents(List motionEvents, double angle) { + List 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; + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ZigZagClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ZigZagClassifierTest.java new file mode 100644 index 0000000000000..976b586d089be --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ZigZagClassifierTest.java @@ -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 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) invocation -> mMotionEvents.get(0)); + when(mDataProvider.getLastMotionEvent()).thenAnswer( + (Answer) invocation -> mMotionEvents.get(mMotionEvents.size() - 1)); + when(mDataProvider.isHorizontal()).thenAnswer( + (Answer) invocation -> Math.abs(mDy) < Math.abs(mDx)); + when(mDataProvider.isVertical()).thenAnswer( + (Answer) invocation -> Math.abs(mDy) > Math.abs(mDx)); + when(mDataProvider.isRight()).thenAnswer((Answer) invocation -> mDx > 0); + when(mDataProvider.isUp()).thenAnswer((Answer) 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); + } +}