Merge changes from topic "b111394067-new-falsing-manager" into qt-dev

am: acd240fbb3

Change-Id: I83640ae5c6a6681eb33c87a70a629573f189d024
This commit is contained in:
Dave Mankoff
2019-06-20 13:07:12 -07:00
committed by android-build-merger
18 changed files with 3318 additions and 0 deletions

View File

@@ -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
*/

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.classifier.brightline;
import android.graphics.Point;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
/**
* Penalizes gestures that change direction in either the x or y too much.
*/
class ZigZagClassifier extends FalsingClassifier {
// Define how far one can move back and forth over one inch of travel before being falsed.
// `PRIMARY` defines how far one can deviate in the primary direction of travel. I.e. if you're
// swiping vertically, you shouldn't have a lot of zig zag in the vertical direction. Since
// most swipes will follow somewhat of a 'C' or 'S' shape, we allow more deviance along the
// `SECONDARY` axis.
private static final float MAX_X_PRIMARY_DEVIANCE = .05f;
private static final float MAX_Y_PRIMARY_DEVIANCE = .05f;
private static final float MAX_X_SECONDARY_DEVIANCE = .3f;
private static final float MAX_Y_SECONDARY_DEVIANCE = .3f;
ZigZagClassifier(FalsingDataProvider dataProvider) {
super(dataProvider);
}
@Override
boolean isFalseTouch() {
List<MotionEvent> motionEvents = getRecentMotionEvents();
// Rotate horizontal gestures to be horizontal between their first and last point.
// Rotate vertical gestures to be vertical between their first and last point.
// Sum the absolute value of every dx and dy along the gesture. Compare this with the dx
// and dy
// between the first and last point.
// For horizontal lines, the difference in the x direction should be small.
// For vertical lines, the difference in the y direction should be small.
if (motionEvents.size() < 3) {
return false;
}
List<Point> rotatedPoints;
if (isHorizontal()) {
rotatedPoints = rotateHorizontal();
} else {
rotatedPoints = rotateVertical();
}
float actualDx = Math
.abs(rotatedPoints.get(0).x - rotatedPoints.get(rotatedPoints.size() - 1).x);
float actualDy = Math
.abs(rotatedPoints.get(0).y - rotatedPoints.get(rotatedPoints.size() - 1).y);
logDebug("Actual: (" + actualDx + "," + actualDy + ")");
float runningAbsDx = 0;
float runningAbsDy = 0;
float pX = 0;
float pY = 0;
boolean firstLoop = true;
for (Point point : rotatedPoints) {
if (firstLoop) {
pX = point.x;
pY = point.y;
firstLoop = false;
continue;
}
runningAbsDx += Math.abs(point.x - pX);
runningAbsDy += Math.abs(point.y - pY);
pX = point.x;
pY = point.y;
logDebug("(x, y, runningAbsDx, runningAbsDy) - (" + pX + ", " + pY + ", " + runningAbsDx
+ ", " + runningAbsDy + ")");
}
float devianceX = runningAbsDx - actualDx;
float devianceY = runningAbsDy - actualDy;
float distanceXIn = actualDx / getXdpi();
float distanceYIn = actualDy / getYdpi();
float totalDistanceIn = (float) Math
.sqrt(distanceXIn * distanceXIn + distanceYIn * distanceYIn);
float maxXDeviance;
float maxYDeviance;
if (actualDx > actualDy) {
maxXDeviance = MAX_X_PRIMARY_DEVIANCE * totalDistanceIn * getXdpi();
maxYDeviance = MAX_Y_SECONDARY_DEVIANCE * totalDistanceIn * getYdpi();
} else {
maxXDeviance = MAX_X_SECONDARY_DEVIANCE * totalDistanceIn * getXdpi();
maxYDeviance = MAX_Y_PRIMARY_DEVIANCE * totalDistanceIn * getYdpi();
}
logDebug("Straightness Deviance: (" + devianceX + "," + devianceY + ") vs "
+ "(" + maxXDeviance + "," + maxYDeviance + ")");
return devianceX > maxXDeviance || devianceY > maxYDeviance;
}
private float getAtan2LastPoint() {
MotionEvent firstEvent = getFirstMotionEvent();
MotionEvent lastEvent = getLastMotionEvent();
float offsetX = firstEvent.getX();
float offsetY = firstEvent.getY();
float lastX = lastEvent.getX() - offsetX;
float lastY = lastEvent.getY() - offsetY;
return (float) Math.atan2(lastY, lastX);
}
private List<Point> rotateVertical() {
// Calculate the angle relative to the y axis.
double angle = Math.PI / 2 - getAtan2LastPoint();
logDebug("Rotating to vertical by: " + angle);
return rotateMotionEvents(getRecentMotionEvents(), -angle);
}
private List<Point> rotateHorizontal() {
// Calculate the angle relative to the x axis.
double angle = getAtan2LastPoint();
logDebug("Rotating to horizontal by: " + angle);
return rotateMotionEvents(getRecentMotionEvents(), angle);
}
private List<Point> rotateMotionEvents(List<MotionEvent> motionEvents, double angle) {
List<Point> points = new ArrayList<>();
double cosAngle = Math.cos(angle);
double sinAngle = Math.sin(angle);
MotionEvent firstEvent = motionEvents.get(0);
float offsetX = firstEvent.getX();
float offsetY = firstEvent.getY();
for (MotionEvent motionEvent : motionEvents) {
float x = motionEvent.getX() - offsetX;
float y = motionEvent.getY() - offsetY;
double rotatedX = cosAngle * x + sinAngle * y + offsetX;
double rotatedY = -sinAngle * x + cosAngle * y + offsetY;
points.add(new Point((int) rotatedX, (int) rotatedY));
}
MotionEvent lastEvent = motionEvents.get(motionEvents.size() - 1);
Point firstPoint = points.get(0);
Point lastPoint = points.get(points.size() - 1);
logDebug(
"Before: (" + firstEvent.getX() + "," + firstEvent.getY() + "), ("
+ lastEvent.getX() + ","
+ lastEvent.getY() + ")");
logDebug(
"After: (" + firstPoint.x + "," + firstPoint.y + "), (" + lastPoint.x + ","
+ lastPoint.y
+ ")");
return points;
}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,467 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.classifier.brightline;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class ZigZagClassifierTest extends SysuiTestCase {
private static final long NS_PER_MS = 1000000;
@Mock
private FalsingDataProvider mDataProvider;
private FalsingClassifier mClassifier;
private List<MotionEvent> mMotionEvents = new ArrayList<>();
private float mOffsetX = 0;
private float mOffsetY = 0;
private float mDx;
private float mDy;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(mDataProvider.getXdpi()).thenReturn(100f);
when(mDataProvider.getYdpi()).thenReturn(100f);
when(mDataProvider.getRecentMotionEvents()).thenReturn(mMotionEvents);
mClassifier = new ZigZagClassifier(mDataProvider);
// Calculate the response to these calls on the fly, otherwise Mockito gets bogged down
// everytime we call appendMotionEvent.
when(mDataProvider.getFirstRecentMotionEvent()).thenAnswer(
(Answer<MotionEvent>) invocation -> mMotionEvents.get(0));
when(mDataProvider.getLastMotionEvent()).thenAnswer(
(Answer<MotionEvent>) invocation -> mMotionEvents.get(mMotionEvents.size() - 1));
when(mDataProvider.isHorizontal()).thenAnswer(
(Answer<Boolean>) invocation -> Math.abs(mDy) < Math.abs(mDx));
when(mDataProvider.isVertical()).thenAnswer(
(Answer<Boolean>) invocation -> Math.abs(mDy) > Math.abs(mDx));
when(mDataProvider.isRight()).thenAnswer((Answer<Boolean>) invocation -> mDx > 0);
when(mDataProvider.isUp()).thenAnswer((Answer<Boolean>) invocation -> mDy < 0);
}
@After
public void tearDown() {
for (MotionEvent motionEvent : mMotionEvents) {
motionEvent.recycle();
}
mMotionEvents.clear();
}
@Test
public void testPass_fewTouchesVertical() {
assertThat(mClassifier.isFalseTouch(), is(false));
appendMotionEvent(0, 0);
assertThat(mClassifier.isFalseTouch(), is(false));
appendMotionEvent(0, 100);
assertThat(mClassifier.isFalseTouch(), is(false));
}
@Test
public void testPass_vertical() {
appendMotionEvent(0, 0);
appendMotionEvent(0, 100);
appendMotionEvent(0, 200);
assertThat(mClassifier.isFalseTouch(), is(false));
}
@Test
public void testPass_fewTouchesHorizontal() {
assertThat(mClassifier.isFalseTouch(), is(false));
appendMotionEvent(0, 0);
assertThat(mClassifier.isFalseTouch(), is(false));
appendMotionEvent(100, 0);
assertThat(mClassifier.isFalseTouch(), is(false));
}
@Test
public void testPass_horizontal() {
appendMotionEvent(0, 0);
appendMotionEvent(100, 0);
appendMotionEvent(200, 0);
assertThat(mClassifier.isFalseTouch(), is(false));
}
@Test
public void testFail_minimumTouchesVertical() {
appendMotionEvent(0, 0);
appendMotionEvent(0, 100);
appendMotionEvent(0, 1);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void testFail_minimumTouchesHorizontal() {
appendMotionEvent(0, 0);
appendMotionEvent(100, 0);
appendMotionEvent(1, 0);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void testPass_fortyFiveDegreesStraight() {
appendMotionEvent(0, 0);
appendMotionEvent(10, 10);
appendMotionEvent(20, 20);
assertThat(mClassifier.isFalseTouch(), is(false));
}
@Test
public void testPass_horizontalZigZagVerticalStraight() {
// This test looks just like testFail_horizontalZigZagVerticalStraight but with
// a longer y range, making it look straighter.
appendMotionEvent(0, 0);
appendMotionEvent(5, 100);
appendMotionEvent(-5, 200);
assertThat(mClassifier.isFalseTouch(), is(false));
}
@Test
public void testPass_horizontalStraightVerticalZigZag() {
// This test looks just like testFail_horizontalStraightVerticalZigZag but with
// a longer x range, making it look straighter.
appendMotionEvent(0, 0);
appendMotionEvent(100, 5);
appendMotionEvent(200, -5);
assertThat(mClassifier.isFalseTouch(), is(false));
}
@Test
public void testFail_horizontalZigZagVerticalStraight() {
// This test looks just like testPass_horizontalZigZagVerticalStraight but with
// a shorter y range, making it look more crooked.
appendMotionEvent(0, 0);
appendMotionEvent(5, 10);
appendMotionEvent(-5, 20);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void testFail_horizontalStraightVerticalZigZag() {
// This test looks just like testPass_horizontalStraightVerticalZigZag but with
// a shorter x range, making it look more crooked.
appendMotionEvent(0, 0);
appendMotionEvent(10, 5);
appendMotionEvent(20, -5);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void test_between0And45() {
appendMotionEvent(0, 0);
appendMotionEvent(100, 5);
appendMotionEvent(200, 10);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(100, 0);
appendMotionEvent(200, 10);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(100, -10);
appendMotionEvent(200, 10);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(100, -10);
appendMotionEvent(200, 50);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void test_between45And90() {
appendMotionEvent(0, 0);
appendMotionEvent(10, 50);
appendMotionEvent(8, 100);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(1, 800);
appendMotionEvent(2, 900);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-10, 600);
appendMotionEvent(30, 700);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(40, 100);
appendMotionEvent(0, 101);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void test_between90And135() {
appendMotionEvent(0, 0);
appendMotionEvent(-10, 50);
appendMotionEvent(-24, 100);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-20, 800);
appendMotionEvent(-20, 900);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(30, 600);
appendMotionEvent(-10, 700);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-80, 100);
appendMotionEvent(-10, 101);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void test_between135And180() {
appendMotionEvent(0, 0);
appendMotionEvent(-120, 10);
appendMotionEvent(-200, 20);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-20, 8);
appendMotionEvent(-40, 2);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-500, -2);
appendMotionEvent(-600, 70);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-80, 100);
appendMotionEvent(-100, 1);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void test_between180And225() {
appendMotionEvent(0, 0);
appendMotionEvent(-120, -10);
appendMotionEvent(-200, -20);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-20, -8);
appendMotionEvent(-40, -2);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-500, 2);
appendMotionEvent(-600, -70);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-80, -100);
appendMotionEvent(-100, -1);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void test_between225And270() {
appendMotionEvent(0, 0);
appendMotionEvent(-12, -20);
appendMotionEvent(-20, -40);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-20, -130);
appendMotionEvent(-40, -260);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(1, -100);
appendMotionEvent(-6, -200);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-80, -100);
appendMotionEvent(-10, -110);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void test_between270And315() {
appendMotionEvent(0, 0);
appendMotionEvent(12, -20);
appendMotionEvent(20, -40);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(20, -130);
appendMotionEvent(40, -260);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(-1, -100);
appendMotionEvent(6, -200);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(80, -100);
appendMotionEvent(10, -110);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void test_between315And360() {
appendMotionEvent(0, 0);
appendMotionEvent(120, -20);
appendMotionEvent(200, -40);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(200, -13);
appendMotionEvent(400, -30);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(100, 10);
appendMotionEvent(600, -20);
assertThat(mClassifier.isFalseTouch(), is(false));
mMotionEvents.clear();
appendMotionEvent(0, 0);
appendMotionEvent(80, -100);
appendMotionEvent(100, -1);
assertThat(mClassifier.isFalseTouch(), is(true));
}
@Test
public void test_randomOrigins() {
// The purpose of this test is to try all the other tests from different starting points.
// We use a pre-determined seed to make this test repeatable.
Random rand = new Random(23);
for (int i = 0; i < 100; i++) {
mOffsetX = rand.nextInt(2000) - 1000;
mOffsetY = rand.nextInt(2000) - 1000;
try {
mMotionEvents.clear();
testPass_fewTouchesVertical();
mMotionEvents.clear();
testPass_vertical();
mMotionEvents.clear();
testFail_horizontalStraightVerticalZigZag();
mMotionEvents.clear();
testFail_horizontalZigZagVerticalStraight();
mMotionEvents.clear();
testFail_minimumTouchesHorizontal();
mMotionEvents.clear();
testFail_minimumTouchesVertical();
mMotionEvents.clear();
testPass_fewTouchesHorizontal();
mMotionEvents.clear();
testPass_fortyFiveDegreesStraight();
mMotionEvents.clear();
testPass_horizontal();
mMotionEvents.clear();
testPass_horizontalStraightVerticalZigZag();
mMotionEvents.clear();
testPass_horizontalZigZagVerticalStraight();
mMotionEvents.clear();
test_between0And45();
mMotionEvents.clear();
test_between45And90();
mMotionEvents.clear();
test_between90And135();
mMotionEvents.clear();
test_between135And180();
mMotionEvents.clear();
test_between180And225();
mMotionEvents.clear();
test_between225And270();
mMotionEvents.clear();
test_between270And315();
mMotionEvents.clear();
test_between315And360();
} catch (AssertionError e) {
throw new AssertionError("Random origin failure in iteration " + i, e);
}
}
}
private void appendMotionEvent(float x, float y) {
x += mOffsetX;
y += mOffsetY;
long eventTime = mMotionEvents.size() + 1;
MotionEvent motionEvent = MotionEvent.obtain(1, eventTime, MotionEvent.ACTION_DOWN, x, y,
0);
mMotionEvents.add(motionEvent);
mDx = mDataProvider.getFirstRecentMotionEvent().getX()
- mDataProvider.getLastMotionEvent().getX();
mDy = mDataProvider.getFirstRecentMotionEvent().getY()
- mDataProvider.getLastMotionEvent().getY();
mClassifier.onTouchEvent(motionEvent);
}
}