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