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