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