blob: 800f0e10cc5e63bb710d5ce27f89c8d55ea0f0fc [file] [log] [blame]
/*
* Copyright (C) 2015 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.accessibilityservice.IAccessibilityServiceClient;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Slog;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.WindowManagerPolicy;
import android.view.accessibility.AccessibilityEvent;
import com.android.internal.os.SomeArgs;
import com.android.server.accessibility.AccessibilityManagerService.Service;
import java.util.List;
/**
* Injects MotionEvents to permit {@code AccessibilityService}s to touch the screen on behalf of
* users.
*
* All methods except {@code injectEvents} must be called only from the main thread.
*/
public class MotionEventInjector implements EventStreamTransformation {
private static final String LOG_TAG = "MotionEventInjector";
private static final int MESSAGE_SEND_MOTION_EVENT = 1;
private static final int MESSAGE_INJECT_EVENTS = 2;
private static final int MAX_POINTERS = 11; // Non-binding maximum
private final Handler mHandler;
private final SparseArray<Boolean> mOpenGesturesInProgress = new SparseArray<>();
// These two arrays must be the same length
private MotionEvent.PointerProperties[] mPointerProperties =
new MotionEvent.PointerProperties[MAX_POINTERS];
private MotionEvent.PointerCoords[] mPointerCoords =
new MotionEvent.PointerCoords[MAX_POINTERS];
private EventStreamTransformation mNext;
private IAccessibilityServiceClient mServiceInterfaceForCurrentGesture;
private int mSequenceForCurrentGesture;
private int mSourceOfInjectedGesture = InputDevice.SOURCE_UNKNOWN;
private boolean mIsDestroyed = false;
/**
* @param looper A looper on the main thread to use for dispatching new events
*/
public MotionEventInjector(Looper looper) {
mHandler = new Handler(looper, new Callback());
}
/**
* Schedule a series of events for injection. These events must comprise a complete, valid
* sequence. All gestures currently in progress will be cancelled, and all {@code downTime}
* and {@code eventTime} fields will be offset by the current time.
*
* @param events The events to inject. Must all be from the same source.
* @param serviceInterface The interface to call back with a result when the gesture is
* either complete or cancelled.
*/
public void injectEvents(List<MotionEvent> events,
IAccessibilityServiceClient serviceInterface, int sequence) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = events;
args.arg2 = serviceInterface;
args.argi1 = sequence;
mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_INJECT_EVENTS, args));
}
@Override
public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
cancelAnyPendingInjectedEvents();
sendMotionEventToNext(event, rawEvent, policyFlags);
}
@Override
public void onKeyEvent(KeyEvent event, int policyFlags) {
if (mNext != null) {
mNext.onKeyEvent(event, policyFlags);
}
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (mNext != null) {
mNext.onAccessibilityEvent(event);
}
}
@Override
public void setNext(EventStreamTransformation next) {
mNext = next;
}
@Override
public void clearEvents(int inputSource) {
/*
* Reset state for motion events passing through so we won't send a cancel event for
* them.
*/
if (!mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) {
mOpenGesturesInProgress.put(inputSource, false);
}
}
@Override
public void onDestroy() {
cancelAnyPendingInjectedEvents();
mIsDestroyed = true;
}
private void injectEventsMainThread(List<MotionEvent> events,
IAccessibilityServiceClient serviceInterface, int sequence) {
if (mIsDestroyed) {
try {
serviceInterface.onPerformGestureResult(sequence, false);
} catch (RemoteException re) {
Slog.e(LOG_TAG, "Error sending status with mIsDestroyed to " + serviceInterface,
re);
}
return;
}
cancelAnyPendingInjectedEvents();
mSourceOfInjectedGesture = events.get(0).getSource();
cancelAnyGestureInProgress(mSourceOfInjectedGesture);
mServiceInterfaceForCurrentGesture = serviceInterface;
mSequenceForCurrentGesture = sequence;
if (mNext == null) {
notifyService(false);
return;
}
long startTime = SystemClock.uptimeMillis();
for (int i = 0; i < events.size(); i++) {
MotionEvent event = events.get(i);
int numPointers = event.getPointerCount();
if (numPointers > mPointerCoords.length) {
mPointerCoords = new MotionEvent.PointerCoords[numPointers];
mPointerProperties = new MotionEvent.PointerProperties[numPointers];
}
for (int j = 0; j < numPointers; j++) {
if (mPointerCoords[j] == null) {
mPointerCoords[j] = new MotionEvent.PointerCoords();
mPointerProperties[j] = new MotionEvent.PointerProperties();
}
event.getPointerCoords(j, mPointerCoords[j]);
event.getPointerProperties(j, mPointerProperties[j]);
}
/*
* MotionEvent doesn't have a setEventTime() method (it carries around history data,
* which could become inconsistent), so we need to obtain a new one.
*/
MotionEvent offsetEvent = MotionEvent.obtain(startTime + event.getDownTime(),
startTime + event.getEventTime(), event.getAction(), numPointers,
mPointerProperties, mPointerCoords, event.getMetaState(),
event.getButtonState(), event.getXPrecision(), event.getYPrecision(),
event.getDeviceId(), event.getEdgeFlags(), event.getSource(),
event.getFlags());
Message message = mHandler.obtainMessage(MESSAGE_SEND_MOTION_EVENT, offsetEvent);
mHandler.sendMessageDelayed(message, event.getEventTime());
}
}
private void sendMotionEventToNext(MotionEvent event, MotionEvent rawEvent,
int policyFlags) {
if (mNext != null) {
mNext.onMotionEvent(event, rawEvent, policyFlags);
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mOpenGesturesInProgress.put(event.getSource(), true);
}
if ((event.getActionMasked() == MotionEvent.ACTION_UP)
|| (event.getActionMasked() == MotionEvent.ACTION_CANCEL)) {
mOpenGesturesInProgress.put(event.getSource(), false);
}
}
}
private void cancelAnyGestureInProgress(int source) {
if ((mNext != null) && mOpenGesturesInProgress.get(source, false)) {
long now = SystemClock.uptimeMillis();
MotionEvent cancelEvent =
MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
sendMotionEventToNext(cancelEvent, cancelEvent,
WindowManagerPolicy.FLAG_PASS_TO_USER);
}
}
private void cancelAnyPendingInjectedEvents() {
if (mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) {
cancelAnyGestureInProgress(mSourceOfInjectedGesture);
mHandler.removeMessages(MESSAGE_SEND_MOTION_EVENT);
notifyService(false);
}
}
private void notifyService(boolean success) {
try {
mServiceInterfaceForCurrentGesture.onPerformGestureResult(
mSequenceForCurrentGesture, success);
} catch (RemoteException re) {
Slog.e(LOG_TAG, "Error sending motion event injection status to "
+ mServiceInterfaceForCurrentGesture, re);
}
}
private class Callback implements Handler.Callback {
@Override
public boolean handleMessage(Message message) {
if (message.what == MESSAGE_INJECT_EVENTS) {
SomeArgs args = (SomeArgs) message.obj;
injectEventsMainThread((List<MotionEvent>) args.arg1,
(IAccessibilityServiceClient) args.arg2, args.argi1);
args.recycle();
return true;
}
if (message.what != MESSAGE_SEND_MOTION_EVENT) {
throw new IllegalArgumentException("Unknown message: " + message.what);
}
MotionEvent motionEvent = (MotionEvent) message.obj;
sendMotionEventToNext(motionEvent, motionEvent,
WindowManagerPolicy.FLAG_PASS_TO_USER);
// If the message queue is now empty, then this gesture is complete
if (!mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) {
notifyService(true);
}
return true;
}
}
}