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.magnification;
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.internal.accessibility.util.AccessibilityStatsLogUtils.logMagnificationTripleTap;
28 import static com.android.server.accessibility.gestures.GestureUtils.distance;
29 import static com.android.server.accessibility.gestures.GestureUtils.distanceClosestPointerToPoint;
30 
31 import static java.lang.Math.abs;
32 import static java.util.Arrays.asList;
33 import static java.util.Arrays.copyOfRange;
34 
35 import android.accessibilityservice.MagnificationConfig;
36 import android.annotation.NonNull;
37 import android.annotation.Nullable;
38 import android.annotation.UiContext;
39 import android.content.BroadcastReceiver;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.IntentFilter;
43 import android.graphics.PointF;
44 import android.graphics.Region;
45 import android.os.Handler;
46 import android.os.Looper;
47 import android.os.Message;
48 import android.os.SystemClock;
49 import android.os.VibrationEffect;
50 import android.os.Vibrator;
51 import android.provider.Settings;
52 import android.util.Log;
53 import android.util.MathUtils;
54 import android.util.Slog;
55 import android.util.TypedValue;
56 import android.view.GestureDetector;
57 import android.view.GestureDetector.SimpleOnGestureListener;
58 import android.view.MotionEvent;
59 import android.view.MotionEvent.PointerCoords;
60 import android.view.MotionEvent.PointerProperties;
61 import android.view.ScaleGestureDetector;
62 import android.view.ScaleGestureDetector.OnScaleGestureListener;
63 import android.view.ViewConfiguration;
64 
65 import com.android.internal.R;
66 import com.android.internal.annotations.VisibleForTesting;
67 import com.android.server.accessibility.AccessibilityManagerService;
68 import com.android.server.accessibility.AccessibilityTraceManager;
69 import com.android.server.accessibility.gestures.GestureUtils;
70 
71 /**
72  * This class handles full screen magnification in response to touch events.
73  *
74  * The behavior is as follows:
75  *
76  * 1. Triple tap toggles permanent screen magnification which is magnifying
77  *    the area around the location of the triple tap. One can think of the
78  *    location of the triple tap as the center of the magnified viewport.
79  *    For example, a triple tap when not magnified would magnify the screen
80  *    and leave it in a magnified state. A triple tapping when magnified would
81  *    clear magnification and leave the screen in a not magnified state.
82  *
83  * 2. Triple tap and hold would magnify the screen if not magnified and enable
84  *    viewport dragging mode until the finger goes up. One can think of this
85  *    mode as a way to move the magnified viewport since the area around the
86  *    moving finger will be magnified to fit the screen. For example, if the
87  *    screen was not magnified and the user triple taps and holds the screen
88  *    would magnify and the viewport will follow the user's finger. When the
89  *    finger goes up the screen will zoom out. If the same user interaction
90  *    is performed when the screen is magnified, the viewport movement will
91  *    be the same but when the finger goes up the screen will stay magnified.
92  *    In other words, the initial magnified state is sticky.
93  *
94  * 3. Magnification can optionally be "triggered" by some external shortcut
95  *    affordance. When this occurs via {@link #notifyShortcutTriggered()} a
96  *    subsequent tap in a magnifiable region will engage permanent screen
97  *    magnification as described in #1. Alternatively, a subsequent long-press
98  *    or drag will engage magnification with viewport dragging as described in
99  *    #2. Once magnified, all following behaviors apply whether magnification
100  *    was engaged via a triple-tap or by a triggered shortcut.
101  *
102  * 4. Pinching with any number of additional fingers when viewport dragging
103  *    is enabled, i.e. the user triple tapped and holds, would adjust the
104  *    magnification scale which will become the current default magnification
105  *    scale. The next time the user magnifies the same magnification scale
106  *    would be used.
107  *
108  * 5. When in a permanent magnified state the user can use two or more fingers
109  *    to pan the viewport. Note that in this mode the content is panned as
110  *    opposed to the viewport dragging mode in which the viewport is moved.
111  *
112  * 6. When in a permanent magnified state the user can use two or more
113  *    fingers to change the magnification scale which will become the current
114  *    default magnification scale. The next time the user magnifies the same
115  *    magnification scale would be used.
116  *
117  * 7. The magnification scale will be persisted in settings and in the cloud.
118  */
119 @SuppressWarnings("WeakerAccess")
120 public class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler {
121 
122     private static final boolean DEBUG_STATE_TRANSITIONS = false | DEBUG_ALL;
123     private static final boolean DEBUG_DETECTING = false | DEBUG_ALL;
124     private static final boolean DEBUG_PANNING_SCALING = false | DEBUG_ALL;
125 
126     // The MIN_SCALE is different from MagnificationScaleProvider.MIN_SCALE due
127     // to AccessibilityService.MagnificationController#setScale() has
128     // different scale range
129     private static final float MIN_SCALE = 1.0f;
130     private static final float MAX_SCALE = MagnificationScaleProvider.MAX_SCALE;
131 
132     @VisibleForTesting final FullScreenMagnificationController mFullScreenMagnificationController;
133 
134     private final FullScreenMagnificationController.MagnificationInfoChangedCallback
135             mMagnificationInfoChangedCallback;
136     @VisibleForTesting final DelegatingState mDelegatingState;
137     @VisibleForTesting final DetectingState mDetectingState;
138     @VisibleForTesting final PanningScalingState mPanningScalingState;
139     @VisibleForTesting final ViewportDraggingState mViewportDraggingState;
140 
141     private final ScreenStateReceiver mScreenStateReceiver;
142     private final WindowMagnificationPromptController mPromptController;
143 
144     @VisibleForTesting State mCurrentState;
145     @VisibleForTesting State mPreviousState;
146 
147     private PointerCoords[] mTempPointerCoords;
148     private PointerProperties[] mTempPointerProperties;
149 
FullScreenMagnificationGestureHandler(@iContext Context context, FullScreenMagnificationController fullScreenMagnificationController, AccessibilityTraceManager trace, Callback callback, boolean detectTripleTap, boolean detectShortcutTrigger, @NonNull WindowMagnificationPromptController promptController, int displayId)150     public FullScreenMagnificationGestureHandler(@UiContext Context context,
151             FullScreenMagnificationController fullScreenMagnificationController,
152             AccessibilityTraceManager trace,
153             Callback callback,
154             boolean detectTripleTap,
155             boolean detectShortcutTrigger,
156             @NonNull WindowMagnificationPromptController promptController,
157             int displayId) {
158         super(displayId, detectTripleTap, detectShortcutTrigger, trace, callback);
159         if (DEBUG_ALL) {
160             Log.i(mLogTag,
161                     "FullScreenMagnificationGestureHandler(detectTripleTap = " + detectTripleTap
162                             + ", detectShortcutTrigger = " + detectShortcutTrigger + ")");
163         }
164         mFullScreenMagnificationController = fullScreenMagnificationController;
165         mMagnificationInfoChangedCallback =
166                 new FullScreenMagnificationController.MagnificationInfoChangedCallback() {
167                     @Override
168                     public void onRequestMagnificationSpec(int displayId, int serviceId) {
169                         return;
170                     }
171 
172                     @Override
173                     public void onFullScreenMagnificationActivationState(int displayId,
174                             boolean activated) {
175                         if (displayId != mDisplayId) {
176                             return;
177                         }
178 
179                         if (!activated) {
180                             // cancel the magnification shortcut
181                             mDetectingState.setShortcutTriggered(false);
182                         }
183                     }
184 
185                     @Override
186                     public void onImeWindowVisibilityChanged(int displayId, boolean shown) {
187                         return;
188                     }
189 
190                     @Override
191                     public void onFullScreenMagnificationChanged(int displayId,
192                             @NonNull Region region,
193                             @NonNull MagnificationConfig config) {
194                         return;
195                     }
196                 };
197         mFullScreenMagnificationController.addInfoChangedCallback(
198                 mMagnificationInfoChangedCallback);
199 
200         mPromptController = promptController;
201 
202         mDelegatingState = new DelegatingState();
203         mDetectingState = new DetectingState(context);
204         mViewportDraggingState = new ViewportDraggingState();
205         mPanningScalingState = new PanningScalingState(context);
206 
207         if (mDetectShortcutTrigger) {
208             mScreenStateReceiver = new ScreenStateReceiver(context, this);
209             mScreenStateReceiver.register();
210         } else {
211             mScreenStateReceiver = null;
212         }
213 
214         transitionTo(mDetectingState);
215     }
216 
217     @Override
onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags)218     void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
219         handleEventWith(mCurrentState, event, rawEvent, policyFlags);
220     }
221 
handleEventWith(State stateHandler, MotionEvent event, MotionEvent rawEvent, int policyFlags)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         try {
229             stateHandler.onMotionEvent(event, rawEvent, policyFlags);
230         } catch (GestureException e) {
231             Slog.e(mLogTag, "Error processing motion event", e);
232             clearAndTransitionToStateDetecting();
233         }
234     }
235 
236     @Override
clearEvents(int inputSource)237     public void clearEvents(int inputSource) {
238         if (inputSource == SOURCE_TOUCHSCREEN) {
239             clearAndTransitionToStateDetecting();
240         }
241 
242         super.clearEvents(inputSource);
243     }
244 
245     @Override
onDestroy()246     public void onDestroy() {
247         if (DEBUG_STATE_TRANSITIONS) {
248             Slog.i(mLogTag, "onDestroy(); delayed = "
249                     + MotionEventInfo.toString(mDetectingState.mDelayedEventQueue));
250         }
251 
252         if (mScreenStateReceiver != null) {
253             mScreenStateReceiver.unregister();
254         }
255         mPromptController.onDestroy();
256         // Check if need to reset when MagnificationGestureHandler is the last magnifying service.
257         mFullScreenMagnificationController.resetIfNeeded(
258                 mDisplayId, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
259         mFullScreenMagnificationController.removeInfoChangedCallback(
260                 mMagnificationInfoChangedCallback);
261         clearAndTransitionToStateDetecting();
262     }
263 
264     @Override
handleShortcutTriggered()265     public void handleShortcutTriggered() {
266         final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId);
267 
268         if (isActivated) {
269             zoomOff();
270             clearAndTransitionToStateDetecting();
271         } else {
272             mDetectingState.toggleShortcutTriggered();
273         }
274 
275         if (mDetectingState.isShortcutTriggered()) {
276             mPromptController.showNotificationIfNeeded();
277             zoomToScale(1.0f, Float.NaN, Float.NaN);
278         }
279     }
280 
281     @Override
getMode()282     public int getMode() {
283         return Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
284     }
285 
clearAndTransitionToStateDetecting()286     void clearAndTransitionToStateDetecting() {
287         mCurrentState = mDetectingState;
288         mDetectingState.clear();
289         mViewportDraggingState.clear();
290         mPanningScalingState.clear();
291     }
292 
getTempPointerCoordsWithMinSize(int size)293     private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
294         final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
295         if (oldSize < size) {
296             PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
297             mTempPointerCoords = new PointerCoords[size];
298             if (oldTempPointerCoords != null) {
299                 System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
300             }
301         }
302         for (int i = oldSize; i < size; i++) {
303             mTempPointerCoords[i] = new PointerCoords();
304         }
305         return mTempPointerCoords;
306     }
307 
getTempPointerPropertiesWithMinSize(int size)308     private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
309         final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length
310                 : 0;
311         if (oldSize < size) {
312             PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
313             mTempPointerProperties = new PointerProperties[size];
314             if (oldTempPointerProperties != null) {
315                 System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0,
316                         oldSize);
317             }
318         }
319         for (int i = oldSize; i < size; i++) {
320             mTempPointerProperties[i] = new PointerProperties();
321         }
322         return mTempPointerProperties;
323     }
324 
325     @VisibleForTesting
transitionTo(State state)326     void transitionTo(State state) {
327         if (DEBUG_STATE_TRANSITIONS) {
328             Slog.i(mLogTag,
329                     (State.nameOf(mCurrentState) + " -> " + State.nameOf(state)
330                     + " at " + asList(copyOfRange(new RuntimeException().getStackTrace(), 1, 5)))
331                     .replace(getClass().getName(), ""));
332         }
333         mPreviousState = mCurrentState;
334         if (state == mPanningScalingState) {
335             mPanningScalingState.prepareForState();
336         }
337         mCurrentState = state;
338     }
339 
340     interface State {
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)341         void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)
342                 throws GestureException;
343 
clear()344         default void clear() {}
345 
name()346         default String name() {
347             return getClass().getSimpleName();
348         }
349 
nameOf(@ullable State s)350         static String nameOf(@Nullable State s) {
351             return s != null ? s.name() : "null";
352         }
353     }
354 
355     /**
356      * This class determines if the user is performing a scale or pan gesture.
357      *
358      * Unlike when {@link ViewportDraggingState dragging the viewport}, in panning mode the viewport
359      * moves in the same direction as the fingers, and allows to easily and precisely scale the
360      * magnification level.
361      * This makes it the preferred mode for one-off adjustments, due to its precision and ease of
362      * triggering.
363      */
364     final class PanningScalingState extends SimpleOnGestureListener
365             implements OnScaleGestureListener, State {
366 
367         private final Context mContext;
368         private final ScaleGestureDetector mScaleGestureDetector;
369         private final GestureDetector mScrollGestureDetector;
370         final float mScalingThreshold;
371 
372         float mInitialScaleFactor = -1;
373         @VisibleForTesting boolean mScaling;
374 
375         /**
376          * Whether it needs to detect the target scale passes
377          * {@link FullScreenMagnificationController#getPersistedScale} during panning scale.
378          */
379         @VisibleForTesting boolean mDetectingPassPersistedScale;
380 
381         // The threshold for relative difference from given scale to persisted scale. If the
382         // difference >= threshold, we can start detecting if the scale passes the persisted
383         // scale during panning.
384         @VisibleForTesting static final float CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD = 0.2f;
385         // The threshold for relative difference from given scale to persisted scale. If the
386         // difference < threshold, we can decide that the scale passes the persisted scale.
387         @VisibleForTesting static final float PASSING_PERSISTED_SCALE_THRESHOLD = 0.01f;
388 
PanningScalingState(Context context)389         PanningScalingState(Context context) {
390             final TypedValue scaleValue = new TypedValue();
391             context.getResources().getValue(
392                     R.dimen.config_screen_magnification_scaling_threshold,
393                     scaleValue, false);
394             mContext = context;
395             mScalingThreshold = scaleValue.getFloat();
396             mScaleGestureDetector = new ScaleGestureDetector(context, this, Handler.getMain());
397             mScaleGestureDetector.setQuickScaleEnabled(false);
398             mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
399         }
400 
401         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)402         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
403             int action = event.getActionMasked();
404 
405             if (action == ACTION_POINTER_UP
406                     && event.getPointerCount() == 2 // includes the pointer currently being released
407                     && mPreviousState == mViewportDraggingState) {
408 
409                 persistScaleAndTransitionTo(mViewportDraggingState);
410 
411             } else if (action == ACTION_UP || action == ACTION_CANCEL) {
412 
413                 persistScaleAndTransitionTo(mDetectingState);
414             }
415         }
416 
417 
prepareForState()418         void prepareForState() {
419             checkShouldDetectPassPersistedScale();
420         }
421 
checkShouldDetectPassPersistedScale()422         private void checkShouldDetectPassPersistedScale() {
423             if (mDetectingPassPersistedScale) {
424                 return;
425             }
426 
427             final float currentScale =
428                     mFullScreenMagnificationController.getScale(mDisplayId);
429             final float persistedScale =
430                     mFullScreenMagnificationController.getPersistedScale(mDisplayId);
431 
432             mDetectingPassPersistedScale =
433                     (abs(currentScale - persistedScale) / persistedScale)
434                             >= CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD;
435         }
436 
persistScaleAndTransitionTo(State state)437         public void persistScaleAndTransitionTo(State state) {
438             mFullScreenMagnificationController.persistScale(mDisplayId);
439             clear();
440             transitionTo(state);
441         }
442 
443         @VisibleForTesting
setScaleAndClearIfNeeded(float scale, float pivotX, float pivotY)444         void setScaleAndClearIfNeeded(float scale, float pivotX, float pivotY) {
445             if (mDetectingPassPersistedScale) {
446                 final float persistedScale =
447                         mFullScreenMagnificationController.getPersistedScale(mDisplayId);
448                 // If the scale passes the persisted scale during panning, perform a vibration
449                 // feedback to user. Also, call {@link clear} to create a buffer zone so that
450                 // user needs to panning more than {@link mScalingThreshold} to change scale again.
451                 if (abs(scale - persistedScale) / persistedScale
452                         < PASSING_PERSISTED_SCALE_THRESHOLD) {
453                     scale = persistedScale;
454                     final Vibrator vibrator = mContext.getSystemService(Vibrator.class);
455                     if (vibrator != null) {
456                         vibrator.vibrate(
457                                 VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK));
458                     }
459                     clear();
460                 }
461             }
462 
463             if (DEBUG_PANNING_SCALING) Slog.i(mLogTag, "Scaled content to: " + scale + "x");
464             mFullScreenMagnificationController.setScale(mDisplayId, scale, pivotX, pivotY, false,
465                     AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
466 
467             checkShouldDetectPassPersistedScale();
468         }
469 
470         @Override
onScroll(MotionEvent first, MotionEvent second, float distanceX, float distanceY)471         public boolean onScroll(MotionEvent first, MotionEvent second,
472                 float distanceX, float distanceY) {
473             if (mCurrentState != mPanningScalingState) {
474                 return true;
475             }
476             if (DEBUG_PANNING_SCALING) {
477                 Slog.i(mLogTag, "Panned content by scrollX: " + distanceX
478                         + " scrollY: " + distanceY);
479             }
480             mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX,
481                     distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
482             return /* event consumed: */ true;
483         }
484 
485         @Override
onScale(ScaleGestureDetector detector)486         public boolean onScale(ScaleGestureDetector detector) {
487             if (!mScaling) {
488                 if (mInitialScaleFactor < 0) {
489                     mInitialScaleFactor = detector.getScaleFactor();
490                     return false;
491                 }
492                 final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
493                 mScaling = abs(deltaScale) > mScalingThreshold;
494                 return mScaling;
495             }
496             final float initialScale = mFullScreenMagnificationController.getScale(mDisplayId);
497             final float targetScale = initialScale * detector.getScaleFactor();
498 
499             // Don't allow a gesture to move the user further outside the
500             // desired bounds for gesture-controlled scaling.
501             final float scale;
502             if (targetScale > MAX_SCALE && targetScale > initialScale) {
503                 // The target scale is too big and getting bigger.
504                 scale = MAX_SCALE;
505             } else if (targetScale < MIN_SCALE && targetScale < initialScale) {
506                 // The target scale is too small and getting smaller.
507                 scale = MIN_SCALE;
508             } else {
509                 // The target scale may be outside our bounds, but at least
510                 // it's moving in the right direction. This avoids a "jump" if
511                 // we're at odds with some other service's desired bounds.
512                 scale = targetScale;
513             }
514 
515             setScaleAndClearIfNeeded(scale, detector.getFocusX(), detector.getFocusY());
516             return /* handled: */ true;
517         }
518 
519         @Override
onScaleBegin(ScaleGestureDetector detector)520         public boolean onScaleBegin(ScaleGestureDetector detector) {
521             return /* continue recognizing: */ (mCurrentState == mPanningScalingState);
522         }
523 
524         @Override
onScaleEnd(ScaleGestureDetector detector)525         public void onScaleEnd(ScaleGestureDetector detector) {
526             clear();
527         }
528 
529         @Override
clear()530         public void clear() {
531             mInitialScaleFactor = -1;
532             mScaling = false;
533             mDetectingPassPersistedScale = false;
534         }
535 
536         @Override
toString()537         public String toString() {
538             return "PanningScalingState{" + "mInitialScaleFactor=" + mInitialScaleFactor
539                     + ", mScaling=" + mScaling
540                     + '}';
541         }
542     }
543 
544     /**
545      * This class handles motion events when the event dispatcher has
546      * determined that the user is performing a single-finger drag of the
547      * magnification viewport.
548      *
549      * Unlike when {@link PanningScalingState panning}, the viewport moves in the opposite direction
550      * of the finger, and any part of the screen is reachable without lifting the finger.
551      * This makes it the preferable mode for tasks like reading text spanning full screen width.
552      */
553     final class ViewportDraggingState implements State {
554 
555         /**
556          * The cached scale for recovering after dragging ends.
557          * If the scale >= 1.0, the magnifier needs to recover to scale.
558          * Otherwise, the magnifier should be disabled.
559          */
560         @VisibleForTesting float mScaleToRecoverAfterDraggingEnd = Float.NaN;
561 
562         private boolean mLastMoveOutsideMagnifiedRegion;
563 
564         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)565         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)
566                 throws GestureException {
567             final int action = event.getActionMasked();
568             switch (action) {
569                 case ACTION_POINTER_DOWN: {
570                     clearAndTransitToPanningScalingState();
571                 }
572                 break;
573                 case ACTION_MOVE: {
574                     if (event.getPointerCount() != 1) {
575                         throw new GestureException("Should have one pointer down.");
576                     }
577                     final float eventX = event.getX();
578                     final float eventY = event.getY();
579                     if (mFullScreenMagnificationController.magnificationRegionContains(
580                             mDisplayId, eventX, eventY)) {
581                         mFullScreenMagnificationController.setCenter(mDisplayId, eventX, eventY,
582                                 /* animate */ mLastMoveOutsideMagnifiedRegion,
583                                 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
584                         mLastMoveOutsideMagnifiedRegion = false;
585                     } else {
586                         mLastMoveOutsideMagnifiedRegion = true;
587                     }
588                 }
589                 break;
590 
591                 case ACTION_UP:
592                 case ACTION_CANCEL: {
593                     // If mScaleToRecoverAfterDraggingEnd >= 1.0, the dragging state is triggered
594                     // by zoom in temporary, and the magnifier needs to recover to original scale
595                     // after exiting dragging state.
596                     // Otherwise, the magnifier should be disabled.
597                     if (mScaleToRecoverAfterDraggingEnd >= 1.0f) {
598                         zoomToScale(mScaleToRecoverAfterDraggingEnd, event.getX(),
599                                 event.getY());
600                     } else {
601                         zoomOff();
602                     }
603                     clear();
604                     mScaleToRecoverAfterDraggingEnd = Float.NaN;
605                     transitionTo(mDetectingState);
606                 }
607                     break;
608 
609                 case ACTION_DOWN:
610                 case ACTION_POINTER_UP: {
611                     throw new GestureException(
612                             "Unexpected event type: " + MotionEvent.actionToString(action));
613                 }
614             }
615         }
616 
isAlwaysOnMagnificationEnabled()617         private boolean isAlwaysOnMagnificationEnabled() {
618             return mFullScreenMagnificationController.isAlwaysOnMagnificationEnabled();
619         }
620 
prepareForZoomInTemporary(boolean shortcutTriggered)621         public void prepareForZoomInTemporary(boolean shortcutTriggered) {
622             boolean shouldRecoverAfterDraggingEnd;
623             if (mFullScreenMagnificationController.isActivated(mDisplayId)) {
624                 // For b/267210808, if always-on feature is not enabled, we keep the expected
625                 // behavior. If users tap shortcut and then tap-and-hold to zoom in temporary,
626                 // the magnifier should be disabled after release.
627                 // If always-on feature is enabled, in the same scenario the magnifier would
628                 // zoom to 1.0 and keep activated.
629                 if (shortcutTriggered) {
630                     shouldRecoverAfterDraggingEnd = isAlwaysOnMagnificationEnabled();
631                 } else {
632                     shouldRecoverAfterDraggingEnd = true;
633                 }
634             } else {
635                 shouldRecoverAfterDraggingEnd = false;
636             }
637 
638             mScaleToRecoverAfterDraggingEnd = shouldRecoverAfterDraggingEnd
639                     ? mFullScreenMagnificationController.getScale(mDisplayId) : Float.NaN;
640         }
641 
clearAndTransitToPanningScalingState()642         private void clearAndTransitToPanningScalingState() {
643             final float scaleToRecovery = mScaleToRecoverAfterDraggingEnd;
644             clear();
645             mScaleToRecoverAfterDraggingEnd = scaleToRecovery;
646             transitionTo(mPanningScalingState);
647         }
648 
649         @Override
clear()650         public void clear() {
651             mLastMoveOutsideMagnifiedRegion = false;
652 
653             mScaleToRecoverAfterDraggingEnd = Float.NaN;
654         }
655 
656         @Override
toString()657         public String toString() {
658             return "ViewportDraggingState{"
659                     + "mScaleToRecoverAfterDraggingEnd=" + mScaleToRecoverAfterDraggingEnd
660                     + ", mLastMoveOutsideMagnifiedRegion=" + mLastMoveOutsideMagnifiedRegion
661                     + '}';
662         }
663     }
664 
665     final class DelegatingState implements State {
666         /**
667          * Time of last {@link MotionEvent#ACTION_DOWN} while in {@link DelegatingState}
668          */
669         public long mLastDelegatedDownEventTime;
670 
671         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)672         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
673 
674             // Ensures that the state at the end of delegation is consistent with the last delegated
675             // UP/DOWN event in queue: still delegating if pointer is down, detecting otherwise
676             switch (event.getActionMasked()) {
677                 case ACTION_UP:
678                 case ACTION_CANCEL: {
679                     transitionTo(mDetectingState);
680                 }
681                     break;
682 
683                 case ACTION_DOWN: {
684                     transitionTo(mDelegatingState);
685                     mLastDelegatedDownEventTime = event.getDownTime();
686                 } break;
687             }
688 
689             if (getNext() != null) {
690                 // We cache some events to see if the user wants to trigger magnification.
691                 // If no magnification is triggered we inject these events with adjusted
692                 // time and down time to prevent subsequent transformations being confused
693                 // by stale events. After the cached events, which always have a down, are
694                 // injected we need to also update the down time of all subsequent non cached
695                 // events. All delegated events cached and non-cached are delivered here.
696                 event.setDownTime(mLastDelegatedDownEventTime);
697                 dispatchTransformedEvent(event, rawEvent, policyFlags);
698             }
699         }
700     }
701 
702     /**
703      * This class handles motion events when the event dispatch has not yet
704      * determined what the user is doing. It watches for various tap events.
705      */
706     final class DetectingState implements State, Handler.Callback {
707 
708         private static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1;
709         private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
710         private static final int MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE = 3;
711 
712         final int mLongTapMinDelay;
713         final int mSwipeMinDistance;
714         final int mMultiTapMaxDelay;
715         final int mMultiTapMaxDistance;
716 
717         private MotionEventInfo mDelayedEventQueue;
718         MotionEvent mLastDown;
719         private MotionEvent mPreLastDown;
720         private MotionEvent mLastUp;
721         private MotionEvent mPreLastUp;
722         private PointF mSecondPointerDownLocation = new PointF(Float.NaN, Float.NaN);
723 
724         private long mLastDetectingDownEventTime;
725 
726         @VisibleForTesting boolean mShortcutTriggered;
727 
728         @VisibleForTesting Handler mHandler = new Handler(Looper.getMainLooper(), this);
729 
DetectingState(Context context)730         DetectingState(Context context) {
731             mLongTapMinDelay = ViewConfiguration.getLongPressTimeout();
732             mMultiTapMaxDelay = ViewConfiguration.getDoubleTapTimeout()
733                     + context.getResources().getInteger(
734                     R.integer.config_screen_magnification_multi_tap_adjustment);
735             mSwipeMinDistance = ViewConfiguration.get(context).getScaledTouchSlop();
736             mMultiTapMaxDistance = ViewConfiguration.get(context).getScaledDoubleTapSlop();
737         }
738 
739         @Override
handleMessage(Message message)740         public boolean handleMessage(Message message) {
741             final int type = message.what;
742             switch (type) {
743                 case MESSAGE_ON_TRIPLE_TAP_AND_HOLD: {
744                     MotionEvent down = (MotionEvent) message.obj;
745                     transitionToViewportDraggingStateAndClear(down);
746                     down.recycle();
747                 }
748                 break;
749                 case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
750                     transitionToDelegatingStateAndClear();
751                 }
752                 break;
753                 case MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE: {
754                     transitToPanningScalingStateAndClear();
755                 }
756                 break;
757                 default: {
758                     throw new IllegalArgumentException("Unknown message type: " + type);
759                 }
760             }
761             return true;
762         }
763 
764         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)765         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
766             cacheDelayedMotionEvent(event, rawEvent, policyFlags);
767             switch (event.getActionMasked()) {
768                 case MotionEvent.ACTION_DOWN: {
769 
770                     mLastDetectingDownEventTime = event.getDownTime();
771                     mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
772 
773                     if (!mFullScreenMagnificationController.magnificationRegionContains(
774                             mDisplayId, event.getX(), event.getY())) {
775 
776                         transitionToDelegatingStateAndClear();
777 
778                     } else if (isMultiTapTriggered(2 /* taps */)) {
779 
780                         // 3tap and hold
781                         afterLongTapTimeoutTransitionToDraggingState(event);
782 
783                     } else if (isTapOutOfDistanceSlop()) {
784 
785                         transitionToDelegatingStateAndClear();
786 
787                     } else if (mDetectTripleTap
788                             // If activated, delay an ACTION_DOWN for mMultiTapMaxDelay
789                             // to ensure reachability of
790                             // STATE_PANNING_SCALING(triggerable with ACTION_POINTER_DOWN)
791                             || isActivated()) {
792 
793                         afterMultiTapTimeoutTransitionToDelegatingState();
794 
795                     } else {
796 
797                         // Delegate pending events without delay
798                         transitionToDelegatingStateAndClear();
799                     }
800                 }
801                 break;
802                 case ACTION_POINTER_DOWN: {
803                     if (isActivated() && event.getPointerCount() == 2) {
804                         storeSecondPointerDownLocation(event);
805                         mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
806                                 ViewConfiguration.getTapTimeout());
807                     } else {
808                         transitionToDelegatingStateAndClear();
809                     }
810                 }
811                 break;
812                 case ACTION_POINTER_UP: {
813                     transitionToDelegatingStateAndClear();
814                 }
815                 break;
816                 case ACTION_MOVE: {
817                     if (isFingerDown()
818                             && distance(mLastDown, /* move */ event) > mSwipeMinDistance) {
819 
820                         // Swipe detected - transition immediately
821 
822                         // For convenience, viewport dragging takes precedence
823                         // over insta-delegating on 3tap&swipe
824                         // (which is a rare combo to be used aside from magnification)
825                         if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) {
826                             transitionToViewportDraggingStateAndClear(event);
827                         } else if (isActivated() && event.getPointerCount() == 2) {
828                             //Primary pointer is swiping, so transit to PanningScalingState
829                             transitToPanningScalingStateAndClear();
830                         } else {
831                             transitionToDelegatingStateAndClear();
832                         }
833                     } else if (isActivated() && secondPointerDownValid()
834                             && distanceClosestPointerToPoint(
835                             mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) {
836                         //Second pointer is swiping, so transit to PanningScalingState
837                         transitToPanningScalingStateAndClear();
838                     }
839                 }
840                 break;
841                 case ACTION_UP: {
842 
843                     mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
844 
845                     if (!mFullScreenMagnificationController.magnificationRegionContains(
846                             mDisplayId, event.getX(), event.getY())) {
847 
848                         transitionToDelegatingStateAndClear();
849 
850                     } else if (isMultiTapTriggered(3 /* taps */)) {
851 
852                         onTripleTap(/* up */ event);
853 
854                     } else if (
855                             // Possible to be false on: 3tap&drag -> scale -> PTR_UP -> UP
856                             isFingerDown()
857                             //TODO long tap should never happen here
858                             && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay)
859                                     || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))) {
860 
861                         transitionToDelegatingStateAndClear();
862 
863                     }
864                 }
865                 break;
866             }
867         }
868 
storeSecondPointerDownLocation(MotionEvent event)869         private void storeSecondPointerDownLocation(MotionEvent event) {
870             final int index = event.getActionIndex();
871             mSecondPointerDownLocation.set(event.getX(index), event.getY(index));
872         }
873 
secondPointerDownValid()874         private boolean secondPointerDownValid() {
875             return !(Float.isNaN(mSecondPointerDownLocation.x) && Float.isNaN(
876                     mSecondPointerDownLocation.y));
877         }
878 
transitToPanningScalingStateAndClear()879         private void transitToPanningScalingStateAndClear() {
880             transitionTo(mPanningScalingState);
881             clear();
882         }
883 
isMultiTapTriggered(int numTaps)884         public boolean isMultiTapTriggered(int numTaps) {
885 
886             // Shortcut acts as the 2 initial taps
887             if (mShortcutTriggered) return tapCount() + 2 >= numTaps;
888 
889             final boolean multitapTriggered = mDetectTripleTap
890                     && tapCount() >= numTaps
891                     && isMultiTap(mPreLastDown, mLastDown)
892                     && isMultiTap(mPreLastUp, mLastUp);
893 
894             // Only log the triple tap event, use numTaps to filter.
895             if (multitapTriggered && numTaps > 2) {
896                 final boolean enabled = isActivated();
897                 logMagnificationTripleTap(enabled);
898             }
899             return multitapTriggered;
900         }
901 
isMultiTap(MotionEvent first, MotionEvent second)902         private boolean isMultiTap(MotionEvent first, MotionEvent second) {
903             return GestureUtils.isMultiTap(first, second, mMultiTapMaxDelay, mMultiTapMaxDistance);
904         }
905 
isFingerDown()906         public boolean isFingerDown() {
907             return mLastDown != null;
908         }
909 
timeBetween(@ullable MotionEvent a, @Nullable MotionEvent b)910         private long timeBetween(@Nullable MotionEvent a, @Nullable MotionEvent b) {
911             if (a == null && b == null) return 0;
912             return abs(timeOf(a) - timeOf(b));
913         }
914 
915         /**
916          * Nullsafe {@link MotionEvent#getEventTime} that interprets null event as something that
917          * has happened long enough ago to be gone from the event queue.
918          * Thus the time for a null event is a small number, that is below any other non-null
919          * event's time.
920          *
921          * @return {@link MotionEvent#getEventTime}, or {@link Long#MIN_VALUE} if the event is null
922          */
timeOf(@ullable MotionEvent event)923         private long timeOf(@Nullable MotionEvent event) {
924             return event != null ? event.getEventTime() : Long.MIN_VALUE;
925         }
926 
tapCount()927         public int tapCount() {
928             return MotionEventInfo.countOf(mDelayedEventQueue, ACTION_UP);
929         }
930 
931         /** -> {@link DelegatingState} */
afterMultiTapTimeoutTransitionToDelegatingState()932         public void afterMultiTapTimeoutTransitionToDelegatingState() {
933             mHandler.sendEmptyMessageDelayed(
934                     MESSAGE_TRANSITION_TO_DELEGATING_STATE,
935                     mMultiTapMaxDelay);
936         }
937 
938         /** -> {@link ViewportDraggingState} */
afterLongTapTimeoutTransitionToDraggingState(MotionEvent event)939         public void afterLongTapTimeoutTransitionToDraggingState(MotionEvent event) {
940             mHandler.sendMessageDelayed(
941                     mHandler.obtainMessage(MESSAGE_ON_TRIPLE_TAP_AND_HOLD,
942                             MotionEvent.obtain(event)),
943                     ViewConfiguration.getLongPressTimeout());
944         }
945 
946         @Override
clear()947         public void clear() {
948             setShortcutTriggered(false);
949             removePendingDelayedMessages();
950             clearDelayedMotionEvents();
951             mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
952         }
953 
removePendingDelayedMessages()954         private void removePendingDelayedMessages() {
955             mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
956             mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
957             mHandler.removeMessages(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE);
958         }
959 
cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)960         private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
961                 int policyFlags) {
962             if (event.getActionMasked() == ACTION_DOWN) {
963                 mPreLastDown = mLastDown;
964                 mLastDown = MotionEvent.obtain(event);
965             } else if (event.getActionMasked() == ACTION_UP) {
966                 mPreLastUp = mLastUp;
967                 mLastUp = MotionEvent.obtain(event);
968             }
969 
970             MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent,
971                     policyFlags);
972             if (mDelayedEventQueue == null) {
973                 mDelayedEventQueue = info;
974             } else {
975                 MotionEventInfo tail = mDelayedEventQueue;
976                 while (tail.mNext != null) {
977                     tail = tail.mNext;
978                 }
979                 tail.mNext = info;
980             }
981         }
982 
sendDelayedMotionEvents()983         private void sendDelayedMotionEvents() {
984             if (mDelayedEventQueue == null) {
985                 return;
986             }
987 
988             // Adjust down time to prevent subsequent modules being misleading, and also limit
989             // the maximum offset to mMultiTapMaxDelay to prevent the down time of 2nd tap is
990             // in the future when multi-tap happens.
991             final long offset = Math.min(
992                     SystemClock.uptimeMillis() - mLastDetectingDownEventTime, mMultiTapMaxDelay);
993 
994             do {
995                 MotionEventInfo info = mDelayedEventQueue;
996                 mDelayedEventQueue = info.mNext;
997 
998                 info.event.setDownTime(info.event.getDownTime() + offset);
999                 handleEventWith(mDelegatingState, info.event, info.rawEvent, info.policyFlags);
1000 
1001                 info.recycle();
1002             } while (mDelayedEventQueue != null);
1003         }
1004 
clearDelayedMotionEvents()1005         private void clearDelayedMotionEvents() {
1006             while (mDelayedEventQueue != null) {
1007                 MotionEventInfo info = mDelayedEventQueue;
1008                 mDelayedEventQueue = info.mNext;
1009                 info.recycle();
1010             }
1011             mPreLastDown = null;
1012             mPreLastUp = null;
1013             mLastDown = null;
1014             mLastUp = null;
1015         }
1016 
transitionToDelegatingStateAndClear()1017         void transitionToDelegatingStateAndClear() {
1018             transitionTo(mDelegatingState);
1019             sendDelayedMotionEvents();
1020             removePendingDelayedMessages();
1021             mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
1022         }
1023 
1024         /**
1025          * This method could be triggered by both 2 cases.
1026          *      1. direct three tap gesture
1027          *      2. one tap while shortcut triggered (it counts as two taps).
1028          */
onTripleTap(MotionEvent up)1029         private void onTripleTap(MotionEvent up) {
1030             if (DEBUG_DETECTING) {
1031                 Slog.i(mLogTag, "onTripleTap(); delayed: "
1032                         + MotionEventInfo.toString(mDelayedEventQueue));
1033             }
1034 
1035             // We put mShortcutTriggered into conditions.
1036             // The reason is when the shortcut is triggered,
1037             //   the magnifier is activated and keeps in scale 1.0,
1038             //   and in this case, we still want to zoom on the magnifier.
1039             if (!isActivated() || mShortcutTriggered) {
1040                 mPromptController.showNotificationIfNeeded();
1041                 zoomOn(up.getX(), up.getY());
1042             } else {
1043                 zoomOff();
1044             }
1045 
1046             clear();
1047         }
1048 
isActivated()1049         private boolean isActivated() {
1050             return mFullScreenMagnificationController.isActivated(mDisplayId);
1051         }
1052 
transitionToViewportDraggingStateAndClear(MotionEvent down)1053         void transitionToViewportDraggingStateAndClear(MotionEvent down) {
1054 
1055             if (DEBUG_DETECTING) Slog.i(mLogTag, "onTripleTapAndHold()");
1056             final boolean shortcutTriggered = mShortcutTriggered;
1057             clear();
1058 
1059             // Triple tap and hold also belongs to triple tap event.
1060             final boolean enabled = !isActivated();
1061             logMagnificationTripleTap(enabled);
1062 
1063             mViewportDraggingState.prepareForZoomInTemporary(shortcutTriggered);
1064 
1065             zoomInTemporary(down.getX(), down.getY());
1066 
1067             transitionTo(mViewportDraggingState);
1068         }
1069 
1070         @Override
toString()1071         public String toString() {
1072             return "DetectingState{"
1073                     + "tapCount()=" + tapCount()
1074                     + ", mShortcutTriggered=" + mShortcutTriggered
1075                     + ", mDelayedEventQueue=" + MotionEventInfo.toString(mDelayedEventQueue)
1076                     + '}';
1077         }
1078 
toggleShortcutTriggered()1079         void toggleShortcutTriggered() {
1080             setShortcutTriggered(!mShortcutTriggered);
1081         }
1082 
setShortcutTriggered(boolean state)1083         void setShortcutTriggered(boolean state) {
1084             if (mShortcutTriggered == state) {
1085                 return;
1086             }
1087             if (DEBUG_DETECTING) Slog.i(mLogTag, "setShortcutTriggered(" + state + ")");
1088 
1089             mShortcutTriggered = state;
1090         }
1091 
isShortcutTriggered()1092         private boolean isShortcutTriggered() {
1093             return mShortcutTriggered;
1094         }
1095 
1096         /**
1097          * Detects if last action down is out of distance slop between with previous
1098          * one, when triple tap is enabled.
1099          *
1100          * @return true if tap is out of distance slop
1101          */
isTapOutOfDistanceSlop()1102         boolean isTapOutOfDistanceSlop() {
1103             if (!mDetectTripleTap) return false;
1104             if (mPreLastDown == null || mLastDown == null) {
1105                 return false;
1106             }
1107             final boolean outOfDistanceSlop =
1108                     GestureUtils.distance(mPreLastDown, mLastDown) > mMultiTapMaxDistance;
1109             if (tapCount() > 0) {
1110                 return outOfDistanceSlop;
1111             }
1112             // There's no tap in the queue here. We still need to check if this is the case that
1113             // user tap screen quickly and out of distance slop.
1114             if (outOfDistanceSlop
1115                     && !GestureUtils.isTimedOut(mPreLastDown, mLastDown, mMultiTapMaxDelay)) {
1116                 return true;
1117             }
1118             return false;
1119         }
1120     }
1121 
zoomInTemporary(float centerX, float centerY)1122     private void zoomInTemporary(float centerX, float centerY) {
1123         final float currentScale = mFullScreenMagnificationController.getScale(mDisplayId);
1124         final float persistedScale = MathUtils.constrain(
1125                 mFullScreenMagnificationController.getPersistedScale(mDisplayId),
1126                 MIN_SCALE, MAX_SCALE);
1127 
1128         final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId);
1129         final float scale = isActivated ? (currentScale + 1.0f) : persistedScale;
1130         zoomToScale(scale, centerX, centerY);
1131     }
1132 
zoomOn(float centerX, float centerY)1133     private void zoomOn(float centerX, float centerY) {
1134         if (DEBUG_DETECTING) Slog.i(mLogTag, "zoomOn(" + centerX + ", " + centerY + ")");
1135 
1136         final float scale = MathUtils.constrain(
1137                 mFullScreenMagnificationController.getPersistedScale(mDisplayId),
1138                 MIN_SCALE, MAX_SCALE);
1139         zoomToScale(scale, centerX, centerY);
1140     }
1141 
zoomToScale(float scale, float centerX, float centerY)1142     private void zoomToScale(float scale, float centerX, float centerY) {
1143         scale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
1144         mFullScreenMagnificationController.setScaleAndCenter(mDisplayId,
1145                 scale, centerX, centerY,
1146                 /* animate */ true,
1147                 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
1148     }
1149 
zoomOff()1150     private void zoomOff() {
1151         if (DEBUG_DETECTING) Slog.i(mLogTag, "zoomOff()");
1152         mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true);
1153     }
1154 
recycleAndNullify(@ullable MotionEvent event)1155     private static MotionEvent recycleAndNullify(@Nullable MotionEvent event) {
1156         if (event != null) {
1157             event.recycle();
1158         }
1159         return null;
1160     }
1161 
1162     @Override
toString()1163     public String toString() {
1164         return "MagnificationGesture{"
1165                 + "mDetectingState=" + mDetectingState
1166                 + ", mDelegatingState=" + mDelegatingState
1167                 + ", mMagnifiedInteractionState=" + mPanningScalingState
1168                 + ", mViewportDraggingState=" + mViewportDraggingState
1169                 + ", mDetectTripleTap=" + mDetectTripleTap
1170                 + ", mDetectShortcutTrigger=" + mDetectShortcutTrigger
1171                 + ", mCurrentState=" + State.nameOf(mCurrentState)
1172                 + ", mPreviousState=" + State.nameOf(mPreviousState)
1173                 + ", mMagnificationController=" + mFullScreenMagnificationController
1174                 + ", mDisplayId=" + mDisplayId
1175                 + '}';
1176     }
1177 
1178     private static final class MotionEventInfo {
1179 
1180         private static final int MAX_POOL_SIZE = 10;
1181         private static final Object sLock = new Object();
1182         private static MotionEventInfo sPool;
1183         private static int sPoolSize;
1184 
1185         private MotionEventInfo mNext;
1186         private boolean mInPool;
1187 
1188         public MotionEvent event;
1189         public MotionEvent rawEvent;
1190         public int policyFlags;
1191 
obtain(MotionEvent event, MotionEvent rawEvent, int policyFlags)1192         public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent,
1193                 int policyFlags) {
1194             synchronized (sLock) {
1195                 MotionEventInfo info = obtainInternal();
1196                 info.initialize(event, rawEvent, policyFlags);
1197                 return info;
1198             }
1199         }
1200 
1201         @NonNull
obtainInternal()1202         private static MotionEventInfo obtainInternal() {
1203             MotionEventInfo info;
1204             if (sPoolSize > 0) {
1205                 sPoolSize--;
1206                 info = sPool;
1207                 sPool = info.mNext;
1208                 info.mNext = null;
1209                 info.mInPool = false;
1210             } else {
1211                 info = new MotionEventInfo();
1212             }
1213             return info;
1214         }
1215 
initialize(MotionEvent event, MotionEvent rawEvent, int policyFlags)1216         private void initialize(MotionEvent event, MotionEvent rawEvent,
1217                 int policyFlags) {
1218             this.event = MotionEvent.obtain(event);
1219             this.rawEvent = MotionEvent.obtain(rawEvent);
1220             this.policyFlags = policyFlags;
1221         }
1222 
recycle()1223         public void recycle() {
1224             synchronized (sLock) {
1225                 if (mInPool) {
1226                     throw new IllegalStateException("Already recycled.");
1227                 }
1228                 clear();
1229                 if (sPoolSize < MAX_POOL_SIZE) {
1230                     sPoolSize++;
1231                     mNext = sPool;
1232                     sPool = this;
1233                     mInPool = true;
1234                 }
1235             }
1236         }
1237 
clear()1238         private void clear() {
1239             event = recycleAndNullify(event);
1240             rawEvent = recycleAndNullify(rawEvent);
1241             policyFlags = 0;
1242         }
1243 
countOf(MotionEventInfo info, int eventType)1244         static int countOf(MotionEventInfo info, int eventType) {
1245             if (info == null) return 0;
1246             return (info.event.getAction() == eventType ? 1 : 0)
1247                     + countOf(info.mNext, eventType);
1248         }
1249 
toString(MotionEventInfo info)1250         public static String toString(MotionEventInfo info) {
1251             return info == null
1252                     ? ""
1253                     : MotionEvent.actionToString(info.event.getAction()).replace("ACTION_", "")
1254                             + " " + MotionEventInfo.toString(info.mNext);
1255         }
1256     }
1257 
1258     /**
1259      * BroadcastReceiver used to cancel the magnification shortcut when the screen turns off
1260      */
1261     private static class ScreenStateReceiver extends BroadcastReceiver {
1262         private final Context mContext;
1263         private final FullScreenMagnificationGestureHandler mGestureHandler;
1264 
ScreenStateReceiver(Context context, FullScreenMagnificationGestureHandler gestureHandler)1265         ScreenStateReceiver(Context context,
1266                 FullScreenMagnificationGestureHandler gestureHandler) {
1267             mContext = context;
1268             mGestureHandler = gestureHandler;
1269         }
1270 
register()1271         public void register() {
1272             mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
1273         }
1274 
unregister()1275         public void unregister() {
1276             mContext.unregisterReceiver(this);
1277         }
1278 
1279         @Override
onReceive(Context context, Intent intent)1280         public void onReceive(Context context, Intent intent) {
1281             mGestureHandler.mDetectingState.setShortcutTriggered(false);
1282         }
1283     }
1284 
1285     /**
1286      * Indicates an error with a gesture handler or state.
1287      */
1288     private static class GestureException extends Exception {
GestureException(String message)1289         GestureException(String message) {
1290             super(message);
1291         }
1292     }
1293 }
1294