Add base class for new falsing manager and classifiers.

This adds no functional changes. It merely adds the framework
for a new FalsingManager.

Change-Id: I7f0e3b1363c847fa1eefa54bf7793508fefd1926
Test: manual.
Bug: 111394067
This commit is contained in:
Dave Mankoff
2019-06-10 16:36:19 -04:00
parent e64acbea8e
commit 07fb7b7353
6 changed files with 1223 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,320 @@
/*
* 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<>();
// TODO: add classifiers here.
}
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,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.mWidthPixels;
}
int getHeightPixels() {
return mDataProvider.mHeightPixels;
}
float getXdpi() {
return mDataProvider.mXdpi;
}
float getYdpi() {
return mDataProvider.mYdpi;
}
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,218 @@
/*
* 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;
final int mWidthPixels;
final int mHeightPixels;
final float mXdpi;
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: " + mXdpi + ", " + mYdpi);
FalsingClassifier.logInfo("width, height: " + mWidthPixels + ", " + mHeightPixels);
}
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;
}
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;
}
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);
}
}
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,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,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(-3 * 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);
}
}