Dieter Hsu | 01f426f | 2018-08-09 01:45:39 +0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2018 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.server.accessibility; |
| 18 | |
Dieter Hsu | 01f426f | 2018-08-09 01:45:39 +0800 | [diff] [blame] | 19 | import static org.junit.Assert.assertEquals; |
Dieter Hsu | a7fa814 | 2018-08-29 19:48:42 +0800 | [diff] [blame] | 20 | import static org.junit.Assert.assertTrue; |
| 21 | import static org.mockito.Mockito.mock; |
Dieter Hsu | 01f426f | 2018-08-09 01:45:39 +0800 | [diff] [blame] | 22 | |
| 23 | import android.content.Context; |
| 24 | import android.graphics.PointF; |
| 25 | import android.os.SystemClock; |
Dieter Hsu | a7fa814 | 2018-08-29 19:48:42 +0800 | [diff] [blame] | 26 | import android.testing.DexmakerShareClassLoaderRule; |
Dieter Hsu | 01f426f | 2018-08-09 01:45:39 +0800 | [diff] [blame] | 27 | import android.util.DebugUtils; |
| 28 | import android.view.InputDevice; |
| 29 | import android.view.MotionEvent; |
| 30 | |
| 31 | import androidx.test.InstrumentationRegistry; |
| 32 | import androidx.test.runner.AndroidJUnit4; |
| 33 | |
| 34 | import org.junit.Before; |
Dieter Hsu | a7fa814 | 2018-08-29 19:48:42 +0800 | [diff] [blame] | 35 | import org.junit.Rule; |
Dieter Hsu | 01f426f | 2018-08-09 01:45:39 +0800 | [diff] [blame] | 36 | import org.junit.Test; |
| 37 | import org.junit.runner.RunWith; |
| 38 | import org.mockito.ArgumentCaptor; |
| 39 | |
| 40 | import java.util.ArrayList; |
| 41 | import java.util.List; |
| 42 | |
| 43 | @RunWith(AndroidJUnit4.class) |
| 44 | public class TouchExplorerTest { |
| 45 | |
| 46 | public static final int STATE_TOUCH_EXPLORING = 0x00000001; |
| 47 | public static final int STATE_DRAGGING = 0x00000002; |
| 48 | public static final int STATE_DELEGATING = 0x00000004; |
| 49 | |
| 50 | private static final int FLAG_1FINGER = 0x8000; |
| 51 | private static final int FLAG_2FINGERS = 0x0100; |
| 52 | private static final int FLAG_3FINGERS = 0x0200; |
| 53 | private static final int FLAG_MOVING = 0x00010000; |
| 54 | private static final int FLAG_MOVING_DIFF_DIRECTION = 0x00020000; |
| 55 | |
| 56 | private static final int STATE_TOUCH_EXPLORING_1FINGER = STATE_TOUCH_EXPLORING | FLAG_1FINGER; |
| 57 | private static final int STATE_TOUCH_EXPLORING_2FINGER = STATE_TOUCH_EXPLORING | FLAG_2FINGERS; |
| 58 | private static final int STATE_TOUCH_EXPLORING_3FINGER = STATE_TOUCH_EXPLORING | FLAG_3FINGERS; |
| 59 | private static final int STATE_MOVING_2FINGERS = STATE_TOUCH_EXPLORING_2FINGER | FLAG_MOVING; |
| 60 | private static final int STATE_MOVING_3FINGERS = STATE_TOUCH_EXPLORING_3FINGER | FLAG_MOVING; |
| 61 | private static final int STATE_DRAGGING_2FINGERS = STATE_DRAGGING | FLAG_2FINGERS; |
| 62 | private static final int STATE_PINCH_2FINGERS = |
| 63 | STATE_TOUCH_EXPLORING_2FINGER | FLAG_MOVING_DIFF_DIRECTION; |
| 64 | private static final float DEFAULT_X = 301f; |
| 65 | private static final float DEFAULT_Y = 299f; |
| 66 | |
| 67 | private EventStreamTransformation mCaptor; |
| 68 | private MotionEvent mLastEvent; |
| 69 | private TouchExplorer mTouchExplorer; |
| 70 | private long mLastDownTime = Integer.MIN_VALUE; |
| 71 | |
Dieter Hsu | a7fa814 | 2018-08-29 19:48:42 +0800 | [diff] [blame] | 72 | // mock package-private AccessibilityGestureDetector class |
| 73 | @Rule |
| 74 | public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = |
| 75 | new DexmakerShareClassLoaderRule(); |
| 76 | |
Dieter Hsu | 01f426f | 2018-08-09 01:45:39 +0800 | [diff] [blame] | 77 | /** |
| 78 | * {@link TouchExplorer#sendDownForAllNotInjectedPointers} injecting events with the same object |
| 79 | * is resulting {@link ArgumentCaptor} to capture events with last state. Before implementation |
| 80 | * change, this helper class will save copies to verify the result. |
| 81 | */ |
| 82 | private class EventCaptor implements EventStreamTransformation { |
| 83 | List<MotionEvent> mEvents = new ArrayList<>(); |
| 84 | |
| 85 | @Override |
| 86 | public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| 87 | mEvents.add(0, event.copy()); |
| 88 | } |
| 89 | |
| 90 | @Override |
| 91 | public void setNext(EventStreamTransformation next) { |
| 92 | } |
| 93 | |
| 94 | @Override |
| 95 | public EventStreamTransformation getNext() { |
| 96 | return null; |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | @Before |
| 101 | public void setUp() { |
| 102 | Context context = InstrumentationRegistry.getContext(); |
| 103 | AccessibilityManagerService ams = new AccessibilityManagerService(context); |
Dieter Hsu | a7fa814 | 2018-08-29 19:48:42 +0800 | [diff] [blame] | 104 | AccessibilityGestureDetector detector = mock(AccessibilityGestureDetector.class); |
Dieter Hsu | 01f426f | 2018-08-09 01:45:39 +0800 | [diff] [blame] | 105 | mCaptor = new EventCaptor(); |
Dieter Hsu | a7fa814 | 2018-08-29 19:48:42 +0800 | [diff] [blame] | 106 | mTouchExplorer = new TouchExplorer(context, ams, detector); |
Dieter Hsu | 01f426f | 2018-08-09 01:45:39 +0800 | [diff] [blame] | 107 | mTouchExplorer.setNext(mCaptor); |
| 108 | } |
| 109 | |
| 110 | @Test |
| 111 | public void testTwoFingersMove_shouldDelegatingAndInjectActionDownPointerDown() { |
| 112 | goFromStateIdleTo(STATE_MOVING_2FINGERS); |
| 113 | |
| 114 | assertState(STATE_DELEGATING); |
| 115 | assertCapturedEvents( |
| 116 | MotionEvent.ACTION_DOWN, |
| 117 | MotionEvent.ACTION_POINTER_DOWN); |
| 118 | assertCapturedEventsNoHistory(); |
| 119 | } |
| 120 | |
| 121 | @Test |
| 122 | public void testTwoFingersDrag_shouldDraggingAndActionDown() { |
| 123 | goFromStateIdleTo(STATE_DRAGGING_2FINGERS); |
| 124 | |
| 125 | assertState(STATE_DRAGGING); |
| 126 | assertCapturedEvents(MotionEvent.ACTION_DOWN); |
| 127 | assertCapturedEventsNoHistory(); |
| 128 | } |
| 129 | |
| 130 | @Test |
| 131 | public void testTwoFingersNotDrag_shouldDelegatingAndActionUpDownPointerDown() { |
| 132 | // only from dragging state, and withMoveHistory no dragging |
| 133 | goFromStateIdleTo(STATE_PINCH_2FINGERS); |
| 134 | |
| 135 | assertState(STATE_DELEGATING); |
| 136 | assertCapturedEvents( |
| 137 | /* goto dragging state */ MotionEvent.ACTION_DOWN, |
| 138 | /* leave dragging state */ MotionEvent.ACTION_UP, |
| 139 | MotionEvent.ACTION_DOWN, |
| 140 | MotionEvent.ACTION_POINTER_DOWN); |
| 141 | assertCapturedEventsNoHistory(); |
| 142 | } |
| 143 | |
| 144 | @Test |
| 145 | public void testThreeFingersMove_shouldDelegatingAnd3ActionPointerDown() { |
| 146 | goFromStateIdleTo(STATE_MOVING_3FINGERS); |
| 147 | |
| 148 | assertState(STATE_DELEGATING); |
| 149 | assertCapturedEvents( |
| 150 | MotionEvent.ACTION_DOWN, |
| 151 | MotionEvent.ACTION_POINTER_DOWN, |
| 152 | MotionEvent.ACTION_POINTER_DOWN); |
| 153 | assertCapturedEventsNoHistory(); |
| 154 | } |
| 155 | |
| 156 | private static MotionEvent fromTouchscreen(MotionEvent ev) { |
| 157 | ev.setSource(InputDevice.SOURCE_TOUCHSCREEN); |
| 158 | return ev; |
| 159 | } |
| 160 | |
| 161 | private static PointF p(int x, int y) { |
| 162 | return new PointF(x, y); |
| 163 | } |
| 164 | |
| 165 | private static String stateToString(int state) { |
| 166 | return DebugUtils.valueToString(TouchExplorerTest.class, "STATE_", state); |
| 167 | } |
| 168 | |
| 169 | private void goFromStateIdleTo(int state) { |
| 170 | try { |
| 171 | switch (state) { |
| 172 | case STATE_TOUCH_EXPLORING: { |
| 173 | mTouchExplorer.onDestroy(); |
| 174 | } |
| 175 | break; |
| 176 | case STATE_TOUCH_EXPLORING_1FINGER: { |
| 177 | goFromStateIdleTo(STATE_TOUCH_EXPLORING); |
| 178 | send(downEvent()); |
| 179 | } |
| 180 | break; |
| 181 | case STATE_TOUCH_EXPLORING_2FINGER: { |
| 182 | goFromStateIdleTo(STATE_TOUCH_EXPLORING_1FINGER); |
| 183 | send(pointerDownEvent()); |
| 184 | } |
| 185 | break; |
| 186 | case STATE_TOUCH_EXPLORING_3FINGER: { |
| 187 | goFromStateIdleTo(STATE_TOUCH_EXPLORING_2FINGER); |
| 188 | send(thirdPointerDownEvent()); |
| 189 | } |
| 190 | break; |
| 191 | case STATE_MOVING_2FINGERS: { |
| 192 | goFromStateIdleTo(STATE_TOUCH_EXPLORING_2FINGER); |
| 193 | moveEachPointers(mLastEvent, p(10, 0), p(5, 10)); |
| 194 | send(mLastEvent); |
| 195 | } |
| 196 | break; |
| 197 | case STATE_DRAGGING_2FINGERS: { |
| 198 | goFromStateIdleTo(STATE_TOUCH_EXPLORING_2FINGER); |
| 199 | moveEachPointers(mLastEvent, p(10, 0), p(10, 0)); |
| 200 | send(mLastEvent); |
| 201 | } |
| 202 | break; |
| 203 | case STATE_PINCH_2FINGERS: { |
| 204 | goFromStateIdleTo(STATE_DRAGGING_2FINGERS); |
| 205 | moveEachPointers(mLastEvent, p(10, 0), p(-10, 1)); |
| 206 | send(mLastEvent); |
| 207 | } |
| 208 | break; |
| 209 | case STATE_MOVING_3FINGERS: { |
| 210 | goFromStateIdleTo(STATE_TOUCH_EXPLORING_3FINGER); |
| 211 | moveEachPointers(mLastEvent, p(1, 0), p(1, 0), p(1, 0)); |
| 212 | send(mLastEvent); |
| 213 | } |
| 214 | break; |
| 215 | default: |
| 216 | throw new IllegalArgumentException("Illegal state: " + state); |
| 217 | } |
| 218 | } catch (Throwable t) { |
| 219 | throw new RuntimeException("Failed to go to state " + stateToString(state), t); |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | private void send(MotionEvent event) { |
| 224 | final MotionEvent sendEvent = fromTouchscreen(event); |
| 225 | mLastEvent = sendEvent; |
| 226 | try { |
| 227 | mTouchExplorer.onMotionEvent(sendEvent, sendEvent, /* policyFlags */ 0); |
| 228 | } catch (Throwable t) { |
| 229 | throw new RuntimeException("Exception while handling " + sendEvent, t); |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | private void assertState(int expect) { |
| 234 | final String expectState = "STATE_" + stateToString(expect); |
| 235 | assertTrue(String.format("Expect state: %s, but: %s", expectState, mTouchExplorer), |
| 236 | mTouchExplorer.toString().contains(expectState)); |
| 237 | } |
| 238 | |
| 239 | private void assertCapturedEvents(int... actionsInOrder) { |
| 240 | final int eventCount = actionsInOrder.length; |
| 241 | assertEquals(eventCount, getCapturedEvents().size()); |
| 242 | for (int i = 0; i < eventCount; i++) { |
| 243 | assertEquals(actionsInOrder[eventCount - i - 1], getCapturedEvent(i).getActionMasked()); |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | private void assertCapturedEventsNoHistory() { |
| 248 | for (MotionEvent e : getCapturedEvents()) { |
| 249 | assertEquals(0, e.getHistorySize()); |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | private MotionEvent getCapturedEvent(int index) { |
| 254 | return getCapturedEvents().get(index); |
| 255 | } |
| 256 | |
| 257 | private List<MotionEvent> getCapturedEvents() { |
| 258 | return ((EventCaptor) mCaptor).mEvents; |
| 259 | } |
| 260 | |
| 261 | private MotionEvent downEvent() { |
| 262 | mLastDownTime = SystemClock.uptimeMillis(); |
| 263 | return fromTouchscreen( |
| 264 | MotionEvent.obtain(mLastDownTime, mLastDownTime, MotionEvent.ACTION_DOWN, DEFAULT_X, |
| 265 | DEFAULT_Y, 0)); |
| 266 | } |
| 267 | |
| 268 | private MotionEvent pointerDownEvent() { |
| 269 | final int secondPointerId = 0x0100; |
| 270 | final int action = MotionEvent.ACTION_POINTER_DOWN | secondPointerId; |
| 271 | final float[] x = new float[]{DEFAULT_X, DEFAULT_X + 29}; |
| 272 | final float[] y = new float[]{DEFAULT_Y, DEFAULT_Y + 28}; |
| 273 | return manyPointerEvent(action, x, y); |
| 274 | } |
| 275 | |
| 276 | private MotionEvent thirdPointerDownEvent() { |
| 277 | final int thirdPointerId = 0x0200; |
| 278 | final int action = MotionEvent.ACTION_POINTER_DOWN | thirdPointerId; |
| 279 | final float[] x = new float[]{DEFAULT_X, DEFAULT_X + 29, DEFAULT_X + 59}; |
| 280 | final float[] y = new float[]{DEFAULT_Y, DEFAULT_Y + 28, DEFAULT_Y + 58}; |
| 281 | return manyPointerEvent(action, x, y); |
| 282 | } |
| 283 | |
| 284 | private void moveEachPointers(MotionEvent event, PointF... points) { |
| 285 | final float[] x = new float[points.length]; |
| 286 | final float[] y = new float[points.length]; |
| 287 | for (int i = 0; i < points.length; i++) { |
| 288 | x[i] = event.getX(i) + points[i].x; |
| 289 | y[i] = event.getY(i) + points[i].y; |
| 290 | } |
| 291 | MotionEvent newEvent = manyPointerEvent(MotionEvent.ACTION_MOVE, x, y); |
| 292 | event.setAction(MotionEvent.ACTION_MOVE); |
| 293 | // add history count |
| 294 | event.addBatch(newEvent); |
| 295 | } |
| 296 | |
| 297 | private MotionEvent manyPointerEvent(int action, float[] x, float[] y) { |
| 298 | return manyPointerEvent(action, x, y, mLastDownTime); |
| 299 | } |
| 300 | |
| 301 | private MotionEvent manyPointerEvent(int action, float[] x, float[] y, long downTime) { |
| 302 | final int len = x.length; |
| 303 | |
| 304 | final MotionEvent.PointerProperties[] pp = new MotionEvent.PointerProperties[len]; |
| 305 | for (int i = 0; i < len; i++) { |
| 306 | MotionEvent.PointerProperties pointerProperty = new MotionEvent.PointerProperties(); |
| 307 | pointerProperty.id = i; |
| 308 | pointerProperty.toolType = MotionEvent.TOOL_TYPE_FINGER; |
| 309 | pp[i] = pointerProperty; |
| 310 | } |
| 311 | |
| 312 | final MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[len]; |
| 313 | for (int i = 0; i < len; i++) { |
| 314 | MotionEvent.PointerCoords pointerCoord = new MotionEvent.PointerCoords(); |
| 315 | pointerCoord.x = x[i]; |
| 316 | pointerCoord.y = y[i]; |
| 317 | pc[i] = pointerCoord; |
| 318 | } |
| 319 | |
| 320 | return MotionEvent.obtain( |
| 321 | /* downTime */ SystemClock.uptimeMillis(), |
| 322 | /* eventTime */ downTime, |
| 323 | /* action */ action, |
| 324 | /* pointerCount */ pc.length, |
| 325 | /* pointerProperties */ pp, |
| 326 | /* pointerCoords */ pc, |
| 327 | /* metaState */ 0, |
| 328 | /* buttonState */ 0, |
| 329 | /* xPrecision */ 1.0f, |
| 330 | /* yPrecision */ 1.0f, |
| 331 | /* deviceId */ 0, |
| 332 | /* edgeFlags */ 0, |
| 333 | /* source */ InputDevice.SOURCE_TOUCHSCREEN, |
| 334 | /* flags */ 0); |
| 335 | } |
| 336 | } |