summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/Classifier.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java328
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/DiagonalClassifier.java89
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/DistanceClassifier.java158
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java131
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java249
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/PointerCountClassifier.java53
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java134
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java242
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/TypeClassifier.java61
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/brightline/ZigZagClassifier.java168
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DiagonalClassifierTest.java213
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DistanceClassifierTest.java170
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java293
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/PointerCountClassifierTest.java77
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ProximityClassifierTest.java159
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/TypeClassifierTest.java307
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ZigZagClassifierTest.java467
18 files changed, 3318 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
index f8856ce15f83..ae7d142a9e45 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
@@ -16,9 +16,13 @@
package com.android.systemui.classifier;
+import android.annotation.IntDef;
import android.hardware.SensorEvent;
import android.view.MotionEvent;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* An abstract class for classifiers for touch and sensor events.
*/
@@ -34,6 +38,21 @@ public abstract class Classifier {
public static final int BOUNCER_UNLOCK = 8;
public static final int PULSE_EXPAND = 9;
+ @IntDef({
+ QUICK_SETTINGS,
+ NOTIFICATION_DISMISS,
+ NOTIFICATION_DRAG_DOWN,
+ NOTIFICATION_DOUBLE_TAP,
+ UNLOCK,
+ LEFT_AFFORDANCE,
+ RIGHT_AFFORDANCE,
+ GENERIC,
+ BOUNCER_UNLOCK,
+ PULSE_EXPAND
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface InteractionType {}
+
/**
* Contains all the information about touch events from which the classifier can query
*/
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java
new file mode 100644
index 000000000000..8c39e9e9016b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java
@@ -0,0 +1,328 @@
+/*
+ * 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.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.net.Uri;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.systemui.classifier.Classifier;
+import com.android.systemui.plugins.FalsingManager;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * FalsingManager designed to make clear why a touch was rejected.
+ */
+public class BrightLineFalsingManager implements FalsingManager {
+
+ static final boolean DEBUG = false;
+ private static final String TAG = "FalsingManagerPlugin";
+
+ private final SensorManager mSensorManager;
+ private final FalsingDataProvider mDataProvider;
+ private boolean mSessionStarted;
+
+ private final ExecutorService mBackgroundExecutor = Executors.newSingleThreadExecutor();
+
+ private final List<FalsingClassifier> mClassifiers;
+
+ private SensorEventListener mSensorEventListener = new SensorEventListener() {
+ @Override
+ public synchronized void onSensorChanged(SensorEvent event) {
+ onSensorEvent(event);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+ };
+
+ BrightLineFalsingManager(FalsingDataProvider falsingDataProvider, SensorManager sensorManager) {
+ mDataProvider = falsingDataProvider;
+ mSensorManager = sensorManager;
+ mClassifiers = new ArrayList<>();
+ DistanceClassifier distanceClassifier = new DistanceClassifier(mDataProvider);
+ ProximityClassifier proximityClassifier = new ProximityClassifier(distanceClassifier,
+ mDataProvider);
+ mClassifiers.add(new PointerCountClassifier(mDataProvider));
+ mClassifiers.add(new TypeClassifier(mDataProvider));
+ mClassifiers.add(new DiagonalClassifier(mDataProvider));
+ mClassifiers.add(distanceClassifier);
+ mClassifiers.add(proximityClassifier);
+ mClassifiers.add(new ZigZagClassifier(mDataProvider));
+ }
+
+ private void registerSensors() {
+ Sensor s = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ if (s != null) {
+ // This can be expensive, and doesn't need to happen on the main thread.
+ mBackgroundExecutor.submit(() -> {
+ logDebug("registering sensor listener");
+ mSensorManager.registerListener(
+ mSensorEventListener, s, SensorManager.SENSOR_DELAY_GAME);
+ });
+ }
+ }
+
+
+ private void unregisterSensors() {
+ // This can be expensive, and doesn't need to happen on the main thread.
+ mBackgroundExecutor.submit(() -> {
+ logDebug("unregistering sensor listener");
+ mSensorManager.unregisterListener(mSensorEventListener);
+ });
+ }
+
+ private void sessionStart() {
+ logDebug("Starting Session");
+ mSessionStarted = true;
+ registerSensors();
+ mClassifiers.forEach(FalsingClassifier::onSessionStarted);
+ }
+
+ private void sessionEnd() {
+ if (mSessionStarted) {
+ logDebug("Ending Session");
+ mSessionStarted = false;
+ unregisterSensors();
+ mDataProvider.onSessionEnd();
+ mClassifiers.forEach(FalsingClassifier::onSessionEnded);
+ }
+ }
+
+ private void updateInteractionType(@Classifier.InteractionType int type) {
+ logDebug("InteractionType: " + type);
+ mClassifiers.forEach((classifier) -> classifier.setInteractionType(type));
+ }
+
+ @Override
+ public boolean isClassiferEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isFalseTouch() {
+ boolean r = mClassifiers.stream().anyMatch(falsingClassifier -> {
+ boolean result = falsingClassifier.isFalseTouch();
+ if (result) {
+ logInfo(falsingClassifier.getClass().getName() + ": true");
+ } else {
+ logDebug(falsingClassifier.getClass().getName() + ": false");
+ }
+ return result;
+ });
+
+ logDebug("Is false touch? " + r);
+
+ return r;
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent motionEvent, int width, int height) {
+ // TODO: some of these classifiers might allow us to abort early, meaning we don't have to
+ // make these calls.
+ mDataProvider.onMotionEvent(motionEvent);
+ mClassifiers.forEach((classifier) -> classifier.onTouchEvent(motionEvent));
+ }
+
+ private void onSensorEvent(SensorEvent sensorEvent) {
+ // TODO: some of these classifiers might allow us to abort early, meaning we don't have to
+ // make these calls.
+ mClassifiers.forEach((classifier) -> classifier.onSensorEvent(sensorEvent));
+ }
+
+ @Override
+ public void onSucccessfulUnlock() {
+ }
+
+ @Override
+ public void onNotificationActive() {
+ }
+
+ @Override
+ public void setShowingAod(boolean showingAod) {
+ if (showingAod) {
+ sessionEnd();
+ } else {
+ sessionStart();
+ }
+ }
+
+ @Override
+ public void onNotificatonStartDraggingDown() {
+ updateInteractionType(Classifier.NOTIFICATION_DRAG_DOWN);
+
+ }
+
+ @Override
+ public boolean isUnlockingDisabled() {
+ return false;
+ }
+
+
+ @Override
+ public void onNotificatonStopDraggingDown() {
+ }
+
+ @Override
+ public void setNotificationExpanded() {
+ }
+
+ @Override
+ public void onQsDown() {
+ updateInteractionType(Classifier.QUICK_SETTINGS);
+ }
+
+ @Override
+ public void setQsExpanded(boolean b) {
+ }
+
+ @Override
+ public boolean shouldEnforceBouncer() {
+ return false;
+ }
+
+ @Override
+ public void onTrackingStarted(boolean secure) {
+ updateInteractionType(secure ? Classifier.BOUNCER_UNLOCK : Classifier.UNLOCK);
+ }
+
+ @Override
+ public void onTrackingStopped() {
+ }
+
+ @Override
+ public void onLeftAffordanceOn() {
+ }
+
+ @Override
+ public void onCameraOn() {
+ }
+
+ @Override
+ public void onAffordanceSwipingStarted(boolean rightCorner) {
+ updateInteractionType(
+ rightCorner ? Classifier.RIGHT_AFFORDANCE : Classifier.LEFT_AFFORDANCE);
+ }
+
+ @Override
+ public void onAffordanceSwipingAborted() {
+ }
+
+ @Override
+ public void onStartExpandingFromPulse() {
+ updateInteractionType(Classifier.PULSE_EXPAND);
+ }
+
+ @Override
+ public void onExpansionFromPulseStopped() {
+ }
+
+ @Override
+ public Uri reportRejectedTouch() {
+ return null;
+ }
+
+ @Override
+ public void onScreenOnFromTouch() {
+ sessionStart();
+ }
+
+ @Override
+ public boolean isReportingEnabled() {
+ return false;
+ }
+
+ @Override
+ public void onUnlockHintStarted() {
+ }
+
+ @Override
+ public void onCameraHintStarted() {
+ }
+
+ @Override
+ public void onLeftAffordanceHintStarted() {
+ }
+
+ @Override
+ public void onScreenTurningOn() {
+ sessionStart();
+ }
+
+ @Override
+ public void onScreenOff() {
+ sessionEnd();
+ }
+
+
+ @Override
+ public void onNotificatonStopDismissing() {
+ }
+
+ @Override
+ public void onNotificationDismissed() {
+ }
+
+ @Override
+ public void onNotificatonStartDismissing() {
+ updateInteractionType(Classifier.NOTIFICATION_DISMISS);
+ }
+
+ @Override
+ public void onNotificationDoubleTap(boolean b, float v, float v1) {
+ }
+
+ @Override
+ public void onBouncerShown() {
+ }
+
+ @Override
+ public void onBouncerHidden() {
+ }
+
+ @Override
+ public void dump(PrintWriter printWriter) {
+ }
+
+ static void logDebug(String msg) {
+ logDebug(msg, null);
+ }
+
+ static void logDebug(String msg, Throwable throwable) {
+ if (DEBUG) {
+ Log.d(TAG, msg, throwable);
+ }
+ }
+
+ static void logInfo(String msg) {
+ Log.i(TAG, msg);
+ }
+
+ static void logError(String msg) {
+ Log.e(TAG, msg);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/DiagonalClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/DiagonalClassifier.java
new file mode 100644
index 000000000000..730907e1fa9c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/DiagonalClassifier.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE;
+import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;
+
+/**
+ * False on swipes that are too close to 45 degrees.
+ *
+ * Horizontal swipes may have a different threshold than vertical.
+ *
+ * This falser should not run on "affordance" swipes, as they will always be close to 45.
+ */
+class DiagonalClassifier extends FalsingClassifier {
+
+ private static final float HORIZONTAL_ANGLE_RANGE = (float) (5f / 360f * Math.PI * 2f);
+ private static final float VERTICAL_ANGLE_RANGE = (float) (5f / 360f * Math.PI * 2f);
+ private static final float DIAGONAL = (float) (Math.PI / 4); // 45 deg
+ private static final float NINETY_DEG = (float) (Math.PI / 2);
+ private static final float ONE_HUNDRED_EIGHTY_DEG = (float) (Math.PI);
+ private static final float THREE_HUNDRED_SIXTY_DEG = (float) (2 * Math.PI);
+
+ DiagonalClassifier(FalsingDataProvider dataProvider) {
+ super(dataProvider);
+ }
+
+ @Override
+ boolean isFalseTouch() {
+ float angle = getAngle();
+
+ if (angle == Float.MAX_VALUE) { // Unknown angle
+ return false;
+ }
+
+ if (getInteractionType() == LEFT_AFFORDANCE
+ || getInteractionType() == RIGHT_AFFORDANCE) {
+ return false;
+ }
+
+ float minAngle = DIAGONAL - HORIZONTAL_ANGLE_RANGE;
+ float maxAngle = DIAGONAL + HORIZONTAL_ANGLE_RANGE;
+ if (isVertical()) {
+ minAngle = DIAGONAL - VERTICAL_ANGLE_RANGE;
+ maxAngle = DIAGONAL + VERTICAL_ANGLE_RANGE;
+ }
+
+ return angleBetween(angle, minAngle, maxAngle)
+ || angleBetween(angle, minAngle + NINETY_DEG, maxAngle + NINETY_DEG)
+ || angleBetween(angle, minAngle - NINETY_DEG, maxAngle - NINETY_DEG)
+ || angleBetween(angle, minAngle + ONE_HUNDRED_EIGHTY_DEG,
+ maxAngle + ONE_HUNDRED_EIGHTY_DEG);
+ }
+
+ private boolean angleBetween(float angle, float min, float max) {
+ // No need to normalize angle as it is guaranteed to be between 0 and 2*PI.
+ min = normalizeAngle(min);
+ max = normalizeAngle(max);
+
+ if (min > max) { // Can happen when angle is close to 0.
+ return angle >= min || angle <= max;
+ }
+
+ return angle >= min && angle <= max;
+ }
+
+ private float normalizeAngle(float angle) {
+ if (angle < 0) {
+ return THREE_HUNDRED_SIXTY_DEG + (angle % THREE_HUNDRED_SIXTY_DEG);
+ } else if (angle > THREE_HUNDRED_SIXTY_DEG) {
+ return angle % THREE_HUNDRED_SIXTY_DEG;
+ }
+ return angle;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/DistanceClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/DistanceClassifier.java
new file mode 100644
index 000000000000..005ee12c4f61
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/DistanceClassifier.java
@@ -0,0 +1,158 @@
+/*
+ * 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;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java
new file mode 100644
index 000000000000..685e7c534b66
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java
@@ -0,0 +1,131 @@
+/*
+ * 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.hardware.SensorEvent;
+import android.view.MotionEvent;
+
+import com.android.systemui.classifier.Classifier;
+
+import java.util.List;
+
+/**
+ * Base class for rules that determine False touches.
+ */
+abstract class FalsingClassifier {
+ private final FalsingDataProvider mDataProvider;
+
+ FalsingClassifier(FalsingDataProvider dataProvider) {
+ this.mDataProvider = dataProvider;
+ }
+
+ List<MotionEvent> getRecentMotionEvents() {
+ return mDataProvider.getRecentMotionEvents();
+ }
+
+ MotionEvent getFirstMotionEvent() {
+ return mDataProvider.getFirstRecentMotionEvent();
+ }
+
+ MotionEvent getLastMotionEvent() {
+ return mDataProvider.getLastMotionEvent();
+ }
+
+ boolean isHorizontal() {
+ return mDataProvider.isHorizontal();
+ }
+
+ boolean isRight() {
+ return mDataProvider.isRight();
+ }
+
+ boolean isVertical() {
+ return mDataProvider.isVertical();
+ }
+
+ boolean isUp() {
+ return mDataProvider.isUp();
+ }
+
+ float getAngle() {
+ return mDataProvider.getAngle();
+ }
+
+ int getWidthPixels() {
+ return mDataProvider.getWidthPixels();
+ }
+
+ int getHeightPixels() {
+ return mDataProvider.getHeightPixels();
+ }
+
+ float getXdpi() {
+ return mDataProvider.getXdpi();
+ }
+
+ float getYdpi() {
+ return mDataProvider.getYdpi();
+ }
+
+ final @Classifier.InteractionType int getInteractionType() {
+ return mDataProvider.getInteractionType();
+ }
+
+ final void setInteractionType(@Classifier.InteractionType int interactionType) {
+ mDataProvider.setInteractionType(interactionType);
+ }
+
+ /**
+ * Called whenever a MotionEvent occurs.
+ *
+ * Useful for classifiers that need to see every MotionEvent, but most can probably
+ * use {@link #getRecentMotionEvents()} instead, which will return a list of MotionEvents.
+ */
+ void onTouchEvent(MotionEvent motionEvent) {};
+
+ /**
+ * Called whenever a SensorEvent occurs, specifically the ProximitySensor.
+ */
+ void onSensorEvent(SensorEvent sensorEvent) {};
+
+ /**
+ * The phone screen has turned on and we need to begin falsing detection.
+ */
+ void onSessionStarted() {};
+
+ /**
+ * The phone screen has turned off and falsing data can be discarded.
+ */
+ void onSessionEnded() {};
+
+ /**
+ * Returns true if the data captured so far looks like a false touch.
+ */
+ abstract boolean isFalseTouch();
+
+ static void logDebug(String msg) {
+ BrightLineFalsingManager.logDebug(msg);
+ }
+
+ static void logInfo(String msg) {
+ BrightLineFalsingManager.logInfo(msg);
+ }
+
+ static void logError(String msg) {
+ BrightLineFalsingManager.logError(msg);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java
new file mode 100644
index 000000000000..884c011a2d3f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java
@@ -0,0 +1,249 @@
+/*
+ * 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.content.Context;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+import com.android.systemui.classifier.Classifier;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Acts as a cache and utility class for FalsingClassifiers.
+ */
+class FalsingDataProvider {
+
+ private static final long MOTION_EVENT_AGE_MS = 1000;
+ private static final float THREE_HUNDRED_SIXTY_DEG = (float) (2 * Math.PI);
+
+ 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 =
+ new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS);
+
+ private boolean mDirty = true;
+
+ private float mAngle = 0;
+ private MotionEvent mFirstActualMotionEvent;
+ private MotionEvent mFirstRecentMotionEvent;
+ private MotionEvent mLastMotionEvent;
+
+ FalsingDataProvider(Context context) {
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ mXdpi = displayMetrics.xdpi;
+ mYdpi = displayMetrics.ydpi;
+ mWidthPixels = displayMetrics.widthPixels;
+ mHeightPixels = displayMetrics.heightPixels;
+
+ FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi());
+ FalsingClassifier.logInfo("width, height: " + getWidthPixels() + ", " + getHeightPixels());
+ }
+
+ void onMotionEvent(MotionEvent motionEvent) {
+ if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mFirstActualMotionEvent = motionEvent;
+ }
+
+ List<MotionEvent> motionEvents = unpackMotionEvent(motionEvent);
+ FalsingClassifier.logDebug("Unpacked into: " + motionEvents.size());
+ if (BrightLineFalsingManager.DEBUG) {
+ for (MotionEvent m : motionEvents) {
+ FalsingClassifier.logDebug(
+ "x,y,t: " + m.getX() + "," + m.getY() + "," + m.getEventTime());
+ }
+ }
+
+ if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mRecentMotionEvents.clear();
+ }
+ mRecentMotionEvents.addAll(motionEvents);
+
+ FalsingClassifier.logDebug("Size: " + mRecentMotionEvents.size());
+
+ 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;
+ }
+
+ /**
+ * interactionType is defined by {@link com.android.systemui.classifier.Classifier}.
+ */
+ final void setInteractionType(@Classifier.InteractionType int interactionType) {
+ this.mInteractionType = interactionType;
+ }
+
+ final int getInteractionType() {
+ return mInteractionType;
+ }
+
+ MotionEvent getFirstActualMotionEvent() {
+ return mFirstActualMotionEvent;
+ }
+
+ MotionEvent getFirstRecentMotionEvent() {
+ recalculateData();
+ return mFirstRecentMotionEvent;
+ }
+
+ MotionEvent getLastMotionEvent() {
+ recalculateData();
+ return mLastMotionEvent;
+ }
+
+ /**
+ * Returns the angle between the first and last point of the recent points.
+ *
+ * The angle will be in radians, always be between 0 and 2*PI, inclusive.
+ */
+ float getAngle() {
+ recalculateData();
+ return mAngle;
+ }
+
+ boolean isHorizontal() {
+ recalculateData();
+ return Math.abs(mFirstRecentMotionEvent.getX() - mLastMotionEvent.getX()) > Math
+ .abs(mFirstRecentMotionEvent.getY() - mLastMotionEvent.getY());
+ }
+
+ boolean isRight() {
+ recalculateData();
+ return mLastMotionEvent.getX() > mFirstRecentMotionEvent.getX();
+ }
+
+ boolean isVertical() {
+ return !isHorizontal();
+ }
+
+ boolean isUp() {
+ recalculateData();
+ return mLastMotionEvent.getY() < mFirstRecentMotionEvent.getY();
+ }
+
+ private void recalculateData() {
+ if (!mDirty) {
+ return;
+ }
+
+ mFirstRecentMotionEvent = mRecentMotionEvents.get(0);
+ mLastMotionEvent = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1);
+
+ calculateAngleInternal();
+
+ mDirty = false;
+ }
+
+ private void calculateAngleInternal() {
+ if (mRecentMotionEvents.size() < 2) {
+ mAngle = Float.MAX_VALUE;
+ } else {
+ float lastX = mLastMotionEvent.getX() - mFirstRecentMotionEvent.getX();
+ float lastY = mLastMotionEvent.getY() - mFirstRecentMotionEvent.getY();
+
+ mAngle = (float) Math.atan2(lastY, lastX);
+ while (mAngle < 0) {
+ mAngle += THREE_HUNDRED_SIXTY_DEG;
+ }
+ while (mAngle > THREE_HUNDRED_SIXTY_DEG) {
+ mAngle -= THREE_HUNDRED_SIXTY_DEG;
+ }
+ }
+ }
+
+ private List<MotionEvent> unpackMotionEvent(MotionEvent motionEvent) {
+ List<MotionEvent> motionEvents = new ArrayList<>();
+ List<PointerProperties> pointerPropertiesList = new ArrayList<>();
+ int pointerCount = motionEvent.getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ PointerProperties pointerProperties = new PointerProperties();
+ motionEvent.getPointerProperties(i, pointerProperties);
+ pointerPropertiesList.add(pointerProperties);
+ }
+ PointerProperties[] pointerPropertiesArray = new PointerProperties[pointerPropertiesList
+ .size()];
+ pointerPropertiesList.toArray(pointerPropertiesArray);
+
+ int historySize = motionEvent.getHistorySize();
+ for (int i = 0; i < historySize; i++) {
+ List<PointerCoords> pointerCoordsList = new ArrayList<>();
+ for (int j = 0; j < pointerCount; j++) {
+ PointerCoords pointerCoords = new PointerCoords();
+ motionEvent.getHistoricalPointerCoords(j, i, pointerCoords);
+ pointerCoordsList.add(pointerCoords);
+ }
+ motionEvents.add(MotionEvent.obtain(
+ motionEvent.getDownTime(),
+ motionEvent.getHistoricalEventTime(i),
+ motionEvent.getAction(),
+ pointerCount,
+ pointerPropertiesArray,
+ pointerCoordsList.toArray(new PointerCoords[0]),
+ motionEvent.getMetaState(),
+ motionEvent.getButtonState(),
+ motionEvent.getXPrecision(),
+ motionEvent.getYPrecision(),
+ motionEvent.getDeviceId(),
+ motionEvent.getEdgeFlags(),
+ motionEvent.getSource(),
+ motionEvent.getFlags()
+ ));
+ }
+
+ motionEvents.add(MotionEvent.obtainNoHistory(motionEvent));
+
+ return motionEvents;
+ }
+
+ void onSessionEnd() {
+ mFirstActualMotionEvent = null;
+
+ for (MotionEvent ev : mRecentMotionEvents) {
+ ev.recycle();
+ }
+
+ mRecentMotionEvents.clear();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/PointerCountClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/PointerCountClassifier.java
new file mode 100644
index 000000000000..40e141fbf988
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/PointerCountClassifier.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+/**
+ * False touch if more than one finger touches the screen.
+ *
+ * IMPORTANT: This should not be used for certain cases (i.e. a11y) as we expect multiple fingers
+ * for them.
+ */
+class PointerCountClassifier extends FalsingClassifier {
+
+ private static final int MAX_ALLOWED_POINTERS = 1;
+ private int mMaxPointerCount;
+
+ PointerCountClassifier(FalsingDataProvider dataProvider) {
+ super(dataProvider);
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent motionEvent) {
+ int pCount = mMaxPointerCount;
+ if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mMaxPointerCount = motionEvent.getPointerCount();
+ } else {
+ mMaxPointerCount = Math.max(mMaxPointerCount, motionEvent.getPointerCount());
+ }
+ if (pCount != mMaxPointerCount) {
+ logDebug("Pointers observed:" + mMaxPointerCount);
+ }
+ }
+
+ @Override
+ public boolean isFalseTouch() {
+ return mMaxPointerCount > MAX_ALLOWED_POINTERS;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java
new file mode 100644
index 000000000000..94a8ac85b724
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java
@@ -0,0 +1,134 @@
+/*
+ * 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 com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.view.MotionEvent;
+
+
+/**
+ * False touch if proximity sensor is covered for more than a certain percentage of the gesture.
+ *
+ * This classifer is essentially a no-op for QUICK_SETTINGS, as we assume the sensor may be
+ * covered when swiping from the top.
+ */
+class ProximityClassifier extends FalsingClassifier {
+
+ private static final double PERCENT_COVERED_THRESHOLD = 0.1;
+ private final DistanceClassifier mDistanceClassifier;
+
+ private boolean mNear;
+ private long mGestureStartTimeNs;
+ private long mPrevNearTimeNs;
+ private long mNearDurationNs;
+ private float mPercentNear;
+
+ ProximityClassifier(DistanceClassifier distanceClassifier,
+ FalsingDataProvider dataProvider) {
+ super(dataProvider);
+ this.mDistanceClassifier = distanceClassifier;
+ }
+
+ @Override
+ void onSessionStarted() {
+ mPrevNearTimeNs = 0;
+ mPercentNear = 0;
+ }
+
+ @Override
+ void onSessionEnded() {
+ mPrevNearTimeNs = 0;
+ mPercentNear = 0;
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent motionEvent) {
+ int action = motionEvent.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mGestureStartTimeNs = motionEvent.getEventTimeNano();
+ if (mPrevNearTimeNs > 0) {
+ // We only care about if the proximity sensor is triggered while a move event is
+ // happening.
+ mPrevNearTimeNs = motionEvent.getEventTimeNano();
+ }
+ logDebug("Gesture start time: " + mGestureStartTimeNs);
+ mNearDurationNs = 0;
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ update(mNear, motionEvent.getEventTimeNano());
+ long duration = motionEvent.getEventTimeNano() - mGestureStartTimeNs;
+
+ logDebug("Gesture duration, Proximity duration: " + duration + ", " + mNearDurationNs);
+
+ if (duration == 0) {
+ mPercentNear = mNear ? 1.0f : 0.0f;
+ } else {
+ mPercentNear = (float) mNearDurationNs / (float) duration;
+ }
+ }
+
+ }
+
+ @Override
+ public void onSensorEvent(SensorEvent sensorEvent) {
+ if (sensorEvent.sensor.getType() == Sensor.TYPE_PROXIMITY) {
+ logDebug("Sensor is: " + (sensorEvent.values[0] < sensorEvent.sensor.getMaximumRange())
+ + " at time " + sensorEvent.timestamp);
+ update(
+ sensorEvent.values[0] < sensorEvent.sensor.getMaximumRange(),
+ sensorEvent.timestamp);
+ }
+ }
+
+ @Override
+ public boolean isFalseTouch() {
+ if (getInteractionType() == QUICK_SETTINGS) {
+ return false;
+ }
+
+ logInfo("Percent of gesture in proximity: " + mPercentNear);
+
+ if (mPercentNear > PERCENT_COVERED_THRESHOLD) {
+ return !mDistanceClassifier.isLongSwipe();
+ }
+
+ return false;
+ }
+
+ /**
+ * @param near is the sensor showing the near state right now
+ * @param timeStampNs time of this event in nanoseconds
+ */
+ private void update(boolean near, long timeStampNs) {
+ if (mPrevNearTimeNs != 0 && timeStampNs > mPrevNearTimeNs && mNear) {
+ mNearDurationNs += timeStampNs - mPrevNearTimeNs;
+ logDebug("Updating duration: " + mNearDurationNs);
+ }
+
+ if (near) {
+ logDebug("Set prevNearTimeNs: " + timeStampNs);
+ mPrevNearTimeNs = timeStampNs;
+ }
+
+ mNear = near;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java
new file mode 100644
index 000000000000..9a83b5bd8328
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java
@@ -0,0 +1,242 @@
+/*
+ * 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 java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Maintains an ordered list of the last N milliseconds of MotionEvents.
+ *
+ * This class is simply a convenience class designed to look like a simple list, but that
+ * automatically discards old MotionEvents. It functions much like a queue - first in first out -
+ * but does not have a fixed size like a circular buffer.
+ */
+public class TimeLimitedMotionEventBuffer implements List<MotionEvent> {
+
+ private final LinkedList<MotionEvent> mMotionEvents;
+ private long mMaxAgeMs;
+
+ TimeLimitedMotionEventBuffer(long maxAgeMs) {
+ super();
+ this.mMaxAgeMs = maxAgeMs;
+ this.mMotionEvents = new LinkedList<>();
+ }
+
+ private void ejectOldEvents() {
+ if (mMotionEvents.isEmpty()) {
+ return;
+ }
+ Iterator<MotionEvent> iter = listIterator();
+ long mostRecentMs = mMotionEvents.getLast().getEventTime();
+ while (iter.hasNext()) {
+ MotionEvent ev = iter.next();
+ if (mostRecentMs - ev.getEventTime() > mMaxAgeMs) {
+ iter.remove();
+ ev.recycle();
+ }
+ }
+ }
+
+ @Override
+ public void add(int index, MotionEvent element) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public MotionEvent remove(int index) {
+ return mMotionEvents.remove(index);
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ return mMotionEvents.indexOf(o);
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ return mMotionEvents.lastIndexOf(o);
+ }
+
+ @Override
+ public int size() {
+ return mMotionEvents.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return mMotionEvents.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return mMotionEvents.contains(o);
+ }
+
+ @Override
+ public Iterator<MotionEvent> iterator() {
+ return mMotionEvents.iterator();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return mMotionEvents.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(T[] a) {
+ return mMotionEvents.toArray(a);
+ }
+
+ @Override
+ public boolean add(MotionEvent element) {
+ boolean result = mMotionEvents.add(element);
+ ejectOldEvents();
+ return result;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ return mMotionEvents.remove(o);
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c) {
+ return mMotionEvents.containsAll(c);
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends MotionEvent> collection) {
+ boolean result = mMotionEvents.addAll(collection);
+ ejectOldEvents();
+ return result;
+ }
+
+ @Override
+ public boolean addAll(int index, Collection<? extends MotionEvent> elements) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c) {
+ return mMotionEvents.removeAll(c);
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+ return mMotionEvents.retainAll(c);
+ }
+
+ @Override
+ public void clear() {
+ mMotionEvents.clear();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return mMotionEvents.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return mMotionEvents.hashCode();
+ }
+
+ @Override
+ public MotionEvent get(int index) {
+ return mMotionEvents.get(index);
+ }
+
+ @Override
+ public MotionEvent set(int index, MotionEvent element) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ListIterator<MotionEvent> listIterator() {
+ return new Iter(0);
+ }
+
+ @Override
+ public ListIterator<MotionEvent> listIterator(int index) {
+ return new Iter(index);
+ }
+
+ @Override
+ public List<MotionEvent> subList(int fromIndex, int toIndex) {
+ throw new UnsupportedOperationException();
+ }
+
+ class Iter implements ListIterator<MotionEvent> {
+
+ private final ListIterator<MotionEvent> mIterator;
+
+ Iter(int index) {
+ this.mIterator = mMotionEvents.listIterator(index);
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mIterator.hasNext();
+ }
+
+ @Override
+ public MotionEvent next() {
+ return mIterator.next();
+ }
+
+ @Override
+ public boolean hasPrevious() {
+ return mIterator.hasPrevious();
+ }
+
+ @Override
+ public MotionEvent previous() {
+ return mIterator.previous();
+ }
+
+ @Override
+ public int nextIndex() {
+ return mIterator.nextIndex();
+ }
+
+ @Override
+ public int previousIndex() {
+ return mIterator.previousIndex();
+ }
+
+ @Override
+ public void remove() {
+ mIterator.remove();
+ }
+
+ @Override
+ public void set(MotionEvent motionEvent) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void add(MotionEvent element) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/TypeClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/TypeClassifier.java
new file mode 100644
index 000000000000..b6ceab559301
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/TypeClassifier.java
@@ -0,0 +1,61 @@
+/*
+ * 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 com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
+import static com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE;
+import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
+import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN;
+import static com.android.systemui.classifier.Classifier.PULSE_EXPAND;
+import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
+import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;
+import static com.android.systemui.classifier.Classifier.UNLOCK;
+
+/**
+ * Ensure that the swipe direction generally matches that of the interaction type.
+ */
+public class TypeClassifier extends FalsingClassifier {
+ TypeClassifier(FalsingDataProvider dataProvider) {
+ super(dataProvider);
+ }
+
+ @Override
+ public boolean isFalseTouch() {
+ boolean vertical = isVertical();
+ boolean up = isUp();
+ boolean right = isRight();
+
+ switch (getInteractionType()) {
+ case QUICK_SETTINGS:
+ case PULSE_EXPAND:
+ case NOTIFICATION_DRAG_DOWN:
+ return !vertical || up;
+ case NOTIFICATION_DISMISS:
+ return vertical;
+ case UNLOCK:
+ case BOUNCER_UNLOCK:
+ return !vertical || !up;
+ case LEFT_AFFORDANCE: // Swiping from the bottom left corner for camera or similar.
+ return !right || !up;
+ case RIGHT_AFFORDANCE: // Swiping from the bottom right corner for camera or similar.
+ return right || !up;
+ default:
+ return true;
+ }
+ }
+}
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 000000000000..a62574f26399
--- /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<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;
+ }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DiagonalClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DiagonalClassifierTest.java
new file mode 100644
index 000000000000..ade5f36d659e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DiagonalClassifierTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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 com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE;
+import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;
+
+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 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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class DiagonalClassifierTest extends SysuiTestCase {
+
+ // Next variable is not actually five, but is very close. 5 degrees is currently the value
+ // used in the diagonal classifier, so we want slightly less than that to deal with
+ // floating point errors.
+ private static final float FIVE_DEG_IN_RADIANS = (float) (4.99f / 360f * Math.PI * 2f);
+ private static final float UP_IN_RADIANS = (float) (Math.PI / 2f);
+ private static final float DOWN_IN_RADIANS = (float) (3 * Math.PI / 2f);
+ private static final float RIGHT_IN_RADIANS = 0;
+ private static final float LEFT_IN_RADIANS = (float) Math.PI;
+ private static final float FORTY_FIVE_DEG_IN_RADIANS = (float) (Math.PI / 4);
+
+ @Mock
+ private FalsingDataProvider mDataProvider;
+ private FalsingClassifier mClassifier;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mClassifier = new DiagonalClassifier(mDataProvider);
+ }
+
+ @Test
+ public void testPass_UnknownAngle() {
+ when(mDataProvider.getAngle()).thenReturn(Float.MAX_VALUE);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_VerticalSwipe() {
+ when(mDataProvider.getAngle()).thenReturn(UP_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(DOWN_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_MostlyVerticalSwipe() {
+ when(mDataProvider.getAngle()).thenReturn(UP_IN_RADIANS + 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(UP_IN_RADIANS - 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(DOWN_IN_RADIANS + 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(DOWN_IN_RADIANS - 2 * FIVE_DEG_IN_RADIANS * 2);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_BarelyVerticalSwipe() {
+ when(mDataProvider.getAngle()).thenReturn(
+ UP_IN_RADIANS - FORTY_FIVE_DEG_IN_RADIANS + 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ UP_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS - 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ DOWN_IN_RADIANS - FORTY_FIVE_DEG_IN_RADIANS + 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ DOWN_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS - 2 * FIVE_DEG_IN_RADIANS * 2);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_HorizontalSwipe() {
+ when(mDataProvider.getAngle()).thenReturn(RIGHT_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(LEFT_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_MostlyHorizontalSwipe() {
+ when(mDataProvider.getAngle()).thenReturn(RIGHT_IN_RADIANS + 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(RIGHT_IN_RADIANS - 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(LEFT_IN_RADIANS + 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(LEFT_IN_RADIANS - 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_BarelyHorizontalSwipe() {
+ when(mDataProvider.getAngle()).thenReturn(
+ RIGHT_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS - 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ LEFT_IN_RADIANS - FORTY_FIVE_DEG_IN_RADIANS + 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ LEFT_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS - 2 * FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ RIGHT_IN_RADIANS - FORTY_FIVE_DEG_IN_RADIANS + 2 * FIVE_DEG_IN_RADIANS * 2);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_AffordanceSwipe() {
+ when(mDataProvider.getInteractionType()).thenReturn(LEFT_AFFORDANCE);
+ when(mDataProvider.getAngle()).thenReturn(
+ RIGHT_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.getInteractionType()).thenReturn(RIGHT_AFFORDANCE);
+ when(mDataProvider.getAngle()).thenReturn(
+ LEFT_IN_RADIANS - FORTY_FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ // This classifier may return false for other angles, but these are the only
+ // two that actually matter, as affordances generally only travel in these two directions.
+ // We expect other classifiers to false in those cases, so it really doesn't matter what
+ // we do here.
+ }
+
+ @Test
+ public void testFail_DiagonalSwipe() {
+ // Horizontal Swipes
+ when(mDataProvider.isVertical()).thenReturn(false);
+ when(mDataProvider.getAngle()).thenReturn(
+ RIGHT_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS - FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ UP_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS + FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ LEFT_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS - FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ DOWN_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS + FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ // Vertical Swipes
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.getAngle()).thenReturn(
+ RIGHT_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS + FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ UP_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS - FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+
+ when(mDataProvider.getAngle()).thenReturn(
+ LEFT_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS + FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.getAngle()).thenReturn(
+ DOWN_IN_RADIANS + FORTY_FIVE_DEG_IN_RADIANS - FIVE_DEG_IN_RADIANS);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DistanceClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DistanceClassifierTest.java
new file mode 100644
index 000000000000..3d0471bee728
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DistanceClassifierTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java
new file mode 100644
index 000000000000..1da42061c234
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java
@@ -0,0 +1,293 @@
+/*
+ * 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.hamcrest.Matchers.closeTo;
+import static org.junit.Assert.assertThat;
+
+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 java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class FalsingDataProviderTest extends SysuiTestCase {
+
+ private FalsingDataProvider mDataProvider;
+
+ @Before
+ public void setup() {
+ mDataProvider = new FalsingDataProvider(getContext());
+ }
+
+ @Test
+ public void test_trackMotionEvents() {
+ MotionEvent motionEventA = obtainMotionEvent(MotionEvent.ACTION_DOWN, 1, 2, 9);
+ MotionEvent motionEventB = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 4, 7);
+ MotionEvent motionEventC = obtainMotionEvent(MotionEvent.ACTION_UP, 3, 6, 5);
+
+ mDataProvider.onMotionEvent(motionEventA);
+ mDataProvider.onMotionEvent(motionEventB);
+ mDataProvider.onMotionEvent(motionEventC);
+ List<MotionEvent> motionEventList = mDataProvider.getRecentMotionEvents();
+
+ assertThat(motionEventList.size(), is(3));
+ assertThat(motionEventList.get(0).getActionMasked(), is(MotionEvent.ACTION_DOWN));
+ assertThat(motionEventList.get(1).getActionMasked(), is(MotionEvent.ACTION_MOVE));
+ assertThat(motionEventList.get(2).getActionMasked(), is(MotionEvent.ACTION_UP));
+ assertThat(motionEventList.get(0).getEventTime(), is(1L));
+ assertThat(motionEventList.get(1).getEventTime(), is(2L));
+ assertThat(motionEventList.get(2).getEventTime(), is(3L));
+ assertThat(motionEventList.get(0).getX(), is(2f));
+ assertThat(motionEventList.get(1).getX(), is(4f));
+ assertThat(motionEventList.get(2).getX(), is(6f));
+ assertThat(motionEventList.get(0).getY(), is(9f));
+ assertThat(motionEventList.get(1).getY(), is(7f));
+ assertThat(motionEventList.get(2).getY(), is(5f));
+
+ motionEventA.recycle();
+ motionEventB.recycle();
+ motionEventC.recycle();
+ }
+
+ @Test
+ public void test_trackRecentMotionEvents() {
+ MotionEvent motionEventA = obtainMotionEvent(MotionEvent.ACTION_DOWN, 1, 2, 9);
+ MotionEvent motionEventB = obtainMotionEvent(MotionEvent.ACTION_MOVE, 800, 4, 7);
+ MotionEvent motionEventC = obtainMotionEvent(MotionEvent.ACTION_UP, 1200, 6, 5);
+
+ mDataProvider.onMotionEvent(motionEventA);
+ mDataProvider.onMotionEvent(motionEventB);
+ List<MotionEvent> motionEventList = mDataProvider.getRecentMotionEvents();
+
+ assertThat(motionEventList.size(), is(2));
+ assertThat(motionEventList.get(0).getActionMasked(), is(MotionEvent.ACTION_DOWN));
+ assertThat(motionEventList.get(1).getActionMasked(), is(MotionEvent.ACTION_MOVE));
+ assertThat(motionEventList.get(0).getEventTime(), is(1L));
+ assertThat(motionEventList.get(1).getEventTime(), is(800L));
+ assertThat(motionEventList.get(0).getX(), is(2f));
+ assertThat(motionEventList.get(1).getX(), is(4f));
+ assertThat(motionEventList.get(0).getY(), is(9f));
+ assertThat(motionEventList.get(1).getY(), is(7f));
+
+ mDataProvider.onMotionEvent(motionEventC);
+
+ // Still two events, but event a is gone.
+ assertThat(motionEventList.size(), is(2));
+ assertThat(motionEventList.get(0).getActionMasked(), is(MotionEvent.ACTION_MOVE));
+ assertThat(motionEventList.get(1).getActionMasked(), is(MotionEvent.ACTION_UP));
+ assertThat(motionEventList.get(0).getEventTime(), is(800L));
+ assertThat(motionEventList.get(1).getEventTime(), is(1200L));
+ assertThat(motionEventList.get(0).getX(), is(4f));
+ assertThat(motionEventList.get(1).getX(), is(6f));
+ assertThat(motionEventList.get(0).getY(), is(7f));
+ assertThat(motionEventList.get(1).getY(), is(5f));
+
+ // The first, real event should still be a, however.
+ MotionEvent firstRealMotionEvent = mDataProvider.getFirstActualMotionEvent();
+ assertThat(firstRealMotionEvent.getActionMasked(), is(MotionEvent.ACTION_DOWN));
+ assertThat(firstRealMotionEvent.getEventTime(), is(1L));
+ assertThat(firstRealMotionEvent.getX(), is(2f));
+ assertThat(firstRealMotionEvent.getY(), is(9f));
+
+ motionEventA.recycle();
+ motionEventB.recycle();
+ motionEventC.recycle();
+ }
+
+ @Test
+ public void test_unpackMotionEvents() {
+ // Batching only works for motion events of the same type.
+ MotionEvent motionEventA = obtainMotionEvent(MotionEvent.ACTION_MOVE, 1, 2, 9);
+ MotionEvent motionEventB = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 4, 7);
+ MotionEvent motionEventC = obtainMotionEvent(MotionEvent.ACTION_MOVE, 3, 6, 5);
+ motionEventA.addBatch(motionEventB);
+ motionEventA.addBatch(motionEventC);
+ // Note that calling addBatch changes properties on the original event, not just it's
+ // historical artifacts.
+
+ mDataProvider.onMotionEvent(motionEventA);
+ List<MotionEvent> motionEventList = mDataProvider.getRecentMotionEvents();
+
+ assertThat(motionEventList.size(), is(3));
+ assertThat(motionEventList.get(0).getActionMasked(), is(MotionEvent.ACTION_MOVE));
+ assertThat(motionEventList.get(1).getActionMasked(), is(MotionEvent.ACTION_MOVE));
+ assertThat(motionEventList.get(2).getActionMasked(), is(MotionEvent.ACTION_MOVE));
+ assertThat(motionEventList.get(0).getEventTime(), is(1L));
+ assertThat(motionEventList.get(1).getEventTime(), is(2L));
+ assertThat(motionEventList.get(2).getEventTime(), is(3L));
+ assertThat(motionEventList.get(0).getX(), is(2f));
+ assertThat(motionEventList.get(1).getX(), is(4f));
+ assertThat(motionEventList.get(2).getX(), is(6f));
+ assertThat(motionEventList.get(0).getY(), is(9f));
+ assertThat(motionEventList.get(1).getY(), is(7f));
+ assertThat(motionEventList.get(2).getY(), is(5f));
+
+ motionEventA.recycle();
+ motionEventB.recycle();
+ motionEventC.recycle();
+ }
+
+ @Test
+ public void test_getAngle() {
+ MotionEvent motionEventOrigin = obtainMotionEvent(MotionEvent.ACTION_DOWN, 1, 0, 0);
+
+ MotionEvent motionEventA = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 1, 1);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventA);
+ assertThat((double) mDataProvider.getAngle(), closeTo(Math.PI / 4, .001));
+ motionEventA.recycle();
+ mDataProvider.onSessionEnd();
+
+ MotionEvent motionEventB = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, -1, -1);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventB);
+ assertThat((double) mDataProvider.getAngle(), closeTo(5 * Math.PI / 4, .001));
+ motionEventB.recycle();
+ mDataProvider.onSessionEnd();
+
+
+ MotionEvent motionEventC = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 2, 0);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventC);
+ assertThat((double) mDataProvider.getAngle(), closeTo(0, .001));
+ motionEventC.recycle();
+ mDataProvider.onSessionEnd();
+ }
+
+ @Test
+ public void test_isHorizontal() {
+ MotionEvent motionEventOrigin = obtainMotionEvent(MotionEvent.ACTION_DOWN, 1, 0, 0);
+
+ MotionEvent motionEventA = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 1, 1);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventA);
+ assertThat(mDataProvider.isHorizontal(), is(false));
+ motionEventA.recycle();
+ mDataProvider.onSessionEnd();
+
+ MotionEvent motionEventB = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 2, 1);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventB);
+ assertThat(mDataProvider.isHorizontal(), is(true));
+ motionEventB.recycle();
+ mDataProvider.onSessionEnd();
+
+ MotionEvent motionEventC = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, -3, -1);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventC);
+ assertThat(mDataProvider.isHorizontal(), is(true));
+ motionEventC.recycle();
+ mDataProvider.onSessionEnd();
+ }
+
+ @Test
+ public void test_isVertical() {
+ MotionEvent motionEventOrigin = obtainMotionEvent(MotionEvent.ACTION_DOWN, 1, 0, 0);
+
+ MotionEvent motionEventA = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 1, 0);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventA);
+ assertThat(mDataProvider.isVertical(), is(false));
+ motionEventA.recycle();
+ mDataProvider.onSessionEnd();
+
+ MotionEvent motionEventB = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 0, 1);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventB);
+ assertThat(mDataProvider.isVertical(), is(true));
+ motionEventB.recycle();
+ mDataProvider.onSessionEnd();
+
+ MotionEvent motionEventC = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, -3, -10);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventC);
+ assertThat(mDataProvider.isVertical(), is(true));
+ motionEventC.recycle();
+ mDataProvider.onSessionEnd();
+ }
+
+ @Test
+ public void test_isRight() {
+ MotionEvent motionEventOrigin = obtainMotionEvent(MotionEvent.ACTION_DOWN, 1, 0, 0);
+
+ MotionEvent motionEventA = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 1, 1);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventA);
+ assertThat(mDataProvider.isRight(), is(true));
+ motionEventA.recycle();
+ mDataProvider.onSessionEnd();
+
+ MotionEvent motionEventB = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 0, 1);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventB);
+ assertThat(mDataProvider.isRight(), is(false));
+ motionEventB.recycle();
+ mDataProvider.onSessionEnd();
+
+ MotionEvent motionEventC = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, -3, -10);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventC);
+ assertThat(mDataProvider.isRight(), is(false));
+ motionEventC.recycle();
+ mDataProvider.onSessionEnd();
+ }
+
+ @Test
+ public void test_isUp() {
+ // Remember that our y axis is flipped.
+
+ MotionEvent motionEventOrigin = obtainMotionEvent(MotionEvent.ACTION_DOWN, 1, 0, 0);
+
+ MotionEvent motionEventA = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 1, -1);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventA);
+ assertThat(mDataProvider.isUp(), is(true));
+ motionEventA.recycle();
+ mDataProvider.onSessionEnd();
+
+ MotionEvent motionEventB = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 0, 0);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventB);
+ assertThat(mDataProvider.isUp(), is(false));
+ motionEventB.recycle();
+ mDataProvider.onSessionEnd();
+
+ MotionEvent motionEventC = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, -3, 10);
+ mDataProvider.onMotionEvent(motionEventOrigin);
+ mDataProvider.onMotionEvent(motionEventC);
+ assertThat(mDataProvider.isUp(), is(false));
+ motionEventC.recycle();
+ mDataProvider.onSessionEnd();
+ }
+
+ private MotionEvent obtainMotionEvent(int action, long eventTimeMs, float x, float y) {
+ return MotionEvent.obtain(1, eventTimeMs, action, x, y, 0);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/PointerCountClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/PointerCountClassifierTest.java
new file mode 100644
index 000000000000..cba9ee38b306
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/PointerCountClassifierTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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 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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class PointerCountClassifierTest extends SysuiTestCase {
+
+ @Mock
+ private FalsingDataProvider mDataProvider;
+ private FalsingClassifier mClassifier;
+
+ @Before
+ public void setup() {
+ mClassifier = new PointerCountClassifier(mDataProvider);
+ }
+
+ @Test
+ public void testPass_noPointer() {
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_singlePointer() {
+ MotionEvent motionEvent = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 1, 1, 0);
+ mClassifier.onTouchEvent(motionEvent);
+ motionEvent.recycle();
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFail_multiPointer() {
+ MotionEvent.PointerProperties[] pointerProperties =
+ MotionEvent.PointerProperties.createArray(2);
+ pointerProperties[0].id = 0;
+ pointerProperties[1].id = 1;
+ MotionEvent.PointerCoords[] pointerCoords = MotionEvent.PointerCoords.createArray(2);
+ MotionEvent motionEvent = MotionEvent.obtain(
+ 1, 1, MotionEvent.ACTION_DOWN, 2, pointerProperties, pointerCoords, 0, 0, 0, 0, 0,
+ 0,
+ 0, 0);
+ mClassifier.onTouchEvent(motionEvent);
+ motionEvent.recycle();
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ProximityClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ProximityClassifierTest.java
new file mode 100644
index 000000000000..2ed792542efd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ProximityClassifierTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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 com.android.systemui.classifier.Classifier.GENERIC;
+import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+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.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.reflect.Field;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class ProximityClassifierTest extends SysuiTestCase {
+
+ private static final long NS_PER_MS = 1000000;
+
+ @Mock
+ private FalsingDataProvider mDataProvider;
+ @Mock
+ private DistanceClassifier mDistanceClassifier;
+ private FalsingClassifier mClassifier;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mDataProvider.getInteractionType()).thenReturn(GENERIC);
+ when(mDistanceClassifier.isLongSwipe()).thenReturn(false);
+ mClassifier = new ProximityClassifier(mDistanceClassifier, mDataProvider);
+ }
+
+ @Test
+ public void testPass_uncovered() {
+ touchDown();
+ touchUp(10);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_mostlyUncovered() {
+ touchDown();
+ mClassifier.onSensorEvent(createSensorEvent(true, 1));
+ mClassifier.onSensorEvent(createSensorEvent(false, 2));
+ touchUp(20);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testPass_quickSettings() {
+ touchDown();
+ when(mDataProvider.getInteractionType()).thenReturn(QUICK_SETTINGS);
+ mClassifier.onSensorEvent(createSensorEvent(true, 1));
+ mClassifier.onSensorEvent(createSensorEvent(false, 11));
+ touchUp(10);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFail_covered() {
+ touchDown();
+ mClassifier.onSensorEvent(createSensorEvent(true, 1));
+ mClassifier.onSensorEvent(createSensorEvent(false, 11));
+ touchUp(10);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+
+ @Test
+ public void testFail_mostlyCovered() {
+ touchDown();
+ mClassifier.onSensorEvent(createSensorEvent(true, 1));
+ mClassifier.onSensorEvent(createSensorEvent(true, 95));
+ mClassifier.onSensorEvent(createSensorEvent(true, 96));
+ mClassifier.onSensorEvent(createSensorEvent(false, 100));
+ touchUp(100);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+
+ @Test
+ public void testPass_coveredWithLongSwipe() {
+ touchDown();
+ mClassifier.onSensorEvent(createSensorEvent(true, 1));
+ mClassifier.onSensorEvent(createSensorEvent(false, 11));
+ touchUp(10);
+ when(mDistanceClassifier.isLongSwipe()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ private void touchDown() {
+ MotionEvent motionEvent = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 0, 0, 0);
+ mClassifier.onTouchEvent(motionEvent);
+ motionEvent.recycle();
+ }
+
+ private void touchUp(long duration) {
+ MotionEvent motionEvent = MotionEvent.obtain(1, 1 + duration, MotionEvent.ACTION_UP, 0,
+ 100, 0);
+
+ mClassifier.onTouchEvent(motionEvent);
+
+ motionEvent.recycle();
+ }
+
+ private SensorEvent createSensorEvent(boolean covered, long timestampMs) {
+ SensorEvent sensorEvent = Mockito.mock(SensorEvent.class);
+ Sensor sensor = Mockito.mock(Sensor.class);
+ when(sensor.getType()).thenReturn(Sensor.TYPE_PROXIMITY);
+ when(sensor.getMaximumRange()).thenReturn(1f);
+ sensorEvent.sensor = sensor;
+ sensorEvent.timestamp = timestampMs * NS_PER_MS;
+ try {
+ Field valuesField = SensorEvent.class.getField("values");
+ valuesField.setAccessible(true);
+ float[] sensorValue = {covered ? 0 : 1};
+ try {
+ valuesField.set(sensorEvent, sensorValue);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ }
+
+ return sensorEvent;
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/TypeClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/TypeClassifierTest.java
new file mode 100644
index 000000000000..4bb3c15818c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/TypeClassifierTest.java
@@ -0,0 +1,307 @@
+/*
+ * 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 com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
+import static com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE;
+import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
+import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN;
+import static com.android.systemui.classifier.Classifier.PULSE_EXPAND;
+import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
+import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;
+import static com.android.systemui.classifier.Classifier.UNLOCK;
+
+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 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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class TypeClassifierTest extends SysuiTestCase {
+
+ @Mock
+ private FalsingDataProvider mDataProvider;
+
+ private FalsingClassifier mClassifier;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mClassifier = new TypeClassifier(mDataProvider);
+ }
+
+ @Test
+ public void testPass_QuickSettings() {
+ when(mDataProvider.getInteractionType()).thenReturn(QUICK_SETTINGS);
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(false);
+
+ when(mDataProvider.isRight()).thenReturn(false); // right should cause no effect.
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFalse_QuickSettings() {
+ when(mDataProvider.getInteractionType()).thenReturn(QUICK_SETTINGS);
+
+ when(mDataProvider.isVertical()).thenReturn(false);
+ when(mDataProvider.isUp()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+
+ @Test
+ public void testPass_PulseExpand() {
+ when(mDataProvider.getInteractionType()).thenReturn(PULSE_EXPAND);
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(false);
+
+ when(mDataProvider.isRight()).thenReturn(false); // right should cause no effect.
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFalse_PulseExpand() {
+ when(mDataProvider.getInteractionType()).thenReturn(PULSE_EXPAND);
+
+ when(mDataProvider.isVertical()).thenReturn(false);
+ when(mDataProvider.isUp()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+
+ @Test
+ public void testPass_NotificationDragDown() {
+ when(mDataProvider.getInteractionType()).thenReturn(NOTIFICATION_DRAG_DOWN);
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(false);
+
+ when(mDataProvider.isRight()).thenReturn(false); // right should cause no effect.
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFalse_NotificationDragDown() {
+ when(mDataProvider.getInteractionType()).thenReturn(NOTIFICATION_DRAG_DOWN);
+
+ when(mDataProvider.isVertical()).thenReturn(false);
+ when(mDataProvider.isUp()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+
+ @Test
+ public void testPass_NotificationDismiss() {
+ when(mDataProvider.getInteractionType()).thenReturn(NOTIFICATION_DISMISS);
+ when(mDataProvider.isVertical()).thenReturn(false);
+
+ when(mDataProvider.isUp()).thenReturn(false); // up and right should cause no effect.
+ when(mDataProvider.isRight()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isUp()).thenReturn(true);
+ when(mDataProvider.isRight()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isUp()).thenReturn(false);
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isUp()).thenReturn(true);
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFalse_NotificationDismiss() {
+ when(mDataProvider.getInteractionType()).thenReturn(NOTIFICATION_DISMISS);
+ when(mDataProvider.isVertical()).thenReturn(true);
+
+ when(mDataProvider.isUp()).thenReturn(false); // up and right should cause no effect.
+ when(mDataProvider.isRight()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isUp()).thenReturn(true);
+ when(mDataProvider.isRight()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isUp()).thenReturn(false);
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isUp()).thenReturn(true);
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+
+
+ @Test
+ public void testPass_Unlock() {
+ when(mDataProvider.getInteractionType()).thenReturn(UNLOCK);
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(true);
+
+
+ when(mDataProvider.isRight()).thenReturn(false); // right should cause no effect.
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFalse_Unlock() {
+ when(mDataProvider.getInteractionType()).thenReturn(UNLOCK);
+
+ when(mDataProvider.isVertical()).thenReturn(false);
+ when(mDataProvider.isUp()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isVertical()).thenReturn(false);
+ when(mDataProvider.isUp()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+
+ @Test
+ public void testPass_BouncerUnlock() {
+ when(mDataProvider.getInteractionType()).thenReturn(BOUNCER_UNLOCK);
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(true);
+
+
+ when(mDataProvider.isRight()).thenReturn(false); // right should cause no effect.
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFalse_BouncerUnlock() {
+ when(mDataProvider.getInteractionType()).thenReturn(BOUNCER_UNLOCK);
+
+ when(mDataProvider.isVertical()).thenReturn(false);
+ when(mDataProvider.isUp()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isVertical()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isVertical()).thenReturn(false);
+ when(mDataProvider.isUp()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+
+ @Test
+ public void testPass_LeftAffordance() {
+ when(mDataProvider.getInteractionType()).thenReturn(LEFT_AFFORDANCE);
+ when(mDataProvider.isUp()).thenReturn(true);
+ when(mDataProvider.isRight()).thenReturn(true);
+
+
+ when(mDataProvider.isVertical()).thenReturn(false); // vertical should cause no effect.
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isVertical()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFalse_LeftAffordance() {
+ when(mDataProvider.getInteractionType()).thenReturn(LEFT_AFFORDANCE);
+
+ when(mDataProvider.isRight()).thenReturn(false);
+ when(mDataProvider.isUp()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isRight()).thenReturn(true);
+ when(mDataProvider.isUp()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isRight()).thenReturn(false);
+ when(mDataProvider.isUp()).thenReturn(false);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+
+ @Test
+ public void testPass_RightAffordance() {
+ when(mDataProvider.getInteractionType()).thenReturn(RIGHT_AFFORDANCE);
+ when(mDataProvider.isUp()).thenReturn(true);
+ when(mDataProvider.isRight()).thenReturn(false);
+
+
+ when(mDataProvider.isVertical()).thenReturn(false); // vertical should cause no effect.
+ assertThat(mClassifier.isFalseTouch(), is(false));
+
+ when(mDataProvider.isVertical()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(false));
+ }
+
+ @Test
+ public void testFalse_RightAffordance() {
+ when(mDataProvider.getInteractionType()).thenReturn(RIGHT_AFFORDANCE);
+
+ when(mDataProvider.isUp()).thenReturn(true);
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isUp()).thenReturn(false);
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+
+ when(mDataProvider.isUp()).thenReturn(false);
+ when(mDataProvider.isRight()).thenReturn(true);
+ assertThat(mClassifier.isFalseTouch(), is(true));
+ }
+}
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 000000000000..976b586d089b
--- /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<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);
+ }
+}