1/*
2 * Copyright (C) 2011 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
17package com.android.server.accessibility;
18
19import android.content.Context;
20import android.os.PowerManager;
21import android.util.Pools.SimplePool;
22import android.util.Slog;
23import android.view.Choreographer;
24import android.view.Display;
25import android.view.InputDevice;
26import android.view.InputEvent;
27import android.view.InputFilter;
28import android.view.KeyEvent;
29import android.view.MotionEvent;
30import android.view.WindowManagerPolicy;
31import android.view.accessibility.AccessibilityEvent;
32
33/**
34 * This class is an input filter for implementing accessibility features such
35 * as display magnification and explore by touch.
36 *
37 * NOTE: This class has to be created and poked only from the main thread.
38 */
39class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation {
40
41    private static final String TAG = AccessibilityInputFilter.class.getSimpleName();
42
43    private static final boolean DEBUG = false;
44
45    /**
46     * Flag for enabling the screen magnification feature.
47     *
48     * @see #setEnabledFeatures(int)
49     */
50    static final int FLAG_FEATURE_SCREEN_MAGNIFIER = 0x00000001;
51
52    /**
53     * Flag for enabling the touch exploration feature.
54     *
55     * @see #setEnabledFeatures(int)
56     */
57    static final int FLAG_FEATURE_TOUCH_EXPLORATION = 0x00000002;
58
59    /**
60     * Flag for enabling the filtering key events feature.
61     *
62     * @see #setEnabledFeatures(int)
63     */
64    static final int FLAG_FEATURE_FILTER_KEY_EVENTS = 0x00000004;
65
66    private final Runnable mProcessBatchedEventsRunnable = new Runnable() {
67        @Override
68        public void run() {
69            final long frameTimeNanos = mChoreographer.getFrameTimeNanos();
70            if (DEBUG) {
71                Slog.i(TAG, "Begin batch processing for frame: " + frameTimeNanos);
72            }
73            processBatchedEvents(frameTimeNanos);
74            if (DEBUG) {
75                Slog.i(TAG, "End batch processing.");
76            }
77            if (mEventQueue != null) {
78                scheduleProcessBatchedEvents();
79            }
80        }
81    };
82
83    private final Context mContext;
84
85    private final PowerManager mPm;
86
87    private final AccessibilityManagerService mAms;
88
89    private final Choreographer mChoreographer;
90
91    private int mCurrentTouchDeviceId;
92
93    private boolean mInstalled;
94
95    private int mEnabledFeatures;
96
97    private TouchExplorer mTouchExplorer;
98
99    private ScreenMagnifier mScreenMagnifier;
100
101    private EventStreamTransformation mEventHandler;
102
103    private MotionEventHolder mEventQueue;
104
105    private boolean mMotionEventSequenceStarted;
106
107    private boolean mHoverEventSequenceStarted;
108
109    private boolean mKeyEventSequenceStarted;
110
111    private boolean mFilterKeyEvents;
112
113    AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
114        super(context.getMainLooper());
115        mContext = context;
116        mAms = service;
117        mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
118        mChoreographer = Choreographer.getInstance();
119    }
120
121    @Override
122    public void onInstalled() {
123        if (DEBUG) {
124            Slog.d(TAG, "Accessibility input filter installed.");
125        }
126        mInstalled = true;
127        disableFeatures();
128        enableFeatures();
129        super.onInstalled();
130    }
131
132    @Override
133    public void onUninstalled() {
134        if (DEBUG) {
135            Slog.d(TAG, "Accessibility input filter uninstalled.");
136        }
137        mInstalled = false;
138        disableFeatures();
139        super.onUninstalled();
140    }
141
142    @Override
143    public void onInputEvent(InputEvent event, int policyFlags) {
144        if (DEBUG) {
145            Slog.d(TAG, "Received event: " + event + ", policyFlags=0x"
146                    + Integer.toHexString(policyFlags));
147        }
148        if (event instanceof MotionEvent
149                && event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
150            MotionEvent motionEvent = (MotionEvent) event;
151            onMotionEvent(motionEvent, policyFlags);
152        } else if (event instanceof KeyEvent
153                && event.isFromSource(InputDevice.SOURCE_KEYBOARD)) {
154            KeyEvent keyEvent = (KeyEvent) event;
155            onKeyEvent(keyEvent, policyFlags);
156        } else {
157            super.onInputEvent(event, policyFlags);
158        }
159    }
160
161    private void onMotionEvent(MotionEvent event, int policyFlags) {
162        if (mEventHandler == null) {
163            super.onInputEvent(event, policyFlags);
164            return;
165        }
166        if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
167            mMotionEventSequenceStarted = false;
168            mHoverEventSequenceStarted = false;
169            mEventHandler.clear();
170            super.onInputEvent(event, policyFlags);
171            return;
172        }
173        final int deviceId = event.getDeviceId();
174        if (mCurrentTouchDeviceId != deviceId) {
175            mCurrentTouchDeviceId = deviceId;
176            mMotionEventSequenceStarted = false;
177            mHoverEventSequenceStarted = false;
178            mEventHandler.clear();
179        }
180        if (mCurrentTouchDeviceId < 0) {
181            super.onInputEvent(event, policyFlags);
182            return;
183        }
184        // We do not handle scroll events.
185        if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
186            super.onInputEvent(event, policyFlags);
187            return;
188        }
189        // Wait for a down touch event to start processing.
190        if (event.isTouchEvent()) {
191            if (!mMotionEventSequenceStarted) {
192                if (event.getActionMasked() != MotionEvent.ACTION_DOWN) {
193                    return;
194                }
195                mMotionEventSequenceStarted = true;
196            }
197        } else {
198            // Wait for an enter hover event to start processing.
199            if (!mHoverEventSequenceStarted) {
200                if (event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER) {
201                    return;
202                }
203                mHoverEventSequenceStarted = true;
204            }
205        }
206        batchMotionEvent((MotionEvent) event, policyFlags);
207    }
208
209    private void onKeyEvent(KeyEvent event, int policyFlags) {
210        if (!mFilterKeyEvents) {
211            super.onInputEvent(event, policyFlags);
212            return;
213        }
214        if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
215            mKeyEventSequenceStarted = false;
216            super.onInputEvent(event, policyFlags);
217            return;
218        }
219        // Wait for a down key event to start processing.
220        if (!mKeyEventSequenceStarted) {
221            if (event.getAction() != KeyEvent.ACTION_DOWN) {
222                super.onInputEvent(event, policyFlags);
223                return;
224            }
225            mKeyEventSequenceStarted = true;
226        }
227        mAms.notifyKeyEvent(event, policyFlags);
228    }
229
230    private void scheduleProcessBatchedEvents() {
231        mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
232                mProcessBatchedEventsRunnable, null);
233    }
234
235    private void batchMotionEvent(MotionEvent event, int policyFlags) {
236        if (DEBUG) {
237            Slog.i(TAG, "Batching event: " + event + ", policyFlags: " + policyFlags);
238        }
239        if (mEventQueue == null) {
240            mEventQueue = MotionEventHolder.obtain(event, policyFlags);
241            scheduleProcessBatchedEvents();
242            return;
243        }
244        if (mEventQueue.event.addBatch(event)) {
245            return;
246        }
247        MotionEventHolder holder = MotionEventHolder.obtain(event, policyFlags);
248        holder.next = mEventQueue;
249        mEventQueue.previous = holder;
250        mEventQueue = holder;
251    }
252
253    private void processBatchedEvents(long frameNanos) {
254        MotionEventHolder current = mEventQueue;
255        while (current.next != null) {
256            current = current.next;
257        }
258        while (true) {
259            if (current == null) {
260                mEventQueue = null;
261                break;
262            }
263            if (current.event.getEventTimeNano() >= frameNanos) {
264                // Finished with this choreographer frame. Do the rest on the next one.
265                current.next = null;
266                break;
267            }
268            handleMotionEvent(current.event, current.policyFlags);
269            MotionEventHolder prior = current;
270            current = current.previous;
271            prior.recycle();
272        }
273    }
274
275    private void handleMotionEvent(MotionEvent event, int policyFlags) {
276        if (DEBUG) {
277            Slog.i(TAG, "Handling batched event: " + event + ", policyFlags: " + policyFlags);
278        }
279        // Since we do batch processing it is possible that by the time the
280        // next batch is processed the event handle had been set to null.
281        if (mEventHandler != null) {
282            mPm.userActivity(event.getEventTime(), false);
283            MotionEvent transformedEvent = MotionEvent.obtain(event);
284            mEventHandler.onMotionEvent(transformedEvent, event, policyFlags);
285            transformedEvent.recycle();
286        }
287    }
288
289    @Override
290    public void onMotionEvent(MotionEvent transformedEvent, MotionEvent rawEvent,
291            int policyFlags) {
292        sendInputEvent(transformedEvent, policyFlags);
293    }
294
295    @Override
296    public void onAccessibilityEvent(AccessibilityEvent event) {
297        // TODO Implement this to inject the accessibility event
298        //      into the accessibility manager service similarly
299        //      to how this is done for input events.
300    }
301
302    @Override
303    public void setNext(EventStreamTransformation sink) {
304        /* do nothing */
305    }
306
307    @Override
308    public void clear() {
309        /* do nothing */
310    }
311
312    void setEnabledFeatures(int enabledFeatures) {
313        if (mEnabledFeatures == enabledFeatures) {
314            return;
315        }
316        if (mInstalled) {
317            disableFeatures();
318        }
319        mEnabledFeatures = enabledFeatures;
320        if (mInstalled) {
321            enableFeatures();
322        }
323    }
324
325    void notifyAccessibilityEvent(AccessibilityEvent event) {
326        if (mEventHandler != null) {
327            mEventHandler.onAccessibilityEvent(event);
328        }
329    }
330
331    private void enableFeatures() {
332        mMotionEventSequenceStarted = false;
333        mHoverEventSequenceStarted = false;
334        if ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0) {
335            mEventHandler = mScreenMagnifier = new ScreenMagnifier(mContext,
336                    Display.DEFAULT_DISPLAY, mAms);
337            mEventHandler.setNext(this);
338        }
339        if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
340            mTouchExplorer = new TouchExplorer(mContext, mAms);
341            mTouchExplorer.setNext(this);
342            if (mEventHandler != null) {
343                mEventHandler.setNext(mTouchExplorer);
344            } else {
345                mEventHandler = mTouchExplorer;
346            }
347        }
348        if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) {
349            mFilterKeyEvents = true;
350        }
351    }
352
353    void disableFeatures() {
354        if (mTouchExplorer != null) {
355            mTouchExplorer.clear();
356            mTouchExplorer.onDestroy();
357            mTouchExplorer = null;
358        }
359        if (mScreenMagnifier != null) {
360            mScreenMagnifier.clear();
361            mScreenMagnifier.onDestroy();
362            mScreenMagnifier = null;
363        }
364        mEventHandler = null;
365        mKeyEventSequenceStarted = false;
366        mMotionEventSequenceStarted = false;
367        mHoverEventSequenceStarted = false;
368        mFilterKeyEvents = false;
369    }
370
371    @Override
372    public void onDestroy() {
373        /* ignore */
374    }
375
376    private static class MotionEventHolder {
377        private static final int MAX_POOL_SIZE = 32;
378        private static final SimplePool<MotionEventHolder> sPool =
379                new SimplePool<MotionEventHolder>(MAX_POOL_SIZE);
380
381        public int policyFlags;
382        public MotionEvent event;
383        public MotionEventHolder next;
384        public MotionEventHolder previous;
385
386        public static MotionEventHolder obtain(MotionEvent event, int policyFlags) {
387            MotionEventHolder holder = sPool.acquire();
388            if (holder == null) {
389                holder = new MotionEventHolder();
390            }
391            holder.event = MotionEvent.obtain(event);
392            holder.policyFlags = policyFlags;
393            return holder;
394        }
395
396        public void recycle() {
397            event.recycle();
398            event = null;
399            policyFlags = 0;
400            next = null;
401            previous = null;
402            sPool.release(this);
403        }
404    }
405}
406