diff options
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); + } +} |