diff options
| author | 2019-06-20 18:05:19 +0000 | |
|---|---|---|
| committer | 2019-06-20 18:05:19 +0000 | |
| commit | acd240fbb32b8364fc882b2e34aa53d9f25c4a88 (patch) | |
| tree | 45f5dc93d670ef7eb472a047610ec3e1cc41863b | |
| parent | fcbde5243a9a4d188b834d9cd89d737a164fbe16 (diff) | |
| parent | 89ad24688b2a207603ed31fa1516d0624c64b28c (diff) | |
Merge changes from topic "b111394067-new-falsing-manager" into qt-dev
* changes:
Add ZigZagClassifier to the BrightLineFalsingManager.
Add ProximityClassifier to the BrightLineFalsingManager
Add DistanceClassifier to the BrightLineFalsingManager
Add DiagonalClassifier to the BrightLineFalsingManager.
Add TypeClassifier to the BrightLineFalsingManager.
Add PointerCountClassifier to the BrightLineFalsingManager.
Add base class for new falsing manager and classifiers.
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); + } +} |