From f40e94955cba0ca351f587358b9e07496d132a1b Mon Sep 17 00:00:00 2001 From: Hyunyoung Song Date: Thu, 6 Jul 2017 12:35:55 -0700 Subject: Add tests to SwipeDetector (formerly VerticalPullDetector). Change-Id: I09ab4f22d7204ad806825ab0d6374c2b9616bf39 --- .../testcomponent/TouchEventGenerator.java | 275 +++++++++++++++++++++ .../android/launcher3/touch/SwipeDetectorTest.java | 96 +++++++ 2 files changed, 371 insertions(+) create mode 100644 tests/src/com/android/launcher3/testcomponent/TouchEventGenerator.java create mode 100644 tests/src/com/android/launcher3/touch/SwipeDetectorTest.java (limited to 'tests/src/com') diff --git a/tests/src/com/android/launcher3/testcomponent/TouchEventGenerator.java b/tests/src/com/android/launcher3/testcomponent/TouchEventGenerator.java new file mode 100644 index 000000000..80d6341e7 --- /dev/null +++ b/tests/src/com/android/launcher3/testcomponent/TouchEventGenerator.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2017 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.launcher3.testcomponent; + +import android.graphics.Point; +import android.util.Pair; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class to generate MotionEvent event sequences for testing touch gesture detectors. + */ +public class TouchEventGenerator { + + /** + * Amount of time between two generated events. + */ + private static final long TIME_INCREMENT_MS = 20L; + + /** + * Id of the fake device generating the events. + */ + private static final int DEVICE_ID = 2104; + + /** + * The fingers currently present on the emulated touch screen. + */ + private Map mFingers; + + /** + * Initial event time for the current sequence. + */ + private long mInitialTime; + + /** + * Time of the last generated event. + */ + private long mLastEventTime; + + /** + * Time of the next event. + */ + private long mTime; + + /** + * Receives the generated events. + */ + public interface Listener { + + /** + * Called when an event was generated. + */ + void onTouchEvent(MotionEvent event); + } + private final Listener mListener; + + public TouchEventGenerator(Listener listener) { + mListener = listener; + mFingers = new HashMap(); + } + + /** + * Adds a finger on the touchscreen. + */ + public TouchEventGenerator put(int id, int x, int y, long ms) { + checkFingerExistence(id, false); + boolean isInitialDown = mFingers.isEmpty(); + mFingers.put(id, new Point(x, y)); + int action; + if (isInitialDown) { + action = MotionEvent.ACTION_DOWN; + } else { + action = MotionEvent.ACTION_POINTER_DOWN; + // Set the id of the changed pointer. + action |= id << MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } + generateEvent(action, ms); + return this; + } + + /** + * Adds a finger on the touchscreen after advancing default time interval. + */ + public TouchEventGenerator put(int id, int x, int y) { + return put(id, x, y, TIME_INCREMENT_MS); + } + + /** + * Adjusts the position of a finger for an upcoming move event. + * + * @see #move(long ms) + */ + public TouchEventGenerator position(int id, int x, int y) { + checkFingerExistence(id, true); + mFingers.get(id).set(x, y); + return this; + } + + /** + * Commits the finger position changes of {@link #position(int, int, int)} by generating a move + * event. + * + * @see #position(int, int, int) + */ + public TouchEventGenerator move(long ms) { + generateEvent(MotionEvent.ACTION_MOVE, ms); + return this; + } + + /** + * Commits the finger position changes of {@link #position(int, int, int)} by generating a move + * event after advancing the default time interval. + * + * @see #position(int, int, int) + */ + public TouchEventGenerator move() { + return move(TIME_INCREMENT_MS); + } + + /** + * Moves a single finger on the touchscreen. + */ + public TouchEventGenerator move(int id, int x, int y, long ms) { + return position(id, x, y).move(ms); + } + + /** + * Moves a single finger on the touchscreen after advancing default time interval. + */ + public TouchEventGenerator move(int id, int x, int y) { + return move(id, x, y, TIME_INCREMENT_MS); + } + + /** + * Removes an existing finger from the touchscreen. + */ + public TouchEventGenerator lift(int id, long ms) { + checkFingerExistence(id, true); + boolean isFinalUp = mFingers.size() == 1; + int action; + if (isFinalUp) { + action = MotionEvent.ACTION_UP; + } else { + action = MotionEvent.ACTION_POINTER_UP; + // Set the id of the changed pointer. + action |= id << MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } + generateEvent(action, ms); + mFingers.remove(id); + return this; + } + + /** + * Removes a finger from the touchscreen. + */ + public TouchEventGenerator lift(int id, int x, int y, long ms) { + checkFingerExistence(id, true); + mFingers.get(id).set(x, y); + return lift(id, ms); + } + + /** + * Removes an existing finger from the touchscreen after advancing default time interval. + */ + public TouchEventGenerator lift(int id) { + return lift(id, TIME_INCREMENT_MS); + } + + /** + * Cancels an ongoing sequence. + */ + public TouchEventGenerator cancel(long ms) { + generateEvent(MotionEvent.ACTION_CANCEL, ms); + mFingers.clear(); + return this; + } + + /** + * Cancels an ongoing sequence. + */ + public TouchEventGenerator cancel() { + return cancel(TIME_INCREMENT_MS); + } + + private void checkFingerExistence(int id, boolean shouldExist) { + if (shouldExist != mFingers.containsKey(id)) { + throw new IllegalArgumentException( + shouldExist ? "Finger does not exist" : "Finger already exists"); + } + } + + private void generateEvent(int action, long ms) { + mTime = mLastEventTime + ms; + Pair state = getFingerState(); + MotionEvent event = MotionEvent.obtain( + mInitialTime, + mTime, + action, + state.first.length, + state.first, + state.second, + 0 /* metaState */, + 0 /* buttonState */, + 1.0f /* xPrecision */, + 1.0f /* yPrecision */, + DEVICE_ID, + 0 /* edgeFlags */, + InputDevice.SOURCE_TOUCHSCREEN, + 0 /* flags */); + mListener.onTouchEvent(event); + if (action == MotionEvent.ACTION_UP) { + resetTime(); + } + event.recycle(); + mLastEventTime = mTime; + } + + /** + * Returns the description of the fingers' state expected by MotionEvent. + */ + private Pair getFingerState() { + int nFingers = mFingers.size(); + PointerProperties[] properties = new PointerProperties[nFingers]; + PointerCoords[] coordinates = new PointerCoords[nFingers]; + + int index = 0; + for (Map.Entry entry : mFingers.entrySet()) { + int id = entry.getKey(); + Point location = entry.getValue(); + + PointerProperties property = new PointerProperties(); + property.id = id; + property.toolType = MotionEvent.TOOL_TYPE_FINGER; + properties[index] = property; + + PointerCoords coordinate = new PointerCoords(); + coordinate.x = location.x; + coordinate.y = location.y; + coordinate.pressure = 1.0f; + coordinates[index] = coordinate; + + index++; + } + + return new Pair( + properties, coordinates); + } + + /** + * Resets the time references for a new sequence. + */ + private void resetTime() { + mInitialTime = 0L; + mLastEventTime = -1L; + mTime = 0L; + } +} diff --git a/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java new file mode 100644 index 000000000..8724704ed --- /dev/null +++ b/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 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.launcher3.touch; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.launcher3.testcomponent.TouchEventGenerator; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Mockito.verify; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SwipeDetectorTest{ + + private static final String TAG = SwipeDetectorTest.class.getSimpleName(); + public static void L(String s, Object... parts) { + Log.d(TAG, (parts.length == 0) ? s : String.format(s, parts)); + } + + private TouchEventGenerator mGenerator; + private SwipeDetector mDetector; + private int mTouchSlop; + + @Mock + private SwipeDetector.Listener mMockListener; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + Context context = InstrumentationRegistry.getTargetContext(); + mDetector = new SwipeDetector(context); + mGenerator = new TouchEventGenerator(new TouchEventGenerator.Listener() { + @Override + public void onTouchEvent(MotionEvent event) { + mDetector.onTouchEvent(event); + } + }); + mDetector.setListener(mMockListener); + mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + L("mTouchSlop=", mTouchSlop); + } + + @Test + public void testDragStart() throws Exception { + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100, 100 + mTouchSlop); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDragStart(anyBoolean()); + } + + @Test + public void testDrag() throws Exception { + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100, 100 + mTouchSlop); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDrag(anyFloat(), anyFloat()); + } + + @Test + public void testDragEnd() throws Exception { + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100, 100 + mTouchSlop); + mGenerator.move(0, 100, 100 + mTouchSlop * 2); + mGenerator.lift(0); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDragEnd(anyFloat(), anyBoolean()); + } +} -- cgit v1.2.3