/*
* 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);
}
}
}