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