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