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.server.accessibility.gestures.GestureUtils.distance;
28 import static com.android.server.accessibility.gestures.GestureUtils.distanceClosestPointerToPoint;
29 
30 import static java.lang.Math.abs;
31 import static java.util.Arrays.asList;
32 import static java.util.Arrays.copyOfRange;
33 
34 import android.accessibilityservice.MagnificationConfig;
35 import android.annotation.IntDef;
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.content.pm.PackageManager;
44 import android.graphics.PointF;
45 import android.graphics.Rect;
46 import android.graphics.Region;
47 import android.os.Handler;
48 import android.os.Looper;
49 import android.os.Message;
50 import android.os.SystemClock;
51 import android.os.VibrationEffect;
52 import android.os.Vibrator;
53 import android.provider.Settings;
54 import android.util.Log;
55 import android.util.MathUtils;
56 import android.util.Slog;
57 import android.util.TypedValue;
58 import android.view.GestureDetector;
59 import android.view.GestureDetector.SimpleOnGestureListener;
60 import android.view.MotionEvent;
61 import android.view.MotionEvent.PointerCoords;
62 import android.view.MotionEvent.PointerProperties;
63 import android.view.ScaleGestureDetector;
64 import android.view.ScaleGestureDetector.OnScaleGestureListener;
65 import android.view.VelocityTracker;
66 import android.view.ViewConfiguration;
67 
68 import com.android.internal.R;
69 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
70 import com.android.internal.annotations.VisibleForTesting;
71 import com.android.server.accessibility.AccessibilityManagerService;
72 import com.android.server.accessibility.AccessibilityTraceManager;
73 import com.android.server.accessibility.Flags;
74 import com.android.server.accessibility.gestures.GestureUtils;
75 
76 /**
77  * This class handles full screen magnification in response to touch events.
78  *
79  * The behavior is as follows:
80  *
81  * 1. Triple tap toggles permanent screen magnification which is magnifying
82  *    the area around the location of the triple tap. One can think of the
83  *    location of the triple tap as the center of the magnified viewport.
84  *    For example, a triple tap when not magnified would magnify the screen
85  *    and leave it in a magnified state. A triple tapping when magnified would
86  *    clear magnification and leave the screen in a not magnified state.
87  *
88  * 2. Triple tap and hold would magnify the screen if not magnified and enable
89  *    viewport dragging mode until the finger goes up. One can think of this
90  *    mode as a way to move the magnified viewport since the area around the
91  *    moving finger will be magnified to fit the screen. For example, if the
92  *    screen was not magnified and the user triple taps and holds the screen
93  *    would magnify and the viewport will follow the user's finger. When the
94  *    finger goes up the screen will zoom out. If the same user interaction
95  *    is performed when the screen is magnified, the viewport movement will
96  *    be the same but when the finger goes up the screen will stay magnified.
97  *    In other words, the initial magnified state is sticky.
98  *
99  * 3. Magnification can optionally be "triggered" by some external shortcut
100  *    affordance. When this occurs via {@link #notifyShortcutTriggered()} a
101  *    subsequent tap in a magnifiable region will engage permanent screen
102  *    magnification as described in #1. Alternatively, a subsequent long-press
103  *    or drag will engage magnification with viewport dragging as described in
104  *    #2. Once magnified, all following behaviors apply whether magnification
105  *    was engaged via a triple-tap or by a triggered shortcut.
106  *
107  * 4. Pinching with any number of additional fingers when viewport dragging
108  *    is enabled, i.e. the user triple tapped and holds, would adjust the
109  *    magnification scale which will become the current default magnification
110  *    scale. The next time the user magnifies the same magnification scale
111  *    would be used.
112  *
113  * 5. When in a permanent magnified state the user can use two or more fingers
114  *    to pan the viewport. Note that in this mode the content is panned as
115  *    opposed to the viewport dragging mode in which the viewport is moved.
116  *
117  * 6. When in a permanent magnified state the user can use two or more
118  *    fingers to change the magnification scale which will become the current
119  *    default magnification scale. The next time the user magnifies the same
120  *    magnification scale would be used.
121  *
122  * 7. The magnification scale will be persisted in settings and in the cloud.
123  */
124 @SuppressWarnings("WeakerAccess")
125 public class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler {
126 
127     private static final boolean DEBUG_STATE_TRANSITIONS = false | DEBUG_ALL;
128     private static final boolean DEBUG_DETECTING = false | DEBUG_ALL;
129     private static final boolean DEBUG_PANNING_SCALING = false | DEBUG_ALL;
130 
131     // The MIN_SCALE is different from MagnificationScaleProvider.MIN_SCALE due
132     // to AccessibilityService.MagnificationController#setScale() has
133     // different scale range
134     private static final float MIN_SCALE = 1.0f;
135     private static final float MAX_SCALE = MagnificationScaleProvider.MAX_SCALE;
136 
137     @VisibleForTesting final FullScreenMagnificationController mFullScreenMagnificationController;
138 
139     private final FullScreenMagnificationController.MagnificationInfoChangedCallback
140             mMagnificationInfoChangedCallback;
141     @VisibleForTesting final DelegatingState mDelegatingState;
142     @VisibleForTesting final DetectingState mDetectingState;
143     @VisibleForTesting final PanningScalingState mPanningScalingState;
144     @VisibleForTesting final ViewportDraggingState mViewportDraggingState;
145     @VisibleForTesting final SinglePanningState mSinglePanningState;
146 
147     private final ScreenStateReceiver mScreenStateReceiver;
148     private final WindowMagnificationPromptController mPromptController;
149     @NonNull private final MagnificationLogger mMagnificationLogger;
150 
151     @VisibleForTesting State mCurrentState;
152     @VisibleForTesting State mPreviousState;
153 
154     private PointerCoords[] mTempPointerCoords;
155     private PointerProperties[] mTempPointerProperties;
156 
157     @VisibleForTesting static final int OVERSCROLL_NONE = 0;
158     @VisibleForTesting static final int OVERSCROLL_LEFT_EDGE = 1;
159     @VisibleForTesting static final int OVERSCROLL_RIGHT_EDGE = 2;
160     @VisibleForTesting static final int OVERSCROLL_VERTICAL_EDGE = 3;
161 
162     @IntDef({
163         OVERSCROLL_NONE,
164         OVERSCROLL_LEFT_EDGE,
165         OVERSCROLL_RIGHT_EDGE,
166         OVERSCROLL_VERTICAL_EDGE
167     })
168     public @interface OverscrollState {}
169 
170     @VisibleForTesting final OneFingerPanningSettingsProvider mOneFingerPanningSettingsProvider;
171 
172     private final FullScreenMagnificationVibrationHelper mFullScreenMagnificationVibrationHelper;
173 
174     @VisibleForTesting
175     @Nullable
176     final OverscrollHandler mOverscrollHandler;
177 
178     private final float mOverscrollEdgeSlop;
179 
180     private final boolean mIsWatch;
181 
182     @Nullable private VelocityTracker mVelocityTracker;
183     private final int mMinimumVelocity;
184     private final int mMaximumVelocity;
185 
FullScreenMagnificationGestureHandler(@iContext Context context, FullScreenMagnificationController fullScreenMagnificationController, AccessibilityTraceManager trace, Callback callback, boolean detectSingleFingerTripleTap, boolean detectTwoFingerTripleTap, boolean detectShortcutTrigger, @NonNull WindowMagnificationPromptController promptController, int displayId, FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper)186     public FullScreenMagnificationGestureHandler(@UiContext Context context,
187             FullScreenMagnificationController fullScreenMagnificationController,
188             AccessibilityTraceManager trace,
189             Callback callback,
190             boolean detectSingleFingerTripleTap,
191             boolean detectTwoFingerTripleTap,
192             boolean detectShortcutTrigger,
193             @NonNull WindowMagnificationPromptController promptController,
194             int displayId,
195             FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) {
196         this(
197                 context,
198                 fullScreenMagnificationController,
199                 trace,
200                 callback,
201                 detectSingleFingerTripleTap,
202                 detectTwoFingerTripleTap,
203                 detectShortcutTrigger,
204                 promptController,
205                 displayId,
206                 fullScreenMagnificationVibrationHelper,
207                 /* magnificationLogger= */ null,
208                 ViewConfiguration.get(context),
209                 new OneFingerPanningSettingsProvider(
210                         context,
211                         Flags.enableMagnificationOneFingerPanningGesture()
212                 ));
213     }
214 
215     /** Constructor for tests. */
216     @VisibleForTesting
FullScreenMagnificationGestureHandler( @iContext Context context, FullScreenMagnificationController fullScreenMagnificationController, AccessibilityTraceManager trace, Callback callback, boolean detectSingleFingerTripleTap, boolean detectTwoFingerTripleTap, boolean detectShortcutTrigger, @NonNull WindowMagnificationPromptController promptController, int displayId, FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper, MagnificationLogger magnificationLogger, ViewConfiguration viewConfiguration, OneFingerPanningSettingsProvider oneFingerPanningSettingsProvider )217     FullScreenMagnificationGestureHandler(
218             @UiContext Context context,
219             FullScreenMagnificationController fullScreenMagnificationController,
220             AccessibilityTraceManager trace,
221             Callback callback,
222             boolean detectSingleFingerTripleTap,
223             boolean detectTwoFingerTripleTap,
224             boolean detectShortcutTrigger,
225             @NonNull WindowMagnificationPromptController promptController,
226             int displayId,
227             FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper,
228             MagnificationLogger magnificationLogger,
229             ViewConfiguration viewConfiguration,
230             OneFingerPanningSettingsProvider oneFingerPanningSettingsProvider
231     ) {
232         super(displayId, detectSingleFingerTripleTap, detectTwoFingerTripleTap,
233                 detectShortcutTrigger, trace, callback);
234         if (DEBUG_ALL) {
235             Log.i(mLogTag,
236                     "FullScreenMagnificationGestureHandler(detectSingleFingerTripleTap = "
237                             + detectSingleFingerTripleTap
238                             + ", detectTwoFingerTripleTap = " + detectTwoFingerTripleTap
239                             + ", detectShortcutTrigger = " + detectShortcutTrigger + ")");
240         }
241 
242         if (Flags.fullscreenFlingGesture()) {
243             mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
244             mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
245         } else {
246             mMinimumVelocity = 0;
247             mMaximumVelocity = 0;
248         }
249 
250         mFullScreenMagnificationController = fullScreenMagnificationController;
251         mMagnificationInfoChangedCallback =
252                 new FullScreenMagnificationController.MagnificationInfoChangedCallback() {
253                     @Override
254                     public void onRequestMagnificationSpec(int displayId, int serviceId) {
255                         return;
256                     }
257 
258                     @Override
259                     public void onFullScreenMagnificationActivationState(int displayId,
260                             boolean activated) {
261                         if (displayId != mDisplayId) {
262                             return;
263                         }
264 
265                         if (!activated) {
266                             // cancel the magnification shortcut
267                             mDetectingState.setShortcutTriggered(false);
268                         }
269                     }
270 
271                     @Override
272                     public void onImeWindowVisibilityChanged(int displayId, boolean shown) {
273                         return;
274                     }
275 
276                     @Override
277                     public void onFullScreenMagnificationChanged(int displayId,
278                             @NonNull Region region,
279                             @NonNull MagnificationConfig config) {
280                         return;
281                     }
282                 };
283         mFullScreenMagnificationController.addInfoChangedCallback(
284                 mMagnificationInfoChangedCallback);
285 
286         mPromptController = promptController;
287 
288         if (magnificationLogger != null) {
289             mMagnificationLogger = magnificationLogger;
290         } else {
291             mMagnificationLogger = new MagnificationLogger() {
292                 @Override
293                 public void logMagnificationTripleTap(boolean enabled) {
294                     AccessibilityStatsLogUtils.logMagnificationTripleTap(enabled);
295                 }
296 
297                 @Override
298                 public void logMagnificationTwoFingerTripleTap(boolean enabled) {
299                     AccessibilityStatsLogUtils.logMagnificationTwoFingerTripleTap(enabled);
300                 }
301             };
302         }
303 
304         mDelegatingState = new DelegatingState();
305         mDetectingState = Flags.enableMagnificationMultipleFingerMultipleTapGesture()
306                 ? new DetectingStateWithMultiFinger(context)
307                 : new DetectingState(context);
308         mViewportDraggingState = Flags.enableMagnificationMultipleFingerMultipleTapGesture()
309                 ? new ViewportDraggingStateWithMultiFinger()
310                 : new ViewportDraggingState();
311         mPanningScalingState = new PanningScalingState(context);
312         mSinglePanningState = new SinglePanningState(context);
313         mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
314         mOneFingerPanningSettingsProvider = oneFingerPanningSettingsProvider;
315         boolean overscrollHandlerSupported = context.getResources().getBoolean(
316                 R.bool.config_enable_a11y_fullscreen_magnification_overscroll_handler);
317         mOverscrollHandler = overscrollHandlerSupported ? new OverscrollHandler() : null;
318         mOverscrollEdgeSlop = context.getResources().getDimensionPixelSize(
319                 R.dimen.accessibility_fullscreen_magnification_gesture_edge_slop);
320         mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
321 
322         if (mDetectShortcutTrigger) {
323             mScreenStateReceiver = new ScreenStateReceiver(context, this);
324             mScreenStateReceiver.register();
325         } else {
326             mScreenStateReceiver = null;
327         }
328 
329         transitionTo(mDetectingState);
330     }
331 
332     @Override
onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags)333     void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
334         if (event.getActionMasked() == ACTION_DOWN) {
335             cancelFling();
336         }
337 
338         handleEventWith(mCurrentState, event, rawEvent, policyFlags);
339     }
340 
handleEventWith(State stateHandler, MotionEvent event, MotionEvent rawEvent, int policyFlags)341     private void handleEventWith(State stateHandler,
342             MotionEvent event, MotionEvent rawEvent, int policyFlags) {
343         // To keep InputEventConsistencyVerifiers within GestureDetectors happy
344         mPanningScalingState.mScrollGestureDetector.onTouchEvent(event);
345         mPanningScalingState.mScaleGestureDetector.onTouchEvent(event);
346         mSinglePanningState.mScrollGestureDetector.onTouchEvent(event);
347 
348         try {
349             stateHandler.onMotionEvent(event, rawEvent, policyFlags);
350         } catch (GestureException e) {
351             Slog.e(mLogTag, "Error processing motion event", e);
352             clearAndTransitionToStateDetecting();
353         }
354     }
355 
356     @Override
clearEvents(int inputSource)357     public void clearEvents(int inputSource) {
358         if (inputSource == SOURCE_TOUCHSCREEN) {
359             clearAndTransitionToStateDetecting();
360         }
361 
362         super.clearEvents(inputSource);
363     }
364 
365     @Override
onDestroy()366     public void onDestroy() {
367         if (DEBUG_STATE_TRANSITIONS) {
368             Slog.i(mLogTag, "onDestroy(); delayed = "
369                     + MotionEventInfo.toString(mDetectingState.mDelayedEventQueue));
370         }
371         mOneFingerPanningSettingsProvider.unregister();
372 
373         if (mScreenStateReceiver != null) {
374             mScreenStateReceiver.unregister();
375         }
376         mPromptController.onDestroy();
377         // Check if need to reset when MagnificationGestureHandler is the last magnifying service.
378         mFullScreenMagnificationController.resetIfNeeded(
379                 mDisplayId, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
380         mFullScreenMagnificationController.removeInfoChangedCallback(
381                 mMagnificationInfoChangedCallback);
382         clearAndTransitionToStateDetecting();
383     }
384 
385     @Override
handleShortcutTriggered()386     public void handleShortcutTriggered() {
387         final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId);
388 
389         if (isActivated) {
390             zoomOff();
391             clearAndTransitionToStateDetecting();
392         } else {
393             mDetectingState.toggleShortcutTriggered();
394         }
395 
396         if (mDetectingState.isShortcutTriggered()) {
397             mPromptController.showNotificationIfNeeded();
398             zoomToScale(1.0f, Float.NaN, Float.NaN);
399         }
400     }
401 
402     @Override
getMode()403     public int getMode() {
404         return Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
405     }
406 
clearAndTransitionToStateDetecting()407     void clearAndTransitionToStateDetecting() {
408         mCurrentState = mDetectingState;
409         mDetectingState.clear();
410         mViewportDraggingState.clear();
411         mPanningScalingState.clear();
412     }
413 
getTempPointerCoordsWithMinSize(int size)414     private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
415         final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
416         if (oldSize < size) {
417             PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
418             mTempPointerCoords = new PointerCoords[size];
419             if (oldTempPointerCoords != null) {
420                 System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
421             }
422         }
423         for (int i = oldSize; i < size; i++) {
424             mTempPointerCoords[i] = new PointerCoords();
425         }
426         return mTempPointerCoords;
427     }
428 
getTempPointerPropertiesWithMinSize(int size)429     private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
430         final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length
431                 : 0;
432         if (oldSize < size) {
433             PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
434             mTempPointerProperties = new PointerProperties[size];
435             if (oldTempPointerProperties != null) {
436                 System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0,
437                         oldSize);
438             }
439         }
440         for (int i = oldSize; i < size; i++) {
441             mTempPointerProperties[i] = new PointerProperties();
442         }
443         return mTempPointerProperties;
444     }
445 
446     @VisibleForTesting
transitionTo(State state)447     void transitionTo(State state) {
448         if (DEBUG_STATE_TRANSITIONS) {
449             Slog.i(mLogTag,
450                     (State.nameOf(mCurrentState) + " -> " + State.nameOf(state)
451                     + " at " + asList(copyOfRange(new RuntimeException().getStackTrace(), 1, 5)))
452                     .replace(getClass().getName(), ""));
453         }
454         mPreviousState = mCurrentState;
455         if (state == mPanningScalingState) {
456             mPanningScalingState.prepareForState();
457         }
458         mCurrentState = state;
459     }
460 
461     /** An interface that allows testing magnification log events. */
462     interface MagnificationLogger {
logMagnificationTripleTap(boolean enabled)463         void logMagnificationTripleTap(boolean enabled);
logMagnificationTwoFingerTripleTap(boolean enabled)464         void logMagnificationTwoFingerTripleTap(boolean enabled);
465     }
466 
467     interface State {
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)468         void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)
469                 throws GestureException;
470 
clear()471         default void clear() {}
472 
name()473         default String name() {
474             return getClass().getSimpleName();
475         }
476 
nameOf(@ullable State s)477         static String nameOf(@Nullable State s) {
478             return s != null ? s.name() : "null";
479         }
480     }
481 
482     /**
483      * This class determines if the user is performing a scale or pan gesture.
484      *
485      * Unlike when {@link ViewportDraggingState dragging the viewport}, in panning mode the viewport
486      * moves in the same direction as the fingers, and allows to easily and precisely scale the
487      * magnification level.
488      * This makes it the preferred mode for one-off adjustments, due to its precision and ease of
489      * triggering.
490      */
491     final class PanningScalingState extends SimpleOnGestureListener
492             implements OnScaleGestureListener, State {
493 
494         private final Context mContext;
495         private final ScaleGestureDetector mScaleGestureDetector;
496         private final GestureDetector mScrollGestureDetector;
497         final float mScalingThreshold;
498 
499         float mInitialScaleFactor = -1;
500         @VisibleForTesting boolean mScaling;
501 
502         /**
503          * Whether it needs to detect the target scale passes
504          * {@link FullScreenMagnificationController#getPersistedScale} during panning scale.
505          */
506         @VisibleForTesting boolean mDetectingPassPersistedScale;
507 
508         // The threshold for relative difference from given scale to persisted scale. If the
509         // difference >= threshold, we can start detecting if the scale passes the persisted
510         // scale during panning.
511         @VisibleForTesting static final float CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD = 0.2f;
512         // The threshold for relative difference from given scale to persisted scale. If the
513         // difference < threshold, we can decide that the scale passes the persisted scale.
514         @VisibleForTesting static final float PASSING_PERSISTED_SCALE_THRESHOLD = 0.01f;
515 
PanningScalingState(Context context)516         PanningScalingState(Context context) {
517             final TypedValue scaleValue = new TypedValue();
518             context.getResources().getValue(
519                     R.dimen.config_screen_magnification_scaling_threshold,
520                     scaleValue, false);
521             mContext = context;
522             mScalingThreshold = scaleValue.getFloat();
523             mScaleGestureDetector = new ScaleGestureDetector(context, this, Handler.getMain());
524             mScaleGestureDetector.setQuickScaleEnabled(false);
525             mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
526         }
527 
528         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)529         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
530             int action = event.getActionMasked();
531             if (action == ACTION_POINTER_UP
532                     && event.getPointerCount() == 2 // includes the pointer currently being released
533                     && mPreviousState == mViewportDraggingState) {
534                 if (mOverscrollHandler != null) {
535                     mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
536                     mOverscrollHandler.clearEdgeState();
537                 }
538                 persistScaleAndTransitionTo(mViewportDraggingState);
539             } else if (action == ACTION_UP || action == ACTION_CANCEL) {
540                 onPanningFinished(event);
541                 if (mOverscrollHandler != null) {
542                     mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
543                     mOverscrollHandler.clearEdgeState();
544                 }
545                 persistScaleAndTransitionTo(mDetectingState);
546             }
547         }
548 
prepareForState()549         void prepareForState() {
550             checkShouldDetectPassPersistedScale();
551         }
552 
checkShouldDetectPassPersistedScale()553         private void checkShouldDetectPassPersistedScale() {
554             if (mDetectingPassPersistedScale) {
555                 return;
556             }
557 
558             final float currentScale =
559                     mFullScreenMagnificationController.getScale(mDisplayId);
560             final float persistedScale =
561                     mFullScreenMagnificationController.getPersistedScale(mDisplayId);
562 
563             mDetectingPassPersistedScale =
564                     (abs(currentScale - persistedScale) / persistedScale)
565                             >= CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD;
566         }
567 
persistScaleAndTransitionTo(State state)568         public void persistScaleAndTransitionTo(State state) {
569             // If device is a watch don't change user settings scale. On watches, warp effect
570             // is enabled and the current display scale could be differ from the default user
571             // settings scale (should not change the scale due to the warp effect)
572             if (!mIsWatch) {
573                 mFullScreenMagnificationController.persistScale(mDisplayId);
574             }
575             clear();
576             transitionTo(state);
577         }
578 
579         @VisibleForTesting
setScaleAndClearIfNeeded(float scale, float pivotX, float pivotY)580         void setScaleAndClearIfNeeded(float scale, float pivotX, float pivotY) {
581             if (mDetectingPassPersistedScale) {
582                 final float persistedScale =
583                         mFullScreenMagnificationController.getPersistedScale(mDisplayId);
584                 // If the scale passes the persisted scale during panning, perform a vibration
585                 // feedback to user. Also, call {@link clear} to create a buffer zone so that
586                 // user needs to panning more than {@link mScalingThreshold} to change scale again.
587                 if (abs(scale - persistedScale) / persistedScale
588                         < PASSING_PERSISTED_SCALE_THRESHOLD) {
589                     scale = persistedScale;
590                     final Vibrator vibrator = mContext.getSystemService(Vibrator.class);
591                     if (vibrator != null) {
592                         vibrator.vibrate(
593                                 VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK));
594                     }
595                     clear();
596                 }
597             }
598 
599             if (DEBUG_PANNING_SCALING) Slog.i(mLogTag, "Scaled content to: " + scale + "x");
600             mFullScreenMagnificationController.setScale(mDisplayId, scale, pivotX, pivotY, false,
601                     AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
602 
603             checkShouldDetectPassPersistedScale();
604         }
605 
606         @Override
onScroll(MotionEvent first, MotionEvent second, float distanceX, float distanceY)607         public boolean onScroll(MotionEvent first, MotionEvent second,
608                 float distanceX, float distanceY) {
609             if (mCurrentState != mPanningScalingState) {
610                 return true;
611             }
612             if (DEBUG_PANNING_SCALING) {
613                 Slog.i(mLogTag, "Panned content by scrollX: " + distanceX
614                         + " scrollY: " + distanceY);
615             }
616             onPan(second);
617             mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX,
618                     distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
619             if (mOverscrollHandler != null) {
620                 mOverscrollHandler.onScrollStateChanged(first, second);
621             }
622             return /* event consumed: */ true;
623         }
624 
625         @Override
onScale(ScaleGestureDetector detector)626         public boolean onScale(ScaleGestureDetector detector) {
627             if (!mScaling) {
628                 if (mInitialScaleFactor < 0) {
629                     mInitialScaleFactor = detector.getScaleFactor();
630                     return false;
631                 }
632                 final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
633                 mScaling = abs(deltaScale) > mScalingThreshold;
634                 return mScaling;
635             }
636             final float initialScale = mFullScreenMagnificationController.getScale(mDisplayId);
637             final float targetScale = initialScale * detector.getScaleFactor();
638 
639             // Don't allow a gesture to move the user further outside the
640             // desired bounds for gesture-controlled scaling.
641             final float scale;
642             if (targetScale > MAX_SCALE && targetScale > initialScale) {
643                 // The target scale is too big and getting bigger.
644                 scale = MAX_SCALE;
645             } else if (targetScale < MIN_SCALE && targetScale < initialScale) {
646                 // The target scale is too small and getting smaller.
647                 scale = MIN_SCALE;
648             } else {
649                 // The target scale may be outside our bounds, but at least
650                 // it's moving in the right direction. This avoids a "jump" if
651                 // we're at odds with some other service's desired bounds.
652                 scale = targetScale;
653             }
654 
655             setScaleAndClearIfNeeded(scale, detector.getFocusX(), detector.getFocusY());
656             return /* handled: */ true;
657         }
658 
659         @Override
onScaleBegin(ScaleGestureDetector detector)660         public boolean onScaleBegin(ScaleGestureDetector detector) {
661             return /* continue recognizing: */ (mCurrentState == mPanningScalingState);
662         }
663 
664         @Override
onScaleEnd(ScaleGestureDetector detector)665         public void onScaleEnd(ScaleGestureDetector detector) {
666             clear();
667         }
668 
669         @Override
clear()670         public void clear() {
671             mInitialScaleFactor = -1;
672             mScaling = false;
673             mDetectingPassPersistedScale = false;
674         }
675 
676         @Override
toString()677         public String toString() {
678             return "PanningScalingState{" + "mInitialScaleFactor=" + mInitialScaleFactor
679                     + ", mScaling=" + mScaling
680                     + '}';
681         }
682     }
683 
684     final class ViewportDraggingStateWithMultiFinger extends ViewportDraggingState {
685         // LINT.IfChange(viewport_dragging_state_with_multi_finger)
686         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)687         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)
688                 throws GestureException {
689             final int action = event.getActionMasked();
690             switch (action) {
691                 case ACTION_POINTER_DOWN: {
692                     clearAndTransitToPanningScalingState();
693                 }
694                 break;
695                 case ACTION_MOVE: {
696                     if (event.getPointerCount() > 2) {
697                         throw new GestureException("Should have one pointer down.");
698                     }
699                     final float eventX = event.getX();
700                     final float eventY = event.getY();
701                     if (mFullScreenMagnificationController.magnificationRegionContains(
702                             mDisplayId, eventX, eventY)) {
703                         mFullScreenMagnificationController.setCenter(mDisplayId, eventX, eventY,
704                                 /* animate */ mLastMoveOutsideMagnifiedRegion,
705                                 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
706                         mLastMoveOutsideMagnifiedRegion = false;
707                     } else {
708                         mLastMoveOutsideMagnifiedRegion = true;
709                     }
710                 }
711                 break;
712 
713                 case ACTION_UP:
714                 case ACTION_CANCEL: {
715                     // If mScaleToRecoverAfterDraggingEnd >= 1.0, the dragging state is triggered
716                     // by zoom in temporary, and the magnifier needs to recover to original scale
717                     // after exiting dragging state.
718                     // Otherwise, the magnifier should be disabled.
719                     if (mScaleToRecoverAfterDraggingEnd >= 1.0f) {
720                         zoomToScale(mScaleToRecoverAfterDraggingEnd, event.getX(),
721                                 event.getY());
722                     } else {
723                         zoomOff();
724                     }
725                     clear();
726                     mScaleToRecoverAfterDraggingEnd = Float.NaN;
727                     transitionTo(mDetectingState);
728                 }
729                     break;
730 
731                 case ACTION_DOWN: {
732                     throw new GestureException(
733                             "Unexpected event type: " + MotionEvent.actionToString(action));
734                 }
735             }
736         }
737         // LINT.ThenChange(:viewport_dragging_state)
738     }
739 
740     /**
741      * This class handles motion events when the event dispatcher has
742      * determined that the user is performing a single-finger drag of the
743      * magnification viewport.
744      *
745      * Unlike when {@link PanningScalingState panning}, the viewport moves in the opposite direction
746      * of the finger, and any part of the screen is reachable without lifting the finger.
747      * This makes it the preferable mode for tasks like reading text spanning full screen width.
748      */
749     class ViewportDraggingState implements State {
750 
751         /**
752          * The cached scale for recovering after dragging ends.
753          * If the scale >= 1.0, the magnifier needs to recover to scale.
754          * Otherwise, the magnifier should be disabled.
755          */
756         @VisibleForTesting protected float mScaleToRecoverAfterDraggingEnd = Float.NaN;
757 
758         protected boolean mLastMoveOutsideMagnifiedRegion;
759 
760         // LINT.IfChange(viewport_dragging_state)
761         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)762         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)
763                 throws GestureException {
764             final int action = event.getActionMasked();
765             switch (action) {
766                 case ACTION_POINTER_DOWN: {
767                     clearAndTransitToPanningScalingState();
768                 }
769                 break;
770                 case ACTION_MOVE: {
771                     if (event.getPointerCount() != 1) {
772                         throw new GestureException("Should have one pointer down.");
773                     }
774                     final float eventX = event.getX();
775                     final float eventY = event.getY();
776                     if (mFullScreenMagnificationController.magnificationRegionContains(
777                             mDisplayId, eventX, eventY)) {
778                         mFullScreenMagnificationController.setCenter(mDisplayId, eventX, eventY,
779                                 /* animate */ mLastMoveOutsideMagnifiedRegion,
780                                 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
781                         mLastMoveOutsideMagnifiedRegion = false;
782                     } else {
783                         mLastMoveOutsideMagnifiedRegion = true;
784                     }
785                 }
786                 break;
787 
788                 case ACTION_UP:
789                 case ACTION_CANCEL: {
790                     // If mScaleToRecoverAfterDraggingEnd >= 1.0, the dragging state is triggered
791                     // by zoom in temporary, and the magnifier needs to recover to original scale
792                     // after exiting dragging state.
793                     // Otherwise, the magnifier should be disabled.
794                     if (mScaleToRecoverAfterDraggingEnd >= 1.0f) {
795                         zoomToScale(mScaleToRecoverAfterDraggingEnd, event.getX(),
796                                 event.getY());
797                     } else {
798                         zoomOff();
799                     }
800                     clear();
801                     mScaleToRecoverAfterDraggingEnd = Float.NaN;
802                     transitionTo(mDetectingState);
803                 }
804                     break;
805 
806                 case ACTION_DOWN:
807                 case ACTION_POINTER_UP: {
808                     throw new GestureException(
809                             "Unexpected event type: " + MotionEvent.actionToString(action));
810                 }
811             }
812         }
813         // LINT.ThenChange(:viewport_dragging_state_with_multi_finger)
814 
isAlwaysOnMagnificationEnabled()815         private boolean isAlwaysOnMagnificationEnabled() {
816             return mFullScreenMagnificationController.isAlwaysOnMagnificationEnabled();
817         }
818 
prepareForZoomInTemporary(boolean shortcutTriggered)819         public void prepareForZoomInTemporary(boolean shortcutTriggered) {
820             boolean shouldRecoverAfterDraggingEnd;
821             if (mFullScreenMagnificationController.isActivated(mDisplayId)) {
822                 // For b/267210808, if always-on feature is not enabled, we keep the expected
823                 // behavior. If users tap shortcut and then tap-and-hold to zoom in temporary,
824                 // the magnifier should be disabled after release.
825                 // If always-on feature is enabled, in the same scenario the magnifier would
826                 // zoom to 1.0 and keep activated.
827                 if (shortcutTriggered) {
828                     shouldRecoverAfterDraggingEnd = isAlwaysOnMagnificationEnabled();
829                 } else {
830                     shouldRecoverAfterDraggingEnd = true;
831                 }
832             } else {
833                 shouldRecoverAfterDraggingEnd = false;
834             }
835 
836             mScaleToRecoverAfterDraggingEnd = shouldRecoverAfterDraggingEnd
837                     ? mFullScreenMagnificationController.getScale(mDisplayId) : Float.NaN;
838         }
839 
clearAndTransitToPanningScalingState()840         protected void clearAndTransitToPanningScalingState() {
841             final float scaleToRecovery = mScaleToRecoverAfterDraggingEnd;
842             clear();
843             mScaleToRecoverAfterDraggingEnd = scaleToRecovery;
844             transitionTo(mPanningScalingState);
845         }
846 
847         @Override
clear()848         public void clear() {
849             mLastMoveOutsideMagnifiedRegion = false;
850 
851             mScaleToRecoverAfterDraggingEnd = Float.NaN;
852         }
853 
854         @Override
toString()855         public String toString() {
856             return "ViewportDraggingState{"
857                     + "mScaleToRecoverAfterDraggingEnd=" + mScaleToRecoverAfterDraggingEnd
858                     + ", mLastMoveOutsideMagnifiedRegion=" + mLastMoveOutsideMagnifiedRegion
859                     + '}';
860         }
861     }
862 
863     final class DelegatingState implements State {
864         /**
865          * Time of last {@link MotionEvent#ACTION_DOWN} while in {@link DelegatingState}
866          */
867         public long mLastDelegatedDownEventTime;
868 
869         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)870         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
871             // Ensures that the state at the end of delegation is consistent with the last delegated
872             // UP/DOWN event in queue: still delegating if pointer is down, detecting otherwise
873             switch (event.getActionMasked()) {
874                 case ACTION_UP:
875                 case ACTION_CANCEL: {
876                     transitionTo(mDetectingState);
877                 }
878                     break;
879 
880                 case ACTION_DOWN: {
881                     transitionTo(mDelegatingState);
882                     mLastDelegatedDownEventTime = event.getDownTime();
883                 } break;
884             }
885 
886             if (getNext() != null) {
887                 // We cache some events to see if the user wants to trigger magnification.
888                 // If no magnification is triggered we inject these events with adjusted
889                 // time and down time to prevent subsequent transformations being confused
890                 // by stale events. After the cached events, which always have a down, are
891                 // injected we need to also update the down time of all subsequent non cached
892                 // events. All delegated events cached and non-cached are delivered here.
893                 event.setDownTime(mLastDelegatedDownEventTime);
894                 dispatchTransformedEvent(event, rawEvent, policyFlags);
895             }
896         }
897     }
898 
899     final class DetectingStateWithMultiFinger extends DetectingState {
900         private static final int TWO_FINGER_GESTURE_MAX_TAPS = 2;
901         // A flag set to true when two fingers have touched down.
902         // Used to indicate what next finger action should be.
903         private boolean mIsTwoFingerCountReached = false;
904         // A tap counts when two fingers are down and up once.
905         private int mCompletedTapCount = 0;
DetectingStateWithMultiFinger(Context context)906         DetectingStateWithMultiFinger(Context context) {
907             super(context);
908         }
909 
910         // LINT.IfChange(detecting_state_with_multi_finger)
911         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)912         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
913             cacheDelayedMotionEvent(event, rawEvent, policyFlags);
914             switch (event.getActionMasked()) {
915                 case MotionEvent.ACTION_DOWN: {
916                     mLastDetectingDownEventTime = event.getDownTime();
917                     mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
918 
919                     mFirstPointerDownLocation.set(event.getX(), event.getY());
920 
921                     if (!mFullScreenMagnificationController.magnificationRegionContains(
922                             mDisplayId, event.getX(), event.getY())) {
923 
924                         transitionToDelegatingStateAndClear();
925 
926                     } else if (isMultiTapTriggered(2 /* taps */)) {
927 
928                         // 3tap and hold
929                         afterLongTapTimeoutTransitionToDraggingState(event);
930 
931                     } else if (isTapOutOfDistanceSlop()) {
932 
933                         transitionToDelegatingStateAndClear();
934 
935                     } else if (mDetectSingleFingerTripleTap
936                             || mDetectTwoFingerTripleTap
937                             // If activated, delay an ACTION_DOWN for mMultiTapMaxDelay
938                             // to ensure reachability of
939                             // STATE_PANNING_SCALING(triggerable with ACTION_POINTER_DOWN)
940                             || isActivated()) {
941 
942                         afterMultiTapTimeoutTransitionToDelegatingState();
943 
944                     } else {
945 
946                         // Delegate pending events without delay
947                         transitionToDelegatingStateAndClear();
948                     }
949                 }
950                 break;
951                 case ACTION_POINTER_DOWN: {
952                     mIsTwoFingerCountReached = mDetectTwoFingerTripleTap
953                             && event.getPointerCount() == 2;
954                     mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
955 
956                     if (event.getPointerCount() == 2) {
957                         if (isMultiFingerMultiTapTriggered(
958                                 TWO_FINGER_GESTURE_MAX_TAPS - 1, event)) {
959                             // 3tap and hold
960                             afterLongTapTimeoutTransitionToDraggingState(event);
961                         } else {
962                             if (mDetectTwoFingerTripleTap) {
963                                 // If mDetectTwoFingerTripleTap, delay transition to the delegating
964                                 // state for mMultiTapMaxDelay to ensure reachability of
965                                 // multi finger multi tap
966                                 afterMultiTapTimeoutTransitionToDelegatingState();
967                             }
968 
969                             if (isActivated()) {
970                                 // If activated, delay transition to the panning scaling
971                                 // state for tap timeout to ensure reachability of
972                                 // multi finger multi tap
973                                 storePointerDownLocation(mSecondPointerDownLocation, event);
974                                 mHandler.sendEmptyMessageDelayed(
975                                         MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
976                                         ViewConfiguration.getTapTimeout());
977                             }
978                         }
979                     } else {
980                         transitionToDelegatingStateAndClear();
981                     }
982                 }
983                 break;
984                 case ACTION_POINTER_UP: {
985                     // If it is a two-finger gesture, do not transition to the delegating state
986                     // to ensure the reachability of
987                     // the two-finger triple tap (triggerable with ACTION_MOVE and ACTION_UP)
988                     if (!mIsTwoFingerCountReached) {
989                         transitionToDelegatingStateAndClear();
990                     }
991                 }
992                 break;
993                 case ACTION_MOVE: {
994                     if (isFingerDown()
995                             && distance(mLastDown, /* move */ event) > mSwipeMinDistance) {
996                         // Swipe detected - transition immediately
997 
998                         // For convenience, viewport dragging takes precedence
999                         // over insta-delegating on 3tap&swipe
1000                         // (which is a rare combo to be used aside from magnification)
1001                         if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) {
1002                             transitionToViewportDraggingStateAndClear(event);
1003                         } else if (isMultiFingerMultiTapTriggered(
1004                                 TWO_FINGER_GESTURE_MAX_TAPS - 1, event)
1005                                 && event.getPointerCount() == 2) {
1006                             transitionToViewportDraggingStateAndClear(event);
1007                         } else if (isActivated() && event.getPointerCount() == 2) {
1008                             if (mOverscrollHandler != null
1009                                     && overscrollState(event, mFirstPointerDownLocation)
1010                                     == OVERSCROLL_VERTICAL_EDGE) {
1011                                 transitionToDelegatingStateAndClear();
1012                             } else {
1013                                 //Primary pointer is swiping, so transit to PanningScalingState
1014                                 transitToPanningScalingStateAndClear();
1015                             }
1016                         } else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
1017                                 && isActivated()
1018                                 && event.getPointerCount() == 1) {
1019                             if (mOverscrollHandler != null
1020                                     && overscrollState(event, mFirstPointerDownLocation)
1021                                     == OVERSCROLL_VERTICAL_EDGE) {
1022                                 transitionToDelegatingStateAndClear();
1023                             } else if (overscrollState(event, mFirstPointerDownLocation)
1024                                     != OVERSCROLL_NONE) {
1025                                 transitionToDelegatingStateAndClear();
1026                             } else {
1027                                 transitToSinglePanningStateAndClear();
1028                             }
1029                         } else if (!mIsTwoFingerCountReached) {
1030                             // If it is a two-finger gesture, do not transition to the
1031                             // delegating state to ensure the reachability of
1032                             // the two-finger triple tap (triggerable with ACTION_UP)
1033                             transitionToDelegatingStateAndClear();
1034                         }
1035                     } else if (isActivated() && pointerDownValid(mSecondPointerDownLocation)
1036                             && distanceClosestPointerToPoint(
1037                             mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) {
1038                         // Second pointer is swiping, so transit to PanningScalingState
1039                         // Delay an ACTION_MOVE for tap timeout to ensure it is not trigger from
1040                         // multi finger multi tap
1041                         storePointerDownLocation(mSecondPointerDownLocation, event);
1042                         mHandler.sendEmptyMessageDelayed(
1043                                 MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
1044                                 ViewConfiguration.getTapTimeout());
1045                     }
1046                 }
1047                 break;
1048                 case ACTION_UP: {
1049 
1050                     mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
1051                     mHandler.removeMessages(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE);
1052 
1053                     if (!mFullScreenMagnificationController.magnificationRegionContains(
1054                             mDisplayId, event.getX(), event.getY())) {
1055                         transitionToDelegatingStateAndClear();
1056 
1057                     } else if (isMultiFingerMultiTapTriggered(TWO_FINGER_GESTURE_MAX_TAPS, event)) {
1058                         // Placing multiple fingers before a single finger, because achieving a
1059                         // multi finger multi tap also means achieving a single finger triple tap
1060                         onTripleTap(event);
1061 
1062                     } else if (isMultiTapTriggered(3 /* taps */)) {
1063                         onTripleTap(/* up */ event);
1064 
1065                     } else if (
1066                             // Possible to be false on: 3tap&drag -> scale -> PTR_UP -> UP
1067                             isFingerDown()
1068                             //TODO long tap should never happen here
1069                             && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay)
1070                                     || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))
1071                             // If it is a two-finger but not reach 3 tap, do not transition to the
1072                             // delegating state to ensure the reachability of the triple tap
1073                             && mCompletedTapCount == 0) {
1074                         transitionToDelegatingStateAndClear();
1075 
1076                     }
1077                 }
1078                 break;
1079             }
1080         }
1081         // LINT.ThenChange(:detecting_state)
1082 
1083         @Override
clear()1084         public void clear() {
1085             mCompletedTapCount = 0;
1086             setShortcutTriggered(false);
1087             removePendingDelayedMessages();
1088             clearDelayedMotionEvents();
1089             mFirstPointerDownLocation.set(Float.NaN, Float.NaN);
1090             mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
1091         }
1092 
isMultiFingerMultiTapTriggered(int targetTapCount, MotionEvent event)1093         private boolean isMultiFingerMultiTapTriggered(int targetTapCount, MotionEvent event) {
1094             if (event.getActionMasked() == ACTION_UP && mIsTwoFingerCountReached) {
1095                 mCompletedTapCount++;
1096                 mIsTwoFingerCountReached = false;
1097             }
1098 
1099             if (mDetectTwoFingerTripleTap && mCompletedTapCount > TWO_FINGER_GESTURE_MAX_TAPS - 1) {
1100                 final boolean enabled = !isActivated();
1101                 mMagnificationLogger.logMagnificationTwoFingerTripleTap(enabled);
1102             }
1103             return mDetectTwoFingerTripleTap && mCompletedTapCount == targetTapCount;
1104         }
1105 
transitionToDelegatingStateAndClear()1106         void transitionToDelegatingStateAndClear() {
1107             mCompletedTapCount = 0;
1108             transitionTo(mDelegatingState);
1109             sendDelayedMotionEvents();
1110             removePendingDelayedMessages();
1111             mFirstPointerDownLocation.set(Float.NaN, Float.NaN);
1112             mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
1113         }
1114 
transitionToViewportDraggingStateAndClear(MotionEvent down)1115         void transitionToViewportDraggingStateAndClear(MotionEvent down) {
1116 
1117             if (DEBUG_DETECTING) Slog.i(mLogTag, "onTripleTapAndHold()");
1118             final boolean shortcutTriggered = mShortcutTriggered;
1119 
1120             // Only log the 3tap and hold event
1121             if (!shortcutTriggered) {
1122                 final boolean enabled = !isActivated();
1123                 if (mCompletedTapCount == TWO_FINGER_GESTURE_MAX_TAPS - 1) {
1124                     // Two finger triple tap and hold
1125                     mMagnificationLogger.logMagnificationTwoFingerTripleTap(enabled);
1126                 } else {
1127                     // Triple tap and hold also belongs to triple tap event
1128                     mMagnificationLogger.logMagnificationTripleTap(enabled);
1129                 }
1130             }
1131             clear();
1132 
1133             mViewportDraggingState.prepareForZoomInTemporary(shortcutTriggered);
1134             zoomInTemporary(down.getX(), down.getY(), shortcutTriggered);
1135             transitionTo(mViewportDraggingState);
1136         }
1137     }
1138 
1139     /**
1140      * This class handles motion events when the event dispatch has not yet
1141      * determined what the user is doing. It watches for various tap events.
1142      */
1143     class DetectingState implements State, Handler.Callback {
1144 
1145         protected static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1;
1146         protected static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
1147         protected static final int MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE = 3;
1148 
1149         final int mLongTapMinDelay;
1150         final int mSwipeMinDistance;
1151         final int mMultiTapMaxDelay;
1152         final int mMultiTapMaxDistance;
1153 
1154         protected MotionEventInfo mDelayedEventQueue;
1155         protected MotionEvent mLastDown;
1156         protected MotionEvent mPreLastDown;
1157         protected MotionEvent mLastUp;
1158         protected MotionEvent mPreLastUp;
1159 
1160         protected PointF mFirstPointerDownLocation = new PointF(Float.NaN, Float.NaN);
1161         protected PointF mSecondPointerDownLocation = new PointF(Float.NaN, Float.NaN);
1162         protected long mLastDetectingDownEventTime;
1163 
1164         @VisibleForTesting boolean mShortcutTriggered;
1165 
1166         @VisibleForTesting Handler mHandler = new Handler(Looper.getMainLooper(), this);
1167 
DetectingState(Context context)1168         DetectingState(Context context) {
1169             mLongTapMinDelay = ViewConfiguration.getLongPressTimeout();
1170             mMultiTapMaxDelay = ViewConfiguration.getDoubleTapTimeout()
1171                     + context.getResources().getInteger(
1172                     R.integer.config_screen_magnification_multi_tap_adjustment);
1173             mSwipeMinDistance = ViewConfiguration.get(context).getScaledTouchSlop();
1174             mMultiTapMaxDistance = ViewConfiguration.get(context).getScaledDoubleTapSlop();
1175         }
1176 
1177         @Override
handleMessage(Message message)1178         public boolean handleMessage(Message message) {
1179             final int type = message.what;
1180             switch (type) {
1181                 case MESSAGE_ON_TRIPLE_TAP_AND_HOLD: {
1182                     MotionEvent down = (MotionEvent) message.obj;
1183                     transitionToViewportDraggingStateAndClear(down);
1184                     down.recycle();
1185                 }
1186                 break;
1187                 case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
1188                     transitionToDelegatingStateAndClear();
1189                 }
1190                 break;
1191                 case MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE: {
1192                     transitToPanningScalingStateAndClear();
1193                 }
1194                 break;
1195                 default: {
1196                     throw new IllegalArgumentException("Unknown message type: " + type);
1197                 }
1198             }
1199             return true;
1200         }
1201 
1202         // LINT.IfChange(detecting_state)
1203         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)1204         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
1205             cacheDelayedMotionEvent(event, rawEvent, policyFlags);
1206             switch (event.getActionMasked()) {
1207                 case MotionEvent.ACTION_DOWN: {
1208                     mLastDetectingDownEventTime = event.getDownTime();
1209                     mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
1210 
1211                     mFirstPointerDownLocation.set(event.getX(), event.getY());
1212 
1213                     if (!mFullScreenMagnificationController.magnificationRegionContains(
1214                             mDisplayId, event.getX(), event.getY())) {
1215 
1216                         transitionToDelegatingStateAndClear();
1217 
1218                     } else if (isMultiTapTriggered(2 /* taps */)) {
1219 
1220                         // 3tap and hold
1221                         afterLongTapTimeoutTransitionToDraggingState(event);
1222 
1223                     } else if (isTapOutOfDistanceSlop()) {
1224 
1225                         transitionToDelegatingStateAndClear();
1226 
1227                     } else if (mDetectSingleFingerTripleTap
1228                             // If activated, delay an ACTION_DOWN for mMultiTapMaxDelay
1229                             // to ensure reachability of
1230                             // STATE_PANNING_SCALING(triggerable with ACTION_POINTER_DOWN)
1231                             || isActivated()) {
1232 
1233                         afterMultiTapTimeoutTransitionToDelegatingState();
1234 
1235                     } else {
1236 
1237                         // Delegate pending events without delay
1238                         transitionToDelegatingStateAndClear();
1239                     }
1240                 }
1241                 break;
1242                 case ACTION_POINTER_DOWN: {
1243                     if (isActivated() && event.getPointerCount() == 2) {
1244                         storePointerDownLocation(mSecondPointerDownLocation, event);
1245                         mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
1246                                 ViewConfiguration.getTapTimeout());
1247                     } else {
1248                         transitionToDelegatingStateAndClear();
1249                     }
1250                 }
1251                 break;
1252                 case ACTION_POINTER_UP: {
1253                     transitionToDelegatingStateAndClear();
1254                 }
1255                 break;
1256                 case ACTION_MOVE: {
1257                     if (isFingerDown()
1258                             && distance(mLastDown, /* move */ event) > mSwipeMinDistance) {
1259                         // Swipe detected - transition immediately
1260 
1261                         // For convenience, viewport dragging takes precedence
1262                         // over insta-delegating on 3tap&swipe
1263                         // (which is a rare combo to be used aside from magnification)
1264                         if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) {
1265                             transitionToViewportDraggingStateAndClear(event);
1266                         } else if (isActivated() && event.getPointerCount() == 2) {
1267                             if (mOverscrollHandler != null
1268                                     && overscrollState(event, mFirstPointerDownLocation)
1269                                     == OVERSCROLL_VERTICAL_EDGE) {
1270                                 transitionToDelegatingStateAndClear();
1271                             } else {
1272                                 //Primary pointer is swiping, so transit to PanningScalingState
1273                                 transitToPanningScalingStateAndClear();
1274                             }
1275                         } else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
1276                                 && isActivated()
1277                                 && event.getPointerCount() == 1) {
1278                             if (mOverscrollHandler != null
1279                                     && overscrollState(event, mFirstPointerDownLocation)
1280                                     == OVERSCROLL_VERTICAL_EDGE) {
1281                                 transitionToDelegatingStateAndClear();
1282                             } else if (overscrollState(event, mFirstPointerDownLocation)
1283                                     != OVERSCROLL_NONE) {
1284                                 transitionToDelegatingStateAndClear();
1285                             } else {
1286                                 transitToSinglePanningStateAndClear();
1287                             }
1288                         } else {
1289                             transitionToDelegatingStateAndClear();
1290                         }
1291                     } else if (isActivated() && pointerDownValid(mSecondPointerDownLocation)
1292                             && distanceClosestPointerToPoint(
1293                             mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) {
1294                         //Second pointer is swiping, so transit to PanningScalingState
1295                         transitToPanningScalingStateAndClear();
1296                     }
1297                 }
1298                 break;
1299                 case ACTION_UP: {
1300 
1301                     mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
1302 
1303                     if (!mFullScreenMagnificationController.magnificationRegionContains(
1304                             mDisplayId, event.getX(), event.getY())) {
1305                         transitionToDelegatingStateAndClear();
1306 
1307                     } else if (isMultiTapTriggered(3 /* taps */)) {
1308                         onTripleTap(/* up */ event);
1309 
1310                     } else if (
1311                             // Possible to be false on: 3tap&drag -> scale -> PTR_UP -> UP
1312                             isFingerDown()
1313                             //TODO long tap should never happen here
1314                             && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay)
1315                                     || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))) {
1316                         transitionToDelegatingStateAndClear();
1317 
1318                     }
1319                 }
1320                 break;
1321             }
1322         }
1323         // LINT.ThenChange(:detecting_state_with_multi_finger)
1324 
storePointerDownLocation(PointF pointerDownLocation, MotionEvent event)1325         protected void storePointerDownLocation(PointF pointerDownLocation, MotionEvent event) {
1326             final int index = event.getActionIndex();
1327             pointerDownLocation.set(event.getX(index), event.getY(index));
1328         }
1329 
pointerDownValid(PointF pointerDownLocation)1330         protected boolean pointerDownValid(PointF pointerDownLocation) {
1331             return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(
1332                     pointerDownLocation.y));
1333         }
1334 
transitToPanningScalingStateAndClear()1335         protected void transitToPanningScalingStateAndClear() {
1336             transitionTo(mPanningScalingState);
1337             clear();
1338         }
1339 
transitToSinglePanningStateAndClear()1340         protected void transitToSinglePanningStateAndClear() {
1341             transitionTo(mSinglePanningState);
1342             clear();
1343         }
1344 
isMultiTapTriggered(int numTaps)1345         public boolean isMultiTapTriggered(int numTaps) {
1346 
1347             // Shortcut acts as the 2 initial taps
1348             if (mShortcutTriggered) return tapCount() + 2 >= numTaps;
1349 
1350             final boolean multitapTriggered = mDetectSingleFingerTripleTap
1351                     && tapCount() >= numTaps
1352                     && isMultiTap(mPreLastDown, mLastDown)
1353                     && isMultiTap(mPreLastUp, mLastUp);
1354 
1355             // Only log the triple tap event, use numTaps to filter
1356             if (multitapTriggered && numTaps > 2) {
1357                 final boolean enabled = !isActivated();
1358                 mMagnificationLogger.logMagnificationTripleTap(enabled);
1359             }
1360             return multitapTriggered;
1361         }
1362 
isMultiTap(MotionEvent first, MotionEvent second)1363         private boolean isMultiTap(MotionEvent first, MotionEvent second) {
1364             return GestureUtils.isMultiTap(first, second, mMultiTapMaxDelay, mMultiTapMaxDistance);
1365         }
1366 
isFingerDown()1367         public boolean isFingerDown() {
1368             return mLastDown != null;
1369         }
1370 
timeBetween(@ullable MotionEvent a, @Nullable MotionEvent b)1371         protected long timeBetween(@Nullable MotionEvent a, @Nullable MotionEvent b) {
1372             if (a == null && b == null) return 0;
1373             return abs(timeOf(a) - timeOf(b));
1374         }
1375 
1376         /**
1377          * Nullsafe {@link MotionEvent#getEventTime} that interprets null event as something that
1378          * has happened long enough ago to be gone from the event queue.
1379          * Thus the time for a null event is a small number, that is below any other non-null
1380          * event's time.
1381          *
1382          * @return {@link MotionEvent#getEventTime}, or {@link Long#MIN_VALUE} if the event is null
1383          */
timeOf(@ullable MotionEvent event)1384         private long timeOf(@Nullable MotionEvent event) {
1385             return event != null ? event.getEventTime() : Long.MIN_VALUE;
1386         }
1387 
tapCount()1388         public int tapCount() {
1389             return MotionEventInfo.countOf(mDelayedEventQueue, ACTION_UP);
1390         }
1391 
1392         /** -> {@link DelegatingState} */
afterMultiTapTimeoutTransitionToDelegatingState()1393         public void afterMultiTapTimeoutTransitionToDelegatingState() {
1394             mHandler.sendEmptyMessageDelayed(
1395                     MESSAGE_TRANSITION_TO_DELEGATING_STATE,
1396                     mMultiTapMaxDelay);
1397         }
1398 
1399         /** -> {@link ViewportDraggingState} */
afterLongTapTimeoutTransitionToDraggingState(MotionEvent event)1400         public void afterLongTapTimeoutTransitionToDraggingState(MotionEvent event) {
1401             mHandler.sendMessageDelayed(
1402                     mHandler.obtainMessage(MESSAGE_ON_TRIPLE_TAP_AND_HOLD,
1403                             MotionEvent.obtain(event)),
1404                     ViewConfiguration.getLongPressTimeout());
1405         }
1406 
1407         @Override
clear()1408         public void clear() {
1409             setShortcutTriggered(false);
1410             removePendingDelayedMessages();
1411             clearDelayedMotionEvents();
1412             mFirstPointerDownLocation.set(Float.NaN, Float.NaN);
1413             mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
1414         }
1415 
removePendingDelayedMessages()1416         protected void removePendingDelayedMessages() {
1417             mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
1418             mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
1419             mHandler.removeMessages(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE);
1420         }
1421 
cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)1422         protected void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
1423                 int policyFlags) {
1424             if (event.getActionMasked() == ACTION_DOWN) {
1425                 mPreLastDown = mLastDown;
1426                 mLastDown = MotionEvent.obtain(event);
1427             } else if (event.getActionMasked() == ACTION_UP) {
1428                 mPreLastUp = mLastUp;
1429                 mLastUp = MotionEvent.obtain(event);
1430             }
1431 
1432             MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent,
1433                     policyFlags);
1434             if (mDelayedEventQueue == null) {
1435                 mDelayedEventQueue = info;
1436             } else {
1437                 MotionEventInfo tail = mDelayedEventQueue;
1438                 while (tail.mNext != null) {
1439                     tail = tail.mNext;
1440                 }
1441                 tail.mNext = info;
1442             }
1443         }
1444 
sendDelayedMotionEvents()1445         protected void sendDelayedMotionEvents() {
1446             if (mDelayedEventQueue == null) {
1447                 return;
1448             }
1449 
1450             // Adjust down time to prevent subsequent modules being misleading, and also limit
1451             // the maximum offset to mMultiTapMaxDelay to prevent the down time of 2nd tap is
1452             // in the future when multi-tap happens.
1453             final long offset = Math.min(
1454                     SystemClock.uptimeMillis() - mLastDetectingDownEventTime, mMultiTapMaxDelay);
1455 
1456             do {
1457                 MotionEventInfo info = mDelayedEventQueue;
1458                 mDelayedEventQueue = info.mNext;
1459 
1460                 info.event.setDownTime(info.event.getDownTime() + offset);
1461                 handleEventWith(mDelegatingState, info.event, info.rawEvent, info.policyFlags);
1462 
1463                 info.recycle();
1464             } while (mDelayedEventQueue != null);
1465         }
1466 
clearDelayedMotionEvents()1467         protected void clearDelayedMotionEvents() {
1468             while (mDelayedEventQueue != null) {
1469                 MotionEventInfo info = mDelayedEventQueue;
1470                 mDelayedEventQueue = info.mNext;
1471                 info.recycle();
1472             }
1473             mPreLastDown = null;
1474             mPreLastUp = null;
1475             mLastDown = null;
1476             mLastUp = null;
1477         }
1478 
transitionToDelegatingStateAndClear()1479         void transitionToDelegatingStateAndClear() {
1480             transitionTo(mDelegatingState);
1481             sendDelayedMotionEvents();
1482             removePendingDelayedMessages();
1483             mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
1484         }
1485 
1486         /**
1487          * This method could be triggered by both 2 cases.
1488          *      1. direct three tap gesture
1489          *      2. one tap while shortcut triggered (it counts as two taps).
1490          */
onTripleTap(MotionEvent up)1491         protected void onTripleTap(MotionEvent up) {
1492             if (DEBUG_DETECTING) {
1493                 Slog.i(mLogTag, "onTripleTap(); delayed: "
1494                         + MotionEventInfo.toString(mDelayedEventQueue));
1495             }
1496 
1497             // We put mShortcutTriggered into conditions.
1498             // The reason is when the shortcut is triggered,
1499             //   the magnifier is activated and keeps in scale 1.0,
1500             //   and in this case, we still want to zoom on the magnifier.
1501             if (!isActivated() || mShortcutTriggered) {
1502                 mPromptController.showNotificationIfNeeded();
1503                 zoomOn(up.getX(), up.getY());
1504             } else {
1505                 zoomOff();
1506             }
1507 
1508             clear();
1509         }
1510 
isActivated()1511         protected boolean isActivated() {
1512             return mFullScreenMagnificationController.isActivated(mDisplayId);
1513         }
1514 
transitionToViewportDraggingStateAndClear(MotionEvent down)1515         void transitionToViewportDraggingStateAndClear(MotionEvent down) {
1516 
1517             if (DEBUG_DETECTING) Slog.i(mLogTag, "onTripleTapAndHold()");
1518             final boolean shortcutTriggered = mShortcutTriggered;
1519 
1520             // Only log the 3tap and hold event
1521             if (!shortcutTriggered) {
1522                 // Triple tap and hold also belongs to triple tap event
1523                 final boolean enabled = !isActivated();
1524                 mMagnificationLogger.logMagnificationTripleTap(enabled);
1525             }
1526             clear();
1527 
1528             mViewportDraggingState.prepareForZoomInTemporary(shortcutTriggered);
1529             zoomInTemporary(down.getX(), down.getY(), shortcutTriggered);
1530             transitionTo(mViewportDraggingState);
1531         }
1532 
1533         @Override
toString()1534         public String toString() {
1535             return "DetectingState{"
1536                     + "tapCount()=" + tapCount()
1537                     + ", mShortcutTriggered=" + mShortcutTriggered
1538                     + ", mDelayedEventQueue=" + MotionEventInfo.toString(mDelayedEventQueue)
1539                     + '}';
1540         }
1541 
toggleShortcutTriggered()1542         void toggleShortcutTriggered() {
1543             setShortcutTriggered(!mShortcutTriggered);
1544         }
1545 
setShortcutTriggered(boolean state)1546         void setShortcutTriggered(boolean state) {
1547             if (mShortcutTriggered == state) {
1548                 return;
1549             }
1550             if (DEBUG_DETECTING) Slog.i(mLogTag, "setShortcutTriggered(" + state + ")");
1551 
1552             mShortcutTriggered = state;
1553         }
1554 
isShortcutTriggered()1555         private boolean isShortcutTriggered() {
1556             return mShortcutTriggered;
1557         }
1558 
1559         /**
1560          * Detects if last action down is out of distance slop between with previous
1561          * one, when triple tap is enabled.
1562          *
1563          * @return true if tap is out of distance slop
1564          */
isTapOutOfDistanceSlop()1565         boolean isTapOutOfDistanceSlop() {
1566             if (!mDetectSingleFingerTripleTap) return false;
1567             if (mPreLastDown == null || mLastDown == null) {
1568                 return false;
1569             }
1570             final boolean outOfDistanceSlop =
1571                     GestureUtils.distance(mPreLastDown, mLastDown) > mMultiTapMaxDistance;
1572             if (tapCount() > 0) {
1573                 return outOfDistanceSlop;
1574             }
1575             // There's no tap in the queue here. We still need to check if this is the case that
1576             // user tap screen quickly and out of distance slop.
1577             if (outOfDistanceSlop
1578                     && !GestureUtils.isTimedOut(mPreLastDown, mLastDown, mMultiTapMaxDelay)) {
1579                 return true;
1580             }
1581             return false;
1582         }
1583     }
1584 
zoomInTemporary(float centerX, float centerY, boolean shortcutTriggered)1585     private void zoomInTemporary(float centerX, float centerY, boolean shortcutTriggered) {
1586         final float currentScale = mFullScreenMagnificationController.getScale(mDisplayId);
1587         final float persistedScale = MathUtils.constrain(
1588                 mFullScreenMagnificationController.getPersistedScale(mDisplayId),
1589                 MIN_SCALE, MAX_SCALE);
1590 
1591         final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId);
1592         final boolean isShortcutTriggered = shortcutTriggered;
1593         final boolean isZoomedOutFromService =
1594                 mFullScreenMagnificationController.isZoomedOutFromService(mDisplayId);
1595 
1596         boolean zoomInWithPersistedScale =
1597                 !isActivated || isShortcutTriggered || isZoomedOutFromService;
1598         final float scale = zoomInWithPersistedScale ?  persistedScale : (currentScale + 1.0f);
1599         zoomToScale(scale, centerX, centerY);
1600     }
1601 
zoomOn(float centerX, float centerY)1602     private void zoomOn(float centerX, float centerY) {
1603         if (DEBUG_DETECTING) {
1604             Slog.i(mLogTag, "zoomOn(" + centerX + ", " + centerY + ")");
1605         }
1606 
1607         final float scale = MathUtils.constrain(
1608                 mFullScreenMagnificationController.getPersistedScale(mDisplayId),
1609                 MIN_SCALE, MAX_SCALE);
1610         zoomToScale(scale, centerX, centerY);
1611     }
1612 
zoomToScale(float scale, float centerX, float centerY)1613     private void zoomToScale(float scale, float centerX, float centerY) {
1614         scale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
1615         mFullScreenMagnificationController.setScaleAndCenter(mDisplayId,
1616                 scale, centerX, centerY,
1617                 /* animate */ true,
1618                 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
1619     }
1620 
zoomOff()1621     private void zoomOff() {
1622         if (DEBUG_DETECTING) {
1623             Slog.i(mLogTag, "zoomOff()");
1624         }
1625         mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true);
1626     }
1627 
recycleAndNullify(@ullable MotionEvent event)1628     private static MotionEvent recycleAndNullify(@Nullable MotionEvent event) {
1629         if (event != null) {
1630             event.recycle();
1631         }
1632         return null;
1633     }
1634 
1635     @Override
toString()1636     public String toString() {
1637         return "MagnificationGesture{"
1638                 + "mDetectingState=" + mDetectingState
1639                 + ", mDelegatingState=" + mDelegatingState
1640                 + ", mMagnifiedInteractionState=" + mPanningScalingState
1641                 + ", mViewportDraggingState=" + mViewportDraggingState
1642                 + ", mSinglePanningState=" + mSinglePanningState
1643                 + ", mDetectSingleFingerTripleTap=" + mDetectSingleFingerTripleTap
1644                 + ", mDetectShortcutTrigger=" + mDetectShortcutTrigger
1645                 + ", mCurrentState=" + State.nameOf(mCurrentState)
1646                 + ", mPreviousState=" + State.nameOf(mPreviousState)
1647                 + ", mMagnificationController=" + mFullScreenMagnificationController
1648                 + ", mDisplayId=" + mDisplayId
1649                 + ", mIsSinglePanningEnabled="
1650                 + mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
1651                 + ", mOverscrollHandler=" + mOverscrollHandler
1652                 + '}';
1653     }
1654 
overscrollState(MotionEvent event, PointF firstPointerDownLocation)1655     private int overscrollState(MotionEvent event, PointF firstPointerDownLocation) {
1656         if (!pointerValid(firstPointerDownLocation)) {
1657             return OVERSCROLL_NONE;
1658         }
1659         float dX = event.getX() - firstPointerDownLocation.x;
1660         float dY = event.getY() - firstPointerDownLocation.y;
1661         if (isAtLeftEdge() && dX > 0) {
1662             return OVERSCROLL_LEFT_EDGE;
1663         } else if (isAtRightEdge() && dX < 0) {
1664             return OVERSCROLL_RIGHT_EDGE;
1665         } else if ((isAtTopEdge() && dY > 0) || (isAtBottomEdge() && dY < 0)) {
1666             return OVERSCROLL_VERTICAL_EDGE;
1667         }
1668         return OVERSCROLL_NONE;
1669     }
1670 
isAtLeftEdge()1671     private boolean isAtLeftEdge() {
1672         return mFullScreenMagnificationController.isAtLeftEdge(mDisplayId, mOverscrollEdgeSlop);
1673     }
1674 
isAtRightEdge()1675     private boolean isAtRightEdge() {
1676         return mFullScreenMagnificationController.isAtRightEdge(mDisplayId, mOverscrollEdgeSlop);
1677     }
1678 
isAtTopEdge()1679     private boolean isAtTopEdge() {
1680         return mFullScreenMagnificationController.isAtTopEdge(mDisplayId, mOverscrollEdgeSlop);
1681     }
1682 
isAtBottomEdge()1683     private boolean isAtBottomEdge() {
1684         return mFullScreenMagnificationController.isAtBottomEdge(mDisplayId, mOverscrollEdgeSlop);
1685     }
1686 
pointerValid(PointF pointerDownLocation)1687     private boolean pointerValid(PointF pointerDownLocation) {
1688         return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(pointerDownLocation.y));
1689     }
1690 
1691     private static final class MotionEventInfo {
1692 
1693         private static final int MAX_POOL_SIZE = 10;
1694         private static final Object sLock = new Object();
1695         private static MotionEventInfo sPool;
1696         private static int sPoolSize;
1697 
1698         private MotionEventInfo mNext;
1699         private boolean mInPool;
1700 
1701         public MotionEvent event;
1702         public MotionEvent rawEvent;
1703         public int policyFlags;
1704 
obtain(MotionEvent event, MotionEvent rawEvent, int policyFlags)1705         public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent,
1706                 int policyFlags) {
1707             synchronized (sLock) {
1708                 MotionEventInfo info = obtainInternal();
1709                 info.initialize(event, rawEvent, policyFlags);
1710                 return info;
1711             }
1712         }
1713 
1714         @NonNull
obtainInternal()1715         private static MotionEventInfo obtainInternal() {
1716             MotionEventInfo info;
1717             if (sPoolSize > 0) {
1718                 sPoolSize--;
1719                 info = sPool;
1720                 sPool = info.mNext;
1721                 info.mNext = null;
1722                 info.mInPool = false;
1723             } else {
1724                 info = new MotionEventInfo();
1725             }
1726             return info;
1727         }
1728 
initialize(MotionEvent event, MotionEvent rawEvent, int policyFlags)1729         private void initialize(MotionEvent event, MotionEvent rawEvent,
1730                 int policyFlags) {
1731             this.event = MotionEvent.obtain(event);
1732             this.rawEvent = MotionEvent.obtain(rawEvent);
1733             this.policyFlags = policyFlags;
1734         }
1735 
recycle()1736         public void recycle() {
1737             synchronized (sLock) {
1738                 if (mInPool) {
1739                     throw new IllegalStateException("Already recycled.");
1740                 }
1741                 clear();
1742                 if (sPoolSize < MAX_POOL_SIZE) {
1743                     sPoolSize++;
1744                     mNext = sPool;
1745                     sPool = this;
1746                     mInPool = true;
1747                 }
1748             }
1749         }
1750 
clear()1751         private void clear() {
1752             event = recycleAndNullify(event);
1753             rawEvent = recycleAndNullify(rawEvent);
1754             policyFlags = 0;
1755         }
1756 
countOf(MotionEventInfo info, int eventType)1757         static int countOf(MotionEventInfo info, int eventType) {
1758             if (info == null) return 0;
1759             return (info.event.getAction() == eventType ? 1 : 0)
1760                     + countOf(info.mNext, eventType);
1761         }
1762 
toString(MotionEventInfo info)1763         public static String toString(MotionEventInfo info) {
1764             return info == null
1765                     ? ""
1766                     : MotionEvent.actionToString(info.event.getAction()).replace("ACTION_", "")
1767                             + " " + MotionEventInfo.toString(info.mNext);
1768         }
1769     }
1770 
1771     /**
1772      * BroadcastReceiver used to cancel the magnification shortcut when the screen turns off
1773      */
1774     private static class ScreenStateReceiver extends BroadcastReceiver {
1775         private final Context mContext;
1776         private final FullScreenMagnificationGestureHandler mGestureHandler;
1777 
ScreenStateReceiver(Context context, FullScreenMagnificationGestureHandler gestureHandler)1778         ScreenStateReceiver(Context context,
1779                 FullScreenMagnificationGestureHandler gestureHandler) {
1780             mContext = context;
1781             mGestureHandler = gestureHandler;
1782         }
1783 
register()1784         public void register() {
1785             mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
1786         }
1787 
unregister()1788         public void unregister() {
1789             mContext.unregisterReceiver(this);
1790         }
1791 
1792         @Override
onReceive(Context context, Intent intent)1793         public void onReceive(Context context, Intent intent) {
1794             mGestureHandler.mDetectingState.setShortcutTriggered(false);
1795         }
1796     }
1797 
1798     /**
1799      * Indicates an error with a gesture handler or state.
1800      */
1801     private static class GestureException extends Exception {
1802 
GestureException(String message)1803         GestureException(String message) {
1804             super(message);
1805         }
1806     }
1807 
1808     /** Call during MOVE events for a panning gesture. */
onPan(MotionEvent event)1809     private void onPan(MotionEvent event) {
1810         if (!Flags.fullscreenFlingGesture()) {
1811             return;
1812         }
1813 
1814         if (mVelocityTracker == null) {
1815             mVelocityTracker = VelocityTracker.obtain();
1816         }
1817         mVelocityTracker.addMovement(event);
1818     }
1819 
1820     /**
1821      * Call during UP events for a panning gesture, so we can detect a fling and play a physics-
1822      * based fling animation.
1823      */
onPanningFinished(MotionEvent event)1824     private void onPanningFinished(MotionEvent event) {
1825         if (!Flags.fullscreenFlingGesture()) {
1826             return;
1827         }
1828 
1829         if (mVelocityTracker == null) {
1830             Log.e(mLogTag, "onPanningFinished: mVelocityTracker is null");
1831             return;
1832         }
1833         mVelocityTracker.addMovement(event);
1834         mVelocityTracker.computeCurrentVelocity(/* units= */ 1000, mMaximumVelocity);
1835 
1836         float xPixelsPerSecond = mVelocityTracker.getXVelocity();
1837         float yPixelsPerSecond = mVelocityTracker.getYVelocity();
1838 
1839         mVelocityTracker.recycle();
1840         mVelocityTracker = null;
1841 
1842         if (DEBUG_PANNING_SCALING) {
1843             Slog.v(
1844                     mLogTag,
1845                     "onPanningFinished: pixelsPerSecond: "
1846                             + xPixelsPerSecond
1847                             + ", "
1848                             + yPixelsPerSecond
1849                             + " mMinimumVelocity: "
1850                             + mMinimumVelocity);
1851         }
1852 
1853         if ((Math.abs(yPixelsPerSecond) > mMinimumVelocity)
1854                 || (Math.abs(xPixelsPerSecond) > mMinimumVelocity)) {
1855             mFullScreenMagnificationController.startFling(
1856                     mDisplayId,
1857                     xPixelsPerSecond,
1858                     yPixelsPerSecond,
1859                     AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
1860         }
1861     }
1862 
cancelFling()1863     private void cancelFling() {
1864         if (!Flags.fullscreenFlingGesture()) {
1865             return;
1866         }
1867 
1868         mFullScreenMagnificationController.cancelFling(
1869                     mDisplayId,
1870                     AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
1871     }
1872 
1873     final class SinglePanningState extends SimpleOnGestureListener implements State {
1874 
1875         private final GestureDetector mScrollGestureDetector;
1876         private MotionEventInfo mEvent;
SinglePanningState(Context context)1877         SinglePanningState(Context context) {
1878             mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
1879         }
1880 
1881         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)1882         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
1883             int action = event.getActionMasked();
1884             switch (action) {
1885                 case ACTION_UP:
1886                     onPanningFinished(event);
1887                     // fall-through!
1888                 case ACTION_CANCEL:
1889                     if (mOverscrollHandler != null) {
1890                         mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
1891                         mOverscrollHandler.clearEdgeState();
1892                     }
1893                     transitionTo(mDetectingState);
1894                     break;
1895             }
1896         }
1897 
1898         @Override
onScroll( MotionEvent first, MotionEvent second, float distanceX, float distanceY)1899         public boolean onScroll(
1900                 MotionEvent first, MotionEvent second, float distanceX, float distanceY) {
1901             if (mCurrentState != mSinglePanningState) {
1902                 return true;
1903             }
1904             onPan(second);
1905             mFullScreenMagnificationController.offsetMagnifiedRegion(
1906                     mDisplayId,
1907                     distanceX,
1908                     distanceY,
1909                     AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
1910             if (DEBUG_PANNING_SCALING) {
1911                 Slog.i(
1912                         mLogTag,
1913                         "SinglePanningState Panned content by scrollX: "
1914                                 + distanceX
1915                                 + " scrollY: "
1916                                 + distanceY
1917                                 + " isAtEdge: "
1918                                 + mFullScreenMagnificationController.isAtEdge(mDisplayId));
1919             }
1920             if (mOverscrollHandler != null) {
1921                 mOverscrollHandler.onScrollStateChanged(first, second);
1922             }
1923             return /* event consumed: */ true;
1924         }
1925 
1926         @Override
toString()1927         public String toString() {
1928             return "SinglePanningState{"
1929                     + "isEdgeOfView="
1930                     + mFullScreenMagnificationController.isAtEdge(mDisplayId);
1931         }
1932     }
1933 
1934     /** Overscroll Handler handles the logic when user is at the edge and scrolls past an edge */
1935     final class OverscrollHandler {
1936 
1937         @VisibleForTesting int mOverscrollState;
1938 
1939         // mPivotEdge is the point on the edge of the screen when the magnified view hits the edge
1940         // This point sets the center of magnified view when warp/scale effect is triggered
1941         private final PointF mPivotEdge;
1942 
1943         // mReachedEdgeCoord is the user's pointer location on the screen when the magnified view
1944         // has hit the edge
1945         private final PointF mReachedEdgeCoord;
1946 
1947         // mEdgeCooldown value will be set to true when user hits the edge and will be set to false
1948         // once the user moves x distance away from the edge. This is so that vibrating haptic
1949         // doesn't get triggered by slight movements
1950         private boolean mEdgeCooldown;
1951 
OverscrollHandler()1952         OverscrollHandler() {
1953             mOverscrollState = OVERSCROLL_NONE;
1954             mPivotEdge = new PointF(Float.NaN, Float.NaN);
1955             mReachedEdgeCoord = new PointF(Float.NaN, Float.NaN);
1956             mEdgeCooldown = false;
1957         }
1958 
warpEffectReset(MotionEvent second)1959         protected boolean warpEffectReset(MotionEvent second) {
1960             float scale = calculateOverscrollScale(second);
1961             if (scale < 0) return false;
1962             mFullScreenMagnificationController.setScaleAndCenter(
1963                     /* displayId= */ mDisplayId,
1964                     /* scale= */ scale,
1965                     /* centerX= */ mPivotEdge.x,
1966                     /* centerY= */ mPivotEdge.y,
1967                     /* animate= */ true,
1968                     /* id= */ AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
1969             if (scale == 1.0f) {
1970                 return true;
1971             }
1972             return false;
1973         }
1974 
calculateOverscrollScale(MotionEvent second)1975         private float calculateOverscrollScale(MotionEvent second) {
1976             // if at left and overshootDistX is negative or if at right and overshootDistX is
1977             // positive then user is not in overscroll state anymore overscroll state. Reset
1978             // overscroll values by clearing
1979             float overshootDistX = second.getX() - mReachedEdgeCoord.x;
1980             if ((mOverscrollState == OVERSCROLL_LEFT_EDGE && overshootDistX < 0)
1981                     || (mOverscrollState == OVERSCROLL_RIGHT_EDGE && overshootDistX > 0)) {
1982                 clearEdgeState();
1983                 return -1.0f;
1984             }
1985             float overshootDistY = second.getY() - mReachedEdgeCoord.y;
1986             float overshootDist = (float) (Math.hypot(abs(overshootDistX), abs(overshootDistY)));
1987             Rect bounds = new Rect();
1988             mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds);
1989             float overShootFraction = overshootDist / (float) bounds.width();
1990             float minDist = 0.05f * bounds.width();
1991             if (mEdgeCooldown && (overshootDist > minDist)) {
1992                 mEdgeCooldown = false;
1993             }
1994             float scale = (1 - overShootFraction) * getSensitivityScale();
1995             scale =
1996                     MathUtils.constrain(
1997                             /* amount= */ scale,
1998                             /* low= */ 1.0f,
1999                             /* high= */ mFullScreenMagnificationController.getPersistedScale(
2000                                     mDisplayId));
2001             return scale;
2002         }
2003 
getSensitivityScale()2004         private float getSensitivityScale() {
2005             float magnificationScale =
2006                     mFullScreenMagnificationController.getPersistedScale(mDisplayId);
2007             float sensitivityFactor = 0.0f;
2008             if (magnificationScale < 1.7f) {
2009                 sensitivityFactor = 1.0f;
2010             } else if (magnificationScale < 2.0f) {
2011                 sensitivityFactor = 1.0f;
2012             } else if (magnificationScale < 2.2f) {
2013                 sensitivityFactor = 0.95f;
2014             } else if (magnificationScale < 2.5f) {
2015                 sensitivityFactor = 1.1f;
2016             } else if (magnificationScale < 2.7f) {
2017                 sensitivityFactor = 1.3f;
2018             } else if (magnificationScale < 3.0f) {
2019                 sensitivityFactor = 1.0f;
2020             } else {
2021                 sensitivityFactor = 1.0f;
2022             }
2023             return magnificationScale * sensitivityFactor;
2024         }
2025 
vibrateIfNeeded(MotionEvent event)2026         private void vibrateIfNeeded(MotionEvent event) {
2027             if (mOverscrollState != OVERSCROLL_NONE) {
2028                 return;
2029             }
2030             if ((isAtLeftEdge() || isAtRightEdge()) && !mEdgeCooldown) {
2031                 mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
2032             }
2033         }
2034 
setPivotEdge(MotionEvent event)2035         private void setPivotEdge(MotionEvent event) {
2036             if (!pointerValid(mPivotEdge)) {
2037                 Rect bounds = new Rect();
2038                 mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds);
2039                 if (mOverscrollState == OVERSCROLL_LEFT_EDGE) {
2040                     mPivotEdge.set(
2041                             bounds.left, mFullScreenMagnificationController.getCenterY(mDisplayId));
2042                 } else if (mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
2043                     mPivotEdge.set(
2044                             bounds.right,
2045                             mFullScreenMagnificationController.getCenterY(mDisplayId));
2046                 }
2047                 mReachedEdgeCoord.set(event.getX(), event.getY());
2048                 mEdgeCooldown = true;
2049             }
2050         }
2051 
onScrollStateChanged(MotionEvent first, MotionEvent second)2052         private void onScrollStateChanged(MotionEvent first, MotionEvent second) {
2053             if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
2054                 vibrateIfNeeded(second);
2055                 setPivotEdge(second);
2056             }
2057             switch (mOverscrollState) {
2058                 case OVERSCROLL_NONE:
2059                     onNoOverscroll(first, second);
2060                     break;
2061                 case OVERSCROLL_VERTICAL_EDGE:
2062                     onVerticalOverscroll();
2063                     break;
2064                 case OVERSCROLL_LEFT_EDGE:
2065                 case OVERSCROLL_RIGHT_EDGE:
2066                     onHorizontalOverscroll(second);
2067                     break;
2068                 default:
2069                     Slog.d(mLogTag, "Invalid overscroll state");
2070                     break;
2071             }
2072         }
2073 
onNoOverscroll(MotionEvent first, MotionEvent second)2074         public void onNoOverscroll(MotionEvent first, MotionEvent second) {
2075             mOverscrollState = overscrollState(second, new PointF(first.getX(), first.getY()));
2076         }
2077 
onVerticalOverscroll()2078         public void onVerticalOverscroll() {
2079             clearEdgeState();
2080             transitionTo(mDelegatingState);
2081         }
2082 
onHorizontalOverscroll(MotionEvent second)2083         public void onHorizontalOverscroll(MotionEvent second) {
2084             boolean reset = warpEffectReset(second);
2085             if (reset) {
2086                 mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true);
2087                 clearEdgeState();
2088                 transitionTo(mDelegatingState);
2089             }
2090         }
2091 
setScaleAndCenterToEdgeIfNeeded()2092         private void setScaleAndCenterToEdgeIfNeeded() {
2093             if (mOverscrollState == OVERSCROLL_LEFT_EDGE
2094                     || mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
2095                 mFullScreenMagnificationController.setScaleAndCenter(
2096                         mDisplayId,
2097                         mFullScreenMagnificationController.getPersistedScale(mDisplayId),
2098                         mPivotEdge.x,
2099                         mPivotEdge.y,
2100                         true,
2101                         AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
2102             }
2103         }
2104 
clearEdgeState()2105         private void clearEdgeState() {
2106             mOverscrollState = OVERSCROLL_NONE;
2107             mPivotEdge.set(Float.NaN, Float.NaN);
2108             mReachedEdgeCoord.set(Float.NaN, Float.NaN);
2109             mEdgeCooldown = false;
2110         }
2111 
2112         @Override
toString()2113         public String toString() {
2114             return "OverscrollHandler {"
2115                     + "mOverscrollState="
2116                     + mOverscrollState
2117                     + "mPivotEdge.x="
2118                     + mPivotEdge.x
2119                     + "mPivotEdge.y="
2120                     + mPivotEdge.y
2121                     + "mReachedEdgeCoord.x="
2122                     + mReachedEdgeCoord.x
2123                     + "mReachedEdgeCoord.y="
2124                     + mReachedEdgeCoord.y
2125                     + "mEdgeCooldown="
2126                     + mEdgeCooldown
2127                     + "}";
2128         }
2129     }
2130 }
2131