| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.accessibility; |
| |
| import android.content.Context; |
| import android.os.PowerManager; |
| import android.util.Pools.SimplePool; |
| import android.util.Slog; |
| import android.view.Choreographer; |
| import android.view.Display; |
| import android.view.InputDevice; |
| import android.view.InputEvent; |
| import android.view.InputFilter; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.WindowManagerPolicy; |
| import android.view.accessibility.AccessibilityEvent; |
| |
| /** |
| * This class is an input filter for implementing accessibility features such |
| * as display magnification and explore by touch. |
| * |
| * NOTE: This class has to be created and poked only from the main thread. |
| */ |
| class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation { |
| |
| private static final String TAG = AccessibilityInputFilter.class.getSimpleName(); |
| |
| private static final boolean DEBUG = false; |
| |
| /** |
| * Flag for enabling the screen magnification feature. |
| * |
| * @see #setEnabledFeatures(int) |
| */ |
| static final int FLAG_FEATURE_SCREEN_MAGNIFIER = 0x00000001; |
| |
| /** |
| * Flag for enabling the touch exploration feature. |
| * |
| * @see #setEnabledFeatures(int) |
| */ |
| static final int FLAG_FEATURE_TOUCH_EXPLORATION = 0x00000002; |
| |
| /** |
| * Flag for enabling the filtering key events feature. |
| * |
| * @see #setEnabledFeatures(int) |
| */ |
| static final int FLAG_FEATURE_FILTER_KEY_EVENTS = 0x00000004; |
| |
| private final Runnable mProcessBatchedEventsRunnable = new Runnable() { |
| @Override |
| public void run() { |
| final long frameTimeNanos = mChoreographer.getFrameTimeNanos(); |
| if (DEBUG) { |
| Slog.i(TAG, "Begin batch processing for frame: " + frameTimeNanos); |
| } |
| processBatchedEvents(frameTimeNanos); |
| if (DEBUG) { |
| Slog.i(TAG, "End batch processing."); |
| } |
| if (mEventQueue != null) { |
| scheduleProcessBatchedEvents(); |
| } |
| } |
| }; |
| |
| private final Context mContext; |
| |
| private final PowerManager mPm; |
| |
| private final AccessibilityManagerService mAms; |
| |
| private final Choreographer mChoreographer; |
| |
| private int mCurrentTouchDeviceId; |
| |
| private boolean mInstalled; |
| |
| private int mEnabledFeatures; |
| |
| private TouchExplorer mTouchExplorer; |
| |
| private ScreenMagnifier mScreenMagnifier; |
| |
| private EventStreamTransformation mEventHandler; |
| |
| private MotionEventHolder mEventQueue; |
| |
| private boolean mMotionEventSequenceStarted; |
| |
| private boolean mHoverEventSequenceStarted; |
| |
| private boolean mKeyEventSequenceStarted; |
| |
| private boolean mFilterKeyEvents; |
| |
| AccessibilityInputFilter(Context context, AccessibilityManagerService service) { |
| super(context.getMainLooper()); |
| mContext = context; |
| mAms = service; |
| mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); |
| mChoreographer = Choreographer.getInstance(); |
| } |
| |
| @Override |
| public void onInstalled() { |
| if (DEBUG) { |
| Slog.d(TAG, "Accessibility input filter installed."); |
| } |
| mInstalled = true; |
| disableFeatures(); |
| enableFeatures(); |
| super.onInstalled(); |
| } |
| |
| @Override |
| public void onUninstalled() { |
| if (DEBUG) { |
| Slog.d(TAG, "Accessibility input filter uninstalled."); |
| } |
| mInstalled = false; |
| disableFeatures(); |
| super.onUninstalled(); |
| } |
| |
| @Override |
| public void onInputEvent(InputEvent event, int policyFlags) { |
| if (DEBUG) { |
| Slog.d(TAG, "Received event: " + event + ", policyFlags=0x" |
| + Integer.toHexString(policyFlags)); |
| } |
| if (event instanceof MotionEvent |
| && event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) { |
| MotionEvent motionEvent = (MotionEvent) event; |
| onMotionEvent(motionEvent, policyFlags); |
| } else if (event instanceof KeyEvent |
| && event.isFromSource(InputDevice.SOURCE_KEYBOARD)) { |
| KeyEvent keyEvent = (KeyEvent) event; |
| onKeyEvent(keyEvent, policyFlags); |
| } else { |
| super.onInputEvent(event, policyFlags); |
| } |
| } |
| |
| private void onMotionEvent(MotionEvent event, int policyFlags) { |
| if (mEventHandler == null) { |
| super.onInputEvent(event, policyFlags); |
| return; |
| } |
| if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) { |
| mMotionEventSequenceStarted = false; |
| mHoverEventSequenceStarted = false; |
| mEventHandler.clear(); |
| super.onInputEvent(event, policyFlags); |
| return; |
| } |
| final int deviceId = event.getDeviceId(); |
| if (mCurrentTouchDeviceId != deviceId) { |
| mCurrentTouchDeviceId = deviceId; |
| mMotionEventSequenceStarted = false; |
| mHoverEventSequenceStarted = false; |
| mEventHandler.clear(); |
| } |
| if (mCurrentTouchDeviceId < 0) { |
| super.onInputEvent(event, policyFlags); |
| return; |
| } |
| // We do not handle scroll events. |
| if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) { |
| super.onInputEvent(event, policyFlags); |
| return; |
| } |
| // Wait for a down touch event to start processing. |
| if (event.isTouchEvent()) { |
| if (!mMotionEventSequenceStarted) { |
| if (event.getActionMasked() != MotionEvent.ACTION_DOWN) { |
| return; |
| } |
| mMotionEventSequenceStarted = true; |
| } |
| } else { |
| // Wait for an enter hover event to start processing. |
| if (!mHoverEventSequenceStarted) { |
| if (event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER) { |
| return; |
| } |
| mHoverEventSequenceStarted = true; |
| } |
| } |
| batchMotionEvent((MotionEvent) event, policyFlags); |
| } |
| |
| private void onKeyEvent(KeyEvent event, int policyFlags) { |
| if (!mFilterKeyEvents) { |
| super.onInputEvent(event, policyFlags); |
| return; |
| } |
| if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) { |
| mKeyEventSequenceStarted = false; |
| super.onInputEvent(event, policyFlags); |
| return; |
| } |
| // Wait for a down key event to start processing. |
| if (!mKeyEventSequenceStarted) { |
| if (event.getAction() != KeyEvent.ACTION_DOWN) { |
| super.onInputEvent(event, policyFlags); |
| return; |
| } |
| mKeyEventSequenceStarted = true; |
| } |
| mAms.notifyKeyEvent(event, policyFlags); |
| } |
| |
| private void scheduleProcessBatchedEvents() { |
| mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, |
| mProcessBatchedEventsRunnable, null); |
| } |
| |
| private void batchMotionEvent(MotionEvent event, int policyFlags) { |
| if (DEBUG) { |
| Slog.i(TAG, "Batching event: " + event + ", policyFlags: " + policyFlags); |
| } |
| if (mEventQueue == null) { |
| mEventQueue = MotionEventHolder.obtain(event, policyFlags); |
| scheduleProcessBatchedEvents(); |
| return; |
| } |
| if (mEventQueue.event.addBatch(event)) { |
| return; |
| } |
| MotionEventHolder holder = MotionEventHolder.obtain(event, policyFlags); |
| holder.next = mEventQueue; |
| mEventQueue.previous = holder; |
| mEventQueue = holder; |
| } |
| |
| private void processBatchedEvents(long frameNanos) { |
| MotionEventHolder current = mEventQueue; |
| while (current.next != null) { |
| current = current.next; |
| } |
| while (true) { |
| if (current == null) { |
| mEventQueue = null; |
| break; |
| } |
| if (current.event.getEventTimeNano() >= frameNanos) { |
| // Finished with this choreographer frame. Do the rest on the next one. |
| current.next = null; |
| break; |
| } |
| handleMotionEvent(current.event, current.policyFlags); |
| MotionEventHolder prior = current; |
| current = current.previous; |
| prior.recycle(); |
| } |
| } |
| |
| private void handleMotionEvent(MotionEvent event, int policyFlags) { |
| if (DEBUG) { |
| Slog.i(TAG, "Handling batched event: " + event + ", policyFlags: " + policyFlags); |
| } |
| // Since we do batch processing it is possible that by the time the |
| // next batch is processed the event handle had been set to null. |
| if (mEventHandler != null) { |
| mPm.userActivity(event.getEventTime(), false); |
| MotionEvent transformedEvent = MotionEvent.obtain(event); |
| mEventHandler.onMotionEvent(transformedEvent, event, policyFlags); |
| transformedEvent.recycle(); |
| } |
| } |
| |
| @Override |
| public void onMotionEvent(MotionEvent transformedEvent, MotionEvent rawEvent, |
| int policyFlags) { |
| sendInputEvent(transformedEvent, policyFlags); |
| } |
| |
| @Override |
| public void onAccessibilityEvent(AccessibilityEvent event) { |
| // TODO Implement this to inject the accessibility event |
| // into the accessibility manager service similarly |
| // to how this is done for input events. |
| } |
| |
| @Override |
| public void setNext(EventStreamTransformation sink) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void clear() { |
| /* do nothing */ |
| } |
| |
| void setEnabledFeatures(int enabledFeatures) { |
| if (mEnabledFeatures == enabledFeatures) { |
| return; |
| } |
| if (mInstalled) { |
| disableFeatures(); |
| } |
| mEnabledFeatures = enabledFeatures; |
| if (mInstalled) { |
| enableFeatures(); |
| } |
| } |
| |
| void notifyAccessibilityEvent(AccessibilityEvent event) { |
| if (mEventHandler != null) { |
| mEventHandler.onAccessibilityEvent(event); |
| } |
| } |
| |
| private void enableFeatures() { |
| mMotionEventSequenceStarted = false; |
| mHoverEventSequenceStarted = false; |
| if ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0) { |
| mEventHandler = mScreenMagnifier = new ScreenMagnifier(mContext, |
| Display.DEFAULT_DISPLAY, mAms); |
| mEventHandler.setNext(this); |
| } |
| if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) { |
| mTouchExplorer = new TouchExplorer(mContext, mAms); |
| mTouchExplorer.setNext(this); |
| if (mEventHandler != null) { |
| mEventHandler.setNext(mTouchExplorer); |
| } else { |
| mEventHandler = mTouchExplorer; |
| } |
| } |
| if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) { |
| mFilterKeyEvents = true; |
| } |
| } |
| |
| void disableFeatures() { |
| if (mTouchExplorer != null) { |
| mTouchExplorer.clear(); |
| mTouchExplorer.onDestroy(); |
| mTouchExplorer = null; |
| } |
| if (mScreenMagnifier != null) { |
| mScreenMagnifier.clear(); |
| mScreenMagnifier.onDestroy(); |
| mScreenMagnifier = null; |
| } |
| mEventHandler = null; |
| mKeyEventSequenceStarted = false; |
| mMotionEventSequenceStarted = false; |
| mHoverEventSequenceStarted = false; |
| mFilterKeyEvents = false; |
| } |
| |
| @Override |
| public void onDestroy() { |
| /* ignore */ |
| } |
| |
| private static class MotionEventHolder { |
| private static final int MAX_POOL_SIZE = 32; |
| private static final SimplePool<MotionEventHolder> sPool = |
| new SimplePool<MotionEventHolder>(MAX_POOL_SIZE); |
| |
| public int policyFlags; |
| public MotionEvent event; |
| public MotionEventHolder next; |
| public MotionEventHolder previous; |
| |
| public static MotionEventHolder obtain(MotionEvent event, int policyFlags) { |
| MotionEventHolder holder = sPool.acquire(); |
| if (holder == null) { |
| holder = new MotionEventHolder(); |
| } |
| holder.event = MotionEvent.obtain(event); |
| holder.policyFlags = policyFlags; |
| return holder; |
| } |
| |
| public void recycle() { |
| event.recycle(); |
| event = null; |
| policyFlags = 0; |
| next = null; |
| previous = null; |
| sPool.release(this); |
| } |
| } |
| } |