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
17package com.android.server.accessibility;
18
19import android.content.Context;
20import android.os.Handler;
21import android.os.Message;
22import android.util.MathUtils;
23import android.util.Slog;
24import android.util.TypedValue;
25import android.view.GestureDetector;
26import android.view.GestureDetector.SimpleOnGestureListener;
27import android.view.InputDevice;
28import android.view.KeyEvent;
29import android.view.MotionEvent;
30import android.view.MotionEvent.PointerCoords;
31import android.view.MotionEvent.PointerProperties;
32import android.view.ScaleGestureDetector;
33import android.view.ScaleGestureDetector.OnScaleGestureListener;
34import android.view.ViewConfiguration;
35import android.view.accessibility.AccessibilityEvent;
36
37/**
38 * This class handles magnification in response to touch events.
39 *
40 * The behavior is as follows:
41 *
42 * 1. Triple tap toggles permanent screen magnification which is magnifying
43 *    the area around the location of the triple tap. One can think of the
44 *    location of the triple tap as the center of the magnified viewport.
45 *    For example, a triple tap when not magnified would magnify the screen
46 *    and leave it in a magnified state. A triple tapping when magnified would
47 *    clear magnification and leave the screen in a not magnified state.
48 *
49 * 2. Triple tap and hold would magnify the screen if not magnified and enable
50 *    viewport dragging mode until the finger goes up. One can think of this
51 *    mode as a way to move the magnified viewport since the area around the
52 *    moving finger will be magnified to fit the screen. For example, if the
53 *    screen was not magnified and the user triple taps and holds the screen
54 *    would magnify and the viewport will follow the user's finger. When the
55 *    finger goes up the screen will zoom out. If the same user interaction
56 *    is performed when the screen is magnified, the viewport movement will
57 *    be the same but when the finger goes up the screen will stay magnified.
58 *    In other words, the initial magnified state is sticky.
59 *
60 * 3. Pinching with any number of additional fingers when viewport dragging
61 *    is enabled, i.e. the user triple tapped and holds, would adjust the
62 *    magnification scale which will become the current default magnification
63 *    scale. The next time the user magnifies the same magnification scale
64 *    would be used.
65 *
66 * 4. When in a permanent magnified state the user can use two or more fingers
67 *    to pan the viewport. Note that in this mode the content is panned as
68 *    opposed to the viewport dragging mode in which the viewport is moved.
69 *
70 * 5. When in a permanent magnified state the user can use two or more
71 *    fingers to change the magnification scale which will become the current
72 *    default magnification scale. The next time the user magnifies the same
73 *    magnification scale would be used.
74 *
75 * 6. The magnification scale will be persisted in settings and in the cloud.
76 */
77class MagnificationGestureHandler implements EventStreamTransformation {
78    private static final String LOG_TAG = "MagnificationEventHandler";
79
80    private static final boolean DEBUG_STATE_TRANSITIONS = false;
81    private static final boolean DEBUG_DETECTING = false;
82    private static final boolean DEBUG_PANNING = false;
83
84    private static final int STATE_DELEGATING = 1;
85    private static final int STATE_DETECTING = 2;
86    private static final int STATE_VIEWPORT_DRAGGING = 3;
87    private static final int STATE_MAGNIFIED_INTERACTION = 4;
88
89    private static final float MIN_SCALE = 2.0f;
90    private static final float MAX_SCALE = 5.0f;
91
92    private final MagnificationController mMagnificationController;
93    private final DetectingStateHandler mDetectingStateHandler;
94    private final MagnifiedContentInteractionStateHandler mMagnifiedContentInteractionStateHandler;
95    private final StateViewportDraggingHandler mStateViewportDraggingHandler;
96
97
98    private final boolean mDetectControlGestures;
99
100    private EventStreamTransformation mNext;
101
102    private int mCurrentState;
103    private int mPreviousState;
104
105    private boolean mTranslationEnabledBeforePan;
106
107    private PointerCoords[] mTempPointerCoords;
108    private PointerProperties[] mTempPointerProperties;
109
110    private long mDelegatingStateDownTime;
111
112    public MagnificationGestureHandler(Context context, AccessibilityManagerService ams,
113            boolean detectControlGestures) {
114        mMagnificationController = ams.getMagnificationController();
115        mDetectingStateHandler = new DetectingStateHandler(context);
116        mStateViewportDraggingHandler = new StateViewportDraggingHandler();
117        mMagnifiedContentInteractionStateHandler =
118                new MagnifiedContentInteractionStateHandler(context);
119        mDetectControlGestures = detectControlGestures;
120
121        transitionToState(STATE_DETECTING);
122    }
123
124    @Override
125    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
126        if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
127            if (mNext != null) {
128                mNext.onMotionEvent(event, rawEvent, policyFlags);
129            }
130            return;
131        }
132        if (!mDetectControlGestures) {
133            if (mNext != null) {
134                dispatchTransformedEvent(event, rawEvent, policyFlags);
135            }
136            return;
137        }
138        mMagnifiedContentInteractionStateHandler.onMotionEvent(event, rawEvent, policyFlags);
139        switch (mCurrentState) {
140            case STATE_DELEGATING: {
141                handleMotionEventStateDelegating(event, rawEvent, policyFlags);
142            }
143            break;
144            case STATE_DETECTING: {
145                mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags);
146            }
147            break;
148            case STATE_VIEWPORT_DRAGGING: {
149                mStateViewportDraggingHandler.onMotionEvent(event, rawEvent, policyFlags);
150            }
151            break;
152            case STATE_MAGNIFIED_INTERACTION: {
153                // mMagnifiedContentInteractionStateHandler handles events only
154                // if this is the current state since it uses ScaleGestureDetecotr
155                // and a GestureDetector which need well formed event stream.
156            }
157            break;
158            default: {
159                throw new IllegalStateException("Unknown state: " + mCurrentState);
160            }
161        }
162    }
163
164    @Override
165    public void onKeyEvent(KeyEvent event, int policyFlags) {
166        if (mNext != null) {
167            mNext.onKeyEvent(event, policyFlags);
168        }
169    }
170
171    @Override
172    public void onAccessibilityEvent(AccessibilityEvent event) {
173        if (mNext != null) {
174            mNext.onAccessibilityEvent(event);
175        }
176    }
177
178    @Override
179    public void setNext(EventStreamTransformation next) {
180        mNext = next;
181    }
182
183    @Override
184    public void clearEvents(int inputSource) {
185        if (inputSource == InputDevice.SOURCE_TOUCHSCREEN) {
186            clear();
187        }
188
189        if (mNext != null) {
190            mNext.clearEvents(inputSource);
191        }
192    }
193
194    @Override
195    public void onDestroy() {
196        clear();
197    }
198
199    private void clear() {
200        mCurrentState = STATE_DETECTING;
201        mDetectingStateHandler.clear();
202        mStateViewportDraggingHandler.clear();
203        mMagnifiedContentInteractionStateHandler.clear();
204    }
205
206    private void handleMotionEventStateDelegating(MotionEvent event,
207            MotionEvent rawEvent, int policyFlags) {
208        switch (event.getActionMasked()) {
209            case MotionEvent.ACTION_DOWN: {
210                mDelegatingStateDownTime = event.getDownTime();
211            }
212            break;
213            case MotionEvent.ACTION_UP: {
214                if (mDetectingStateHandler.mDelayedEventQueue == null) {
215                    transitionToState(STATE_DETECTING);
216                }
217            }
218            break;
219        }
220        if (mNext != null) {
221            // We cache some events to see if the user wants to trigger magnification.
222            // If no magnification is triggered we inject these events with adjusted
223            // time and down time to prevent subsequent transformations being confused
224            // by stale events. After the cached events, which always have a down, are
225            // injected we need to also update the down time of all subsequent non cached
226            // events. All delegated events cached and non-cached are delivered here.
227            event.setDownTime(mDelegatingStateDownTime);
228            dispatchTransformedEvent(event, rawEvent, policyFlags);
229        }
230    }
231
232    private void dispatchTransformedEvent(MotionEvent event, MotionEvent rawEvent,
233            int policyFlags) {
234        // If the event is within the magnified portion of the screen we have
235        // to change its location to be where the user thinks he is poking the
236        // UI which may have been magnified and panned.
237        final float eventX = event.getX();
238        final float eventY = event.getY();
239        if (mMagnificationController.isMagnifying()
240                && mMagnificationController.magnificationRegionContains(eventX, eventY)) {
241            final float scale = mMagnificationController.getScale();
242            final float scaledOffsetX = mMagnificationController.getOffsetX();
243            final float scaledOffsetY = mMagnificationController.getOffsetY();
244            final int pointerCount = event.getPointerCount();
245            PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
246            PointerProperties[] properties = getTempPointerPropertiesWithMinSize(
247                    pointerCount);
248            for (int i = 0; i < pointerCount; i++) {
249                event.getPointerCoords(i, coords[i]);
250                coords[i].x = (coords[i].x - scaledOffsetX) / scale;
251                coords[i].y = (coords[i].y - scaledOffsetY) / scale;
252                event.getPointerProperties(i, properties[i]);
253            }
254            event = MotionEvent.obtain(event.getDownTime(),
255                    event.getEventTime(), event.getAction(), pointerCount, properties,
256                    coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
257                    event.getFlags());
258        }
259        mNext.onMotionEvent(event, rawEvent, policyFlags);
260    }
261
262    private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
263        final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
264        if (oldSize < size) {
265            PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
266            mTempPointerCoords = new PointerCoords[size];
267            if (oldTempPointerCoords != null) {
268                System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
269            }
270        }
271        for (int i = oldSize; i < size; i++) {
272            mTempPointerCoords[i] = new PointerCoords();
273        }
274        return mTempPointerCoords;
275    }
276
277    private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
278        final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length
279                : 0;
280        if (oldSize < size) {
281            PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
282            mTempPointerProperties = new PointerProperties[size];
283            if (oldTempPointerProperties != null) {
284                System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0,
285                        oldSize);
286            }
287        }
288        for (int i = oldSize; i < size; i++) {
289            mTempPointerProperties[i] = new PointerProperties();
290        }
291        return mTempPointerProperties;
292    }
293
294    private void transitionToState(int state) {
295        if (DEBUG_STATE_TRANSITIONS) {
296            switch (state) {
297                case STATE_DELEGATING: {
298                    Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
299                }
300                break;
301                case STATE_DETECTING: {
302                    Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
303                }
304                break;
305                case STATE_VIEWPORT_DRAGGING: {
306                    Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
307                }
308                break;
309                case STATE_MAGNIFIED_INTERACTION: {
310                    Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION");
311                }
312                break;
313                default: {
314                    throw new IllegalArgumentException("Unknown state: " + state);
315                }
316            }
317        }
318        mPreviousState = mCurrentState;
319        mCurrentState = state;
320    }
321
322    private interface MotionEventHandler {
323
324        void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags);
325
326        void clear();
327    }
328
329    /**
330     * This class determines if the user is performing a scale or pan gesture.
331     */
332    private final class MagnifiedContentInteractionStateHandler extends SimpleOnGestureListener
333            implements OnScaleGestureListener, MotionEventHandler {
334
335        private final ScaleGestureDetector mScaleGestureDetector;
336
337        private final GestureDetector mGestureDetector;
338
339        private final float mScalingThreshold;
340
341        private float mInitialScaleFactor = -1;
342
343        private boolean mScaling;
344
345        public MagnifiedContentInteractionStateHandler(Context context) {
346            final TypedValue scaleValue = new TypedValue();
347            context.getResources().getValue(
348                    com.android.internal.R.dimen.config_screen_magnification_scaling_threshold,
349                    scaleValue, false);
350            mScalingThreshold = scaleValue.getFloat();
351            mScaleGestureDetector = new ScaleGestureDetector(context, this);
352            mScaleGestureDetector.setQuickScaleEnabled(false);
353            mGestureDetector = new GestureDetector(context, this);
354        }
355
356        @Override
357        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
358            mScaleGestureDetector.onTouchEvent(event);
359            mGestureDetector.onTouchEvent(event);
360            if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
361                return;
362            }
363            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
364                clear();
365                mMagnificationController.persistScale();
366                if (mPreviousState == STATE_VIEWPORT_DRAGGING) {
367                    transitionToState(STATE_VIEWPORT_DRAGGING);
368                } else {
369                    transitionToState(STATE_DETECTING);
370                }
371            }
372        }
373
374        @Override
375        public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX,
376                float distanceY) {
377            if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
378                return true;
379            }
380            if (DEBUG_PANNING) {
381                Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX
382                        + " scrollY: " + distanceY);
383            }
384            mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY,
385                    AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
386            return true;
387        }
388
389        @Override
390        public boolean onScale(ScaleGestureDetector detector) {
391            if (!mScaling) {
392                if (mInitialScaleFactor < 0) {
393                    mInitialScaleFactor = detector.getScaleFactor();
394                } else {
395                    final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
396                    if (Math.abs(deltaScale) > mScalingThreshold) {
397                        mScaling = true;
398                        return true;
399                    }
400                }
401                return false;
402            }
403
404            final float initialScale = mMagnificationController.getScale();
405            final float targetScale = initialScale * detector.getScaleFactor();
406
407            // Don't allow a gesture to move the user further outside the
408            // desired bounds for gesture-controlled scaling.
409            final float scale;
410            if (targetScale > MAX_SCALE && targetScale > initialScale) {
411                // The target scale is too big and getting bigger.
412                scale = MAX_SCALE;
413            } else if (targetScale < MIN_SCALE && targetScale < initialScale) {
414                // The target scale is too small and getting smaller.
415                scale = MIN_SCALE;
416            } else {
417                // The target scale may be outside our bounds, but at least
418                // it's moving in the right direction. This avoids a "jump" if
419                // we're at odds with some other service's desired bounds.
420                scale = targetScale;
421            }
422
423            final float pivotX = detector.getFocusX();
424            final float pivotY = detector.getFocusY();
425            mMagnificationController.setScale(scale, pivotX, pivotY, false,
426                    AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
427            return true;
428        }
429
430        @Override
431        public boolean onScaleBegin(ScaleGestureDetector detector) {
432            return (mCurrentState == STATE_MAGNIFIED_INTERACTION);
433        }
434
435        @Override
436        public void onScaleEnd(ScaleGestureDetector detector) {
437            clear();
438        }
439
440        @Override
441        public void clear() {
442            mInitialScaleFactor = -1;
443            mScaling = false;
444        }
445    }
446
447    /**
448     * This class handles motion events when the event dispatcher has
449     * determined that the user is performing a single-finger drag of the
450     * magnification viewport.
451     */
452    private final class StateViewportDraggingHandler implements MotionEventHandler {
453
454        private boolean mLastMoveOutsideMagnifiedRegion;
455
456        @Override
457        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
458            final int action = event.getActionMasked();
459            switch (action) {
460                case MotionEvent.ACTION_DOWN: {
461                    throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN");
462                }
463                case MotionEvent.ACTION_POINTER_DOWN: {
464                    clear();
465                    transitionToState(STATE_MAGNIFIED_INTERACTION);
466                }
467                break;
468                case MotionEvent.ACTION_MOVE: {
469                    if (event.getPointerCount() != 1) {
470                        throw new IllegalStateException("Should have one pointer down.");
471                    }
472                    final float eventX = event.getX();
473                    final float eventY = event.getY();
474                    if (mMagnificationController.magnificationRegionContains(eventX, eventY)) {
475                        if (mLastMoveOutsideMagnifiedRegion) {
476                            mLastMoveOutsideMagnifiedRegion = false;
477                            mMagnificationController.setCenter(eventX, eventY, true,
478                                    AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
479                        } else {
480                            mMagnificationController.setCenter(eventX, eventY, false,
481                                    AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
482                        }
483                    } else {
484                        mLastMoveOutsideMagnifiedRegion = true;
485                    }
486                }
487                break;
488                case MotionEvent.ACTION_UP: {
489                    if (!mTranslationEnabledBeforePan) {
490                        mMagnificationController.reset(true);
491                    }
492                    clear();
493                    transitionToState(STATE_DETECTING);
494                }
495                break;
496                case MotionEvent.ACTION_POINTER_UP: {
497                    throw new IllegalArgumentException(
498                            "Unexpected event type: ACTION_POINTER_UP");
499                }
500            }
501        }
502
503        @Override
504        public void clear() {
505            mLastMoveOutsideMagnifiedRegion = false;
506        }
507    }
508
509    /**
510     * This class handles motion events when the event dispatch has not yet
511     * determined what the user is doing. It watches for various tap events.
512     */
513    private final class DetectingStateHandler implements MotionEventHandler {
514
515        private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1;
516
517        private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
518
519        private static final int ACTION_TAP_COUNT = 3;
520
521        private final int mTapTimeSlop = ViewConfiguration.getJumpTapTimeout();
522
523        private final int mMultiTapTimeSlop;
524
525        private final int mTapDistanceSlop;
526
527        private final int mMultiTapDistanceSlop;
528
529        private MotionEventInfo mDelayedEventQueue;
530
531        private MotionEvent mLastDownEvent;
532
533        private MotionEvent mLastTapUpEvent;
534
535        private int mTapCount;
536
537        public DetectingStateHandler(Context context) {
538            mMultiTapTimeSlop = ViewConfiguration.getDoubleTapTimeout()
539                    + context.getResources().getInteger(
540                    com.android.internal.R.integer.config_screen_magnification_multi_tap_adjustment);
541            mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
542            mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
543        }
544
545        private final Handler mHandler = new Handler() {
546            @Override
547            public void handleMessage(Message message) {
548                final int type = message.what;
549                switch (type) {
550                    case MESSAGE_ON_ACTION_TAP_AND_HOLD: {
551                        MotionEvent event = (MotionEvent) message.obj;
552                        final int policyFlags = message.arg1;
553                        onActionTapAndHold(event, policyFlags);
554                    }
555                    break;
556                    case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
557                        transitionToState(STATE_DELEGATING);
558                        sendDelayedMotionEvents();
559                        clear();
560                    }
561                    break;
562                    default: {
563                        throw new IllegalArgumentException("Unknown message type: " + type);
564                    }
565                }
566            }
567        };
568
569        @Override
570        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
571            cacheDelayedMotionEvent(event, rawEvent, policyFlags);
572            final int action = event.getActionMasked();
573            switch (action) {
574                case MotionEvent.ACTION_DOWN: {
575                    mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
576                    if (!mMagnificationController.magnificationRegionContains(
577                            event.getX(), event.getY())) {
578                        transitionToDelegatingStateAndClear();
579                        return;
580                    }
581                    if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null
582                            && GestureUtils.isMultiTap(mLastDownEvent, event,
583                            mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
584                        Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD,
585                                policyFlags, 0, event);
586                        mHandler.sendMessageDelayed(message,
587                                ViewConfiguration.getLongPressTimeout());
588                    } else if (mTapCount < ACTION_TAP_COUNT) {
589                        Message message = mHandler.obtainMessage(
590                                MESSAGE_TRANSITION_TO_DELEGATING_STATE);
591                        mHandler.sendMessageDelayed(message, mMultiTapTimeSlop);
592                    }
593                    clearLastDownEvent();
594                    mLastDownEvent = MotionEvent.obtain(event);
595                }
596                break;
597                case MotionEvent.ACTION_POINTER_DOWN: {
598                    if (mMagnificationController.isMagnifying()) {
599                        transitionToState(STATE_MAGNIFIED_INTERACTION);
600                        clear();
601                    } else {
602                        transitionToDelegatingStateAndClear();
603                    }
604                }
605                break;
606                case MotionEvent.ACTION_MOVE: {
607                    if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) {
608                        final double distance = GestureUtils.computeDistance(mLastDownEvent,
609                                event, 0);
610                        if (Math.abs(distance) > mTapDistanceSlop) {
611                            transitionToDelegatingStateAndClear();
612                        }
613                    }
614                }
615                break;
616                case MotionEvent.ACTION_UP: {
617                    if (mLastDownEvent == null) {
618                        return;
619                    }
620                    mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
621                    if (!mMagnificationController.magnificationRegionContains(
622                            event.getX(), event.getY())) {
623                        transitionToDelegatingStateAndClear();
624                        return;
625                    }
626                    if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop,
627                            mTapDistanceSlop, 0)) {
628                        transitionToDelegatingStateAndClear();
629                        return;
630                    }
631                    if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent,
632                            event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
633                        transitionToDelegatingStateAndClear();
634                        return;
635                    }
636                    mTapCount++;
637                    if (DEBUG_DETECTING) {
638                        Slog.i(LOG_TAG, "Tap count:" + mTapCount);
639                    }
640                    if (mTapCount == ACTION_TAP_COUNT) {
641                        clear();
642                        onActionTap(event, policyFlags);
643                        return;
644                    }
645                    clearLastTapUpEvent();
646                    mLastTapUpEvent = MotionEvent.obtain(event);
647                }
648                break;
649                case MotionEvent.ACTION_POINTER_UP: {
650                    /* do nothing */
651                }
652                break;
653            }
654        }
655
656        @Override
657        public void clear() {
658            mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
659            mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
660            clearTapDetectionState();
661            clearDelayedMotionEvents();
662        }
663
664        private void clearTapDetectionState() {
665            mTapCount = 0;
666            clearLastTapUpEvent();
667            clearLastDownEvent();
668        }
669
670        private void clearLastTapUpEvent() {
671            if (mLastTapUpEvent != null) {
672                mLastTapUpEvent.recycle();
673                mLastTapUpEvent = null;
674            }
675        }
676
677        private void clearLastDownEvent() {
678            if (mLastDownEvent != null) {
679                mLastDownEvent.recycle();
680                mLastDownEvent = null;
681            }
682        }
683
684        private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
685                int policyFlags) {
686            MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent,
687                    policyFlags);
688            if (mDelayedEventQueue == null) {
689                mDelayedEventQueue = info;
690            } else {
691                MotionEventInfo tail = mDelayedEventQueue;
692                while (tail.mNext != null) {
693                    tail = tail.mNext;
694                }
695                tail.mNext = info;
696            }
697        }
698
699        private void sendDelayedMotionEvents() {
700            while (mDelayedEventQueue != null) {
701                MotionEventInfo info = mDelayedEventQueue;
702                mDelayedEventQueue = info.mNext;
703                MagnificationGestureHandler.this.onMotionEvent(info.mEvent, info.mRawEvent,
704                        info.mPolicyFlags);
705                info.recycle();
706            }
707        }
708
709        private void clearDelayedMotionEvents() {
710            while (mDelayedEventQueue != null) {
711                MotionEventInfo info = mDelayedEventQueue;
712                mDelayedEventQueue = info.mNext;
713                info.recycle();
714            }
715        }
716
717        private void transitionToDelegatingStateAndClear() {
718            transitionToState(STATE_DELEGATING);
719            sendDelayedMotionEvents();
720            clear();
721        }
722
723        private void onActionTap(MotionEvent up, int policyFlags) {
724            if (DEBUG_DETECTING) {
725                Slog.i(LOG_TAG, "onActionTap()");
726            }
727
728            if (!mMagnificationController.isMagnifying()) {
729                final float targetScale = mMagnificationController.getPersistedScale();
730                final float scale = MathUtils.constrain(targetScale, MIN_SCALE, MAX_SCALE);
731                mMagnificationController.setScaleAndCenter(scale, up.getX(), up.getY(), true,
732                        AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
733            } else {
734                mMagnificationController.reset(true);
735            }
736        }
737
738        private void onActionTapAndHold(MotionEvent down, int policyFlags) {
739            if (DEBUG_DETECTING) {
740                Slog.i(LOG_TAG, "onActionTapAndHold()");
741            }
742
743            clear();
744            mTranslationEnabledBeforePan = mMagnificationController.isMagnifying();
745
746            final float targetScale = mMagnificationController.getPersistedScale();
747            final float scale = MathUtils.constrain(targetScale, MIN_SCALE, MAX_SCALE);
748            mMagnificationController.setScaleAndCenter(scale, down.getX(), down.getY(), true,
749                    AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
750
751            transitionToState(STATE_VIEWPORT_DRAGGING);
752        }
753    }
754
755    private static final class MotionEventInfo {
756
757        private static final int MAX_POOL_SIZE = 10;
758
759        private static final Object sLock = new Object();
760
761        private static MotionEventInfo sPool;
762
763        private static int sPoolSize;
764
765        private MotionEventInfo mNext;
766
767        private boolean mInPool;
768
769        public MotionEvent mEvent;
770
771        public MotionEvent mRawEvent;
772
773        public int mPolicyFlags;
774
775        public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent,
776                int policyFlags) {
777            synchronized (sLock) {
778                MotionEventInfo info;
779                if (sPoolSize > 0) {
780                    sPoolSize--;
781                    info = sPool;
782                    sPool = info.mNext;
783                    info.mNext = null;
784                    info.mInPool = false;
785                } else {
786                    info = new MotionEventInfo();
787                }
788                info.initialize(event, rawEvent, policyFlags);
789                return info;
790            }
791        }
792
793        private void initialize(MotionEvent event, MotionEvent rawEvent,
794                int policyFlags) {
795            mEvent = MotionEvent.obtain(event);
796            mRawEvent = MotionEvent.obtain(rawEvent);
797            mPolicyFlags = policyFlags;
798        }
799
800        public void recycle() {
801            synchronized (sLock) {
802                if (mInPool) {
803                    throw new IllegalStateException("Already recycled.");
804                }
805                clear();
806                if (sPoolSize < MAX_POOL_SIZE) {
807                    sPoolSize++;
808                    mNext = sPool;
809                    sPool = this;
810                    mInPool = true;
811                }
812            }
813        }
814
815        private void clear() {
816            mEvent.recycle();
817            mEvent = null;
818            mRawEvent.recycle();
819            mRawEvent = null;
820            mPolicyFlags = 0;
821        }
822    }
823}
824