1 /*
2  * Copyright (C) 2019 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 android.accessibilityservice.AccessibilityTrace;
24 import android.annotation.NonNull;
25 import android.util.Log;
26 import android.util.Slog;
27 import android.view.MotionEvent;
28 
29 import com.android.server.accessibility.AccessibilityTraceManager;
30 import com.android.server.accessibility.BaseEventStreamTransformation;
31 import com.android.server.accessibility.Flags;
32 
33 import java.util.ArrayDeque;
34 import java.util.Queue;
35 
36 /**
37  * A base class that detects gestures and defines common methods for magnification.
38  */
39 public abstract class MagnificationGestureHandler extends BaseEventStreamTransformation {
40 
41     protected final String mLogTag = this.getClass().getSimpleName();
42     protected static final boolean DEBUG_ALL = Log.isLoggable("MagnificationGestureHandler",
43             Log.DEBUG);
44     protected static final boolean DEBUG_EVENT_STREAM = false | DEBUG_ALL;
45     private final Queue<MotionEvent> mDebugInputEventHistory;
46     private final Queue<MotionEvent> mDebugOutputEventHistory;
47 
48     /**
49      * The logical display id.
50      */
51     protected final int mDisplayId;
52 
53     /**
54      * {@code true} if this detector should be "triggerable" by some
55      * external shortcut invoking {@link #notifyShortcutTriggered},
56      * {@code false} if it should ignore such triggers.
57      */
58     protected final boolean mDetectShortcutTrigger;
59 
60     /**
61      * {@code true} if this detector should detect and respond to single-finger triple-tap
62      * gestures for engaging and disengaging magnification,
63      * {@code false} if it should ignore such gestures
64      */
65     protected final boolean mDetectSingleFingerTripleTap;
66 
67     /**
68      * {@code true} if this detector should detect and respond to two-finger triple-tap
69      * gestures for engaging and disengaging magnification,
70      * {@code false} if it should ignore such gestures
71      */
72     protected final boolean mDetectTwoFingerTripleTap;
73 
74     /** Callback interface to report that magnification is interactive with a user. */
75     public interface Callback {
76         /**
77          * Called when the touch interaction is started by a user.
78          *
79          * @param displayId The logical display id
80          * @param mode The magnification mode
81          */
onTouchInteractionStart(int displayId, int mode)82         void onTouchInteractionStart(int displayId, int mode);
83 
84         /**
85          * Called when the touch interaction is ended by a user.
86          *
87          * @param displayId The logical display id
88          * @param mode The magnification mode
89          */
onTouchInteractionEnd(int displayId, int mode)90         void onTouchInteractionEnd(int displayId, int mode);
91     }
92 
93     private final AccessibilityTraceManager mTrace;
94     protected final Callback mCallback;
95 
MagnificationGestureHandler(int displayId, boolean detectSingleFingerTripleTap, boolean detectTwoFingerTripleTap, boolean detectShortcutTrigger, AccessibilityTraceManager trace, @NonNull Callback callback)96     protected MagnificationGestureHandler(int displayId,
97             boolean detectSingleFingerTripleTap,
98             boolean detectTwoFingerTripleTap,
99             boolean detectShortcutTrigger,
100             AccessibilityTraceManager trace,
101             @NonNull Callback callback) {
102         mDisplayId = displayId;
103         mDetectSingleFingerTripleTap = detectSingleFingerTripleTap;
104         mDetectTwoFingerTripleTap = Flags.enableMagnificationMultipleFingerMultipleTapGesture()
105                 && detectTwoFingerTripleTap;
106         mDetectShortcutTrigger = detectShortcutTrigger;
107         mTrace = trace;
108         mCallback = callback;
109 
110         mDebugInputEventHistory = DEBUG_EVENT_STREAM ? new ArrayDeque<>() : null;
111         mDebugOutputEventHistory = DEBUG_EVENT_STREAM ? new ArrayDeque<>() : null;
112     }
113 
114     @Override
onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)115     public final void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
116         if (DEBUG_ALL) {
117             Slog.i(mLogTag, "onMotionEvent(" + event + ")");
118         }
119         if (mTrace.isA11yTracingEnabledForTypes(
120                 AccessibilityTrace.FLAGS_INPUT_FILTER | AccessibilityTrace.FLAGS_GESTURE)) {
121             mTrace.logTrace("MagnificationGestureHandler.onMotionEvent",
122                     AccessibilityTrace.FLAGS_INPUT_FILTER | AccessibilityTrace.FLAGS_GESTURE,
123                     "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags);
124         }
125         if (DEBUG_EVENT_STREAM) {
126             storeEventInto(mDebugInputEventHistory, event);
127         }
128         if (shouldDispatchTransformedEvent(event)) {
129             dispatchTransformedEvent(event, rawEvent, policyFlags);
130         } else {
131             onMotionEventInternal(event, rawEvent, policyFlags);
132 
133             final int action = event.getAction();
134             if (action == MotionEvent.ACTION_DOWN) {
135                 mCallback.onTouchInteractionStart(mDisplayId, getMode());
136             } else if (action == ACTION_UP || action == ACTION_CANCEL) {
137                 mCallback.onTouchInteractionEnd(mDisplayId, getMode());
138             }
139         }
140     }
141 
shouldDispatchTransformedEvent(MotionEvent event)142     private boolean shouldDispatchTransformedEvent(MotionEvent event) {
143         if ((!mDetectSingleFingerTripleTap && !mDetectTwoFingerTripleTap && !mDetectShortcutTrigger)
144                 || !event.isFromSource(SOURCE_TOUCHSCREEN)) {
145             return true;
146         }
147         return false;
148     }
149 
dispatchTransformedEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)150     final void dispatchTransformedEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
151         if (DEBUG_EVENT_STREAM) {
152             storeEventInto(mDebugOutputEventHistory, event);
153             try {
154                 super.onMotionEvent(event, rawEvent, policyFlags);
155                 return;
156             } catch (Exception e) {
157                 throw new RuntimeException(
158                         "Exception downstream following input events: " + mDebugInputEventHistory
159                                 + "\nTransformed into output events: " + mDebugOutputEventHistory,
160                         e);
161             }
162         }
163         super.onMotionEvent(event, rawEvent, policyFlags);
164     }
165 
storeEventInto(Queue<MotionEvent> queue, MotionEvent event)166     private static void storeEventInto(Queue<MotionEvent> queue, MotionEvent event) {
167         queue.add(MotionEvent.obtain(event));
168         // Prune old events
169         while (!queue.isEmpty() && (event.getEventTime() - queue.peek().getEventTime() > 5000)) {
170             queue.remove().recycle();
171         }
172     }
173 
174     /**
175      * Called when this MagnificationGestureHandler handles the motion event.
176      */
onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags)177     abstract void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags);
178 
179     /**
180      * Called when the shortcut target is magnification.
181      */
notifyShortcutTriggered()182     public void notifyShortcutTriggered() {
183         if (DEBUG_ALL) {
184             Slog.i(mLogTag, "notifyShortcutTriggered():");
185         }
186         if (mDetectShortcutTrigger) {
187             handleShortcutTriggered();
188         }
189     }
190 
191     /**
192      * Handles shortcut triggered event.
193      */
handleShortcutTriggered()194     abstract void handleShortcutTriggered();
195 
196     /**
197      * Indicates the magnification mode.
198      *
199      * @return the magnification mode of the handler
200      * @see android.provider.Settings.Secure#ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN
201      * @see android.provider.Settings.Secure#ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW
202      */
getMode()203     public abstract int getMode();
204 }
205