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.Context;
     20 import android.os.Handler;
     21 import android.os.Message;
     22 import android.util.MathUtils;
     23 import android.util.Slog;
     24 import android.util.TypedValue;
     25 import android.view.GestureDetector;
     26 import android.view.GestureDetector.SimpleOnGestureListener;
     27 import android.view.InputDevice;
     28 import android.view.KeyEvent;
     29 import android.view.MotionEvent;
     30 import android.view.MotionEvent.PointerCoords;
     31 import android.view.MotionEvent.PointerProperties;
     32 import android.view.ScaleGestureDetector;
     33 import android.view.ScaleGestureDetector.OnScaleGestureListener;
     34 import android.view.ViewConfiguration;
     35 import 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  */
     77 class 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