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

* changes:
  Add ZigZagClassifier to the BrightLineFalsingManager.
  Add ProximityClassifier to the BrightLineFalsingManager
  Add DistanceClassifier to the BrightLineFalsingManager
  Add DiagonalClassifier to the BrightLineFalsingManager.
  Add TypeClassifier to the BrightLineFalsingManager.
  Add PointerCountClassifier to the BrightLineFalsingManager.
  Add base class for new falsing manager and classifiers.
This commit is contained in:
Dave Mankoff
2019-06-20 18:05:19 +00:00
committed by Android (Google) Code Review
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);
}
}