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
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
index f8856ce..ae7d142 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 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 0000000..71e190f
--- /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<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);
+ }
+}
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 0000000..c6ac2dd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.classifier.brightline;
+
+import android.hardware.SensorEvent;
+import android.view.MotionEvent;
+
+import com.android.systemui.classifier.Classifier;
+
+import java.util.List;
+
+/**
+ * Base class for rules that determine False touches.
+ */
+abstract class FalsingClassifier {
+ private final FalsingDataProvider mDataProvider;
+
+ FalsingClassifier(FalsingDataProvider dataProvider) {
+ this.mDataProvider = dataProvider;
+ }
+
+ List<MotionEvent> getRecentMotionEvents() {
+ return mDataProvider.getRecentMotionEvents();
+ }
+
+ MotionEvent getFirstMotionEvent() {
+ return mDataProvider.getFirstRecentMotionEvent();
+ }
+
+ MotionEvent getLastMotionEvent() {
+ return mDataProvider.getLastMotionEvent();
+ }
+
+ boolean isHorizontal() {
+ return mDataProvider.isHorizontal();
+ }
+
+ boolean isRight() {
+ return mDataProvider.isRight();
+ }
+
+ boolean isVertical() {
+ return mDataProvider.isVertical();
+ }
+
+ boolean isUp() {
+ return mDataProvider.isUp();
+ }
+
+ float getAngle() {
+ return mDataProvider.getAngle();
+ }
+
+ int getWidthPixels() {
+ return mDataProvider.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 0000000..10b34c0
--- /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<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();
+ }
+}
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 0000000..9a83b5b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.classifier.brightline;
+
+import android.view.MotionEvent;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Maintains an ordered list of the last N milliseconds of MotionEvents.
+ *
+ * This class is simply a convenience class designed to look like a simple list, but that
+ * automatically discards old MotionEvents. It functions much like a queue - first in first out -
+ * but does not have a fixed size like a circular buffer.
+ */
+public class TimeLimitedMotionEventBuffer implements List<MotionEvent> {
+
+ private final LinkedList<MotionEvent> mMotionEvents;
+ private long mMaxAgeMs;
+
+ TimeLimitedMotionEventBuffer(long maxAgeMs) {
+ super();
+ this.mMaxAgeMs = maxAgeMs;
+ this.mMotionEvents = new LinkedList<>();
+ }
+
+ private void ejectOldEvents() {
+ if (mMotionEvents.isEmpty()) {
+ return;
+ }
+ Iterator<MotionEvent> iter = listIterator();
+ long mostRecentMs = mMotionEvents.getLast().getEventTime();
+ while (iter.hasNext()) {
+ MotionEvent ev = iter.next();
+ if (mostRecentMs - ev.getEventTime() > mMaxAgeMs) {
+ iter.remove();
+ ev.recycle();
+ }
+ }
+ }
+
+ @Override
+ public void add(int index, MotionEvent element) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public MotionEvent remove(int index) {
+ return mMotionEvents.remove(index);
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ return mMotionEvents.indexOf(o);
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ return mMotionEvents.lastIndexOf(o);
+ }
+
+ @Override
+ public int size() {
+ return mMotionEvents.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return mMotionEvents.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return mMotionEvents.contains(o);
+ }
+
+ @Override
+ public Iterator<MotionEvent> iterator() {
+ return mMotionEvents.iterator();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return mMotionEvents.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(T[] a) {
+ return mMotionEvents.toArray(a);
+ }
+
+ @Override
+ public boolean add(MotionEvent element) {
+ boolean result = mMotionEvents.add(element);
+ ejectOldEvents();
+ return result;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ return mMotionEvents.remove(o);
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c) {
+ return mMotionEvents.containsAll(c);
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends MotionEvent> collection) {
+ boolean result = mMotionEvents.addAll(collection);
+ ejectOldEvents();
+ return result;
+ }
+
+ @Override
+ public boolean addAll(int index, Collection<? extends MotionEvent> elements) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c) {
+ return mMotionEvents.removeAll(c);
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+ return mMotionEvents.retainAll(c);
+ }
+
+ @Override
+ public void clear() {
+ mMotionEvents.clear();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return mMotionEvents.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return mMotionEvents.hashCode();
+ }
+
+ @Override
+ public MotionEvent get(int index) {
+ return mMotionEvents.get(index);
+ }
+
+ @Override
+ public MotionEvent set(int index, MotionEvent element) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ListIterator<MotionEvent> listIterator() {
+ return new Iter(0);
+ }
+
+ @Override
+ public ListIterator<MotionEvent> listIterator(int index) {
+ return new Iter(index);
+ }
+
+ @Override
+ public List<MotionEvent> subList(int fromIndex, int toIndex) {
+ throw new UnsupportedOperationException();
+ }
+
+ class Iter implements ListIterator<MotionEvent> {
+
+ private final ListIterator<MotionEvent> mIterator;
+
+ Iter(int index) {
+ this.mIterator = mMotionEvents.listIterator(index);
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mIterator.hasNext();
+ }
+
+ @Override
+ public MotionEvent next() {
+ return mIterator.next();
+ }
+
+ @Override
+ public boolean hasPrevious() {
+ return mIterator.hasPrevious();
+ }
+
+ @Override
+ public MotionEvent previous() {
+ return mIterator.previous();
+ }
+
+ @Override
+ public int nextIndex() {
+ return mIterator.nextIndex();
+ }
+
+ @Override
+ public int previousIndex() {
+ return mIterator.previousIndex();
+ }
+
+ @Override
+ public void remove() {
+ mIterator.remove();
+ }
+
+ @Override
+ public void set(MotionEvent motionEvent) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void add(MotionEvent element) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}