Home | History | Annotate | Download | only in accessibility
      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.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.IntentFilter;
     23 import android.os.Handler;
     24 import android.os.Message;
     25 import android.util.MathUtils;
     26 import android.util.Slog;
     27 import android.util.TypedValue;
     28 import android.view.GestureDetector;
     29 import android.view.GestureDetector.SimpleOnGestureListener;
     30 import android.view.InputDevice;
     31 import android.view.KeyEvent;
     32 import android.view.MotionEvent;
     33 import android.view.MotionEvent.PointerCoords;
     34 import android.view.MotionEvent.PointerProperties;
     35 import android.view.ScaleGestureDetector;
     36 import android.view.ScaleGestureDetector.OnScaleGestureListener;
     37 import android.view.ViewConfiguration;
     38 import 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  */
     88 class 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