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 static android.view.InputDevice.SOURCE_TOUCHSCREEN;
     20 import static android.view.MotionEvent.ACTION_CANCEL;
     21 import static android.view.MotionEvent.ACTION_DOWN;
     22 import static android.view.MotionEvent.ACTION_MOVE;
     23 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
     24 import static android.view.MotionEvent.ACTION_POINTER_UP;
     25 import static android.view.MotionEvent.ACTION_UP;
     26 
     27 import static com.android.server.accessibility.GestureUtils.distance;
     28 
     29 import static java.lang.Math.abs;
     30 import static java.util.Arrays.asList;
     31 import static java.util.Arrays.copyOfRange;
     32 
     33 import android.annotation.NonNull;
     34 import android.annotation.Nullable;
     35 import android.content.BroadcastReceiver;
     36 import android.content.Context;
     37 import android.content.Intent;
     38 import android.content.IntentFilter;
     39 import android.os.Handler;
     40 import android.os.Looper;
     41 import android.os.Message;
     42 import android.util.Log;
     43 import android.util.MathUtils;
     44 import android.util.Slog;
     45 import android.util.TypedValue;
     46 import android.view.GestureDetector;
     47 import android.view.GestureDetector.SimpleOnGestureListener;
     48 import android.view.MotionEvent;
     49 import android.view.MotionEvent.PointerCoords;
     50 import android.view.MotionEvent.PointerProperties;
     51 import android.view.ScaleGestureDetector;
     52 import android.view.ScaleGestureDetector.OnScaleGestureListener;
     53 import android.view.ViewConfiguration;
     54 
     55 import com.android.internal.annotations.VisibleForTesting;
     56 
     57 import java.util.ArrayDeque;
     58 import java.util.Queue;
     59 
     60 /**
     61  * This class handles magnification in response to touch events.
     62  *
     63  * The behavior is as follows:
     64  *
     65  * 1. Triple tap toggles permanent screen magnification which is magnifying
     66  *    the area around the location of the triple tap. One can think of the
     67  *    location of the triple tap as the center of the magnified viewport.
     68  *    For example, a triple tap when not magnified would magnify the screen
     69  *    and leave it in a magnified state. A triple tapping when magnified would
     70  *    clear magnification and leave the screen in a not magnified state.
     71  *
     72  * 2. Triple tap and hold would magnify the screen if not magnified and enable
     73  *    viewport dragging mode until the finger goes up. One can think of this
     74  *    mode as a way to move the magnified viewport since the area around the
     75  *    moving finger will be magnified to fit the screen. For example, if the
     76  *    screen was not magnified and the user triple taps and holds the screen
     77  *    would magnify and the viewport will follow the user's finger. When the
     78  *    finger goes up the screen will zoom out. If the same user interaction
     79  *    is performed when the screen is magnified, the viewport movement will
     80  *    be the same but when the finger goes up the screen will stay magnified.
     81  *    In other words, the initial magnified state is sticky.
     82  *
     83  * 3. Magnification can optionally be "triggered" by some external shortcut
     84  *    affordance. When this occurs via {@link #notifyShortcutTriggered()} a
     85  *    subsequent tap in a magnifiable region will engage permanent screen
     86  *    magnification as described in #1. Alternatively, a subsequent long-press
     87  *    or drag will engage magnification with viewport dragging as described in
     88  *    #2. Once magnified, all following behaviors apply whether magnification
     89  *    was engaged via a triple-tap or by a triggered shortcut.
     90  *
     91  * 4. Pinching with any number of additional fingers when viewport dragging
     92  *    is enabled, i.e. the user triple tapped and holds, would adjust the
     93  *    magnification scale which will become the current default magnification
     94  *    scale. The next time the user magnifies the same magnification scale
     95  *    would be used.
     96  *
     97  * 5. When in a permanent magnified state the user can use two or more fingers
     98  *    to pan the viewport. Note that in this mode the content is panned as
     99  *    opposed to the viewport dragging mode in which the viewport is moved.
    100  *
    101  * 6. When in a permanent magnified state the user can use two or more
    102  *    fingers to change the magnification scale which will become the current
    103  *    default magnification scale. The next time the user magnifies the same
    104  *    magnification scale would be used.
    105  *
    106  * 7. The magnification scale will be persisted in settings and in the cloud.
    107  */
    108 @SuppressWarnings("WeakerAccess")
    109 class MagnificationGestureHandler extends BaseEventStreamTransformation {
    110     private static final String LOG_TAG = "MagnificationGestureHandler";
    111 
    112     private static final boolean DEBUG_ALL = false;
    113     private static final boolean DEBUG_STATE_TRANSITIONS = false || DEBUG_ALL;
    114     private static final boolean DEBUG_DETECTING = false || DEBUG_ALL;
    115     private static final boolean DEBUG_PANNING_SCALING = false || DEBUG_ALL;
    116     private static final boolean DEBUG_EVENT_STREAM = false || DEBUG_ALL;
    117 
    118     private static final float MIN_SCALE = 2.0f;
    119     private static final float MAX_SCALE = 5.0f;
    120 
    121     @VisibleForTesting final MagnificationController mMagnificationController;
    122 
    123     @VisibleForTesting final DelegatingState mDelegatingState;
    124     @VisibleForTesting final DetectingState mDetectingState;
    125     @VisibleForTesting final PanningScalingState mPanningScalingState;
    126     @VisibleForTesting final ViewportDraggingState mViewportDraggingState;
    127 
    128     private final ScreenStateReceiver mScreenStateReceiver;
    129 
    130     /**
    131      * {@code true} if this detector should detect and respond to triple-tap
    132      * gestures for engaging and disengaging magnification,
    133      * {@code false} if it should ignore such gestures
    134      */
    135     final boolean mDetectTripleTap;
    136 
    137     /**
    138      * Whether {@link DetectingState#mShortcutTriggered shortcut} is enabled
    139      */
    140     final boolean mDetectShortcutTrigger;
    141 
    142     @VisibleForTesting State mCurrentState;
    143     @VisibleForTesting State mPreviousState;
    144 
    145     private PointerCoords[] mTempPointerCoords;
    146     private PointerProperties[] mTempPointerProperties;
    147 
    148     private final Queue<MotionEvent> mDebugInputEventHistory;
    149     private final Queue<MotionEvent> mDebugOutputEventHistory;
    150 
    151     /**
    152      * @param context Context for resolving various magnification-related resources
    153      * @param magnificationController the {@link MagnificationController}
    154      *
    155      * @param detectTripleTap {@code true} if this detector should detect and respond to triple-tap
    156      *                                gestures for engaging and disengaging magnification,
    157      *                                {@code false} if it should ignore such gestures
    158      * @param detectShortcutTrigger {@code true} if this detector should be "triggerable" by some
    159      *                           external shortcut invoking {@link #notifyShortcutTriggered},
    160      *                           {@code false} if it should ignore such triggers.
    161      */
    162     public MagnificationGestureHandler(Context context,
    163             MagnificationController magnificationController,
    164             boolean detectTripleTap,
    165             boolean detectShortcutTrigger) {
    166         if (DEBUG_ALL) {
    167             Log.i(LOG_TAG,
    168                     "MagnificationGestureHandler(detectTripleTap = " + detectTripleTap
    169                             + ", detectShortcutTrigger = " + detectShortcutTrigger + ")");
    170         }
    171 
    172         mMagnificationController = magnificationController;
    173 
    174         mDelegatingState = new DelegatingState();
    175         mDetectingState = new DetectingState(context);
    176         mViewportDraggingState = new ViewportDraggingState();
    177         mPanningScalingState = new PanningScalingState(context);
    178 
    179         mDetectTripleTap = detectTripleTap;
    180         mDetectShortcutTrigger = detectShortcutTrigger;
    181 
    182         if (mDetectShortcutTrigger) {
    183             mScreenStateReceiver = new ScreenStateReceiver(context, this);
    184             mScreenStateReceiver.register();
    185         } else {
    186             mScreenStateReceiver = null;
    187         }
    188 
    189         mDebugInputEventHistory = DEBUG_EVENT_STREAM ? new ArrayDeque<>() : null;
    190         mDebugOutputEventHistory = DEBUG_EVENT_STREAM ? new ArrayDeque<>() : null;
    191 
    192         transitionTo(mDetectingState);
    193     }
    194 
    195     @Override
    196     public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    197         if (DEBUG_EVENT_STREAM) {
    198             storeEventInto(mDebugInputEventHistory, event);
    199             try {
    200                 onMotionEventInternal(event, rawEvent, policyFlags);
    201             } catch (Exception e) {
    202                 throw new RuntimeException(
    203                         "Exception following input events: " + mDebugInputEventHistory, e);
    204             }
    205         } else {
    206             onMotionEventInternal(event, rawEvent, policyFlags);
    207         }
    208     }
    209 
    210     private void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    211         if (DEBUG_ALL) Slog.i(LOG_TAG, "onMotionEvent(" + event + ")");
    212 
    213         if ((!mDetectTripleTap && !mDetectShortcutTrigger)
    214                 || !event.isFromSource(SOURCE_TOUCHSCREEN)) {
    215             dispatchTransformedEvent(event, rawEvent, policyFlags);
    216             return;
    217         }
    218 
    219         handleEventWith(mCurrentState, event, rawEvent, policyFlags);
    220     }
    221 
    222     private void handleEventWith(State stateHandler,
    223             MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    224         // To keep InputEventConsistencyVerifiers within GestureDetectors happy
    225         mPanningScalingState.mScrollGestureDetector.onTouchEvent(event);
    226         mPanningScalingState.mScaleGestureDetector.onTouchEvent(event);
    227 
    228         stateHandler.onMotionEvent(event, rawEvent, policyFlags);
    229     }
    230 
    231     @Override
    232     public void clearEvents(int inputSource) {
    233         if (inputSource == SOURCE_TOUCHSCREEN) {
    234             clearAndTransitionToStateDetecting();
    235         }
    236 
    237         super.clearEvents(inputSource);
    238     }
    239 
    240     @Override
    241     public void onDestroy() {
    242         if (DEBUG_STATE_TRANSITIONS) {
    243             Slog.i(LOG_TAG, "onDestroy(); delayed = "
    244                     + MotionEventInfo.toString(mDetectingState.mDelayedEventQueue));
    245         }
    246 
    247         if (mScreenStateReceiver != null) {
    248             mScreenStateReceiver.unregister();
    249         }
    250         clearAndTransitionToStateDetecting();
    251     }
    252 
    253     void notifyShortcutTriggered() {
    254         if (mDetectShortcutTrigger) {
    255             boolean wasMagnifying = mMagnificationController.resetIfNeeded(/* animate */ true);
    256             if (wasMagnifying) {
    257                 clearAndTransitionToStateDetecting();
    258             } else {
    259                 mDetectingState.toggleShortcutTriggered();
    260             }
    261         }
    262     }
    263 
    264     void clearAndTransitionToStateDetecting() {
    265         mCurrentState = mDetectingState;
    266         mDetectingState.clear();
    267         mViewportDraggingState.clear();
    268         mPanningScalingState.clear();
    269     }
    270 
    271     private void dispatchTransformedEvent(MotionEvent event, MotionEvent rawEvent,
    272             int policyFlags) {
    273         if (DEBUG_ALL) Slog.i(LOG_TAG, "dispatchTransformedEvent(event = " + event + ")");
    274 
    275         // If the touchscreen event is within the magnified portion of the screen we have
    276         // to change its location to be where the user thinks he is poking the
    277         // UI which may have been magnified and panned.
    278         if (mMagnificationController.isMagnifying()
    279                 && event.isFromSource(SOURCE_TOUCHSCREEN)
    280                 && mMagnificationController.magnificationRegionContains(
    281                         event.getX(), event.getY())) {
    282             final float scale = mMagnificationController.getScale();
    283             final float scaledOffsetX = mMagnificationController.getOffsetX();
    284             final float scaledOffsetY = mMagnificationController.getOffsetY();
    285             final int pointerCount = event.getPointerCount();
    286             PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
    287             PointerProperties[] properties = getTempPointerPropertiesWithMinSize(
    288                     pointerCount);
    289             for (int i = 0; i < pointerCount; i++) {
    290                 event.getPointerCoords(i, coords[i]);
    291                 coords[i].x = (coords[i].x - scaledOffsetX) / scale;
    292                 coords[i].y = (coords[i].y - scaledOffsetY) / scale;
    293                 event.getPointerProperties(i, properties[i]);
    294             }
    295             event = MotionEvent.obtain(event.getDownTime(),
    296                     event.getEventTime(), event.getAction(), pointerCount, properties,
    297                     coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
    298                     event.getFlags());
    299         }
    300         if (DEBUG_EVENT_STREAM) {
    301             storeEventInto(mDebugOutputEventHistory, event);
    302             try {
    303                 super.onMotionEvent(event, rawEvent, policyFlags);
    304             } catch (Exception e) {
    305                 throw new RuntimeException(
    306                         "Exception downstream following input events: " + mDebugInputEventHistory
    307                                 + "\nTransformed into output events: " + mDebugOutputEventHistory,
    308                         e);
    309             }
    310         } else {
    311             super.onMotionEvent(event, rawEvent, policyFlags);
    312         }
    313     }
    314 
    315     private static void storeEventInto(Queue<MotionEvent> queue, MotionEvent event) {
    316         queue.add(MotionEvent.obtain(event));
    317         // Prune old events
    318         while (!queue.isEmpty() && (event.getEventTime() - queue.peek().getEventTime() > 5000)) {
    319             queue.remove().recycle();
    320         }
    321     }
    322 
    323     private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
    324         final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
    325         if (oldSize < size) {
    326             PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
    327             mTempPointerCoords = new PointerCoords[size];
    328             if (oldTempPointerCoords != null) {
    329                 System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
    330             }
    331         }
    332         for (int i = oldSize; i < size; i++) {
    333             mTempPointerCoords[i] = new PointerCoords();
    334         }
    335         return mTempPointerCoords;
    336     }
    337 
    338     private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
    339         final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length
    340                 : 0;
    341         if (oldSize < size) {
    342             PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
    343             mTempPointerProperties = new PointerProperties[size];
    344             if (oldTempPointerProperties != null) {
    345                 System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0,
    346                         oldSize);
    347             }
    348         }
    349         for (int i = oldSize; i < size; i++) {
    350             mTempPointerProperties[i] = new PointerProperties();
    351         }
    352         return mTempPointerProperties;
    353     }
    354 
    355     private void transitionTo(State state) {
    356         if (DEBUG_STATE_TRANSITIONS) {
    357             Slog.i(LOG_TAG,
    358                     (State.nameOf(mCurrentState) + " -> " + State.nameOf(state)
    359                     + " at " + asList(copyOfRange(new RuntimeException().getStackTrace(), 1, 5)))
    360                     .replace(getClass().getName(), ""));
    361         }
    362         mPreviousState = mCurrentState;
    363         mCurrentState = state;
    364     }
    365 
    366     interface State {
    367         void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags);
    368 
    369         default void clear() {}
    370 
    371         default String name() {
    372             return getClass().getSimpleName();
    373         }
    374 
    375         static String nameOf(@Nullable State s) {
    376             return s != null ? s.name() : "null";
    377         }
    378     }
    379 
    380     /**
    381      * This class determines if the user is performing a scale or pan gesture.
    382      *
    383      * Unlike when {@link ViewportDraggingState dragging the viewport}, in panning mode the viewport
    384      * moves in the same direction as the fingers, and allows to easily and precisely scale the
    385      * magnification level.
    386      * This makes it the preferred mode for one-off adjustments, due to its precision and ease of
    387      * triggering.
    388      */
    389     final class PanningScalingState extends SimpleOnGestureListener
    390             implements OnScaleGestureListener, State {
    391 
    392         private final ScaleGestureDetector mScaleGestureDetector;
    393         private final GestureDetector mScrollGestureDetector;
    394         final float mScalingThreshold;
    395 
    396         float mInitialScaleFactor = -1;
    397         boolean mScaling;
    398 
    399         public PanningScalingState(Context context) {
    400             final TypedValue scaleValue = new TypedValue();
    401             context.getResources().getValue(
    402                     com.android.internal.R.dimen.config_screen_magnification_scaling_threshold,
    403                     scaleValue, false);
    404             mScalingThreshold = scaleValue.getFloat();
    405             mScaleGestureDetector = new ScaleGestureDetector(context, this, Handler.getMain());
    406             mScaleGestureDetector.setQuickScaleEnabled(false);
    407             mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
    408         }
    409 
    410         @Override
    411         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    412             int action = event.getActionMasked();
    413 
    414             if (action == ACTION_POINTER_UP
    415                     && event.getPointerCount() == 2 // includes the pointer currently being released
    416                     && mPreviousState == mViewportDraggingState) {
    417 
    418                 persistScaleAndTransitionTo(mViewportDraggingState);
    419 
    420             } else if (action == ACTION_UP || action == ACTION_CANCEL) {
    421 
    422                 persistScaleAndTransitionTo(mDetectingState);
    423 
    424             }
    425         }
    426 
    427         public void persistScaleAndTransitionTo(State state) {
    428             mMagnificationController.persistScale();
    429             clear();
    430             transitionTo(state);
    431         }
    432 
    433         @Override
    434         public boolean onScroll(MotionEvent first, MotionEvent second,
    435                 float distanceX, float distanceY) {
    436             if (mCurrentState != mPanningScalingState) {
    437                 return true;
    438             }
    439             if (DEBUG_PANNING_SCALING) {
    440                 Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX
    441                         + " scrollY: " + distanceY);
    442             }
    443             mMagnificationController.offsetMagnifiedRegion(distanceX, distanceY,
    444                     AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
    445             return /* event consumed: */ true;
    446         }
    447 
    448         @Override
    449         public boolean onScale(ScaleGestureDetector detector) {
    450             if (!mScaling) {
    451                 if (mInitialScaleFactor < 0) {
    452                     mInitialScaleFactor = detector.getScaleFactor();
    453                     return false;
    454                 }
    455                 final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
    456                 mScaling = abs(deltaScale) > mScalingThreshold;
    457                 return mScaling;
    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             if (DEBUG_PANNING_SCALING) Slog.i(LOG_TAG, "Scaled content to: " + scale + "x");
    482             mMagnificationController.setScale(scale, pivotX, pivotY, false,
    483                     AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
    484             return /* handled: */ true;
    485         }
    486 
    487         @Override
    488         public boolean onScaleBegin(ScaleGestureDetector detector) {
    489             return /* continue recognizing: */ (mCurrentState == mPanningScalingState);
    490         }
    491 
    492         @Override
    493         public void onScaleEnd(ScaleGestureDetector detector) {
    494             clear();
    495         }
    496 
    497         @Override
    498         public void clear() {
    499             mInitialScaleFactor = -1;
    500             mScaling = false;
    501         }
    502 
    503         @Override
    504         public String toString() {
    505             return "PanningScalingState{" +
    506                     "mInitialScaleFactor=" + mInitialScaleFactor +
    507                     ", mScaling=" + mScaling +
    508                     '}';
    509         }
    510     }
    511 
    512     /**
    513      * This class handles motion events when the event dispatcher has
    514      * determined that the user is performing a single-finger drag of the
    515      * magnification viewport.
    516      *
    517      * Unlike when {@link PanningScalingState panning}, the viewport moves in the opposite direction
    518      * of the finger, and any part of the screen is reachable without lifting the finger.
    519      * This makes it the preferable mode for tasks like reading text spanning full screen width.
    520      */
    521     final class ViewportDraggingState implements State {
    522 
    523         /** Whether to disable zoom after dragging ends */
    524         boolean mZoomedInBeforeDrag;
    525         private boolean mLastMoveOutsideMagnifiedRegion;
    526 
    527         @Override
    528         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    529             final int action = event.getActionMasked();
    530             switch (action) {
    531                 case ACTION_POINTER_DOWN: {
    532                     clear();
    533                     transitionTo(mPanningScalingState);
    534                 }
    535                 break;
    536                 case ACTION_MOVE: {
    537                     if (event.getPointerCount() != 1) {
    538                         throw new IllegalStateException("Should have one pointer down.");
    539                     }
    540                     final float eventX = event.getX();
    541                     final float eventY = event.getY();
    542                     if (mMagnificationController.magnificationRegionContains(eventX, eventY)) {
    543                         mMagnificationController.setCenter(eventX, eventY,
    544                                 /* animate */ mLastMoveOutsideMagnifiedRegion,
    545                                 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
    546                         mLastMoveOutsideMagnifiedRegion = false;
    547                     } else {
    548                         mLastMoveOutsideMagnifiedRegion = true;
    549                     }
    550                 }
    551                 break;
    552 
    553                 case ACTION_UP:
    554                 case ACTION_CANCEL: {
    555                     if (!mZoomedInBeforeDrag) zoomOff();
    556                     clear();
    557                     transitionTo(mDetectingState);
    558                 }
    559                 break;
    560 
    561                 case ACTION_DOWN:
    562                 case ACTION_POINTER_UP: {
    563                     throw new IllegalArgumentException(
    564                             "Unexpected event type: " + MotionEvent.actionToString(action));
    565                 }
    566             }
    567         }
    568 
    569         @Override
    570         public void clear() {
    571             mLastMoveOutsideMagnifiedRegion = false;
    572         }
    573 
    574         @Override
    575         public String toString() {
    576             return "ViewportDraggingState{" +
    577                     "mZoomedInBeforeDrag=" + mZoomedInBeforeDrag +
    578                     ", mLastMoveOutsideMagnifiedRegion=" + mLastMoveOutsideMagnifiedRegion +
    579                     '}';
    580         }
    581     }
    582 
    583     final class DelegatingState implements State {
    584         /**
    585          * Time of last {@link MotionEvent#ACTION_DOWN} while in {@link DelegatingState}
    586          */
    587         public long mLastDelegatedDownEventTime;
    588 
    589         @Override
    590         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    591 
    592         	// Ensure that the state at the end of delegation is consistent with the last delegated
    593             // UP/DOWN event in queue: still delegating if pointer is down, detecting otherwise
    594             switch (event.getActionMasked()) {
    595                 case ACTION_UP:
    596                 case ACTION_CANCEL: {
    597                     transitionTo(mDetectingState);
    598                 } break;
    599 
    600                 case ACTION_DOWN: {
    601                 	transitionTo(mDelegatingState);
    602                     mLastDelegatedDownEventTime = event.getDownTime();
    603                 } break;
    604             }
    605 
    606             if (getNext() != null) {
    607                 // We cache some events to see if the user wants to trigger magnification.
    608                 // If no magnification is triggered we inject these events with adjusted
    609                 // time and down time to prevent subsequent transformations being confused
    610                 // by stale events. After the cached events, which always have a down, are
    611                 // injected we need to also update the down time of all subsequent non cached
    612                 // events. All delegated events cached and non-cached are delivered here.
    613                 event.setDownTime(mLastDelegatedDownEventTime);
    614                 dispatchTransformedEvent(event, rawEvent, policyFlags);
    615             }
    616         }
    617     }
    618 
    619     /**
    620      * This class handles motion events when the event dispatch has not yet
    621      * determined what the user is doing. It watches for various tap events.
    622      */
    623     final class DetectingState implements State, Handler.Callback {
    624 
    625         private static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1;
    626         private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
    627 
    628         final int mLongTapMinDelay;
    629         final int mSwipeMinDistance;
    630         final int mMultiTapMaxDelay;
    631         final int mMultiTapMaxDistance;
    632 
    633         private MotionEventInfo mDelayedEventQueue;
    634         MotionEvent mLastDown;
    635         private MotionEvent mPreLastDown;
    636         private MotionEvent mLastUp;
    637         private MotionEvent mPreLastUp;
    638 
    639         @VisibleForTesting boolean mShortcutTriggered;
    640 
    641         @VisibleForTesting Handler mHandler = new Handler(Looper.getMainLooper(), this);
    642 
    643         public DetectingState(Context context) {
    644             mLongTapMinDelay = ViewConfiguration.getLongPressTimeout();
    645             mMultiTapMaxDelay = ViewConfiguration.getDoubleTapTimeout()
    646                     + context.getResources().getInteger(
    647                     com.android.internal.R.integer.config_screen_magnification_multi_tap_adjustment);
    648             mSwipeMinDistance = ViewConfiguration.get(context).getScaledTouchSlop();
    649             mMultiTapMaxDistance = ViewConfiguration.get(context).getScaledDoubleTapSlop();
    650         }
    651 
    652         @Override
    653         public boolean handleMessage(Message message) {
    654             final int type = message.what;
    655             switch (type) {
    656                 case MESSAGE_ON_TRIPLE_TAP_AND_HOLD: {
    657                     MotionEvent down = (MotionEvent) message.obj;
    658                     transitionToViewportDraggingStateAndClear(down);
    659                     down.recycle();
    660                 }
    661                 break;
    662                 case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
    663                     transitionToDelegatingStateAndClear();
    664                 }
    665                 break;
    666                 default: {
    667                     throw new IllegalArgumentException("Unknown message type: " + type);
    668                 }
    669             }
    670             return true;
    671         }
    672 
    673         @Override
    674         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    675             cacheDelayedMotionEvent(event, rawEvent, policyFlags);
    676             switch (event.getActionMasked()) {
    677                 case MotionEvent.ACTION_DOWN: {
    678 
    679                     mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
    680 
    681                     if (!mMagnificationController.magnificationRegionContains(
    682                             event.getX(), event.getY())) {
    683 
    684                         transitionToDelegatingStateAndClear();
    685 
    686                     } else if (isMultiTapTriggered(2 /* taps */)) {
    687 
    688                         // 3tap and hold
    689                         afterLongTapTimeoutTransitionToDraggingState(event);
    690 
    691                     } else if (mDetectTripleTap
    692                             // If magnified, delay an ACTION_DOWN for mMultiTapMaxDelay
    693                             // to ensure reachability of
    694                             // STATE_PANNING_SCALING(triggerable with ACTION_POINTER_DOWN)
    695                             || mMagnificationController.isMagnifying()) {
    696 
    697                         afterMultiTapTimeoutTransitionToDelegatingState();
    698 
    699                     } else {
    700 
    701                         // Delegate pending events without delay
    702                         transitionToDelegatingStateAndClear();
    703                     }
    704                 }
    705                 break;
    706                 case ACTION_POINTER_DOWN: {
    707                     if (mMagnificationController.isMagnifying()) {
    708                         transitionTo(mPanningScalingState);
    709                         clear();
    710                     } else {
    711                         transitionToDelegatingStateAndClear();
    712                     }
    713                 }
    714                 break;
    715                 case ACTION_MOVE: {
    716                     if (isFingerDown()
    717                             && distance(mLastDown, /* move */ event) > mSwipeMinDistance) {
    718 
    719                         // Swipe detected - transition immediately
    720 
    721                         // For convenience, viewport dragging takes precedence
    722                         // over insta-delegating on 3tap&swipe
    723                         // (which is a rare combo to be used aside from magnification)
    724                         if (isMultiTapTriggered(2 /* taps */)) {
    725                             transitionToViewportDraggingStateAndClear(event);
    726                         } else {
    727                             transitionToDelegatingStateAndClear();
    728                         }
    729                     }
    730                 }
    731                 break;
    732                 case ACTION_UP: {
    733 
    734                     mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
    735 
    736                     if (!mMagnificationController.magnificationRegionContains(
    737                             event.getX(), event.getY())) {
    738 
    739                         transitionToDelegatingStateAndClear();
    740 
    741                     } else if (isMultiTapTriggered(3 /* taps */)) {
    742 
    743                         onTripleTap(/* up */ event);
    744 
    745                     } else if (
    746                             // Possible to be false on: 3tap&drag -> scale -> PTR_UP -> UP
    747                             isFingerDown()
    748                             //TODO long tap should never happen here
    749                             && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay)
    750                                     || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))) {
    751 
    752                         transitionToDelegatingStateAndClear();
    753 
    754                     }
    755                 }
    756                 break;
    757             }
    758         }
    759 
    760         public boolean isMultiTapTriggered(int numTaps) {
    761 
    762             // Shortcut acts as the 2 initial taps
    763             if (mShortcutTriggered) return tapCount() + 2 >= numTaps;
    764 
    765             return mDetectTripleTap
    766                     && tapCount() >= numTaps
    767                     && isMultiTap(mPreLastDown, mLastDown)
    768                     && isMultiTap(mPreLastUp, mLastUp);
    769         }
    770 
    771         private boolean isMultiTap(MotionEvent first, MotionEvent second) {
    772             return GestureUtils.isMultiTap(first, second, mMultiTapMaxDelay, mMultiTapMaxDistance);
    773         }
    774 
    775         public boolean isFingerDown() {
    776             return mLastDown != null;
    777         }
    778 
    779         private long timeBetween(@Nullable MotionEvent a, @Nullable MotionEvent b) {
    780             if (a == null && b == null) return 0;
    781             return abs(timeOf(a) - timeOf(b));
    782         }
    783 
    784         /**
    785          * Nullsafe {@link MotionEvent#getEventTime} that interprets null event as something that
    786          * has happened long enough ago to be gone from the event queue.
    787          * Thus the time for a null event is a small number, that is below any other non-null
    788          * event's time.
    789          *
    790          * @return {@link MotionEvent#getEventTime}, or {@link Long#MIN_VALUE} if the event is null
    791          */
    792         private long timeOf(@Nullable MotionEvent event) {
    793             return event != null ? event.getEventTime() : Long.MIN_VALUE;
    794         }
    795 
    796         public int tapCount() {
    797             return MotionEventInfo.countOf(mDelayedEventQueue, ACTION_UP);
    798         }
    799 
    800         /** -> {@link DelegatingState} */
    801         public void afterMultiTapTimeoutTransitionToDelegatingState() {
    802             mHandler.sendEmptyMessageDelayed(
    803                     MESSAGE_TRANSITION_TO_DELEGATING_STATE,
    804                     mMultiTapMaxDelay);
    805         }
    806 
    807         /** -> {@link ViewportDraggingState} */
    808         public void afterLongTapTimeoutTransitionToDraggingState(MotionEvent event) {
    809             mHandler.sendMessageDelayed(
    810                     mHandler.obtainMessage(MESSAGE_ON_TRIPLE_TAP_AND_HOLD,
    811                             MotionEvent.obtain(event)),
    812                     ViewConfiguration.getLongPressTimeout());
    813         }
    814 
    815         @Override
    816         public void clear() {
    817             setShortcutTriggered(false);
    818             removePendingDelayedMessages();
    819             clearDelayedMotionEvents();
    820         }
    821 
    822         private void removePendingDelayedMessages() {
    823             mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
    824             mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
    825         }
    826 
    827         private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
    828                 int policyFlags) {
    829             if (event.getActionMasked() == ACTION_DOWN) {
    830                 mPreLastDown = mLastDown;
    831                 mLastDown = MotionEvent.obtain(event);
    832             } else if (event.getActionMasked() == ACTION_UP) {
    833                 mPreLastUp = mLastUp;
    834                 mLastUp = MotionEvent.obtain(event);
    835             }
    836 
    837             MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent,
    838                     policyFlags);
    839             if (mDelayedEventQueue == null) {
    840                 mDelayedEventQueue = info;
    841             } else {
    842                 MotionEventInfo tail = mDelayedEventQueue;
    843                 while (tail.mNext != null) {
    844                     tail = tail.mNext;
    845                 }
    846                 tail.mNext = info;
    847             }
    848         }
    849 
    850         private void sendDelayedMotionEvents() {
    851             while (mDelayedEventQueue != null) {
    852                 MotionEventInfo info = mDelayedEventQueue;
    853                 mDelayedEventQueue = info.mNext;
    854 
    855                 handleEventWith(mDelegatingState, info.event, info.rawEvent, info.policyFlags);
    856 
    857                 info.recycle();
    858             }
    859         }
    860 
    861         private void clearDelayedMotionEvents() {
    862             while (mDelayedEventQueue != null) {
    863                 MotionEventInfo info = mDelayedEventQueue;
    864                 mDelayedEventQueue = info.mNext;
    865                 info.recycle();
    866             }
    867             mPreLastDown = null;
    868             mPreLastUp = null;
    869             mLastDown = null;
    870             mLastUp = null;
    871         }
    872 
    873         void transitionToDelegatingStateAndClear() {
    874             transitionTo(mDelegatingState);
    875             sendDelayedMotionEvents();
    876             removePendingDelayedMessages();
    877         }
    878 
    879         private void onTripleTap(MotionEvent up) {
    880 
    881             if (DEBUG_DETECTING) {
    882                 Slog.i(LOG_TAG, "onTripleTap(); delayed: "
    883                         + MotionEventInfo.toString(mDelayedEventQueue));
    884             }
    885             clear();
    886 
    887             // Toggle zoom
    888             if (mMagnificationController.isMagnifying()) {
    889                 zoomOff();
    890             } else {
    891                 zoomOn(up.getX(), up.getY());
    892             }
    893         }
    894 
    895         void transitionToViewportDraggingStateAndClear(MotionEvent down) {
    896 
    897             if (DEBUG_DETECTING) Slog.i(LOG_TAG, "onTripleTapAndHold()");
    898             clear();
    899 
    900             mViewportDraggingState.mZoomedInBeforeDrag =
    901                     mMagnificationController.isMagnifying();
    902 
    903             zoomOn(down.getX(), down.getY());
    904 
    905             transitionTo(mViewportDraggingState);
    906         }
    907 
    908         @Override
    909         public String toString() {
    910             return "DetectingState{" +
    911                     "tapCount()=" + tapCount() +
    912                     ", mShortcutTriggered=" + mShortcutTriggered +
    913                     ", mDelayedEventQueue=" + MotionEventInfo.toString(mDelayedEventQueue) +
    914                     '}';
    915         }
    916 
    917         void toggleShortcutTriggered() {
    918             setShortcutTriggered(!mShortcutTriggered);
    919         }
    920 
    921         void setShortcutTriggered(boolean state) {
    922             if (mShortcutTriggered == state) {
    923                 return;
    924             }
    925             if (DEBUG_DETECTING) Slog.i(LOG_TAG, "setShortcutTriggered(" + state + ")");
    926 
    927             mShortcutTriggered = state;
    928             mMagnificationController.setForceShowMagnifiableBounds(state);
    929         }
    930     }
    931 
    932     private void zoomOn(float centerX, float centerY) {
    933         if (DEBUG_DETECTING) Slog.i(LOG_TAG, "zoomOn(" + centerX + ", " + centerY + ")");
    934 
    935         final float scale = MathUtils.constrain(
    936                 mMagnificationController.getPersistedScale(),
    937                 MIN_SCALE, MAX_SCALE);
    938         mMagnificationController.setScaleAndCenter(
    939                 scale, centerX, centerY,
    940                 /* animate */ true,
    941                 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
    942     }
    943 
    944     private void zoomOff() {
    945         if (DEBUG_DETECTING) Slog.i(LOG_TAG, "zoomOff()");
    946 
    947         mMagnificationController.reset(/* animate */ true);
    948     }
    949 
    950     private static MotionEvent recycleAndNullify(@Nullable MotionEvent event) {
    951         if (event != null) {
    952             event.recycle();
    953         }
    954         return null;
    955     }
    956 
    957     @Override
    958     public String toString() {
    959         return "MagnificationGesture{" +
    960                 "mDetectingState=" + mDetectingState +
    961                 ", mDelegatingState=" + mDelegatingState +
    962                 ", mMagnifiedInteractionState=" + mPanningScalingState +
    963                 ", mViewportDraggingState=" + mViewportDraggingState +
    964                 ", mDetectTripleTap=" + mDetectTripleTap +
    965                 ", mDetectShortcutTrigger=" + mDetectShortcutTrigger +
    966                 ", mCurrentState=" + State.nameOf(mCurrentState) +
    967                 ", mPreviousState=" + State.nameOf(mPreviousState) +
    968                 ", mMagnificationController=" + mMagnificationController +
    969                 '}';
    970     }
    971 
    972     private static final class MotionEventInfo {
    973 
    974         private static final int MAX_POOL_SIZE = 10;
    975         private static final Object sLock = new Object();
    976         private static MotionEventInfo sPool;
    977         private static int sPoolSize;
    978 
    979         private MotionEventInfo mNext;
    980         private boolean mInPool;
    981 
    982         public MotionEvent event;
    983         public MotionEvent rawEvent;
    984         public int policyFlags;
    985 
    986         public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent,
    987                 int policyFlags) {
    988             synchronized (sLock) {
    989                 MotionEventInfo info = obtainInternal();
    990                 info.initialize(event, rawEvent, policyFlags);
    991                 return info;
    992             }
    993         }
    994 
    995         @NonNull
    996         private static MotionEventInfo obtainInternal() {
    997             MotionEventInfo info;
    998             if (sPoolSize > 0) {
    999                 sPoolSize--;
   1000                 info = sPool;
   1001                 sPool = info.mNext;
   1002                 info.mNext = null;
   1003                 info.mInPool = false;
   1004             } else {
   1005                 info = new MotionEventInfo();
   1006             }
   1007             return info;
   1008         }
   1009 
   1010         private void initialize(MotionEvent event, MotionEvent rawEvent,
   1011                 int policyFlags) {
   1012             this.event = MotionEvent.obtain(event);
   1013             this.rawEvent = MotionEvent.obtain(rawEvent);
   1014             this.policyFlags = policyFlags;
   1015         }
   1016 
   1017         public void recycle() {
   1018             synchronized (sLock) {
   1019                 if (mInPool) {
   1020                     throw new IllegalStateException("Already recycled.");
   1021                 }
   1022                 clear();
   1023                 if (sPoolSize < MAX_POOL_SIZE) {
   1024                     sPoolSize++;
   1025                     mNext = sPool;
   1026                     sPool = this;
   1027                     mInPool = true;
   1028                 }
   1029             }
   1030         }
   1031 
   1032         private void clear() {
   1033             event = recycleAndNullify(event);
   1034             rawEvent = recycleAndNullify(rawEvent);
   1035             policyFlags = 0;
   1036         }
   1037 
   1038         static int countOf(MotionEventInfo info, int eventType) {
   1039             if (info == null) return 0;
   1040             return (info.event.getAction() == eventType ? 1 : 0)
   1041                     + countOf(info.mNext, eventType);
   1042         }
   1043 
   1044         public static String toString(MotionEventInfo info) {
   1045             return info == null
   1046                     ? ""
   1047                     : MotionEvent.actionToString(info.event.getAction()).replace("ACTION_", "")
   1048                             + " " + MotionEventInfo.toString(info.mNext);
   1049         }
   1050     }
   1051 
   1052     /**
   1053      * BroadcastReceiver used to cancel the magnification shortcut when the screen turns off
   1054      */
   1055     private static class ScreenStateReceiver extends BroadcastReceiver {
   1056         private final Context mContext;
   1057         private final MagnificationGestureHandler mGestureHandler;
   1058 
   1059         public ScreenStateReceiver(Context context, MagnificationGestureHandler gestureHandler) {
   1060             mContext = context;
   1061             mGestureHandler = gestureHandler;
   1062         }
   1063 
   1064         public void register() {
   1065             mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
   1066         }
   1067 
   1068         public void unregister() {
   1069             mContext.unregisterReceiver(this);
   1070         }
   1071 
   1072         @Override
   1073         public void onReceive(Context context, Intent intent) {
   1074             mGestureHandler.mDetectingState.setShortcutTriggered(false);
   1075         }
   1076     }
   1077 }
   1078