summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/MagnificationGestureDetector.java195
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java75
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationGestureDetectorTest.java173
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java47
5 files changed, 453 insertions, 49 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationGestureDetector.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationGestureDetector.java
new file mode 100644
index 000000000000..4c892e29f386
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationGestureDetector.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2020 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.accessibility;
+
+import android.annotation.DisplayContext;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.PointF;
+import android.os.Handler;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * Detects single tap and drag gestures using the supplied {@link MotionEvent}s. The {@link
+ * OnGestureListener} callback will notify users when a particular motion event has occurred. This
+ * class should only be used with {@link MotionEvent}s reported via touch (don't use for trackball
+ * events).
+ */
+class MagnificationGestureDetector {
+
+ interface OnGestureListener {
+ /**
+ * Called when a tap is completed within {@link ViewConfiguration#getLongPressTimeout()} and
+ * the offset between {@link MotionEvent}s and the down event doesn't exceed {@link
+ * ViewConfiguration#getScaledTouchSlop()}.
+ *
+ * @return {@code true} if this gesture is handled.
+ */
+ boolean onSingleTap();
+
+ /**
+ * Called when the user is performing dragging gesture. It is started after the offset
+ * between the down location and the move event location exceed
+ * {@link ViewConfiguration#getScaledTouchSlop()}.
+ *
+ * @param offsetX The X offset in screen coordinate.
+ * @param offsetY The Y offset in screen coordinate.
+ * @return {@code true} if this gesture is handled.
+ */
+ boolean onDrag(float offsetX, float offsetY);
+
+ /**
+ * Notified when a tap occurs with the down {@link MotionEvent} that triggered it. This will
+ * be triggered immediately for every down event. All other events should be preceded by
+ * this.
+ *
+ * @param x The X coordinate of the down event.
+ * @param y The Y coordinate of the down event.
+ * @return {@code true} if the down event is handled, otherwise the events won't be sent to
+ * the view.
+ */
+ boolean onStart(float x, float y);
+
+ /**
+ * Called when the detection is finished. In other words, it is called when up/cancel {@link
+ * MotionEvent} is received. It will be triggered after single-tap
+ *
+ * @param x The X coordinate on the screen of the up event or the cancel event.
+ * @param y The Y coordinate on the screen of the up event or the cancel event.
+ * @return {@code true} if the event is handled.
+ */
+ boolean onFinish(float x, float y);
+ }
+
+ private final PointF mPointerDown = new PointF();
+ private final PointF mPointerLocation = new PointF(Float.NaN, Float.NaN);
+ private final Handler mHandler;
+ private final Runnable mCancelTapGestureRunnable;
+ private final OnGestureListener mOnGestureListener;
+ private int mTouchSlopSquare;
+ // Assume the gesture default is a single-tap. Set it to false if the gesture couldn't be a
+ // single-tap anymore.
+ private boolean mDetectSingleTap = true;
+ private boolean mDraggingDetected = false;
+
+ /**
+ * @param context {@link Context} that is from {@link Context#createDisplayContext(Display)}.
+ * @param handler The handler to post the runnable.
+ * @param listener The listener invoked for all the callbacks.
+ */
+ MagnificationGestureDetector(@DisplayContext Context context, @NonNull Handler handler,
+ @NonNull OnGestureListener listener) {
+ final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mTouchSlopSquare = touchSlop * touchSlop;
+ mHandler = handler;
+ mOnGestureListener = listener;
+ mCancelTapGestureRunnable = () -> mDetectSingleTap = false;
+ }
+
+ /**
+ * Analyzes the given motion event and if applicable to trigger the appropriate callbacks on the
+ * {@link OnGestureListener} supplied.
+ *
+ * @param event The current motion event.
+ * @return {@code True} if the {@link OnGestureListener} consumes the event, else false.
+ */
+ boolean onTouch(MotionEvent event) {
+ final float rawX = event.getRawX();
+ final float rawY = event.getRawY();
+ boolean handled = false;
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mPointerDown.set(rawX, rawY);
+ mHandler.postAtTime(mCancelTapGestureRunnable,
+ event.getDownTime() + ViewConfiguration.getLongPressTimeout());
+ handled |= mOnGestureListener.onStart(rawX, rawY);
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ stopSingleTapDetection();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ stopSingleTapDetectionIfNeeded(rawX, rawY);
+ handled |= notifyDraggingGestureIfNeeded(rawX, rawY);
+ break;
+ case MotionEvent.ACTION_UP:
+ stopSingleTapDetectionIfNeeded(rawX, rawY);
+ if (mDetectSingleTap) {
+ handled |= mOnGestureListener.onSingleTap();
+ }
+ // Fall through
+ case MotionEvent.ACTION_CANCEL:
+ handled |= mOnGestureListener.onFinish(rawX, rawY);
+ reset();
+ break;
+ }
+ return handled;
+ }
+
+ private void stopSingleTapDetectionIfNeeded(float x, float y) {
+ if (mDraggingDetected) {
+ return;
+ }
+ if (!isLocationValid(mPointerDown)) {
+ return;
+ }
+
+ final int deltaX = (int) (mPointerDown.x - x);
+ final int deltaY = (int) (mPointerDown.y - y);
+ final int distanceSquare = (deltaX * deltaX) + (deltaY * deltaY);
+ if (distanceSquare > mTouchSlopSquare) {
+ mDraggingDetected = true;
+ stopSingleTapDetection();
+ }
+ }
+
+ private void stopSingleTapDetection() {
+ mHandler.removeCallbacks(mCancelTapGestureRunnable);
+ mDetectSingleTap = false;
+ }
+
+ private boolean notifyDraggingGestureIfNeeded(float x, float y) {
+ if (!mDraggingDetected) {
+ return false;
+ }
+ if (!isLocationValid(mPointerLocation)) {
+ mPointerLocation.set(mPointerDown);
+ }
+ final float offsetX = x - mPointerLocation.x;
+ final float offsetY = y - mPointerLocation.y;
+ mPointerLocation.set(x, y);
+ return mOnGestureListener.onDrag(offsetX, offsetY);
+ }
+
+ private void reset() {
+ resetPointF(mPointerDown);
+ resetPointF(mPointerLocation);
+ mHandler.removeCallbacks(mCancelTapGestureRunnable);
+ mDetectSingleTap = true;
+ mDraggingDetected = false;
+ }
+
+ private static void resetPointF(PointF pointF) {
+ pointF.x = Float.NaN;
+ pointF.y = Float.NaN;
+ }
+
+ private static boolean isLocationValid(PointF location) {
+ return !Float.isNaN(location.x) && !Float.isNaN(location.y);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java
index edc3216e0b81..c1cf8d31bd67 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java
@@ -22,16 +22,13 @@ import android.annotation.NonNull;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.PixelFormat;
-import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
-import android.util.MathUtils;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.accessibility.AccessibilityManager;
@@ -46,11 +43,11 @@ import java.util.Collections;
/**
* Shows/hides a {@link android.widget.ImageView} on the screen and changes the values of
- * {@link Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE} when the UI is toggled.
+ * {@link Settings.Secure#ACCESSIBILITY_MAGNIFICATION_MODE} when the UI is toggled.
* The button icon is movable by dragging. And the button UI would automatically be dismissed after
* displaying for a period of time.
*/
-class MagnificationModeSwitch {
+class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureListener {
@VisibleForTesting
static final long FADING_ANIMATION_DURATION_MS = 300;
@@ -66,13 +63,11 @@ class MagnificationModeSwitch {
private final AccessibilityManager mAccessibilityManager;
private final WindowManager mWindowManager;
private final ImageView mImageView;
- private final PointF mLastDown = new PointF();
- private final PointF mLastDrag = new PointF();
- private final int mTapTimeout = ViewConfiguration.getTapTimeout();
- private final int mTouchSlop;
private int mMagnificationMode = Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
private final LayoutParams mParams;
private boolean mIsVisible = false;
+ private final MagnificationGestureDetector mGestureDetector;
+ private boolean mSingleTapDetected = false;
MagnificationModeSwitch(Context context) {
this(context, createView(context));
@@ -86,7 +81,6 @@ class MagnificationModeSwitch {
Context.WINDOW_SERVICE);
mParams = createLayoutParams(context);
mImageView = imageView;
- mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
applyResourcesValues();
mImageView.setOnTouchListener(this::onTouch);
mImageView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
@@ -127,6 +121,8 @@ class MagnificationModeSwitch {
.start();
mIsFadeOutAnimating = true;
};
+ mGestureDetector = new MagnificationGestureDetector(context,
+ context.getMainThreadHandler(), this);
}
private CharSequence formatStateDescription() {
@@ -144,39 +140,38 @@ class MagnificationModeSwitch {
}
private boolean onTouch(View v, MotionEvent event) {
- if (!mIsVisible || mImageView == null) {
+ if (!mIsVisible) {
return false;
}
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- stopFadeOutAnimation();
- mLastDown.set(event.getRawX(), event.getRawY());
- mLastDrag.set(event.getRawX(), event.getRawY());
- return true;
- case MotionEvent.ACTION_MOVE:
- // Move the button position.
- moveButton(event.getRawX() - mLastDrag.x,
- event.getRawY() - mLastDrag.y);
- mLastDrag.set(event.getRawX(), event.getRawY());
- return true;
- case MotionEvent.ACTION_UP:
- // Single tap to toggle magnification mode and the button position will be reset
- // after the action is performed.
- final float distance = MathUtils.dist(mLastDown.x, mLastDown.y,
- event.getRawX(), event.getRawY());
- if ((event.getEventTime() - event.getDownTime()) <= mTapTimeout
- && distance <= mTouchSlop) {
- handleSingleTap();
- } else {
- showButton(mMagnificationMode);
- }
- return true;
- case MotionEvent.ACTION_CANCEL:
- showButton(mMagnificationMode);
- return true;
- default:
- return false;
+ return mGestureDetector.onTouch(event);
+ }
+
+ @Override
+ public boolean onSingleTap() {
+ mSingleTapDetected = true;
+ handleSingleTap();
+ return true;
+ }
+
+ @Override
+ public boolean onDrag(float offsetX, float offsetY) {
+ moveButton(offsetX, offsetY);
+ return true;
+ }
+
+ @Override
+ public boolean onStart(float x, float y) {
+ stopFadeOutAnimation();
+ return true;
+ }
+
+ @Override
+ public boolean onFinish(float xOffset, float yOffset) {
+ if (!mSingleTapDetected) {
+ showButton(mMagnificationMode);
}
+ mSingleTapDetected = false;
+ return true;
}
private void moveButton(float offsetX, float offsetY) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationGestureDetectorTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationGestureDetectorTest.java
new file mode 100644
index 000000000000..6f4846a601d9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationGestureDetectorTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 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.accessibility;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import android.testing.AndroidTestingRunner;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class MagnificationGestureDetectorTest extends SysuiTestCase {
+
+ private static final float ACTION_DOWN_X = 100;
+ private static final float ACTION_DOWN_Y = 200;
+ private int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ private MagnificationGestureDetector mGestureDetector;
+ private MotionEventHelper mMotionEventHelper = new MotionEventHelper();
+ @Mock
+ private MagnificationGestureDetector.OnGestureListener mListener;
+ @Mock
+ private Handler mHandler;
+ private Runnable mCancelSingleTapRunnable;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ doAnswer((invocation) -> {
+ mCancelSingleTapRunnable = invocation.getArgument(0);
+ return null;
+ }).when(mHandler).postAtTime(any(Runnable.class), anyLong());
+ mGestureDetector = new MagnificationGestureDetector(mContext, mHandler, mListener);
+ }
+
+ @After
+ public void tearDown() {
+ mMotionEventHelper.recycleEvents();
+ }
+
+ @Test
+ public void onActionDown_invokeDownCallback() {
+ final long downTime = SystemClock.uptimeMillis();
+ final MotionEvent downEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_DOWN, ACTION_DOWN_X, ACTION_DOWN_Y);
+
+ mGestureDetector.onTouch(downEvent);
+
+ mListener.onStart(ACTION_DOWN_X, ACTION_DOWN_Y);
+ }
+
+ @Test
+ public void performSingleTap_invokeCallbacksInOrder() {
+ final long downTime = SystemClock.uptimeMillis();
+ final MotionEvent downEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_DOWN, ACTION_DOWN_X, ACTION_DOWN_Y);
+ final MotionEvent upEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_UP, ACTION_DOWN_X, ACTION_DOWN_Y);
+
+ mGestureDetector.onTouch(downEvent);
+ mGestureDetector.onTouch(upEvent);
+
+ InOrder inOrder = Mockito.inOrder(mListener);
+ inOrder.verify(mListener).onStart(ACTION_DOWN_X, ACTION_DOWN_Y);
+ inOrder.verify(mListener).onSingleTap();
+ inOrder.verify(mListener).onFinish(ACTION_DOWN_X, ACTION_DOWN_Y);
+ verify(mListener, never()).onDrag(anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void performSingleTapWithActionCancel_notInvokeOnSingleTapCallback() {
+ final long downTime = SystemClock.uptimeMillis();
+ final MotionEvent downEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_DOWN, ACTION_DOWN_X, ACTION_DOWN_Y);
+ final MotionEvent cancelEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_CANCEL, ACTION_DOWN_X, ACTION_DOWN_Y);
+
+ mGestureDetector.onTouch(downEvent);
+ mGestureDetector.onTouch(cancelEvent);
+
+ verify(mListener, never()).onSingleTap();
+ }
+
+ @Test
+ public void performSingleTapWithTwoPointers_notInvokeSingleTapCallback() {
+ final long downTime = SystemClock.uptimeMillis();
+ final MotionEvent downEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_DOWN, ACTION_DOWN_X, ACTION_DOWN_Y);
+ final MotionEvent upEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_POINTER_DOWN, ACTION_DOWN_X, ACTION_DOWN_Y);
+
+ mGestureDetector.onTouch(downEvent);
+ mGestureDetector.onTouch(upEvent);
+
+ verify(mListener, never()).onSingleTap();
+ }
+
+ @Test
+ public void performLongPress_invokeCallbacksInOrder() {
+ final long downTime = SystemClock.uptimeMillis();
+ final MotionEvent downEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_DOWN, ACTION_DOWN_X, ACTION_DOWN_Y);
+ final MotionEvent upEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_UP, ACTION_DOWN_X, ACTION_DOWN_Y);
+
+ mGestureDetector.onTouch(downEvent);
+ // Execute the pending message for stopping single-tap detection.
+ mCancelSingleTapRunnable.run();
+ mGestureDetector.onTouch(upEvent);
+
+ InOrder inOrder = Mockito.inOrder(mListener);
+ inOrder.verify(mListener).onStart(ACTION_DOWN_X, ACTION_DOWN_Y);
+ inOrder.verify(mListener).onFinish(ACTION_DOWN_X, ACTION_DOWN_Y);
+ verify(mListener, never()).onSingleTap();
+ }
+
+ @Test
+ public void performDrag_invokeCallbacksInOrder() {
+ final long downTime = SystemClock.uptimeMillis();
+ final float dragOffset = mTouchSlop + 10;
+ final MotionEvent downEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_DOWN, ACTION_DOWN_X, ACTION_DOWN_Y);
+ final MotionEvent moveEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_MOVE, ACTION_DOWN_X + dragOffset, ACTION_DOWN_Y);
+ final MotionEvent upEvent = mMotionEventHelper.obtainMotionEvent(downTime, downTime,
+ MotionEvent.ACTION_UP, ACTION_DOWN_X, ACTION_DOWN_Y);
+
+ mGestureDetector.onTouch(downEvent);
+ mGestureDetector.onTouch(moveEvent);
+ mGestureDetector.onTouch(upEvent);
+
+ InOrder inOrder = Mockito.inOrder(mListener);
+ inOrder.verify(mListener).onStart(ACTION_DOWN_X, ACTION_DOWN_Y);
+ inOrder.verify(mListener).onDrag(dragOffset, 0);
+ inOrder.verify(mListener).onFinish(ACTION_DOWN_X, ACTION_DOWN_Y);
+ verify(mListener, never()).onSingleTap();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java
index 96f3c156c978..3d504fbefefe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java
@@ -73,7 +73,6 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.util.ArrayList;
import java.util.List;
@SmallTest
@@ -91,8 +90,8 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
private ViewPropertyAnimator mViewPropertyAnimator;
private MagnificationModeSwitch mMagnificationModeSwitch;
private View.OnTouchListener mTouchListener;
- private List<MotionEvent> mMotionEvents = new ArrayList<>();
private Runnable mFadeOutAnimation;
+ private MotionEventHelper mMotionEventHelper = new MotionEventHelper();
@Before
public void setUp() throws Exception {
@@ -117,11 +116,8 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
@After
public void tearDown() {
- for (MotionEvent event:mMotionEvents) {
- event.recycle();
- }
- mMotionEvents.clear();
mFadeOutAnimation = null;
+ mMotionEventHelper.recycleEvents();
}
@Test
@@ -436,9 +432,7 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
float y) {
- MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
- mMotionEvents.add(event);
- return event;
+ return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y);
}
private void executeFadeOutAnimation() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java
new file mode 100644
index 000000000000..92dad9bdb120
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 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.accessibility;
+
+import android.view.MotionEvent;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class MotionEventHelper {
+ @GuardedBy("this")
+ private final List<MotionEvent> mMotionEvents = new ArrayList<>();
+
+ void recycleEvents() {
+ for (MotionEvent event:mMotionEvents) {
+ event.recycle();
+ }
+ synchronized (this) {
+ mMotionEvents.clear();
+ }
+ }
+
+ MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
+ float y) {
+ MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
+ synchronized (this) {
+ mMotionEvents.add(event);
+ }
+ return event;
+ }
+}