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