1 /*
2  * Copyright (C) 2020 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_MOVE;
22 import static android.view.MotionEvent.ACTION_UP;
23 
24 import static java.util.Arrays.asList;
25 import static java.util.Arrays.copyOfRange;
26 
27 import android.annotation.Nullable;
28 import android.annotation.UiContext;
29 import android.content.Context;
30 import android.graphics.Point;
31 import android.os.SystemClock;
32 import android.provider.Settings;
33 import android.util.MathUtils;
34 import android.util.Slog;
35 import android.view.Display;
36 import android.view.MotionEvent;
37 
38 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.server.accessibility.AccessibilityTraceManager;
41 import com.android.server.accessibility.EventStreamTransformation;
42 import com.android.server.accessibility.gestures.MultiTap;
43 import com.android.server.accessibility.gestures.MultiTapAndHold;
44 
45 import java.util.List;
46 
47 /**
48  * This class handles window magnification in response to touch events and shortcut.
49  *
50  * The behavior is as follows:
51  *
52  * <ol>
53  *   <li> 1. Toggle Window magnification by triple-tap gesture shortcut. It is triggered via
54  *   {@link #onTripleTap(MotionEvent)}.
55  *   <li> 2. Toggle Window magnification by tapping shortcut. It is triggered via
56  *   {@link #notifyShortcutTriggered()}.
57  *   <li> When the window magnifier is visible, pinching with any number of additional fingers
58  *   would adjust the magnification scale .<strong>Note</strong> that this operation is valid only
59  *   when at least one finger is in the window.
60  *   <li> When the window magnifier is visible, to do scrolling to move the window magnifier,
61  *   the user can use two or more fingers and at least one of them is inside the window.
62  *   <br><strong>Note</strong> that the offset of this callback is opposed to moving direction.
63  *  The operation becomes invalid after performing scaling operation until all fingers are
64  *  lifted.
65  * </ol>
66  */
67 @SuppressWarnings("WeakerAccess")
68 public class WindowMagnificationGestureHandler extends MagnificationGestureHandler {
69 
70     private static final boolean DEBUG_STATE_TRANSITIONS = false | DEBUG_ALL;
71     private static final boolean DEBUG_DETECTING = false | DEBUG_ALL;
72 
73     //Ensure the range has consistency with FullScreenMagnificationGestureHandler.
74     private static final float MIN_SCALE = 1.0f;
75     private static final float MAX_SCALE = MagnificationScaleProvider.MAX_SCALE;
76 
77     private final WindowMagnificationManager mWindowMagnificationMgr;
78     @VisibleForTesting
79     final DelegatingState mDelegatingState;
80     @VisibleForTesting
81     final DetectingState mDetectingState;
82     @VisibleForTesting
83     final PanningScalingGestureState mObservePanningScalingState;
84     @VisibleForTesting
85     final ViewportDraggingState mViewportDraggingState;
86 
87     @VisibleForTesting
88     State mCurrentState;
89     @VisibleForTesting
90     State mPreviousState;
91 
92     private MotionEventDispatcherDelegate mMotionEventDispatcherDelegate;
93     private final Context mContext;
94     private final Point mTempPoint = new Point();
95 
96     private long mTripleTapAndHoldStartedTime = 0;
97 
WindowMagnificationGestureHandler(@iContext Context context, WindowMagnificationManager windowMagnificationMgr, AccessibilityTraceManager trace, Callback callback, boolean detectTripleTap, boolean detectShortcutTrigger, int displayId)98     public WindowMagnificationGestureHandler(@UiContext Context context,
99             WindowMagnificationManager windowMagnificationMgr,
100             AccessibilityTraceManager trace,
101             Callback callback,
102             boolean detectTripleTap, boolean detectShortcutTrigger, int displayId) {
103         super(displayId, detectTripleTap, detectShortcutTrigger, trace, callback);
104         if (DEBUG_ALL) {
105             Slog.i(mLogTag,
106                     "WindowMagnificationGestureHandler() , displayId = " + displayId + ")");
107         }
108         mContext = context;
109         mWindowMagnificationMgr = windowMagnificationMgr;
110         mMotionEventDispatcherDelegate = new MotionEventDispatcherDelegate(context,
111                 (event, rawEvent, policyFlags) -> dispatchTransformedEvent(event, rawEvent,
112                         policyFlags));
113         mDelegatingState = new DelegatingState(mMotionEventDispatcherDelegate);
114         mDetectingState = new DetectingState(context, mDetectTripleTap);
115         mViewportDraggingState = new ViewportDraggingState();
116         mObservePanningScalingState = new PanningScalingGestureState(
117                 new PanningScalingHandler(context, MAX_SCALE, MIN_SCALE, true,
118                         new PanningScalingHandler.MagnificationDelegate() {
119                             @Override
120                             public boolean processScroll(int displayId, float distanceX,
121                                     float distanceY) {
122                                 return mWindowMagnificationMgr.processScroll(displayId, distanceX,
123                                         distanceY);
124                             }
125 
126                             @Override
127                             public void setScale(int displayId, float scale) {
128                                 mWindowMagnificationMgr.setScale(displayId, scale);
129                             }
130 
131                             @Override
132                             public float getScale(int displayId) {
133                                 return mWindowMagnificationMgr.getScale(displayId);
134                             }
135                         }));
136 
137         transitionTo(mDetectingState);
138     }
139 
140     @Override
onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags)141     void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
142         // To keep InputEventConsistencyVerifiers within GestureDetectors happy.
143         mObservePanningScalingState.mPanningScalingHandler.onTouchEvent(event);
144         mCurrentState.onMotionEvent(event, rawEvent, policyFlags);
145     }
146 
147     @Override
clearEvents(int inputSource)148     public void clearEvents(int inputSource) {
149         if (inputSource == SOURCE_TOUCHSCREEN) {
150             resetToDetectState();
151         }
152         super.clearEvents(inputSource);
153     }
154 
155     @Override
onDestroy()156     public void onDestroy() {
157         if (DEBUG_ALL) {
158             Slog.i(mLogTag, "onDestroy(); delayed = "
159                     + mDetectingState.toString());
160         }
161         mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, true);
162         resetToDetectState();
163     }
164 
165     @Override
handleShortcutTriggered()166     public void handleShortcutTriggered() {
167         final Point screenSize = mTempPoint;
168         getScreenSize(mTempPoint);
169         toggleMagnification(screenSize.x / 2.0f, screenSize.y / 2.0f,
170                 WindowMagnificationManager.WINDOW_POSITION_AT_CENTER);
171     }
172 
getScreenSize(Point outSize)173     private  void getScreenSize(Point outSize) {
174         final Display display = mContext.getDisplay();
175         display.getRealSize(outSize);
176     }
177 
178     @Override
getMode()179     public int getMode() {
180         return Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
181     }
182 
enableWindowMagnifier(float centerX, float centerY, @WindowMagnificationManager.WindowPosition int windowPosition)183     private void enableWindowMagnifier(float centerX, float centerY,
184             @WindowMagnificationManager.WindowPosition int windowPosition) {
185         if (DEBUG_ALL) {
186             Slog.i(mLogTag, "enableWindowMagnifier :"
187                     + centerX + ", " + centerY + ", " + windowPosition);
188         }
189 
190         final float scale = MathUtils.constrain(
191                 mWindowMagnificationMgr.getPersistedScale(mDisplayId), MIN_SCALE, MAX_SCALE);
192         mWindowMagnificationMgr.enableWindowMagnification(mDisplayId, scale, centerX, centerY,
193                 windowPosition);
194     }
195 
disableWindowMagnifier()196     private void disableWindowMagnifier() {
197         if (DEBUG_ALL) {
198             Slog.i(mLogTag, "disableWindowMagnifier()");
199         }
200         mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, false);
201     }
202 
toggleMagnification(float centerX, float centerY, @WindowMagnificationManager.WindowPosition int windowPosition)203     private void toggleMagnification(float centerX, float centerY,
204             @WindowMagnificationManager.WindowPosition int windowPosition) {
205         if (mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId)) {
206             disableWindowMagnifier();
207         } else {
208             enableWindowMagnifier(centerX, centerY, windowPosition);
209         }
210     }
211 
onTripleTap(MotionEvent up)212     private void onTripleTap(MotionEvent up) {
213         if (DEBUG_DETECTING) {
214             Slog.i(mLogTag, "onTripleTap()");
215         }
216         toggleMagnification(up.getX(), up.getY(),
217                 WindowMagnificationManager.WINDOW_POSITION_AT_CENTER);
218     }
219 
220     @VisibleForTesting
onTripleTapAndHold(MotionEvent up)221     void onTripleTapAndHold(MotionEvent up) {
222         if (DEBUG_DETECTING) {
223             Slog.i(mLogTag, "onTripleTapAndHold()");
224         }
225         mViewportDraggingState.mEnabledBeforeDrag =
226                 mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId);
227         enableWindowMagnifier(up.getX(), up.getY(),
228                 WindowMagnificationManager.WINDOW_POSITION_AT_TOP_LEFT);
229         mTripleTapAndHoldStartedTime = SystemClock.uptimeMillis();
230         transitionTo(mViewportDraggingState);
231     }
232 
233     @VisibleForTesting
releaseTripleTapAndHold()234     void releaseTripleTapAndHold() {
235         if (!mViewportDraggingState.mEnabledBeforeDrag) {
236             mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, true);
237         }
238         transitionTo(mDetectingState);
239         if (mTripleTapAndHoldStartedTime != 0) {
240             final long duration = SystemClock.uptimeMillis() - mTripleTapAndHoldStartedTime;
241             logMagnificationTripleTapAndHoldSession(duration);
242             mTripleTapAndHoldStartedTime = 0;
243         }
244     }
245 
246     /**
247      * Logs the duration for the magnification session which is activated by the triple tap and
248      * hold gesture.
249      *
250      * @param duration The duration of a triple-tap-and-hold activation session.
251      */
252     @VisibleForTesting
logMagnificationTripleTapAndHoldSession(long duration)253     void logMagnificationTripleTapAndHoldSession(long duration) {
254         AccessibilityStatsLogUtils.logMagnificationTripleTapAndHoldSession(duration);
255     }
256 
resetToDetectState()257     void resetToDetectState() {
258         transitionTo(mDetectingState);
259     }
260 
261     /**
262      * An interface to intercept the {@link MotionEvent} for gesture detection. The intercepted
263      * events should be delivered to next {@link EventStreamTransformation} with {
264      * {@link EventStreamTransformation#onMotionEvent(MotionEvent, MotionEvent, int)}} if there is
265      * no valid gestures.
266      */
267     interface State {
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)268         void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags);
269 
clear()270         default void clear() {
271         }
272 
onEnter()273         default void onEnter() {
274         }
275 
onExit()276         default void onExit() {
277         }
278 
name()279         default String name() {
280             return getClass().getSimpleName();
281         }
282 
nameOf(@ullable State s)283         static String nameOf(@Nullable State s) {
284             return s != null ? s.name() : "null";
285         }
286     }
287 
transitionTo(State state)288     private void transitionTo(State state) {
289         if (DEBUG_STATE_TRANSITIONS) {
290             Slog.i(mLogTag, "state transition: " + (State.nameOf(mCurrentState) + " -> "
291                     + State.nameOf(state) + " at "
292                     + asList(copyOfRange(new RuntimeException().getStackTrace(), 1, 5)))
293                     .replace(getClass().getName(), ""));
294         }
295         mPreviousState = mCurrentState;
296         if (mPreviousState != null) {
297             mPreviousState.onExit();
298         }
299         mCurrentState = state;
300         if (mCurrentState != null) {
301             mCurrentState.onEnter();
302         }
303     }
304 
305     /**
306      * When entering this state, {@link PanningScalingHandler} will be enabled to address the
307      * gestures until receiving {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL}.
308      * When leaving this state, current scale will be persisted.
309      */
310     final class PanningScalingGestureState implements State {
311         private final PanningScalingHandler mPanningScalingHandler;
312 
PanningScalingGestureState(PanningScalingHandler panningScalingHandler)313         PanningScalingGestureState(PanningScalingHandler panningScalingHandler) {
314             mPanningScalingHandler = panningScalingHandler;
315         }
316 
317         @Override
onEnter()318         public void onEnter() {
319             mPanningScalingHandler.setEnabled(true);
320         }
321 
322         @Override
onExit()323         public void onExit() {
324             mPanningScalingHandler.setEnabled(false);
325             mWindowMagnificationMgr.persistScale(mDisplayId);
326             clear();
327         }
328 
329         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)330         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
331             int action = event.getActionMasked();
332             if (action == ACTION_UP || action == ACTION_CANCEL) {
333                 transitionTo(mDetectingState);
334             }
335         }
336 
337         @Override
clear()338         public void clear() {
339             mPanningScalingHandler.clear();
340         }
341 
342         @Override
toString()343         public String toString() {
344             return "PanningScalingState{"
345                     + "mPanningScalingHandler=" + mPanningScalingHandler + '}';
346         }
347     }
348 
349     /**
350      * A state not to intercept {@link MotionEvent}. Leaving this state until receiving
351      * {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL}.
352      */
353     final class DelegatingState implements State {
354         private final MotionEventDispatcherDelegate mMotionEventDispatcherDelegate;
355 
DelegatingState(MotionEventDispatcherDelegate motionEventDispatcherDelegate)356         DelegatingState(MotionEventDispatcherDelegate motionEventDispatcherDelegate) {
357             mMotionEventDispatcherDelegate = motionEventDispatcherDelegate;
358         }
359 
360         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)361         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
362             mMotionEventDispatcherDelegate.dispatchMotionEvent(event, rawEvent, policyFlags);
363             switch (event.getActionMasked()) {
364                 case ACTION_UP:
365                 case ACTION_CANCEL: {
366                     transitionTo(mDetectingState);
367                 }
368                     break;
369             }
370         }
371     }
372 
373 
374     /**
375      * This class handles motion events when the event dispatcher has
376      * determined that the user is performing a single-finger drag of the
377      * magnification viewport.
378      *
379      * Leaving this state until receiving {@link MotionEvent#ACTION_UP}
380      * or {@link MotionEvent#ACTION_CANCEL}.
381      */
382     final class ViewportDraggingState implements State {
383 
384         /** Whether to disable zoom after dragging ends */
385         boolean mEnabledBeforeDrag;
386 
387         private float mLastX = Float.NaN;
388         private float mLastY = Float.NaN;
389 
390         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)391         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
392             final int action = event.getActionMasked();
393             switch (action) {
394                 case ACTION_MOVE: {
395                     if (!Float.isNaN(mLastX) && !Float.isNaN(mLastY)) {
396                         float offsetX = event.getX() - mLastX;
397                         float offsetY = event.getY() - mLastY;
398                         mWindowMagnificationMgr.moveWindowMagnification(mDisplayId, offsetX,
399                                 offsetY);
400                     }
401                     mLastX = event.getX();
402                     mLastY = event.getY();
403                 }
404                 break;
405 
406                 case ACTION_UP:
407                 case ACTION_CANCEL: {
408                     releaseTripleTapAndHold();
409                 }
410                     break;
411             }
412         }
413 
414         @Override
clear()415         public void clear() {
416             mLastX = Float.NaN;
417             mLastY = Float.NaN;
418         }
419 
420         @Override
onExit()421         public void onExit() {
422             clear();
423         }
424 
425         @Override
toString()426         public String toString() {
427             return "ViewportDraggingState{"
428                     + "mLastX=" + mLastX
429                     + ",mLastY=" + mLastY
430                     + '}';
431         }
432     }
433 
434     /**
435      * This class handles motion events in a duration to determine if the user is going to
436      * manipulate the window magnifier or want to interact with current UI. The rule of leaving
437      * this state is as follows:
438      * <ol>
439      *   <li> If {@link MagnificationGestureMatcher#GESTURE_TWO_FINGERS_DOWN_OR_SWIPE} is detected,
440      *   {@link State} will be transited to {@link PanningScalingGestureState}.</li>
441      *   <li> If other gesture is detected and the last motion event is neither ACTION_UP nor
442      *   ACTION_CANCEL.
443      * </ol>
444      *  <b>Note</b> The motion events will be cached and dispatched before leaving this state.
445      */
446     final class DetectingState implements State,
447             MagnificationGesturesObserver.Callback {
448 
449         private final MagnificationGesturesObserver mGesturesObserver;
450 
451         /**
452          * {@code true} if this detector should detect and respond to triple-tap
453          * gestures for engaging and disengaging magnification,
454          * {@code false} if it should ignore such gestures
455          */
456         private final boolean mDetectTripleTap;
457 
DetectingState(@iContext Context context, boolean detectTripleTap)458         DetectingState(@UiContext Context context, boolean detectTripleTap) {
459             mDetectTripleTap = detectTripleTap;
460             final MultiTap multiTap = new MultiTap(context, mDetectTripleTap ? 3 : 1,
461                     mDetectTripleTap
462                             ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP
463                             : MagnificationGestureMatcher.GESTURE_SINGLE_TAP, null);
464             final MultiTapAndHold multiTapAndHold = new MultiTapAndHold(context,
465                     mDetectTripleTap ? 3 : 1,
466                     mDetectTripleTap
467                             ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD
468                             : MagnificationGestureMatcher.GESTURE_SINGLE_TAP_AND_HOLD, null);
469             mGesturesObserver = new MagnificationGesturesObserver(this,
470                     new SimpleSwipe(context),
471                     multiTap,
472                     multiTapAndHold,
473                     new TwoFingersDownOrSwipe(context));
474         }
475 
476         @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)477         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
478             mGesturesObserver.onMotionEvent(event, rawEvent, policyFlags);
479         }
480 
481         @Override
toString()482         public String toString() {
483             return "DetectingState{"
484                     + "mGestureTimeoutObserver=" + mGesturesObserver
485                     + '}';
486         }
487 
488         @Override
shouldStopDetection(MotionEvent motionEvent)489         public boolean shouldStopDetection(MotionEvent motionEvent) {
490             return !mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId)
491                     && !mDetectTripleTap;
492         }
493 
494         @Override
onGestureCompleted(int gestureId, long lastDownEventTime, List<MotionEventInfo> delayedEventQueue, MotionEvent motionEvent)495         public void onGestureCompleted(int gestureId, long lastDownEventTime,
496                 List<MotionEventInfo> delayedEventQueue,
497                 MotionEvent motionEvent) {
498             if (DEBUG_DETECTING) {
499                 Slog.d(mLogTag, "onGestureDetected : gesture = "
500                         + MagnificationGestureMatcher.gestureIdToString(
501                         gestureId));
502                 Slog.d(mLogTag,
503                         "onGestureDetected : delayedEventQueue = " + delayedEventQueue);
504             }
505             if (gestureId == MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE
506                     && mWindowMagnificationMgr.pointersInWindow(mDisplayId, motionEvent) > 0) {
507                 transitionTo(mObservePanningScalingState);
508             } else if (gestureId == MagnificationGestureMatcher.GESTURE_TRIPLE_TAP) {
509                 onTripleTap(motionEvent);
510             } else if (gestureId == MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD) {
511                 onTripleTapAndHold(motionEvent);
512             } else {
513                 mMotionEventDispatcherDelegate.sendDelayedMotionEvents(delayedEventQueue,
514                         lastDownEventTime);
515                 changeToDelegateStateIfNeed(motionEvent);
516             }
517         }
518 
519         @Override
onGestureCancelled(long lastDownEventTime, List<MotionEventInfo> delayedEventQueue, MotionEvent motionEvent)520         public void onGestureCancelled(long lastDownEventTime,
521                 List<MotionEventInfo> delayedEventQueue,
522                 MotionEvent motionEvent) {
523             if (DEBUG_DETECTING) {
524                 Slog.d(mLogTag,
525                         "onGestureCancelled : delayedEventQueue = " + delayedEventQueue);
526             }
527             mMotionEventDispatcherDelegate.sendDelayedMotionEvents(delayedEventQueue,
528                     lastDownEventTime);
529             changeToDelegateStateIfNeed(motionEvent);
530         }
531 
changeToDelegateStateIfNeed(MotionEvent motionEvent)532         private void changeToDelegateStateIfNeed(MotionEvent motionEvent) {
533             if (motionEvent != null && (motionEvent.getActionMasked() == ACTION_UP
534                     || motionEvent.getActionMasked() == ACTION_CANCEL)) {
535                 return;
536             }
537             transitionTo(mDelegatingState);
538         }
539     }
540 
541     @Override
toString()542     public String toString() {
543         return "WindowMagnificationGestureHandler{"
544                 + "mDetectingState=" + mDetectingState
545                 + ", mDelegatingState=" + mDelegatingState
546                 + ", mViewportDraggingState=" + mViewportDraggingState
547                 + ", mMagnifiedInteractionState=" + mObservePanningScalingState
548                 + ", mCurrentState=" + State.nameOf(mCurrentState)
549                 + ", mPreviousState=" + State.nameOf(mPreviousState)
550                 + ", mWindowMagnificationMgr=" + mWindowMagnificationMgr
551                 + ", mDisplayId=" + mDisplayId
552                 + '}';
553     }
554 }
555