1 /* 2 * Copyright (C) 2020 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.gestures; 18 19 import static android.view.MotionEvent.INVALID_POINTER_ID; 20 21 import static com.android.server.accessibility.gestures.GestureUtils.getActionIndex; 22 import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG; 23 24 import android.content.Context; 25 import android.graphics.PointF; 26 import android.os.Handler; 27 import android.util.DisplayMetrics; 28 import android.util.Slog; 29 import android.view.MotionEvent; 30 import android.view.ViewConfiguration; 31 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 35 /** 36 * This class is responsible for matching one-finger swipe gestures. Each instance matches one swipe 37 * gesture. A swipe is specified as a series of one or more directions e.g. left, left and up, etc. 38 * At this time swipes with more than two directions are not supported. 39 */ 40 class MultiFingerSwipe extends GestureMatcher { 41 42 // Direction constants. 43 public static final int LEFT = 0; 44 public static final int RIGHT = 1; 45 public static final int UP = 2; 46 public static final int DOWN = 3; 47 48 // Buffer for storing points for gesture detection. 49 private final ArrayList<PointF>[] mStrokeBuffers; 50 51 // The swipe direction for this matcher. 52 private int mDirection; 53 private int[] mPointerIds; 54 // The starting point of each finger's path in the gesture. 55 private PointF[] mBase; 56 // The most recent entry in each finger's gesture path. 57 private PointF[] mPreviousGesturePoint; 58 private int mTargetFingerCount; 59 private int mCurrentFingerCount; 60 // Whether the appropriate number of fingers have gone down at some point. This is reset only on 61 // clear. 62 private boolean mTargetFingerCountReached = false; 63 // Constants for sampling motion event points. 64 // We sample based on a minimum distance between points, primarily to improve accuracy by 65 // reducing noisy minor changes in direction. 66 private static final float MIN_CM_BETWEEN_SAMPLES = 0.25f; 67 private final float mMinPixelsBetweenSamplesX; 68 private final float mMinPixelsBetweenSamplesY; 69 // The minmimum distance the finger must travel before we evaluate the initial direction of the 70 // swipe. 71 // Anything less is still considered a touch. 72 private int mTouchSlop; 73 MultiFingerSwipe( Context context, int fingerCount, int direction, int gesture, GestureMatcher.StateChangeListener listener)74 MultiFingerSwipe( 75 Context context, 76 int fingerCount, 77 int direction, 78 int gesture, 79 GestureMatcher.StateChangeListener listener) { 80 super(gesture, new Handler(context.getMainLooper()), listener); 81 mTargetFingerCount = fingerCount; 82 mPointerIds = new int[mTargetFingerCount]; 83 mBase = new PointF[mTargetFingerCount]; 84 mPreviousGesturePoint = new PointF[mTargetFingerCount]; 85 mStrokeBuffers = new ArrayList[mTargetFingerCount]; 86 mDirection = direction; 87 DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); 88 // Calculate gesture sampling interval. 89 final float pixelsPerCmX = displayMetrics.xdpi / GestureUtils.CM_PER_INCH; 90 final float pixelsPerCmY = displayMetrics.ydpi / GestureUtils.CM_PER_INCH; 91 mMinPixelsBetweenSamplesX = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmX; 92 mMinPixelsBetweenSamplesY = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmY; 93 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 94 clear(); 95 } 96 97 @Override clear()98 public void clear() { 99 mTargetFingerCountReached = false; 100 mCurrentFingerCount = 0; 101 for (int i = 0; i < mTargetFingerCount; ++i) { 102 mPointerIds[i] = INVALID_POINTER_ID; 103 if (mBase[i] == null) { 104 mBase[i] = new PointF(); 105 } 106 mBase[i].x = Float.NaN; 107 mBase[i].y = Float.NaN; 108 if (mPreviousGesturePoint[i] == null) { 109 mPreviousGesturePoint[i] = new PointF(); 110 } 111 mPreviousGesturePoint[i].x = Float.NaN; 112 mPreviousGesturePoint[i].y = Float.NaN; 113 if (mStrokeBuffers[i] == null) { 114 mStrokeBuffers[i] = new ArrayList<>(100); 115 } 116 mStrokeBuffers[i].clear(); 117 } 118 super.clear(); 119 } 120 121 @Override onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags)122 protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 123 if (mCurrentFingerCount > 0) { 124 cancelGesture(event, rawEvent, policyFlags); 125 return; 126 } 127 mCurrentFingerCount = 1; 128 final int actionIndex = getActionIndex(rawEvent); 129 final int pointerId = rawEvent.getPointerId(actionIndex); 130 int pointerIndex = rawEvent.getPointerCount() - 1; 131 if (pointerId < 0) { 132 // Nonsensical pointer id. 133 cancelGesture(event, rawEvent, policyFlags); 134 return; 135 } 136 if (mPointerIds[pointerIndex] != INVALID_POINTER_ID) { 137 // Inconsistent event stream. 138 cancelGesture(event, rawEvent, policyFlags); 139 return; 140 } 141 mPointerIds[pointerIndex] = pointerId; 142 if (Float.isNaN(mBase[pointerIndex].x) && Float.isNaN(mBase[pointerIndex].y)) { 143 final float x = rawEvent.getX(actionIndex); 144 final float y = rawEvent.getY(actionIndex); 145 if (x < 0f || y < 0f) { 146 cancelGesture(event, rawEvent, policyFlags); 147 return; 148 } 149 mBase[pointerIndex].x = x; 150 mBase[pointerIndex].y = y; 151 mPreviousGesturePoint[pointerIndex].x = x; 152 mPreviousGesturePoint[pointerIndex].y = y; 153 } else { 154 // This event doesn't make sense in the middle of a gesture. 155 cancelGesture(event, rawEvent, policyFlags); 156 return; 157 } 158 } 159 160 @Override onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags)161 protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 162 if (event.getPointerCount() > mTargetFingerCount) { 163 cancelGesture(event, rawEvent, policyFlags); 164 return; 165 } 166 mCurrentFingerCount += 1; 167 if (mCurrentFingerCount != rawEvent.getPointerCount()) { 168 cancelGesture(event, rawEvent, policyFlags); 169 return; 170 } 171 if (mCurrentFingerCount == mTargetFingerCount) { 172 mTargetFingerCountReached = true; 173 } 174 final int actionIndex = getActionIndex(rawEvent); 175 final int pointerId = rawEvent.getPointerId(actionIndex); 176 if (pointerId < 0) { 177 // Nonsensical pointer id. 178 cancelGesture(event, rawEvent, policyFlags); 179 return; 180 } 181 int pointerIndex = mCurrentFingerCount - 1; 182 if (mPointerIds[pointerIndex] != INVALID_POINTER_ID) { 183 // Inconsistent event stream. 184 cancelGesture(event, rawEvent, policyFlags); 185 return; 186 } 187 mPointerIds[pointerIndex] = pointerId; 188 if (Float.isNaN(mBase[pointerIndex].x) && Float.isNaN(mBase[pointerIndex].y)) { 189 final float x = rawEvent.getX(actionIndex); 190 final float y = rawEvent.getY(actionIndex); 191 if (x < 0f || y < 0f) { 192 cancelGesture(event, rawEvent, policyFlags); 193 return; 194 } 195 mBase[pointerIndex].x = x; 196 mBase[pointerIndex].y = y; 197 mPreviousGesturePoint[pointerIndex].x = x; 198 mPreviousGesturePoint[pointerIndex].y = y; 199 } else { 200 cancelGesture(event, rawEvent, policyFlags); 201 return; 202 } 203 } 204 205 @Override onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags)206 protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 207 if (!mTargetFingerCountReached) { 208 cancelGesture(event, rawEvent, policyFlags); 209 return; 210 } 211 mCurrentFingerCount -= 1; 212 final int actionIndex = getActionIndex(event); 213 final int pointerId = event.getPointerId(actionIndex); 214 if (pointerId < 0) { 215 // Nonsensical pointer id. 216 cancelGesture(event, rawEvent, policyFlags); 217 return; 218 } 219 final int pointerIndex = Arrays.binarySearch(mPointerIds, pointerId); 220 if (pointerIndex < 0) { 221 cancelGesture(event, rawEvent, policyFlags); 222 return; 223 } 224 final float x = rawEvent.getX(actionIndex); 225 final float y = rawEvent.getY(actionIndex); 226 if (x < 0f || y < 0f) { 227 cancelGesture(event, rawEvent, policyFlags); 228 return; 229 } 230 final float dX = Math.abs(x - mPreviousGesturePoint[pointerIndex].x); 231 final float dY = Math.abs(y - mPreviousGesturePoint[pointerIndex].y); 232 if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { 233 mStrokeBuffers[pointerIndex].add(new PointF(x, y)); 234 } 235 // We will evaluate all the paths on ACTION_UP. 236 } 237 238 @Override onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags)239 protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 240 for (int pointerIndex = 0; pointerIndex < mTargetFingerCount; ++pointerIndex) { 241 if (mPointerIds[pointerIndex] == INVALID_POINTER_ID) { 242 // Fingers have started to move before the required number of fingers are down. 243 // However, they can still move less than the touch slop and still be considered 244 // touching, not moving. 245 // So we just ignore fingers that haven't been assigned a pointer id and process 246 // those who have. 247 continue; 248 } 249 if (DEBUG) { 250 Slog.d(getGestureName(), "Processing move on finger " + pointerIndex); 251 } 252 int index = rawEvent.findPointerIndex(mPointerIds[pointerIndex]); 253 if (index < 0) { 254 // This finger is not present in this event. It could have gone up just before this 255 // movement. 256 if (DEBUG) { 257 Slog.d( 258 getGestureName(), 259 "Finger " + pointerIndex + " not found in this event. skipping."); 260 } 261 continue; 262 } 263 final float x = rawEvent.getX(index); 264 final float y = rawEvent.getY(index); 265 if (x < 0f || y < 0f) { 266 cancelGesture(event, rawEvent, policyFlags); 267 return; 268 } 269 final float dX = Math.abs(x - mPreviousGesturePoint[pointerIndex].x); 270 final float dY = Math.abs(y - mPreviousGesturePoint[pointerIndex].y); 271 final double moveDelta = 272 Math.hypot( 273 Math.abs(x - mBase[pointerIndex].x), 274 Math.abs(y - mBase[pointerIndex].y)); 275 if (DEBUG) { 276 Slog.d(getGestureName(), "moveDelta:" + moveDelta); 277 } 278 if (getState() == STATE_CLEAR) { 279 if (moveDelta < (mTargetFingerCount * mTouchSlop)) { 280 // This still counts as a touch not a swipe. 281 continue; 282 } 283 // First, make sure we have the right number of fingers down. 284 if (mCurrentFingerCount != mTargetFingerCount) { 285 cancelGesture(event, rawEvent, policyFlags); 286 return; 287 } 288 // Then, make sure the pointer is going in the right direction. 289 int direction = toDirection(x - mBase[pointerIndex].x, y - mBase[pointerIndex].y); 290 if (direction != mDirection) { 291 cancelGesture(event, rawEvent, policyFlags); 292 return; 293 } 294 // This is confirmed to be some kind of swipe so start tracking points. 295 startGesture(event, rawEvent, policyFlags); 296 for (int i = 0; i < mTargetFingerCount; ++i) { 297 mStrokeBuffers[i].add(new PointF(mBase[i])); 298 } 299 } else if (getState() == STATE_GESTURE_STARTED) { 300 // Cancel if the finger starts to go the wrong way. 301 // Note that this only works because this matcher assumes one direction. 302 int direction = toDirection(x - mBase[pointerIndex].x, y - mBase[pointerIndex].y); 303 if (direction != mDirection) { 304 cancelGesture(event, rawEvent, policyFlags); 305 return; 306 } 307 if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { 308 // Sample every 2.5 MM in order to guard against minor variations in path. 309 mPreviousGesturePoint[pointerIndex].x = x; 310 mPreviousGesturePoint[pointerIndex].y = y; 311 mStrokeBuffers[pointerIndex].add(new PointF(x, y)); 312 } 313 } 314 } 315 } 316 317 @Override onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags)318 protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 319 if (getState() != STATE_GESTURE_STARTED) { 320 cancelGesture(event, rawEvent, policyFlags); 321 return; 322 } 323 mCurrentFingerCount = 0; 324 final int actionIndex = getActionIndex(event); 325 final int pointerId = event.getPointerId(actionIndex); 326 final int pointerIndex = Arrays.binarySearch(mPointerIds, pointerId); 327 if (pointerIndex < 0) { 328 cancelGesture(event, rawEvent, policyFlags); 329 return; 330 } 331 final float x = rawEvent.getX(actionIndex); 332 final float y = rawEvent.getY(actionIndex); 333 if (x < 0f || y < 0f) { 334 cancelGesture(event, rawEvent, policyFlags); 335 return; 336 } 337 final float dX = Math.abs(x - mPreviousGesturePoint[pointerIndex].x); 338 final float dY = Math.abs(y - mPreviousGesturePoint[pointerIndex].y); 339 if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { 340 mStrokeBuffers[pointerIndex].add(new PointF(x, y)); 341 } 342 recognizeGesture(event, rawEvent, policyFlags); 343 } 344 345 /** 346 * Looks at the sequence of motions in mStrokeBuffer, classifies the gesture, then transitions 347 * to the complete or cancel state depending on the result. 348 */ recognizeGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags)349 private void recognizeGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 350 // Check the path of each finger against the specified direction. 351 // Note that we sample every 2.5 MMm, and the direction matching is extremely tolerant (each 352 // direction has a 90-degree arch of tolerance) meaning that minor perpendicular movements 353 // should not create false negatives. 354 for (int i = 0; i < mTargetFingerCount; ++i) { 355 if (DEBUG) { 356 Slog.d(getGestureName(), "Recognizing finger: " + i); 357 } 358 if (mStrokeBuffers[i].size() < 2) { 359 Slog.d(getGestureName(), "Too few points."); 360 cancelGesture(event, rawEvent, policyFlags); 361 return; 362 } 363 ArrayList<PointF> path = mStrokeBuffers[i]; 364 365 if (DEBUG) { 366 Slog.d(getGestureName(), "path=" + path.toString()); 367 } 368 // Classify line segments, and call Listener callbacks. 369 if (!recognizeGesturePath(event, rawEvent, policyFlags, path)) { 370 cancelGesture(event, rawEvent, policyFlags); 371 return; 372 } 373 } 374 // If we reach this point then all paths match. 375 completeGesture(event, rawEvent, policyFlags); 376 } 377 378 /** 379 * Tests the path of a given finger against the direction specified in this matcher. 380 * 381 * @return True if the path matches the specified direction for this matcher, otherwise false. 382 */ recognizeGesturePath( MotionEvent event, MotionEvent rawEvent, int policyFlags, ArrayList<PointF> path)383 private boolean recognizeGesturePath( 384 MotionEvent event, MotionEvent rawEvent, int policyFlags, ArrayList<PointF> path) { 385 386 final int displayId = event.getDisplayId(); 387 for (int i = 0; i < path.size() - 1; ++i) { 388 PointF start = path.get(i); 389 PointF end = path.get(i + 1); 390 391 float dX = end.x - start.x; 392 float dY = end.y - start.y; 393 int direction = toDirection(dX, dY); 394 if (direction != mDirection) { 395 if (DEBUG) { 396 Slog.d( 397 getGestureName(), 398 "Found direction " 399 + directionToString(direction) 400 + " when expecting " 401 + directionToString(mDirection)); 402 } 403 return false; 404 } 405 } 406 if (DEBUG) { 407 Slog.d(getGestureName(), "Completed."); 408 } 409 return true; 410 } 411 toDirection(float dX, float dY)412 private static int toDirection(float dX, float dY) { 413 if (Math.abs(dX) > Math.abs(dY)) { 414 // Horizontal 415 return (dX < 0) ? LEFT : RIGHT; 416 } else { 417 // Vertical 418 return (dY < 0) ? UP : DOWN; 419 } 420 } 421 directionToString(int direction)422 public static String directionToString(int direction) { 423 switch (direction) { 424 case LEFT: 425 return "left"; 426 case RIGHT: 427 return "right"; 428 case UP: 429 return "up"; 430 case DOWN: 431 return "down"; 432 default: 433 return "Unknown Direction"; 434 } 435 } 436 437 @Override getGestureName()438 protected String getGestureName() { 439 StringBuilder builder = new StringBuilder(); 440 builder.append(mTargetFingerCount).append("-finger "); 441 builder.append("Swipe ").append(directionToString(mDirection)); 442 return builder.toString(); 443 } 444 445 @Override toString()446 public String toString() { 447 StringBuilder builder = new StringBuilder(super.toString()); 448 if (getState() != STATE_GESTURE_CANCELED) { 449 builder.append(", mBase: ") 450 .append(Arrays.toString(mBase)) 451 .append(", mMinPixelsBetweenSamplesX:") 452 .append(mMinPixelsBetweenSamplesX) 453 .append(", mMinPixelsBetweenSamplesY:") 454 .append(mMinPixelsBetweenSamplesY); 455 } 456 return builder.toString(); 457 } 458 } 459