From 07fb7b73533fa14d34fd28e2a2c4c170f38fca69 Mon Sep 17 00:00:00 2001 From: Dave Mankoff Date: Mon, 10 Jun 2019 16:36:19 -0400 Subject: [PATCH] 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 --- .../systemui/classifier/Classifier.java | 19 ++ .../brightline/BrightLineFalsingManager.java | 320 ++++++++++++++++++ .../brightline/FalsingClassifier.java | 131 +++++++ .../brightline/FalsingDataProvider.java | 218 ++++++++++++ .../TimeLimitedMotionEventBuffer.java | 242 +++++++++++++ .../brightline/FalsingDataProviderTest.java | 293 ++++++++++++++++ 6 files changed, 1223 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java create mode 100644 packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java create mode 100644 packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java create mode 100644 packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java index f8856ce15f83e..ae7d142a9e45e 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java @@ -16,9 +16,13 @@ package com.android.systemui.classifier; +import android.annotation.IntDef; import android.hardware.SensorEvent; import android.view.MotionEvent; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * An abstract class for classifiers for touch and sensor events. */ @@ -34,6 +38,21 @@ public abstract class Classifier { public static final int BOUNCER_UNLOCK = 8; public static final int PULSE_EXPAND = 9; + @IntDef({ + QUICK_SETTINGS, + NOTIFICATION_DISMISS, + NOTIFICATION_DRAG_DOWN, + NOTIFICATION_DOUBLE_TAP, + UNLOCK, + LEFT_AFFORDANCE, + RIGHT_AFFORDANCE, + GENERIC, + BOUNCER_UNLOCK, + PULSE_EXPAND + }) + @Retention(RetentionPolicy.SOURCE) + public @interface InteractionType {} + /** * Contains all the information about touch events from which the classifier can query */ diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java new file mode 100644 index 0000000000000..71e190f0a01c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java @@ -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 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); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java new file mode 100644 index 0000000000000..c6ac2dd5a2713 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.classifier.brightline; + +import android.hardware.SensorEvent; +import android.view.MotionEvent; + +import com.android.systemui.classifier.Classifier; + +import java.util.List; + +/** + * Base class for rules that determine False touches. + */ +abstract class FalsingClassifier { + private final FalsingDataProvider mDataProvider; + + FalsingClassifier(FalsingDataProvider dataProvider) { + this.mDataProvider = dataProvider; + } + + List 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); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java new file mode 100644 index 0000000000000..10b34c08634bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java @@ -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 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 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 unpackMotionEvent(MotionEvent motionEvent) { + List motionEvents = new ArrayList<>(); + List 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 pointerCoordsList = new ArrayList<>(); + for (int j = 0; j < pointerCount; j++) { + PointerCoords pointerCoords = new PointerCoords(); + motionEvent.getHistoricalPointerCoords(j, i, pointerCoords); + pointerCoordsList.add(pointerCoords); + } + motionEvents.add(MotionEvent.obtain( + motionEvent.getDownTime(), + motionEvent.getHistoricalEventTime(i), + motionEvent.getAction(), + pointerCount, + pointerPropertiesArray, + pointerCoordsList.toArray(new PointerCoords[0]), + motionEvent.getMetaState(), + motionEvent.getButtonState(), + motionEvent.getXPrecision(), + motionEvent.getYPrecision(), + motionEvent.getDeviceId(), + motionEvent.getEdgeFlags(), + motionEvent.getSource(), + motionEvent.getFlags() + )); + } + + motionEvents.add(MotionEvent.obtainNoHistory(motionEvent)); + + return motionEvents; + } + + void onSessionEnd() { + mFirstActualMotionEvent = null; + + for (MotionEvent ev : mRecentMotionEvents) { + ev.recycle(); + } + + mRecentMotionEvents.clear(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java new file mode 100644 index 0000000000000..9a83b5bd83289 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.classifier.brightline; + +import android.view.MotionEvent; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +/** + * Maintains an ordered list of the last N milliseconds of MotionEvents. + * + * This class is simply a convenience class designed to look like a simple list, but that + * automatically discards old MotionEvents. It functions much like a queue - first in first out - + * but does not have a fixed size like a circular buffer. + */ +public class TimeLimitedMotionEventBuffer implements List { + + private final LinkedList mMotionEvents; + private long mMaxAgeMs; + + TimeLimitedMotionEventBuffer(long maxAgeMs) { + super(); + this.mMaxAgeMs = maxAgeMs; + this.mMotionEvents = new LinkedList<>(); + } + + private void ejectOldEvents() { + if (mMotionEvents.isEmpty()) { + return; + } + Iterator 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 iterator() { + return mMotionEvents.iterator(); + } + + @Override + public Object[] toArray() { + return mMotionEvents.toArray(); + } + + @Override + public 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 collection) { + boolean result = mMotionEvents.addAll(collection); + ejectOldEvents(); + return result; + } + + @Override + public boolean addAll(int index, Collection 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 listIterator() { + return new Iter(0); + } + + @Override + public ListIterator listIterator(int index) { + return new Iter(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + class Iter implements ListIterator { + + private final ListIterator mIterator; + + Iter(int index) { + this.mIterator = mMotionEvents.listIterator(index); + } + + @Override + public boolean hasNext() { + return mIterator.hasNext(); + } + + @Override + public MotionEvent next() { + return mIterator.next(); + } + + @Override + public boolean hasPrevious() { + return mIterator.hasPrevious(); + } + + @Override + public MotionEvent previous() { + return mIterator.previous(); + } + + @Override + public int nextIndex() { + return mIterator.nextIndex(); + } + + @Override + public int previousIndex() { + return mIterator.previousIndex(); + } + + @Override + public void remove() { + mIterator.remove(); + } + + @Override + public void set(MotionEvent motionEvent) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(MotionEvent element) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java new file mode 100644 index 0000000000000..5ffaf75d6376c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.classifier.brightline; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.closeTo; +import static org.junit.Assert.assertThat; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.MotionEvent; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class FalsingDataProviderTest extends SysuiTestCase { + + private FalsingDataProvider mDataProvider; + + @Before + public void setup() { + mDataProvider = new FalsingDataProvider(getContext()); + } + + @Test + public void test_trackMotionEvents() { + MotionEvent motionEventA = obtainMotionEvent(MotionEvent.ACTION_DOWN, 1, 2, 9); + MotionEvent motionEventB = obtainMotionEvent(MotionEvent.ACTION_MOVE, 2, 4, 7); + MotionEvent motionEventC = obtainMotionEvent(MotionEvent.ACTION_UP, 3, 6, 5); + + mDataProvider.onMotionEvent(motionEventA); + mDataProvider.onMotionEvent(motionEventB); + mDataProvider.onMotionEvent(motionEventC); + List 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 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 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); + } +}