1 /* 2 * Copyright (C) 2015 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 19 import android.accessibilityservice.AccessibilityTrace; 20 import android.accessibilityservice.GestureDescription; 21 import android.accessibilityservice.GestureDescription.GestureStep; 22 import android.accessibilityservice.GestureDescription.TouchPoint; 23 import android.accessibilityservice.IAccessibilityServiceClient; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.Message; 27 import android.os.RemoteException; 28 import android.os.SystemClock; 29 import android.util.IntArray; 30 import android.util.Slog; 31 import android.util.SparseArray; 32 import android.util.SparseIntArray; 33 import android.view.InputDevice; 34 import android.view.KeyCharacterMap; 35 import android.view.MotionEvent; 36 import android.view.WindowManagerPolicyConstants; 37 38 import com.android.internal.os.SomeArgs; 39 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.List; 43 44 /** 45 * Injects MotionEvents to permit {@code AccessibilityService}s to touch the screen on behalf of 46 * users. 47 * <p> 48 * All methods except {@code injectEvents} must be called only from the main thread. 49 */ 50 public class MotionEventInjector extends BaseEventStreamTransformation implements Handler.Callback { 51 private static final String LOG_TAG = "MotionEventInjector"; 52 private static final int MESSAGE_SEND_MOTION_EVENT = 1; 53 private static final int MESSAGE_INJECT_EVENTS = 2; 54 55 /** 56 * Constants used to initialize all MotionEvents 57 */ 58 private static final int EVENT_META_STATE = 0; 59 private static final int EVENT_BUTTON_STATE = 0; 60 private static final int EVENT_EDGE_FLAGS = 0; 61 private static final int EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN; 62 private static final int EVENT_FLAGS = 0; 63 private static final float EVENT_X_PRECISION = 1; 64 private static final float EVENT_Y_PRECISION = 1; 65 66 private static MotionEvent.PointerCoords[] sPointerCoords; 67 private static MotionEvent.PointerProperties[] sPointerProps; 68 69 private final Handler mHandler; 70 private final SparseArray<Boolean> mOpenGesturesInProgress = new SparseArray<>(); 71 72 private final AccessibilityTraceManager mTrace; 73 private IAccessibilityServiceClient mServiceInterfaceForCurrentGesture; 74 private IntArray mSequencesInProgress = new IntArray(5); 75 private boolean mIsDestroyed = false; 76 private TouchPoint[] mLastTouchPoints; 77 private int mNumLastTouchPoints; 78 private long mDownTime; 79 private long mLastScheduledEventTime; 80 private SparseIntArray mStrokeIdToPointerId = new SparseIntArray(5); 81 82 /** 83 * @param looper A looper on the main thread to use for dispatching new events 84 */ MotionEventInjector(Looper looper, AccessibilityTraceManager trace)85 public MotionEventInjector(Looper looper, AccessibilityTraceManager trace) { 86 mHandler = new Handler(looper, this); 87 mTrace = trace; 88 } 89 90 /** 91 * @param handler A handler to post messages. Exposes internal state for testing only. 92 */ MotionEventInjector(Handler handler, AccessibilityTraceManager trace)93 public MotionEventInjector(Handler handler, AccessibilityTraceManager trace) { 94 mHandler = handler; 95 mTrace = trace; 96 } 97 98 /** 99 * Schedule a gesture for injection. The gesture is defined by a set of {@code GestureStep}s, 100 * from which {@code MotionEvent}s will be derived. All gestures currently in progress will be 101 * cancelled. 102 * 103 * @param gestureSteps The gesture steps to inject. 104 * @param serviceInterface The interface to call back with a result when the gesture is 105 * either complete or cancelled. 106 */ injectEvents(List<GestureStep> gestureSteps, IAccessibilityServiceClient serviceInterface, int sequence, int displayId)107 public void injectEvents(List<GestureStep> gestureSteps, 108 IAccessibilityServiceClient serviceInterface, int sequence, int displayId) { 109 SomeArgs args = SomeArgs.obtain(); 110 args.arg1 = gestureSteps; 111 args.arg2 = serviceInterface; 112 args.argi1 = sequence; 113 args.argi2 = displayId; 114 mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_INJECT_EVENTS, args)); 115 } 116 117 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)118 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 119 if (mTrace.isA11yTracingEnabledForTypes( 120 AccessibilityTrace.FLAGS_INPUT_FILTER | AccessibilityTrace.FLAGS_GESTURE)) { 121 mTrace.logTrace(LOG_TAG + ".onMotionEvent", 122 AccessibilityTrace.FLAGS_INPUT_FILTER | AccessibilityTrace.FLAGS_GESTURE, 123 "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags); 124 } 125 // MotionEventInjector would cancel any injected gesture when any MotionEvent arrives. 126 // For user using an external device to control the pointer movement, it's almost 127 // impossible to perform the gestures. Any slightly unintended movement results in the 128 // cancellation of the gesture. 129 if ((event.isFromSource(InputDevice.SOURCE_MOUSE) 130 && event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) 131 && mOpenGesturesInProgress.get(EVENT_SOURCE, false)) { 132 return; 133 } 134 cancelAnyPendingInjectedEvents(); 135 // Indicate that the input event is injected from accessibility, to let applications 136 // distinguish it from events injected by other means. 137 policyFlags |= WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY; 138 sendMotionEventToNext(event, rawEvent, policyFlags); 139 } 140 141 @Override clearEvents(int inputSource)142 public void clearEvents(int inputSource) { 143 /* 144 * Reset state for motion events passing through so we won't send a cancel event for 145 * them. 146 */ 147 if (!mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) { 148 mOpenGesturesInProgress.put(inputSource, false); 149 } 150 } 151 152 @Override onDestroy()153 public void onDestroy() { 154 cancelAnyPendingInjectedEvents(); 155 mIsDestroyed = true; 156 } 157 158 @Override handleMessage(Message message)159 public boolean handleMessage(Message message) { 160 if (message.what == MESSAGE_INJECT_EVENTS) { 161 SomeArgs args = (SomeArgs) message.obj; 162 injectEventsMainThread((List<GestureStep>) args.arg1, 163 (IAccessibilityServiceClient) args.arg2, args.argi1, args.argi2); 164 args.recycle(); 165 return true; 166 } 167 if (message.what != MESSAGE_SEND_MOTION_EVENT) { 168 Slog.e(LOG_TAG, "Unknown message: " + message.what); 169 return false; 170 } 171 MotionEvent motionEvent = (MotionEvent) message.obj; 172 sendMotionEventToNext(motionEvent, motionEvent, 173 WindowManagerPolicyConstants.FLAG_PASS_TO_USER 174 | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY); 175 boolean isEndOfSequence = message.arg1 != 0; 176 if (isEndOfSequence) { 177 notifyService(mServiceInterfaceForCurrentGesture, mSequencesInProgress.get(0), true); 178 mSequencesInProgress.remove(0); 179 } 180 return true; 181 } 182 injectEventsMainThread(List<GestureStep> gestureSteps, IAccessibilityServiceClient serviceInterface, int sequence, int displayId)183 private void injectEventsMainThread(List<GestureStep> gestureSteps, 184 IAccessibilityServiceClient serviceInterface, int sequence, int displayId) { 185 if (mIsDestroyed) { 186 try { 187 serviceInterface.onPerformGestureResult(sequence, false); 188 } catch (RemoteException re) { 189 Slog.e(LOG_TAG, "Error sending status with mIsDestroyed to " + serviceInterface, 190 re); 191 } 192 return; 193 } 194 195 if (getNext() == null) { 196 notifyService(serviceInterface, sequence, false); 197 return; 198 } 199 200 boolean continuingGesture = newGestureTriesToContinueOldOne(gestureSteps); 201 202 if (continuingGesture) { 203 if ((serviceInterface != mServiceInterfaceForCurrentGesture) 204 || !prepareToContinueOldGesture(gestureSteps)) { 205 cancelAnyPendingInjectedEvents(); 206 notifyService(serviceInterface, sequence, false); 207 return; 208 } 209 } 210 if (!continuingGesture) { 211 cancelAnyPendingInjectedEvents(); 212 // Injected gestures have been canceled, but real gestures still need cancelling 213 cancelAnyGestureInProgress(EVENT_SOURCE); 214 } 215 mServiceInterfaceForCurrentGesture = serviceInterface; 216 217 long currentTime = SystemClock.uptimeMillis(); 218 List<MotionEvent> events = getMotionEventsFromGestureSteps(gestureSteps, 219 (mSequencesInProgress.size() == 0) ? currentTime : mLastScheduledEventTime); 220 if (events.isEmpty()) { 221 notifyService(serviceInterface, sequence, false); 222 return; 223 } 224 mSequencesInProgress.add(sequence); 225 226 for (int i = 0; i < events.size(); i++) { 227 MotionEvent event = events.get(i); 228 event.setDisplayId(displayId); 229 int isEndOfSequence = (i == events.size() - 1) ? 1 : 0; 230 Message message = mHandler.obtainMessage( 231 MESSAGE_SEND_MOTION_EVENT, isEndOfSequence, 0, event); 232 mLastScheduledEventTime = event.getEventTime(); 233 mHandler.sendMessageDelayed(message, Math.max(0, event.getEventTime() - currentTime)); 234 } 235 } 236 newGestureTriesToContinueOldOne(List<GestureStep> gestureSteps)237 private boolean newGestureTriesToContinueOldOne(List<GestureStep> gestureSteps) { 238 if (gestureSteps.isEmpty()) { 239 return false; 240 } 241 GestureStep firstStep = gestureSteps.get(0); 242 for (int i = 0; i < firstStep.numTouchPoints; i++) { 243 if (!firstStep.touchPoints[i].mIsStartOfPath) { 244 return true; 245 } 246 } 247 return false; 248 } 249 250 /** 251 * A gesture can only continue a gesture if it contains intermediate points that continue 252 * each continued stroke of the last gesture, and no extra points. 253 * 254 * @param gestureSteps The steps of the new gesture 255 * @return {@code true} if the new gesture could continue the last one dispatched. {@code false} 256 * otherwise. 257 */ prepareToContinueOldGesture(List<GestureStep> gestureSteps)258 private boolean prepareToContinueOldGesture(List<GestureStep> gestureSteps) { 259 if (gestureSteps.isEmpty() || (mLastTouchPoints == null) || (mNumLastTouchPoints == 0)) { 260 return false; 261 } 262 GestureStep firstStep = gestureSteps.get(0); 263 // Make sure all of the continuing paths match up 264 int numContinuedStrokes = 0; 265 for (int i = 0; i < firstStep.numTouchPoints; i++) { 266 TouchPoint touchPoint = firstStep.touchPoints[i]; 267 if (!touchPoint.mIsStartOfPath) { 268 int continuedPointerId = mStrokeIdToPointerId 269 .get(touchPoint.mContinuedStrokeId, -1); 270 if (continuedPointerId == -1) { 271 Slog.w(LOG_TAG, "Can't continue gesture due to unknown continued stroke id in " 272 + touchPoint); 273 return false; 274 } 275 mStrokeIdToPointerId.put(touchPoint.mStrokeId, continuedPointerId); 276 int lastPointIndex = findPointByStrokeId( 277 mLastTouchPoints, mNumLastTouchPoints, touchPoint.mContinuedStrokeId); 278 if (lastPointIndex < 0) { 279 Slog.w(LOG_TAG, "Can't continue gesture due continued gesture id of " 280 + touchPoint + " not matching any previous strokes in " 281 + Arrays.asList(mLastTouchPoints)); 282 return false; 283 } 284 if (mLastTouchPoints[lastPointIndex].mIsEndOfPath 285 || (mLastTouchPoints[lastPointIndex].mX != touchPoint.mX) 286 || (mLastTouchPoints[lastPointIndex].mY != touchPoint.mY)) { 287 Slog.w(LOG_TAG, "Can't continue gesture due to points mismatch between " 288 + mLastTouchPoints[lastPointIndex] + " and " + touchPoint); 289 return false; 290 } 291 // Update the last touch point to match the continuation, so the gestures will 292 // line up 293 mLastTouchPoints[lastPointIndex].mStrokeId = touchPoint.mStrokeId; 294 } 295 numContinuedStrokes++; 296 } 297 // Make sure we didn't miss any paths 298 for (int i = 0; i < mNumLastTouchPoints; i++) { 299 if (!mLastTouchPoints[i].mIsEndOfPath) { 300 numContinuedStrokes--; 301 } 302 } 303 return numContinuedStrokes == 0; 304 } 305 sendMotionEventToNext(MotionEvent event, MotionEvent rawEvent, int policyFlags)306 private void sendMotionEventToNext(MotionEvent event, MotionEvent rawEvent, 307 int policyFlags) { 308 if (getNext() != null) { 309 super.onMotionEvent(event, rawEvent, policyFlags); 310 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 311 mOpenGesturesInProgress.put(event.getSource(), true); 312 } 313 if ((event.getActionMasked() == MotionEvent.ACTION_UP) 314 || (event.getActionMasked() == MotionEvent.ACTION_CANCEL)) { 315 mOpenGesturesInProgress.put(event.getSource(), false); 316 } 317 } 318 } 319 cancelAnyGestureInProgress(int source)320 private void cancelAnyGestureInProgress(int source) { 321 if ((getNext() != null) && mOpenGesturesInProgress.get(source, false)) { 322 long now = SystemClock.uptimeMillis(); 323 MotionEvent cancelEvent = 324 obtainMotionEvent(now, now, MotionEvent.ACTION_CANCEL, getLastTouchPoints(), 1); 325 sendMotionEventToNext(cancelEvent, cancelEvent, 326 WindowManagerPolicyConstants.FLAG_PASS_TO_USER 327 | WindowManagerPolicyConstants.FLAG_INJECTED_FROM_ACCESSIBILITY); 328 mOpenGesturesInProgress.put(source, false); 329 } 330 } 331 cancelAnyPendingInjectedEvents()332 private void cancelAnyPendingInjectedEvents() { 333 if (mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) { 334 mHandler.removeMessages(MESSAGE_SEND_MOTION_EVENT); 335 cancelAnyGestureInProgress(EVENT_SOURCE); 336 for (int i = mSequencesInProgress.size() - 1; i >= 0; i--) { 337 notifyService(mServiceInterfaceForCurrentGesture, 338 mSequencesInProgress.get(i), false); 339 mSequencesInProgress.remove(i); 340 } 341 } else if (mNumLastTouchPoints != 0) { 342 // An injected gesture is in progress and waiting for a continuation. Cancel it. 343 cancelAnyGestureInProgress(EVENT_SOURCE); 344 } 345 mNumLastTouchPoints = 0; 346 mStrokeIdToPointerId.clear(); 347 } 348 notifyService(IAccessibilityServiceClient service, int sequence, boolean success)349 private void notifyService(IAccessibilityServiceClient service, int sequence, boolean success) { 350 try { 351 service.onPerformGestureResult(sequence, success); 352 } catch (RemoteException re) { 353 Slog.e(LOG_TAG, "Error sending motion event injection status to " 354 + mServiceInterfaceForCurrentGesture, re); 355 } 356 } 357 getMotionEventsFromGestureSteps( List<GestureStep> steps, long startTime)358 private List<MotionEvent> getMotionEventsFromGestureSteps( 359 List<GestureStep> steps, long startTime) { 360 final List<MotionEvent> motionEvents = new ArrayList<>(); 361 362 TouchPoint[] lastTouchPoints = getLastTouchPoints(); 363 364 for (int i = 0; i < steps.size(); i++) { 365 GestureDescription.GestureStep step = steps.get(i); 366 int currentTouchPointSize = step.numTouchPoints; 367 if (currentTouchPointSize > lastTouchPoints.length) { 368 mNumLastTouchPoints = 0; 369 motionEvents.clear(); 370 return motionEvents; 371 } 372 373 appendMoveEventIfNeeded(motionEvents, step.touchPoints, currentTouchPointSize, 374 startTime + step.timeSinceGestureStart); 375 appendUpEvents(motionEvents, step.touchPoints, currentTouchPointSize, 376 startTime + step.timeSinceGestureStart); 377 appendDownEvents(motionEvents, step.touchPoints, currentTouchPointSize, 378 startTime + step.timeSinceGestureStart); 379 } 380 return motionEvents; 381 } 382 getLastTouchPoints()383 private TouchPoint[] getLastTouchPoints() { 384 if (mLastTouchPoints == null) { 385 int capacity = GestureDescription.getMaxStrokeCount(); 386 mLastTouchPoints = new TouchPoint[capacity]; 387 for (int i = 0; i < capacity; i++) { 388 mLastTouchPoints[i] = new GestureDescription.TouchPoint(); 389 } 390 } 391 return mLastTouchPoints; 392 } 393 appendMoveEventIfNeeded(List<MotionEvent> motionEvents, TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime)394 private void appendMoveEventIfNeeded(List<MotionEvent> motionEvents, 395 TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) { 396 /* Look for pointers that have moved */ 397 boolean moveFound = false; 398 TouchPoint[] lastTouchPoints = getLastTouchPoints(); 399 for (int i = 0; i < currentTouchPointsSize; i++) { 400 int lastPointsIndex = findPointByStrokeId(lastTouchPoints, mNumLastTouchPoints, 401 currentTouchPoints[i].mStrokeId); 402 if (lastPointsIndex >= 0) { 403 moveFound |= (lastTouchPoints[lastPointsIndex].mX != currentTouchPoints[i].mX) 404 || (lastTouchPoints[lastPointsIndex].mY != currentTouchPoints[i].mY); 405 lastTouchPoints[lastPointsIndex].copyFrom(currentTouchPoints[i]); 406 } 407 } 408 409 if (moveFound) { 410 motionEvents.add(obtainMotionEvent(mDownTime, currentTime, MotionEvent.ACTION_MOVE, 411 lastTouchPoints, mNumLastTouchPoints)); 412 } 413 } 414 appendUpEvents(List<MotionEvent> motionEvents, TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime)415 private void appendUpEvents(List<MotionEvent> motionEvents, 416 TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) { 417 /* Look for a pointer at the end of its path */ 418 TouchPoint[] lastTouchPoints = getLastTouchPoints(); 419 for (int i = 0; i < currentTouchPointsSize; i++) { 420 if (currentTouchPoints[i].mIsEndOfPath) { 421 int indexOfUpEvent = findPointByStrokeId(lastTouchPoints, mNumLastTouchPoints, 422 currentTouchPoints[i].mStrokeId); 423 if (indexOfUpEvent < 0) { 424 continue; // Should not happen 425 } 426 int action = (mNumLastTouchPoints == 1) ? MotionEvent.ACTION_UP 427 : MotionEvent.ACTION_POINTER_UP; 428 action |= indexOfUpEvent << MotionEvent.ACTION_POINTER_INDEX_SHIFT; 429 motionEvents.add(obtainMotionEvent(mDownTime, currentTime, action, 430 lastTouchPoints, mNumLastTouchPoints)); 431 /* Remove this point from lastTouchPoints */ 432 for (int j = indexOfUpEvent; j < mNumLastTouchPoints - 1; j++) { 433 lastTouchPoints[j].copyFrom(mLastTouchPoints[j + 1]); 434 } 435 mNumLastTouchPoints--; 436 if (mNumLastTouchPoints == 0) { 437 mStrokeIdToPointerId.clear(); 438 } 439 } 440 } 441 } 442 appendDownEvents(List<MotionEvent> motionEvents, TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime)443 private void appendDownEvents(List<MotionEvent> motionEvents, 444 TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) { 445 /* Look for a pointer that is just starting */ 446 TouchPoint[] lastTouchPoints = getLastTouchPoints(); 447 for (int i = 0; i < currentTouchPointsSize; i++) { 448 if (currentTouchPoints[i].mIsStartOfPath) { 449 /* Add the point to last coords and use the new array to generate the event */ 450 lastTouchPoints[mNumLastTouchPoints++].copyFrom(currentTouchPoints[i]); 451 int action = (mNumLastTouchPoints == 1) ? MotionEvent.ACTION_DOWN 452 : MotionEvent.ACTION_POINTER_DOWN; 453 if (action == MotionEvent.ACTION_DOWN) { 454 mDownTime = currentTime; 455 } 456 action |= i << MotionEvent.ACTION_POINTER_INDEX_SHIFT; 457 motionEvents.add(obtainMotionEvent(mDownTime, currentTime, action, 458 lastTouchPoints, mNumLastTouchPoints)); 459 } 460 } 461 } 462 obtainMotionEvent(long downTime, long eventTime, int action, TouchPoint[] touchPoints, int touchPointsSize)463 private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, 464 TouchPoint[] touchPoints, int touchPointsSize) { 465 if ((sPointerCoords == null) || (sPointerCoords.length < touchPointsSize)) { 466 sPointerCoords = new MotionEvent.PointerCoords[touchPointsSize]; 467 for (int i = 0; i < touchPointsSize; i++) { 468 sPointerCoords[i] = new MotionEvent.PointerCoords(); 469 } 470 } 471 if ((sPointerProps == null) || (sPointerProps.length < touchPointsSize)) { 472 sPointerProps = new MotionEvent.PointerProperties[touchPointsSize]; 473 for (int i = 0; i < touchPointsSize; i++) { 474 sPointerProps[i] = new MotionEvent.PointerProperties(); 475 } 476 } 477 for (int i = 0; i < touchPointsSize; i++) { 478 int pointerId = mStrokeIdToPointerId.get(touchPoints[i].mStrokeId, -1); 479 if (pointerId == -1) { 480 pointerId = getUnusedPointerId(); 481 mStrokeIdToPointerId.put(touchPoints[i].mStrokeId, pointerId); 482 } 483 sPointerProps[i].id = pointerId; 484 sPointerProps[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN; 485 sPointerCoords[i].clear(); 486 sPointerCoords[i].pressure = 1.0f; 487 sPointerCoords[i].size = 1.0f; 488 sPointerCoords[i].x = touchPoints[i].mX; 489 sPointerCoords[i].y = touchPoints[i].mY; 490 } 491 return MotionEvent.obtain(downTime, eventTime, action, touchPointsSize, 492 sPointerProps, sPointerCoords, EVENT_META_STATE, EVENT_BUTTON_STATE, 493 EVENT_X_PRECISION, EVENT_Y_PRECISION, KeyCharacterMap.VIRTUAL_KEYBOARD, 494 EVENT_EDGE_FLAGS, EVENT_SOURCE, EVENT_FLAGS); 495 } 496 findPointByStrokeId(TouchPoint[] touchPoints, int touchPointsSize, int strokeId)497 private static int findPointByStrokeId(TouchPoint[] touchPoints, int touchPointsSize, 498 int strokeId) { 499 for (int i = 0; i < touchPointsSize; i++) { 500 if (touchPoints[i].mStrokeId == strokeId) { 501 return i; 502 } 503 } 504 return -1; 505 } getUnusedPointerId()506 private int getUnusedPointerId() { 507 int MAX_POINTER_ID = 10; 508 int pointerId = 0; 509 while (mStrokeIdToPointerId.indexOfValue(pointerId) >= 0) { 510 pointerId++; 511 if (pointerId >= MAX_POINTER_ID) { 512 return MAX_POINTER_ID; 513 } 514 } 515 return pointerId; 516 } 517 } 518