summaryrefslogtreecommitdiffstats
path: root/services/accessibility
diff options
context:
space:
mode:
authorAmith Yamasani <yamasani@google.com>2013-11-22 08:25:26 -0800
committerAmith Yamasani <yamasani@google.com>2013-12-19 15:25:37 -0800
commit9158825f9c41869689d6b1786d7c7aa8bdd524ce (patch)
treef41944461539f0c70030668b4558296469c307d3 /services/accessibility
parent30d032928a294fbb6f385e9d0367a75b7bf2649b (diff)
downloadframeworks_base-9158825f9c41869689d6b1786d7c7aa8bdd524ce.zip
frameworks_base-9158825f9c41869689d6b1786d7c7aa8bdd524ce.tar.gz
frameworks_base-9158825f9c41869689d6b1786d7c7aa8bdd524ce.tar.bz2
Move some system services to separate directories
Refactored the directory structure so that services can be optionally excluded. This is step 1. Will be followed by another change that makes it possible to remove services from the build. Change-Id: Ideacedfd34b5e213217ad3ff4ebb21c4a8e73f85
Diffstat (limited to 'services/accessibility')
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java404
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java3131
-rw-r--r--services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java93
-rw-r--r--services/accessibility/java/com/android/server/accessibility/GestureUtils.java102
-rw-r--r--services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java1177
-rw-r--r--services/accessibility/java/com/android/server/accessibility/TouchExplorer.java1937
-rw-r--r--services/accessibility/java/service.mk11
7 files changed, 6855 insertions, 0 deletions
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
new file mode 100644
index 0000000..9e893da
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -0,0 +1,404 @@
+/*
+ * 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) {
+ 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);
+ }
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
new file mode 100644
index 0000000..ccac0d3
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -0,0 +1,3131 @@
+/*
+ ** Copyright 2009, 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 static android.accessibilityservice.AccessibilityServiceInfo.DEFAULT;
+
+import android.Manifest;
+import android.accessibilityservice.AccessibilityService;
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.database.ContentObserver;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.hardware.input.InputManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.text.TextUtils.SimpleStringSplitter;
+import android.util.Pools.Pool;
+import android.util.Pools.SimplePool;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.IWindow;
+import android.view.IWindowManager;
+import android.view.InputDevice;
+import android.view.InputEventConsistencyVerifier;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MagnificationSpec;
+import android.view.WindowManager;
+import android.view.WindowManagerPolicy;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.IAccessibilityInteractionConnection;
+import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.view.accessibility.IAccessibilityManager;
+import android.view.accessibility.IAccessibilityManagerClient;
+
+import com.android.internal.R;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.statusbar.IStatusBarService;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * This class is instantiated by the system as a system level service and can be
+ * accessed only by the system. The task of this service is to be a centralized
+ * event dispatch for {@link AccessibilityEvent}s generated across all processes
+ * on the device. Events are dispatched to {@link AccessibilityService}s.
+ *
+ * @hide
+ */
+public class AccessibilityManagerService extends IAccessibilityManager.Stub {
+
+ private static final boolean DEBUG = false;
+
+ private static final String LOG_TAG = "AccessibilityManagerService";
+
+ // TODO: This is arbitrary. When there is time implement this by watching
+ // when that accessibility services are bound.
+ private static final int WAIT_FOR_USER_STATE_FULLY_INITIALIZED_MILLIS = 3000;
+
+ private static final String FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE =
+ "registerUiTestAutomationService";
+
+ private static final String TEMPORARY_ENABLE_ACCESSIBILITY_UNTIL_KEYGUARD_REMOVED =
+ "temporaryEnableAccessibilityStateUntilKeyguardRemoved";
+
+ private static final ComponentName sFakeAccessibilityServiceComponentName =
+ new ComponentName("foo.bar", "FakeService");
+
+ private static final String FUNCTION_DUMP = "dump";
+
+ private static final char COMPONENT_NAME_SEPARATOR = ':';
+
+ private static final int OWN_PROCESS_ID = android.os.Process.myPid();
+
+ private static final int MAX_POOL_SIZE = 10;
+
+ private static int sIdCounter = 0;
+
+ private static int sNextWindowId;
+
+ private final Context mContext;
+
+ private final Object mLock = new Object();
+
+ private final Pool<PendingEvent> mPendingEventPool =
+ new SimplePool<PendingEvent>(MAX_POOL_SIZE);
+
+ private final SimpleStringSplitter mStringColonSplitter =
+ new SimpleStringSplitter(COMPONENT_NAME_SEPARATOR);
+
+ private final List<AccessibilityServiceInfo> mEnabledServicesForFeedbackTempList =
+ new ArrayList<AccessibilityServiceInfo>();
+
+ private final Rect mTempRect = new Rect();
+
+ private final Point mTempPoint = new Point();
+
+ private final Display mDefaultDisplay;
+
+ private final PackageManager mPackageManager;
+
+ private final IWindowManager mWindowManagerService;
+
+ private final SecurityPolicy mSecurityPolicy;
+
+ private final MainHandler mMainHandler;
+
+ private Service mQueryBridge;
+
+ private AlertDialog mEnableTouchExplorationDialog;
+
+ private AccessibilityInputFilter mInputFilter;
+
+ private boolean mHasInputFilter;
+
+ private final Set<ComponentName> mTempComponentNameSet = new HashSet<ComponentName>();
+
+ private final List<AccessibilityServiceInfo> mTempAccessibilityServiceInfoList =
+ new ArrayList<AccessibilityServiceInfo>();
+
+ private final RemoteCallbackList<IAccessibilityManagerClient> mGlobalClients =
+ new RemoteCallbackList<IAccessibilityManagerClient>();
+
+ private final SparseArray<AccessibilityConnectionWrapper> mGlobalInteractionConnections =
+ new SparseArray<AccessibilityConnectionWrapper>();
+
+ private final SparseArray<IBinder> mGlobalWindowTokens = new SparseArray<IBinder>();
+
+ private final SparseArray<UserState> mUserStates = new SparseArray<UserState>();
+
+ private int mCurrentUserId = UserHandle.USER_OWNER;
+
+ //TODO: Remove this hack
+ private boolean mInitialized;
+
+ private UserState getCurrentUserStateLocked() {
+ return getUserStateLocked(mCurrentUserId);
+ }
+
+ private UserState getUserStateLocked(int userId) {
+ UserState state = mUserStates.get(userId);
+ if (state == null) {
+ state = new UserState(userId);
+ mUserStates.put(userId, state);
+ }
+ return state;
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param context A {@link Context} instance.
+ */
+ public AccessibilityManagerService(Context context) {
+ mContext = context;
+ mPackageManager = mContext.getPackageManager();
+ mWindowManagerService = (IWindowManager) ServiceManager.getService(Context.WINDOW_SERVICE);
+ mSecurityPolicy = new SecurityPolicy();
+ mMainHandler = new MainHandler(mContext.getMainLooper());
+ //TODO: (multi-display) We need to support multiple displays.
+ DisplayManager displayManager = (DisplayManager)
+ mContext.getSystemService(Context.DISPLAY_SERVICE);
+ mDefaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ registerBroadcastReceivers();
+ new AccessibilityContentObserver(mMainHandler).register(
+ context.getContentResolver());
+ }
+
+ private void registerBroadcastReceivers() {
+ PackageMonitor monitor = new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ synchronized (mLock) {
+ if (getChangingUserId() != mCurrentUserId) {
+ return;
+ }
+ // We will update when the automation service dies.
+ UserState userState = getCurrentUserStateLocked();
+ // We have to reload the installed services since some services may
+ // have different attributes, resolve info (does not support equals),
+ // etc. Remove them then to force reload. Do it even if automation is
+ // running since when it goes away, we will have to reload as well.
+ userState.mInstalledServices.clear();
+ if (userState.mUiAutomationService == null) {
+ if (readConfigurationForUserStateLocked(userState)) {
+ onUserStateChangedLocked(userState);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onPackageRemoved(String packageName, int uid) {
+ synchronized (mLock) {
+ final int userId = getChangingUserId();
+ if (userId != mCurrentUserId) {
+ return;
+ }
+ UserState userState = getUserStateLocked(userId);
+ Iterator<ComponentName> it = userState.mEnabledServices.iterator();
+ while (it.hasNext()) {
+ ComponentName comp = it.next();
+ String compPkg = comp.getPackageName();
+ if (compPkg.equals(packageName)) {
+ it.remove();
+ // Update the enabled services setting.
+ persistComponentNamesToSettingLocked(
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ userState.mEnabledServices, userId);
+ // Update the touch exploration granted services setting.
+ userState.mTouchExplorationGrantedServices.remove(comp);
+ persistComponentNamesToSettingLocked(
+ Settings.Secure.
+ TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
+ userState.mTouchExplorationGrantedServices, userId);
+ // We will update when the automation service dies.
+ if (userState.mUiAutomationService == null) {
+ onUserStateChangedLocked(userState);
+ }
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onHandleForceStop(Intent intent, String[] packages,
+ int uid, boolean doit) {
+ synchronized (mLock) {
+ final int userId = getChangingUserId();
+ if (userId != mCurrentUserId) {
+ return false;
+ }
+ UserState userState = getUserStateLocked(userId);
+ Iterator<ComponentName> it = userState.mEnabledServices.iterator();
+ while (it.hasNext()) {
+ ComponentName comp = it.next();
+ String compPkg = comp.getPackageName();
+ for (String pkg : packages) {
+ if (compPkg.equals(pkg)) {
+ if (!doit) {
+ return true;
+ }
+ it.remove();
+ persistComponentNamesToSettingLocked(
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ userState.mEnabledServices, userId);
+ // We will update when the automation service dies.
+ if (userState.mUiAutomationService == null) {
+ onUserStateChangedLocked(userState);
+ }
+ }
+ }
+ }
+ return false;
+ }
+ }
+ };
+
+ // package changes
+ monitor.register(mContext, null, UserHandle.ALL, true);
+
+ // user change and unlock
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
+ intentFilter.addAction(Intent.ACTION_USER_REMOVED);
+ intentFilter.addAction(Intent.ACTION_USER_PRESENT);
+
+ mContext.registerReceiverAsUser(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+ switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
+ } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
+ removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
+ } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
+ // We will update when the automation service dies.
+ UserState userState = getCurrentUserStateLocked();
+ if (userState.mUiAutomationService == null) {
+ if (readConfigurationForUserStateLocked(userState)) {
+ onUserStateChangedLocked(userState);
+ }
+ }
+ }
+ }
+ }, UserHandle.ALL, intentFilter, null, null);
+ }
+
+ public int addClient(IAccessibilityManagerClient client, int userId) {
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(userId);
+ // If the client is from a process that runs across users such as
+ // the system UI or the system we add it to the global state that
+ // is shared across users.
+ UserState userState = getUserStateLocked(resolvedUserId);
+ if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) {
+ mGlobalClients.register(client);
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Added global client for pid:" + Binder.getCallingPid());
+ }
+ return userState.getClientState();
+ } else {
+ userState.mClients.register(client);
+ // If this client is not for the current user we do not
+ // return a state since it is not for the foreground user.
+ // We will send the state to the client on a user switch.
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Added user client for pid:" + Binder.getCallingPid()
+ + " and userId:" + mCurrentUserId);
+ }
+ return (resolvedUserId == mCurrentUserId) ? userState.getClientState() : 0;
+ }
+ }
+ }
+
+ public boolean sendAccessibilityEvent(AccessibilityEvent event, int userId) {
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(userId);
+ // This method does nothing for a background user.
+ if (resolvedUserId != mCurrentUserId) {
+ return true; // yes, recycle the event
+ }
+ if (mSecurityPolicy.canDispatchAccessibilityEvent(event)) {
+ mSecurityPolicy.updateEventSourceLocked(event);
+ mMainHandler.obtainMessage(MainHandler.MSG_UPDATE_ACTIVE_WINDOW,
+ event.getWindowId(), event.getEventType()).sendToTarget();
+ notifyAccessibilityServicesDelayedLocked(event, false);
+ notifyAccessibilityServicesDelayedLocked(event, true);
+ }
+ if (mHasInputFilter && mInputFilter != null) {
+ mMainHandler.obtainMessage(MainHandler.MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER,
+ AccessibilityEvent.obtain(event)).sendToTarget();
+ }
+ event.recycle();
+ getUserStateLocked(resolvedUserId).mHandledFeedbackTypes = 0;
+ }
+ return (OWN_PROCESS_ID != Binder.getCallingPid());
+ }
+
+ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(int userId) {
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(userId);
+ // The automation service is a fake one and should not be reported
+ // to clients as being installed - it really is not.
+ UserState userState = getUserStateLocked(resolvedUserId);
+ if (userState.mUiAutomationService != null) {
+ List<AccessibilityServiceInfo> installedServices =
+ new ArrayList<AccessibilityServiceInfo>();
+ installedServices.addAll(userState.mInstalledServices);
+ installedServices.remove(userState.mUiAutomationService);
+ return installedServices;
+ }
+ return userState.mInstalledServices;
+ }
+ }
+
+ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType,
+ int userId) {
+ List<AccessibilityServiceInfo> result = null;
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(userId);
+
+ // The automation service is a fake one and should not be reported
+ // to clients as being enabled. The automation service is always the
+ // only active one, if it exists.
+ UserState userState = getUserStateLocked(resolvedUserId);
+ if (userState.mUiAutomationService != null) {
+ return Collections.emptyList();
+ }
+
+ result = mEnabledServicesForFeedbackTempList;
+ result.clear();
+ List<Service> services = userState.mBoundServices;
+ while (feedbackType != 0) {
+ final int feedbackTypeBit = (1 << Integer.numberOfTrailingZeros(feedbackType));
+ feedbackType &= ~feedbackTypeBit;
+ final int serviceCount = services.size();
+ for (int i = 0; i < serviceCount; i++) {
+ Service service = services.get(i);
+ if ((service.mFeedbackType & feedbackTypeBit) != 0) {
+ result.add(service.mAccessibilityServiceInfo);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public void interrupt(int userId) {
+ CopyOnWriteArrayList<Service> services;
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(userId);
+ // This method does nothing for a background user.
+ if (resolvedUserId != mCurrentUserId) {
+ return;
+ }
+ services = getUserStateLocked(resolvedUserId).mBoundServices;
+ }
+ for (int i = 0, count = services.size(); i < count; i++) {
+ Service service = services.get(i);
+ try {
+ service.mServiceInterface.onInterrupt();
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Error during sending interrupt request to "
+ + service.mService, re);
+ }
+ }
+ }
+
+ public int addAccessibilityInteractionConnection(IWindow windowToken,
+ IAccessibilityInteractionConnection connection, int userId) throws RemoteException {
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(userId);
+ final int windowId = sNextWindowId++;
+ // If the window is from a process that runs across users such as
+ // the system UI or the system we add it to the global state that
+ // is shared across users.
+ if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) {
+ AccessibilityConnectionWrapper wrapper = new AccessibilityConnectionWrapper(
+ windowId, connection, UserHandle.USER_ALL);
+ wrapper.linkToDeath();
+ mGlobalInteractionConnections.put(windowId, wrapper);
+ mGlobalWindowTokens.put(windowId, windowToken.asBinder());
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Added global connection for pid:" + Binder.getCallingPid()
+ + " with windowId: " + windowId);
+ }
+ } else {
+ AccessibilityConnectionWrapper wrapper = new AccessibilityConnectionWrapper(
+ windowId, connection, resolvedUserId);
+ wrapper.linkToDeath();
+ UserState userState = getUserStateLocked(resolvedUserId);
+ userState.mInteractionConnections.put(windowId, wrapper);
+ userState.mWindowTokens.put(windowId, windowToken.asBinder());
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Added user connection for pid:" + Binder.getCallingPid()
+ + " with windowId: " + windowId + " and userId:" + mCurrentUserId);
+ }
+ }
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Adding interaction connection to windowId: " + windowId);
+ }
+ return windowId;
+ }
+ }
+
+ public void removeAccessibilityInteractionConnection(IWindow window) {
+ synchronized (mLock) {
+ mSecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked(
+ UserHandle.getCallingUserId());
+ IBinder token = window.asBinder();
+ final int removedWindowId = removeAccessibilityInteractionConnectionInternalLocked(
+ token, mGlobalWindowTokens, mGlobalInteractionConnections);
+ if (removedWindowId >= 0) {
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Removed global connection for pid:" + Binder.getCallingPid()
+ + " with windowId: " + removedWindowId);
+ }
+ return;
+ }
+ final int userCount = mUserStates.size();
+ for (int i = 0; i < userCount; i++) {
+ UserState userState = mUserStates.valueAt(i);
+ final int removedWindowIdForUser =
+ removeAccessibilityInteractionConnectionInternalLocked(
+ token, userState.mWindowTokens, userState.mInteractionConnections);
+ if (removedWindowIdForUser >= 0) {
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Removed user connection for pid:" + Binder.getCallingPid()
+ + " with windowId: " + removedWindowIdForUser + " and userId:"
+ + mUserStates.keyAt(i));
+ }
+ return;
+ }
+ }
+ }
+ }
+
+ private int removeAccessibilityInteractionConnectionInternalLocked(IBinder windowToken,
+ SparseArray<IBinder> windowTokens,
+ SparseArray<AccessibilityConnectionWrapper> interactionConnections) {
+ final int count = windowTokens.size();
+ for (int i = 0; i < count; i++) {
+ if (windowTokens.valueAt(i) == windowToken) {
+ final int windowId = windowTokens.keyAt(i);
+ windowTokens.removeAt(i);
+ AccessibilityConnectionWrapper wrapper = interactionConnections.get(windowId);
+ wrapper.unlinkToDeath();
+ interactionConnections.remove(windowId);
+ return windowId;
+ }
+ }
+ return -1;
+ }
+
+ public void registerUiTestAutomationService(IBinder owner,
+ IAccessibilityServiceClient serviceClient,
+ AccessibilityServiceInfo accessibilityServiceInfo) {
+ mSecurityPolicy.enforceCallingPermission(Manifest.permission.RETRIEVE_WINDOW_CONTENT,
+ FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE);
+
+ accessibilityServiceInfo.setComponentName(sFakeAccessibilityServiceComponentName);
+
+ synchronized (mLock) {
+ UserState userState = getCurrentUserStateLocked();
+
+ if (userState.mUiAutomationService != null) {
+ throw new IllegalStateException("UiAutomationService " + serviceClient
+ + "already registered!");
+ }
+
+ try {
+ owner.linkToDeath(userState.mUiAutomationSerivceOnwerDeathRecipient, 0);
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Couldn't register for the death of a"
+ + " UiTestAutomationService!", re);
+ return;
+ }
+
+ userState.mUiAutomationServiceOwner = owner;
+ userState.mUiAutomationServiceClient = serviceClient;
+
+ // Set the temporary state.
+ userState.mIsAccessibilityEnabled = true;
+ userState.mIsTouchExplorationEnabled = false;
+ userState.mIsEnhancedWebAccessibilityEnabled = false;
+ userState.mIsDisplayMagnificationEnabled = false;
+ userState.mInstalledServices.add(accessibilityServiceInfo);
+ userState.mEnabledServices.clear();
+ userState.mEnabledServices.add(sFakeAccessibilityServiceComponentName);
+ userState.mTouchExplorationGrantedServices.add(sFakeAccessibilityServiceComponentName);
+
+ // Use the new state instead of settings.
+ onUserStateChangedLocked(userState);
+ }
+ }
+
+ public void unregisterUiTestAutomationService(IAccessibilityServiceClient serviceClient) {
+ synchronized (mLock) {
+ UserState userState = getCurrentUserStateLocked();
+ // Automation service is not bound, so pretend it died to perform clean up.
+ if (userState.mUiAutomationService != null
+ && serviceClient != null
+ && userState.mUiAutomationService != null
+ && userState.mUiAutomationService.mServiceInterface != null
+ && userState.mUiAutomationService.mServiceInterface.asBinder()
+ == serviceClient.asBinder()) {
+ userState.mUiAutomationService.binderDied();
+ } else {
+ throw new IllegalStateException("UiAutomationService " + serviceClient
+ + " not registered!");
+ }
+ }
+ }
+
+ public void temporaryEnableAccessibilityStateUntilKeyguardRemoved(
+ ComponentName service, boolean touchExplorationEnabled) {
+ mSecurityPolicy.enforceCallingPermission(
+ Manifest.permission.TEMPORARY_ENABLE_ACCESSIBILITY,
+ TEMPORARY_ENABLE_ACCESSIBILITY_UNTIL_KEYGUARD_REMOVED);
+ try {
+ if (!mWindowManagerService.isKeyguardLocked()) {
+ return;
+ }
+ } catch (RemoteException re) {
+ return;
+ }
+ synchronized (mLock) {
+ // Set the temporary state.
+ UserState userState = getCurrentUserStateLocked();
+
+ // This is a nop if UI automation is enabled.
+ if (userState.mUiAutomationService != null) {
+ return;
+ }
+
+ userState.mIsAccessibilityEnabled = true;
+ userState.mIsTouchExplorationEnabled = touchExplorationEnabled;
+ userState.mIsEnhancedWebAccessibilityEnabled = false;
+ userState.mIsDisplayMagnificationEnabled = false;
+ userState.mEnabledServices.clear();
+ userState.mEnabledServices.add(service);
+ userState.mBindingServices.clear();
+ userState.mTouchExplorationGrantedServices.clear();
+ userState.mTouchExplorationGrantedServices.add(service);
+
+ // User the current state instead settings.
+ onUserStateChangedLocked(userState);
+ }
+ }
+
+ boolean onGesture(int gestureId) {
+ synchronized (mLock) {
+ boolean handled = notifyGestureLocked(gestureId, false);
+ if (!handled) {
+ handled = notifyGestureLocked(gestureId, true);
+ }
+ return handled;
+ }
+ }
+
+ boolean notifyKeyEvent(KeyEvent event, int policyFlags) {
+ synchronized (mLock) {
+ KeyEvent localClone = KeyEvent.obtain(event);
+ boolean handled = notifyKeyEventLocked(localClone, policyFlags, false);
+ if (!handled) {
+ handled = notifyKeyEventLocked(localClone, policyFlags, true);
+ }
+ return handled;
+ }
+ }
+
+ /**
+ * Gets the bounds of the accessibility focus in the active window.
+ *
+ * @param outBounds The output to which to write the focus bounds.
+ * @return Whether accessibility focus was found and the bounds are populated.
+ */
+ // TODO: (multi-display) Make sure this works for multiple displays.
+ boolean getAccessibilityFocusBoundsInActiveWindow(Rect outBounds) {
+ // Instead of keeping track of accessibility focus events per
+ // window to be able to find the focus in the active window,
+ // we take a stateless approach and look it up. This is fine
+ // since we do this only when the user clicks/long presses.
+ Service service = getQueryBridge();
+ final int connectionId = service.mId;
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ client.addConnection(connectionId, service);
+ try {
+ AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance()
+ .getRootInActiveWindow(connectionId);
+ if (root == null) {
+ return false;
+ }
+ AccessibilityNodeInfo focus = root.findFocus(
+ AccessibilityNodeInfo.FOCUS_ACCESSIBILITY);
+ if (focus == null) {
+ return false;
+ }
+ focus.getBoundsInScreen(outBounds);
+
+ MagnificationSpec spec = service.getCompatibleMagnificationSpec(focus.getWindowId());
+ if (spec != null && !spec.isNop()) {
+ outBounds.offset((int) -spec.offsetX, (int) -spec.offsetY);
+ outBounds.scale(1 / spec.scale);
+ }
+
+ // Clip to the window rectangle.
+ Rect windowBounds = mTempRect;
+ getActiveWindowBounds(windowBounds);
+ outBounds.intersect(windowBounds);
+ // Clip to the screen rectangle.
+ mDefaultDisplay.getRealSize(mTempPoint);
+ outBounds.intersect(0, 0, mTempPoint.x, mTempPoint.y);
+
+ return true;
+ } finally {
+ client.removeConnection(connectionId);
+ }
+ }
+
+ /**
+ * Gets the bounds of the active window.
+ *
+ * @param outBounds The output to which to write the bounds.
+ */
+ boolean getActiveWindowBounds(Rect outBounds) {
+ IBinder token;
+ synchronized (mLock) {
+ final int windowId = mSecurityPolicy.mActiveWindowId;
+ token = mGlobalWindowTokens.get(windowId);
+ if (token == null) {
+ token = getCurrentUserStateLocked().mWindowTokens.get(windowId);
+ }
+ }
+ try {
+ mWindowManagerService.getWindowFrame(token, outBounds);
+ if (!outBounds.isEmpty()) {
+ return true;
+ }
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ return false;
+ }
+
+ int getActiveWindowId() {
+ return mSecurityPolicy.mActiveWindowId;
+ }
+
+ void onTouchInteractionStart() {
+ mSecurityPolicy.onTouchInteractionStart();
+ }
+
+ void onTouchInteractionEnd() {
+ mSecurityPolicy.onTouchInteractionEnd();
+ }
+
+ void onMagnificationStateChanged() {
+ notifyClearAccessibilityNodeInfoCacheLocked();
+ }
+
+ private void switchUser(int userId) {
+ synchronized (mLock) {
+ if (mCurrentUserId == userId && mInitialized) {
+ return;
+ }
+
+ // Disconnect from services for the old user.
+ UserState oldUserState = getUserStateLocked(mCurrentUserId);
+ oldUserState.onSwitchToAnotherUser();
+
+ // Disable the local managers for the old user.
+ if (oldUserState.mClients.getRegisteredCallbackCount() > 0) {
+ mMainHandler.obtainMessage(MainHandler.MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER,
+ oldUserState.mUserId, 0).sendToTarget();
+ }
+
+ // Announce user changes only if more that one exist.
+ UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ final boolean announceNewUser = userManager.getUsers().size() > 1;
+
+ // The user changed.
+ mCurrentUserId = userId;
+
+ UserState userState = getCurrentUserStateLocked();
+ if (userState.mUiAutomationService != null) {
+ // Switching users disables the UI automation service.
+ userState.mUiAutomationService.binderDied();
+ }
+
+ readConfigurationForUserStateLocked(userState);
+ // Even if reading did not yield change, we have to update
+ // the state since the context in which the current user
+ // state was used has changed since it was inactive.
+ onUserStateChangedLocked(userState);
+
+ if (announceNewUser) {
+ // Schedule announcement of the current user if needed.
+ mMainHandler.sendEmptyMessageDelayed(MainHandler.MSG_ANNOUNCE_NEW_USER_IF_NEEDED,
+ WAIT_FOR_USER_STATE_FULLY_INITIALIZED_MILLIS);
+ }
+ }
+ }
+
+ private void removeUser(int userId) {
+ synchronized (mLock) {
+ mUserStates.remove(userId);
+ }
+ }
+
+ private Service getQueryBridge() {
+ if (mQueryBridge == null) {
+ AccessibilityServiceInfo info = new AccessibilityServiceInfo();
+ info.setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT);
+ mQueryBridge = new Service(UserHandle.USER_NULL,
+ sFakeAccessibilityServiceComponentName, info);
+ }
+ return mQueryBridge;
+ }
+
+ private boolean notifyGestureLocked(int gestureId, boolean isDefault) {
+ // TODO: Now we are giving the gestures to the last enabled
+ // service that can handle them which is the last one
+ // in our list since we write the last enabled as the
+ // last record in the enabled services setting. Ideally,
+ // the user should make the call which service handles
+ // gestures. However, only one service should handle
+ // gestures to avoid user frustration when different
+ // behavior is observed from different combinations of
+ // enabled accessibility services.
+ UserState state = getCurrentUserStateLocked();
+ for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
+ Service service = state.mBoundServices.get(i);
+ if (service.mRequestTouchExplorationMode && service.mIsDefault == isDefault) {
+ service.notifyGesture(gestureId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean notifyKeyEventLocked(KeyEvent event, int policyFlags, boolean isDefault) {
+ // TODO: Now we are giving the key events to the last enabled
+ // service that can handle them Ideally, the user should
+ // make the call which service handles key events. However,
+ // only one service should handle key events to avoid user
+ // frustration when different behavior is observed from
+ // different combinations of enabled accessibility services.
+ UserState state = getCurrentUserStateLocked();
+ for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
+ Service service = state.mBoundServices.get(i);
+ // Key events are handled only by services that declared
+ // this capability and requested to filter key events.
+ if (!service.mRequestFilterKeyEvents ||
+ (service.mAccessibilityServiceInfo.getCapabilities() & AccessibilityServiceInfo
+ .CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS) == 0) {
+ continue;
+ }
+ if (service.mIsDefault == isDefault) {
+ service.notifyKeyEvent(event, policyFlags);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void notifyClearAccessibilityNodeInfoCacheLocked() {
+ UserState state = getCurrentUserStateLocked();
+ for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
+ Service service = state.mBoundServices.get(i);
+ service.notifyClearAccessibilityNodeInfoCache();
+ }
+ }
+
+ /**
+ * Removes an AccessibilityInteractionConnection.
+ *
+ * @param windowId The id of the window to which the connection is targeted.
+ * @param userId The id of the user owning the connection. UserHandle.USER_ALL
+ * if global.
+ */
+ private void removeAccessibilityInteractionConnectionLocked(int windowId, int userId) {
+ if (userId == UserHandle.USER_ALL) {
+ mGlobalWindowTokens.remove(windowId);
+ mGlobalInteractionConnections.remove(windowId);
+ } else {
+ UserState userState = getCurrentUserStateLocked();
+ userState.mWindowTokens.remove(windowId);
+ userState.mInteractionConnections.remove(windowId);
+ }
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Removing interaction connection to windowId: " + windowId);
+ }
+ }
+
+ private boolean readInstalledAccessibilityServiceLocked(UserState userState) {
+ mTempAccessibilityServiceInfoList.clear();
+
+ List<ResolveInfo> installedServices = mPackageManager.queryIntentServicesAsUser(
+ new Intent(AccessibilityService.SERVICE_INTERFACE),
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
+ mCurrentUserId);
+
+ for (int i = 0, count = installedServices.size(); i < count; i++) {
+ ResolveInfo resolveInfo = installedServices.get(i);
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (!android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE.equals(
+ serviceInfo.permission)) {
+ Slog.w(LOG_TAG, "Skipping accessibilty service " + new ComponentName(
+ serviceInfo.packageName, serviceInfo.name).flattenToShortString()
+ + ": it does not require the permission "
+ + android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE);
+ continue;
+ }
+ AccessibilityServiceInfo accessibilityServiceInfo;
+ try {
+ accessibilityServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext);
+ mTempAccessibilityServiceInfoList.add(accessibilityServiceInfo);
+ } catch (XmlPullParserException xppe) {
+ Slog.e(LOG_TAG, "Error while initializing AccessibilityServiceInfo", xppe);
+ } catch (IOException ioe) {
+ Slog.e(LOG_TAG, "Error while initializing AccessibilityServiceInfo", ioe);
+ }
+ }
+
+ if (!mTempAccessibilityServiceInfoList.equals(userState.mInstalledServices)) {
+ userState.mInstalledServices.clear();
+ userState.mInstalledServices.addAll(mTempAccessibilityServiceInfoList);
+ mTempAccessibilityServiceInfoList.clear();
+ return true;
+ }
+
+ mTempAccessibilityServiceInfoList.clear();
+ return false;
+ }
+
+ private boolean readEnabledAccessibilityServicesLocked(UserState userState) {
+ mTempComponentNameSet.clear();
+ readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ userState.mUserId, mTempComponentNameSet);
+ if (!mTempComponentNameSet.equals(userState.mEnabledServices)) {
+ userState.mEnabledServices.clear();
+ userState.mEnabledServices.addAll(mTempComponentNameSet);
+ mTempComponentNameSet.clear();
+ return true;
+ }
+ mTempComponentNameSet.clear();
+ return false;
+ }
+
+ private boolean readTouchExplorationGrantedAccessibilityServicesLocked(
+ UserState userState) {
+ mTempComponentNameSet.clear();
+ readComponentNamesFromSettingLocked(
+ Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
+ userState.mUserId, mTempComponentNameSet);
+ if (!mTempComponentNameSet.equals(userState.mTouchExplorationGrantedServices)) {
+ userState.mTouchExplorationGrantedServices.clear();
+ userState.mTouchExplorationGrantedServices.addAll(mTempComponentNameSet);
+ mTempComponentNameSet.clear();
+ return true;
+ }
+ mTempComponentNameSet.clear();
+ return false;
+ }
+
+ /**
+ * Performs {@link AccessibilityService}s delayed notification. The delay is configurable
+ * and denotes the period after the last event before notifying the service.
+ *
+ * @param event The event.
+ * @param isDefault True to notify default listeners, not default services.
+ */
+ private void notifyAccessibilityServicesDelayedLocked(AccessibilityEvent event,
+ boolean isDefault) {
+ try {
+ UserState state = getCurrentUserStateLocked();
+ for (int i = 0, count = state.mBoundServices.size(); i < count; i++) {
+ Service service = state.mBoundServices.get(i);
+
+ if (service.mIsDefault == isDefault) {
+ if (canDispathEventLocked(service, event, state.mHandledFeedbackTypes)) {
+ state.mHandledFeedbackTypes |= service.mFeedbackType;
+ service.notifyAccessibilityEvent(event);
+ }
+ }
+ }
+ } catch (IndexOutOfBoundsException oobe) {
+ // An out of bounds exception can happen if services are going away
+ // as the for loop is running. If that happens, just bail because
+ // there are no more services to notify.
+ return;
+ }
+ }
+
+ private void addServiceLocked(Service service, UserState userState) {
+ try {
+ service.linkToOwnDeathLocked();
+ userState.mBoundServices.add(service);
+ userState.mComponentNameToServiceMap.put(service.mComponentName, service);
+ } catch (RemoteException re) {
+ /* do nothing */
+ }
+ }
+
+ /**
+ * Removes a service.
+ *
+ * @param service The service.
+ * @return True if the service was removed, false otherwise.
+ */
+ private void removeServiceLocked(Service service, UserState userState) {
+ userState.mBoundServices.remove(service);
+ userState.mComponentNameToServiceMap.remove(service.mComponentName);
+ service.unlinkToOwnDeathLocked();
+ }
+
+ /**
+ * Determines if given event can be dispatched to a service based on the package of the
+ * event source and already notified services for that event type. Specifically, a
+ * service is notified if it is interested in events from the package and no other service
+ * providing the same feedback type has been notified. Exception are services the
+ * provide generic feedback (feedback type left as a safety net for unforeseen feedback
+ * types) which are always notified.
+ *
+ * @param service The potential receiver.
+ * @param event The event.
+ * @param handledFeedbackTypes The feedback types for which services have been notified.
+ * @return True if the listener should be notified, false otherwise.
+ */
+ private boolean canDispathEventLocked(Service service, AccessibilityEvent event,
+ int handledFeedbackTypes) {
+
+ if (!service.canReceiveEventsLocked()) {
+ return false;
+ }
+
+ if (!event.isImportantForAccessibility()
+ && (service.mFetchFlags
+ & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) == 0) {
+ return false;
+ }
+
+ int eventType = event.getEventType();
+ if ((service.mEventTypes & eventType) != eventType) {
+ return false;
+ }
+
+ Set<String> packageNames = service.mPackageNames;
+ CharSequence packageName = event.getPackageName();
+
+ if (packageNames.isEmpty() || packageNames.contains(packageName)) {
+ int feedbackType = service.mFeedbackType;
+ if ((handledFeedbackTypes & feedbackType) != feedbackType
+ || feedbackType == AccessibilityServiceInfo.FEEDBACK_GENERIC) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void unbindAllServicesLocked(UserState userState) {
+ List<Service> services = userState.mBoundServices;
+ for (int i = 0, count = services.size(); i < count; i++) {
+ Service service = services.get(i);
+ if (service.unbindLocked()) {
+ i--;
+ count--;
+ }
+ }
+ }
+
+ /**
+ * Populates a set with the {@link ComponentName}s stored in a colon
+ * separated value setting for a given user.
+ *
+ * @param settingName The setting to parse.
+ * @param userId The user id.
+ * @param outComponentNames The output component names.
+ */
+ private void readComponentNamesFromSettingLocked(String settingName, int userId,
+ Set<ComponentName> outComponentNames) {
+ String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+ settingName, userId);
+ outComponentNames.clear();
+ if (settingValue != null) {
+ TextUtils.SimpleStringSplitter splitter = mStringColonSplitter;
+ splitter.setString(settingValue);
+ while (splitter.hasNext()) {
+ String str = splitter.next();
+ if (str == null || str.length() <= 0) {
+ continue;
+ }
+ ComponentName enabledService = ComponentName.unflattenFromString(str);
+ if (enabledService != null) {
+ outComponentNames.add(enabledService);
+ }
+ }
+ }
+ }
+
+ /**
+ * Persists the component names in the specified setting in a
+ * colon separated fashion.
+ *
+ * @param settingName The setting name.
+ * @param componentNames The component names.
+ */
+ private void persistComponentNamesToSettingLocked(String settingName,
+ Set<ComponentName> componentNames, int userId) {
+ StringBuilder builder = new StringBuilder();
+ for (ComponentName componentName : componentNames) {
+ if (builder.length() > 0) {
+ builder.append(COMPONENT_NAME_SEPARATOR);
+ }
+ builder.append(componentName.flattenToShortString());
+ }
+ Settings.Secure.putStringForUser(mContext.getContentResolver(),
+ settingName, builder.toString(), userId);
+ }
+
+ private void manageServicesLocked(UserState userState) {
+ Map<ComponentName, Service> componentNameToServiceMap =
+ userState.mComponentNameToServiceMap;
+ boolean isEnabled = userState.mIsAccessibilityEnabled;
+
+ for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) {
+ AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i);
+ ComponentName componentName = ComponentName.unflattenFromString(
+ installedService.getId());
+ Service service = componentNameToServiceMap.get(componentName);
+
+ if (isEnabled) {
+ // Wait for the binding if it is in process.
+ if (userState.mBindingServices.contains(componentName)) {
+ continue;
+ }
+ if (userState.mEnabledServices.contains(componentName)) {
+ if (service == null) {
+ service = new Service(userState.mUserId, componentName, installedService);
+ } else if (userState.mBoundServices.contains(service)) {
+ continue;
+ }
+ service.bindLocked();
+ } else {
+ if (service != null) {
+ service.unbindLocked();
+ }
+ }
+ } else {
+ if (service != null) {
+ service.unbindLocked();
+ } else {
+ userState.mBindingServices.remove(componentName);
+ }
+ }
+ }
+
+ // No enabled installed services => disable accessibility to avoid
+ // sending accessibility events with no recipient across processes.
+ if (isEnabled && userState.mEnabledServices.isEmpty()) {
+ userState.mIsAccessibilityEnabled = false;
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_ENABLED, 0, userState.mUserId);
+ }
+ }
+
+ private void scheduleUpdateClientsIfNeededLocked(UserState userState) {
+ final int clientState = userState.getClientState();
+ if (userState.mLastSentClientState != clientState
+ && (mGlobalClients.getRegisteredCallbackCount() > 0
+ || userState.mClients.getRegisteredCallbackCount() > 0)) {
+ userState.mLastSentClientState = clientState;
+ mMainHandler.obtainMessage(MainHandler.MSG_SEND_STATE_TO_CLIENTS,
+ clientState, userState.mUserId) .sendToTarget();
+ }
+ }
+
+ private void scheduleUpdateInputFilter(UserState userState) {
+ mMainHandler.obtainMessage(MainHandler.MSG_UPDATE_INPUT_FILTER, userState).sendToTarget();
+ }
+
+ private void updateInputFilter(UserState userState) {
+ boolean setInputFilter = false;
+ AccessibilityInputFilter inputFilter = null;
+ synchronized (mLock) {
+ int flags = 0;
+ if (userState.mIsDisplayMagnificationEnabled) {
+ flags |= AccessibilityInputFilter.FLAG_FEATURE_SCREEN_MAGNIFIER;
+ }
+ // Touch exploration without accessibility makes no sense.
+ if (userState.mIsAccessibilityEnabled && userState.mIsTouchExplorationEnabled) {
+ flags |= AccessibilityInputFilter.FLAG_FEATURE_TOUCH_EXPLORATION;
+ }
+ if (userState.mIsFilterKeyEventsEnabled) {
+ flags |= AccessibilityInputFilter.FLAG_FEATURE_FILTER_KEY_EVENTS;
+ }
+ if (flags != 0) {
+ if (!mHasInputFilter) {
+ mHasInputFilter = true;
+ if (mInputFilter == null) {
+ mInputFilter = new AccessibilityInputFilter(mContext,
+ AccessibilityManagerService.this);
+ }
+ inputFilter = mInputFilter;
+ setInputFilter = true;
+ }
+ mInputFilter.setEnabledFeatures(flags);
+ } else {
+ if (mHasInputFilter) {
+ mHasInputFilter = false;
+ mInputFilter.disableFeatures();
+ inputFilter = null;
+ setInputFilter = true;
+ }
+ }
+ }
+ if (setInputFilter) {
+ try {
+ mWindowManagerService.setInputFilter(inputFilter);
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ }
+ }
+
+ private void showEnableTouchExplorationDialog(final Service service) {
+ synchronized (mLock) {
+ String label = service.mResolveInfo.loadLabel(
+ mContext.getPackageManager()).toString();
+
+ final UserState state = getCurrentUserStateLocked();
+ if (state.mIsTouchExplorationEnabled) {
+ return;
+ }
+ if (mEnableTouchExplorationDialog != null
+ && mEnableTouchExplorationDialog.isShowing()) {
+ return;
+ }
+ mEnableTouchExplorationDialog = new AlertDialog.Builder(mContext)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // The user allowed the service to toggle touch exploration.
+ state.mTouchExplorationGrantedServices.add(service.mComponentName);
+ persistComponentNamesToSettingLocked(
+ Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
+ state.mTouchExplorationGrantedServices, state.mUserId);
+ // Enable touch exploration.
+ UserState userState = getUserStateLocked(service.mUserId);
+ userState.mIsTouchExplorationEnabled = true;
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
+ Settings.Secure.TOUCH_EXPLORATION_ENABLED, 1,
+ service.mUserId);
+ onUserStateChangedLocked(userState);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .setTitle(R.string.enable_explore_by_touch_warning_title)
+ .setMessage(mContext.getString(
+ R.string.enable_explore_by_touch_warning_message, label))
+ .create();
+ mEnableTouchExplorationDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ mEnableTouchExplorationDialog.getWindow().getAttributes().privateFlags
+ |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+ mEnableTouchExplorationDialog.setCanceledOnTouchOutside(true);
+ mEnableTouchExplorationDialog.show();
+ }
+ }
+
+ private void onUserStateChangedLocked(UserState userState) {
+ // TODO: Remove this hack
+ mInitialized = true;
+ updateLegacyCapabilities(userState);
+ updateServicesLocked(userState);
+ updateFilterKeyEventsLocked(userState);
+ updateTouchExplorationLocked(userState);
+ updateEnhancedWebAccessibilityLocked(userState);
+ scheduleUpdateInputFilter(userState);
+ scheduleUpdateClientsIfNeededLocked(userState);
+ }
+
+ private void updateLegacyCapabilities(UserState userState) {
+ // Up to JB-MR1 we had a white list with services that can enable touch
+ // exploration. When a service is first started we show a dialog to the
+ // use to get a permission to white list the service.
+ final int installedServiceCount = userState.mInstalledServices.size();
+ for (int i = 0; i < installedServiceCount; i++) {
+ AccessibilityServiceInfo serviceInfo = userState.mInstalledServices.get(i);
+ ResolveInfo resolveInfo = serviceInfo.getResolveInfo();
+ if ((serviceInfo.getCapabilities()
+ & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION) == 0
+ && resolveInfo.serviceInfo.applicationInfo.targetSdkVersion
+ <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ ComponentName componentName = new ComponentName(
+ resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
+ if (userState.mTouchExplorationGrantedServices.contains(componentName)) {
+ serviceInfo.setCapabilities(serviceInfo.getCapabilities()
+ | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION);
+ }
+ }
+ }
+ }
+
+ private void updateFilterKeyEventsLocked(UserState userState) {
+ final int serviceCount = userState.mBoundServices.size();
+ for (int i = 0; i < serviceCount; i++) {
+ Service service = userState.mBoundServices.get(i);
+ if (service.mRequestFilterKeyEvents
+ && (service.mAccessibilityServiceInfo.getCapabilities()
+ & AccessibilityServiceInfo
+ .CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS) != 0) {
+ userState.mIsFilterKeyEventsEnabled = true;
+ return;
+ }
+ }
+ userState.mIsFilterKeyEventsEnabled = false;
+ }
+
+ private void updateServicesLocked(UserState userState) {
+ if (userState.mIsAccessibilityEnabled) {
+ manageServicesLocked(userState);
+ } else {
+ unbindAllServicesLocked(userState);
+ }
+ }
+
+ private boolean readConfigurationForUserStateLocked(UserState userState) {
+ boolean somthingChanged = false;
+ somthingChanged |= readAccessibilityEnabledSettingLocked(userState);
+ somthingChanged |= readInstalledAccessibilityServiceLocked(userState);
+ somthingChanged |= readEnabledAccessibilityServicesLocked(userState);
+ somthingChanged |= readTouchExplorationGrantedAccessibilityServicesLocked(userState);
+ somthingChanged |= readTouchExplorationEnabledSettingLocked(userState);
+ somthingChanged |= readEnhancedWebAccessibilityEnabledChangedLocked(userState);
+ somthingChanged |= readDisplayMagnificationEnabledSettingLocked(userState);
+ return somthingChanged;
+ }
+
+ private boolean readAccessibilityEnabledSettingLocked(UserState userState) {
+ final boolean accessibilityEnabled = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_ENABLED, 0, userState.mUserId) == 1;
+ if (accessibilityEnabled != userState.mIsAccessibilityEnabled) {
+ userState.mIsAccessibilityEnabled = accessibilityEnabled;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean readTouchExplorationEnabledSettingLocked(UserState userState) {
+ final boolean touchExplorationEnabled = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0, userState.mUserId) == 1;
+ if (touchExplorationEnabled != userState.mIsTouchExplorationEnabled) {
+ userState.mIsTouchExplorationEnabled = touchExplorationEnabled;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean readDisplayMagnificationEnabledSettingLocked(UserState userState) {
+ final boolean displayMagnificationEnabled = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED,
+ 0, userState.mUserId) == 1;
+ if (displayMagnificationEnabled != userState.mIsDisplayMagnificationEnabled) {
+ userState.mIsDisplayMagnificationEnabled = displayMagnificationEnabled;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean readEnhancedWebAccessibilityEnabledChangedLocked(UserState userState) {
+ final boolean enhancedWeAccessibilityEnabled = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION,
+ 0, userState.mUserId) == 1;
+ if (enhancedWeAccessibilityEnabled != userState.mIsEnhancedWebAccessibilityEnabled) {
+ userState.mIsEnhancedWebAccessibilityEnabled = enhancedWeAccessibilityEnabled;
+ return true;
+ }
+ return false;
+ }
+
+ private void updateTouchExplorationLocked(UserState userState) {
+ boolean enabled = false;
+ final int serviceCount = userState.mBoundServices.size();
+ for (int i = 0; i < serviceCount; i++) {
+ Service service = userState.mBoundServices.get(i);
+ if (canRequestAndRequestsTouchExplorationLocked(service)) {
+ enabled = true;
+ break;
+ }
+ }
+ if (enabled != userState.mIsTouchExplorationEnabled) {
+ userState.mIsTouchExplorationEnabled = enabled;
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
+ Settings.Secure.TOUCH_EXPLORATION_ENABLED, enabled ? 1 : 0,
+ userState.mUserId);
+ }
+ try {
+ mWindowManagerService.setTouchExplorationEnabled(enabled);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private boolean canRequestAndRequestsTouchExplorationLocked(Service service) {
+ // Service not ready or cannot request the feature - well nothing to do.
+ if (!service.canReceiveEventsLocked() || !service.mRequestTouchExplorationMode) {
+ return false;
+ }
+ // UI test automation service can always enable it.
+ if (service.mIsAutomation) {
+ return true;
+ }
+ if (service.mResolveInfo.serviceInfo.applicationInfo.targetSdkVersion
+ <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ // Up to JB-MR1 we had a white list with services that can enable touch
+ // exploration. When a service is first started we show a dialog to the
+ // use to get a permission to white list the service.
+ UserState userState = getUserStateLocked(service.mUserId);
+ if (userState.mTouchExplorationGrantedServices.contains(service.mComponentName)) {
+ return true;
+ } else if (mEnableTouchExplorationDialog == null
+ || !mEnableTouchExplorationDialog.isShowing()) {
+ mMainHandler.obtainMessage(
+ MainHandler.MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG,
+ service).sendToTarget();
+ }
+ } else {
+ // Starting in JB-MR2 we request an accessibility service to declare
+ // certain capabilities in its meta-data to allow it to enable the
+ // corresponding features.
+ if ((service.mAccessibilityServiceInfo.getCapabilities()
+ & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void updateEnhancedWebAccessibilityLocked(UserState userState) {
+ boolean enabled = false;
+ final int serviceCount = userState.mBoundServices.size();
+ for (int i = 0; i < serviceCount; i++) {
+ Service service = userState.mBoundServices.get(i);
+ if (canRequestAndRequestsEnhancedWebAccessibilityLocked(service)) {
+ enabled = true;
+ break;
+ }
+ }
+ if (enabled != userState.mIsEnhancedWebAccessibilityEnabled) {
+ userState.mIsEnhancedWebAccessibilityEnabled = enabled;
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, enabled ? 1 : 0,
+ userState.mUserId);
+ }
+ }
+
+ private boolean canRequestAndRequestsEnhancedWebAccessibilityLocked(Service service) {
+ if (!service.canReceiveEventsLocked() || !service.mRequestEnhancedWebAccessibility ) {
+ return false;
+ }
+ if (service.mIsAutomation || (service.mAccessibilityServiceInfo.getCapabilities()
+ & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
+ mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP);
+ synchronized (mLock) {
+ pw.println("ACCESSIBILITY MANAGER (dumpsys accessibility)");
+ pw.println();
+ final int userCount = mUserStates.size();
+ for (int i = 0; i < userCount; i++) {
+ UserState userState = mUserStates.valueAt(i);
+ pw.append("User state[attributes:{id=" + userState.mUserId);
+ pw.append(", currentUser=" + (userState.mUserId == mCurrentUserId));
+ pw.append(", accessibilityEnabled=" + userState.mIsAccessibilityEnabled);
+ pw.append(", touchExplorationEnabled=" + userState.mIsTouchExplorationEnabled);
+ pw.append(", displayMagnificationEnabled="
+ + userState.mIsDisplayMagnificationEnabled);
+ if (userState.mUiAutomationService != null) {
+ pw.append(", ");
+ userState.mUiAutomationService.dump(fd, pw, args);
+ pw.println();
+ }
+ pw.append("}");
+ pw.println();
+ pw.append(" services:{");
+ final int serviceCount = userState.mBoundServices.size();
+ for (int j = 0; j < serviceCount; j++) {
+ if (j > 0) {
+ pw.append(", ");
+ pw.println();
+ pw.append(" ");
+ }
+ Service service = userState.mBoundServices.get(j);
+ service.dump(fd, pw, args);
+ }
+ pw.println("}]");
+ pw.println();
+ }
+ }
+ }
+
+ private class AccessibilityConnectionWrapper implements DeathRecipient {
+ private final int mWindowId;
+ private final int mUserId;
+ private final IAccessibilityInteractionConnection mConnection;
+
+ public AccessibilityConnectionWrapper(int windowId,
+ IAccessibilityInteractionConnection connection, int userId) {
+ mWindowId = windowId;
+ mUserId = userId;
+ mConnection = connection;
+ }
+
+ public void linkToDeath() throws RemoteException {
+ mConnection.asBinder().linkToDeath(this, 0);
+ }
+
+ public void unlinkToDeath() {
+ mConnection.asBinder().unlinkToDeath(this, 0);
+ }
+
+ @Override
+ public void binderDied() {
+ unlinkToDeath();
+ synchronized (mLock) {
+ removeAccessibilityInteractionConnectionLocked(mWindowId, mUserId);
+ }
+ }
+ }
+
+ private final class MainHandler extends Handler {
+ public static final int MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER = 1;
+ public static final int MSG_SEND_STATE_TO_CLIENTS = 2;
+ public static final int MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER = 3;
+ public static final int MSG_UPDATE_ACTIVE_WINDOW = 4;
+ public static final int MSG_ANNOUNCE_NEW_USER_IF_NEEDED = 5;
+ public static final int MSG_UPDATE_INPUT_FILTER = 6;
+ public static final int MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG = 7;
+ public static final int MSG_SEND_KEY_EVENT_TO_INPUT_FILTER = 8;
+
+ public MainHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final int type = msg.what;
+ switch (type) {
+ case MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER: {
+ AccessibilityEvent event = (AccessibilityEvent) msg.obj;
+ synchronized (mLock) {
+ if (mHasInputFilter && mInputFilter != null) {
+ mInputFilter.notifyAccessibilityEvent(event);
+ }
+ }
+ event.recycle();
+ } break;
+ case MSG_SEND_KEY_EVENT_TO_INPUT_FILTER: {
+ KeyEvent event = (KeyEvent) msg.obj;
+ final int policyFlags = msg.arg1;
+ synchronized (mLock) {
+ if (mHasInputFilter && mInputFilter != null) {
+ mInputFilter.sendInputEvent(event, policyFlags);
+ }
+ }
+ event.recycle();
+ } break;
+ case MSG_SEND_STATE_TO_CLIENTS: {
+ final int clientState = msg.arg1;
+ final int userId = msg.arg2;
+ sendStateToClients(clientState, mGlobalClients);
+ sendStateToClientsForUser(clientState, userId);
+ } break;
+ case MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER: {
+ final int userId = msg.arg1;
+ sendStateToClientsForUser(0, userId);
+ } break;
+ case MSG_UPDATE_ACTIVE_WINDOW: {
+ final int windowId = msg.arg1;
+ final int eventType = msg.arg2;
+ mSecurityPolicy.updateActiveWindow(windowId, eventType);
+ } break;
+ case MSG_ANNOUNCE_NEW_USER_IF_NEEDED: {
+ announceNewUserIfNeeded();
+ } break;
+ case MSG_UPDATE_INPUT_FILTER: {
+ UserState userState = (UserState) msg.obj;
+ updateInputFilter(userState);
+ } break;
+ case MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG: {
+ Service service = (Service) msg.obj;
+ showEnableTouchExplorationDialog(service);
+ } break;
+ }
+ }
+
+ private void announceNewUserIfNeeded() {
+ synchronized (mLock) {
+ UserState userState = getCurrentUserStateLocked();
+ if (userState.mIsAccessibilityEnabled) {
+ UserManager userManager = (UserManager) mContext.getSystemService(
+ Context.USER_SERVICE);
+ String message = mContext.getString(R.string.user_switched,
+ userManager.getUserInfo(mCurrentUserId).name);
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ event.getText().add(message);
+ event.setWindowId(mSecurityPolicy.getRetrievalAllowingWindowLocked());
+ sendAccessibilityEvent(event, mCurrentUserId);
+ }
+ }
+ }
+
+ private void sendStateToClientsForUser(int clientState, int userId) {
+ final UserState userState;
+ synchronized (mLock) {
+ userState = getUserStateLocked(userId);
+ }
+ sendStateToClients(clientState, userState.mClients);
+ }
+
+ private void sendStateToClients(int clientState,
+ RemoteCallbackList<IAccessibilityManagerClient> clients) {
+ try {
+ final int userClientCount = clients.beginBroadcast();
+ for (int i = 0; i < userClientCount; i++) {
+ IAccessibilityManagerClient client = clients.getBroadcastItem(i);
+ try {
+ client.setState(clientState);
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ }
+ } finally {
+ clients.finishBroadcast();
+ }
+ }
+ }
+
+ private PendingEvent obtainPendingEventLocked(KeyEvent event, int policyFlags, int sequence) {
+ PendingEvent pendingEvent = mPendingEventPool.acquire();
+ if (pendingEvent == null) {
+ pendingEvent = new PendingEvent();
+ }
+ pendingEvent.event = event;
+ pendingEvent.policyFlags = policyFlags;
+ pendingEvent.sequence = sequence;
+ return pendingEvent;
+ }
+
+ private void recyclePendingEventLocked(PendingEvent pendingEvent) {
+ pendingEvent.clear();
+ mPendingEventPool.release(pendingEvent);
+ }
+
+ /**
+ * This class represents an accessibility service. It stores all per service
+ * data required for the service management, provides API for starting/stopping the
+ * service and is responsible for adding/removing the service in the data structures
+ * for service management. The class also exposes configuration interface that is
+ * passed to the service it represents as soon it is bound. It also serves as the
+ * connection for the service.
+ */
+ class Service extends IAccessibilityServiceConnection.Stub
+ implements ServiceConnection, DeathRecipient {;
+
+ final int mUserId;
+
+ int mId = 0;
+
+ AccessibilityServiceInfo mAccessibilityServiceInfo;
+
+ IBinder mService;
+
+ IAccessibilityServiceClient mServiceInterface;
+
+ int mEventTypes;
+
+ int mFeedbackType;
+
+ Set<String> mPackageNames = new HashSet<String>();
+
+ boolean mIsDefault;
+
+ boolean mRequestTouchExplorationMode;
+
+ boolean mRequestEnhancedWebAccessibility;
+
+ boolean mRequestFilterKeyEvents;
+
+ int mFetchFlags;
+
+ long mNotificationTimeout;
+
+ ComponentName mComponentName;
+
+ Intent mIntent;
+
+ boolean mIsAutomation;
+
+ final Rect mTempBounds = new Rect();
+
+ final ResolveInfo mResolveInfo;
+
+ // the events pending events to be dispatched to this service
+ final SparseArray<AccessibilityEvent> mPendingEvents =
+ new SparseArray<AccessibilityEvent>();
+
+ final KeyEventDispatcher mKeyEventDispatcher = new KeyEventDispatcher();
+
+ boolean mWasConnectedAndDied;
+
+ // Handler only for dispatching accessibility events since we use event
+ // types as message types allowing us to remove messages per event type.
+ public Handler mEventDispatchHandler = new Handler(mMainHandler.getLooper()) {
+ @Override
+ public void handleMessage(Message message) {
+ final int eventType = message.what;
+ notifyAccessibilityEventInternal(eventType);
+ }
+ };
+
+ // Handler for scheduling method invocations on the main thread.
+ public InvocationHandler mInvocationHandler = new InvocationHandler(
+ mMainHandler.getLooper());
+
+ public Service(int userId, ComponentName componentName,
+ AccessibilityServiceInfo accessibilityServiceInfo) {
+ mUserId = userId;
+ mResolveInfo = accessibilityServiceInfo.getResolveInfo();
+ mId = sIdCounter++;
+ mComponentName = componentName;
+ mAccessibilityServiceInfo = accessibilityServiceInfo;
+ mIsAutomation = (sFakeAccessibilityServiceComponentName.equals(componentName));
+ if (!mIsAutomation) {
+ mIntent = new Intent().setComponent(mComponentName);
+ mIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
+ com.android.internal.R.string.accessibility_binding_label);
+ mIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
+ mContext, 0, new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS), 0));
+ }
+ setDynamicallyConfigurableProperties(accessibilityServiceInfo);
+ }
+
+ public void setDynamicallyConfigurableProperties(AccessibilityServiceInfo info) {
+ mEventTypes = info.eventTypes;
+ mFeedbackType = info.feedbackType;
+ String[] packageNames = info.packageNames;
+ if (packageNames != null) {
+ mPackageNames.addAll(Arrays.asList(packageNames));
+ }
+ mNotificationTimeout = info.notificationTimeout;
+ mIsDefault = (info.flags & DEFAULT) != 0;
+
+ if (mIsAutomation || info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion
+ >= Build.VERSION_CODES.JELLY_BEAN) {
+ if ((info.flags & AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0) {
+ mFetchFlags |= AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+ } else {
+ mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+ }
+ }
+
+ if ((info.flags & AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS) != 0) {
+ mFetchFlags |= AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
+ } else {
+ mFetchFlags &= ~AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
+ }
+
+ mRequestTouchExplorationMode = (info.flags
+ & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0;
+ mRequestEnhancedWebAccessibility = (info.flags
+ & AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0;
+ mRequestFilterKeyEvents = (info.flags
+ & AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS) != 0;
+ }
+
+ /**
+ * Binds to the accessibility service.
+ *
+ * @return True if binding is successful.
+ */
+ public boolean bindLocked() {
+ UserState userState = getUserStateLocked(mUserId);
+ if (!mIsAutomation) {
+ if (mService == null && mContext.bindServiceAsUser(
+ mIntent, this, Context.BIND_AUTO_CREATE, new UserHandle(mUserId))) {
+ userState.mBindingServices.add(mComponentName);
+ }
+ } else {
+ userState.mBindingServices.add(mComponentName);
+ mService = userState.mUiAutomationServiceClient.asBinder();
+ onServiceConnected(mComponentName, mService);
+ userState.mUiAutomationService = this;
+ }
+ return false;
+ }
+
+ /**
+ * Unbinds form the accessibility service and removes it from the data
+ * structures for service management.
+ *
+ * @return True if unbinding is successful.
+ */
+ public boolean unbindLocked() {
+ if (mService == null) {
+ return false;
+ }
+ UserState userState = getUserStateLocked(mUserId);
+ mKeyEventDispatcher.flush();
+ if (!mIsAutomation) {
+ mContext.unbindService(this);
+ } else {
+ userState.destroyUiAutomationService();
+ }
+ removeServiceLocked(this, userState);
+ resetLocked();
+ return true;
+ }
+
+ public boolean canReceiveEventsLocked() {
+ return (mEventTypes != 0 && mFeedbackType != 0 && mService != null);
+ }
+
+ @Override
+ public void setOnKeyEventResult(boolean handled, int sequence) {
+ mKeyEventDispatcher.setOnKeyEventResult(handled, sequence);
+ }
+
+ @Override
+ public AccessibilityServiceInfo getServiceInfo() {
+ synchronized (mLock) {
+ return mAccessibilityServiceInfo;
+ }
+ }
+
+ @Override
+ public void setServiceInfo(AccessibilityServiceInfo info) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ // If the XML manifest had data to configure the service its info
+ // should be already set. In such a case update only the dynamically
+ // configurable properties.
+ AccessibilityServiceInfo oldInfo = mAccessibilityServiceInfo;
+ if (oldInfo != null) {
+ oldInfo.updateDynamicallyConfigurableProperties(info);
+ setDynamicallyConfigurableProperties(oldInfo);
+ } else {
+ setDynamicallyConfigurableProperties(info);
+ }
+ UserState userState = getUserStateLocked(mUserId);
+ onUserStateChangedLocked(userState);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder service) {
+ synchronized (mLock) {
+ mService = service;
+ mServiceInterface = IAccessibilityServiceClient.Stub.asInterface(service);
+ UserState userState = getUserStateLocked(mUserId);
+ addServiceLocked(this, userState);
+ if (userState.mBindingServices.contains(mComponentName) || mWasConnectedAndDied) {
+ userState.mBindingServices.remove(mComponentName);
+ mWasConnectedAndDied = false;
+ try {
+ mServiceInterface.setConnection(this, mId);
+ onUserStateChangedLocked(userState);
+ } catch (RemoteException re) {
+ Slog.w(LOG_TAG, "Error while setting connection for service: "
+ + service, re);
+ binderDied();
+ }
+ } else {
+ binderDied();
+ }
+ }
+ }
+
+ @Override
+ public boolean findAccessibilityNodeInfosByViewId(int accessibilityWindowId,
+ long accessibilityNodeId, String viewIdResName, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
+ throws RemoteException {
+ final int resolvedWindowId;
+ IAccessibilityInteractionConnection connection = null;
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(
+ UserHandle.getCallingUserId());
+ if (resolvedUserId != mCurrentUserId) {
+ return false;
+ }
+ mSecurityPolicy.enforceCanRetrieveWindowContent(this);
+ final boolean permissionGranted = mSecurityPolicy.canRetrieveWindowContent(this);
+ if (!permissionGranted) {
+ return false;
+ } else {
+ resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
+ connection = getConnectionLocked(resolvedWindowId);
+ if (connection == null) {
+ return false;
+ }
+ }
+ }
+ final int interrogatingPid = Binder.getCallingPid();
+ final long identityToken = Binder.clearCallingIdentity();
+ MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId);
+ try {
+ connection.findAccessibilityNodeInfosByViewId(accessibilityNodeId,
+ viewIdResName, interactionId, callback, mFetchFlags, interrogatingPid,
+ interrogatingTid, spec);
+ return true;
+ } catch (RemoteException re) {
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "Error findAccessibilityNodeInfoByViewId().");
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean findAccessibilityNodeInfosByText(int accessibilityWindowId,
+ long accessibilityNodeId, String text, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
+ throws RemoteException {
+ final int resolvedWindowId;
+ IAccessibilityInteractionConnection connection = null;
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(
+ UserHandle.getCallingUserId());
+ if (resolvedUserId != mCurrentUserId) {
+ return false;
+ }
+ mSecurityPolicy.enforceCanRetrieveWindowContent(this);
+ resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
+ final boolean permissionGranted =
+ mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
+ if (!permissionGranted) {
+ return false;
+ } else {
+ connection = getConnectionLocked(resolvedWindowId);
+ if (connection == null) {
+ return false;
+ }
+ }
+ }
+ final int interrogatingPid = Binder.getCallingPid();
+ final long identityToken = Binder.clearCallingIdentity();
+ MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId);
+ try {
+ connection.findAccessibilityNodeInfosByText(accessibilityNodeId, text,
+ interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid,
+ spec);
+ return true;
+ } catch (RemoteException re) {
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfosByText()");
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean findAccessibilityNodeInfoByAccessibilityId(
+ int accessibilityWindowId, long accessibilityNodeId, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags,
+ long interrogatingTid) throws RemoteException {
+ final int resolvedWindowId;
+ IAccessibilityInteractionConnection connection = null;
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(
+ UserHandle.getCallingUserId());
+ if (resolvedUserId != mCurrentUserId) {
+ return false;
+ }
+ mSecurityPolicy.enforceCanRetrieveWindowContent(this);
+ resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
+ final boolean permissionGranted =
+ mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
+ if (!permissionGranted) {
+ return false;
+ } else {
+ connection = getConnectionLocked(resolvedWindowId);
+ if (connection == null) {
+ return false;
+ }
+ }
+ }
+ final int interrogatingPid = Binder.getCallingPid();
+ final long identityToken = Binder.clearCallingIdentity();
+ MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId);
+ try {
+ connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId,
+ interactionId, callback, mFetchFlags | flags, interrogatingPid,
+ interrogatingTid, spec);
+ return true;
+ } catch (RemoteException re) {
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean findFocus(int accessibilityWindowId, long accessibilityNodeId,
+ int focusType, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
+ throws RemoteException {
+ final int resolvedWindowId;
+ IAccessibilityInteractionConnection connection = null;
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(
+ UserHandle.getCallingUserId());
+ if (resolvedUserId != mCurrentUserId) {
+ return false;
+ }
+ mSecurityPolicy.enforceCanRetrieveWindowContent(this);
+ resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
+ final boolean permissionGranted =
+ mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
+ if (!permissionGranted) {
+ return false;
+ } else {
+ connection = getConnectionLocked(resolvedWindowId);
+ if (connection == null) {
+ return false;
+ }
+ }
+ }
+ final int interrogatingPid = Binder.getCallingPid();
+ final long identityToken = Binder.clearCallingIdentity();
+ MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId);
+ try {
+ connection.findFocus(accessibilityNodeId, focusType, interactionId, callback,
+ mFetchFlags, interrogatingPid, interrogatingTid, spec);
+ return true;
+ } catch (RemoteException re) {
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "Error calling findAccessibilityFocus()");
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean focusSearch(int accessibilityWindowId, long accessibilityNodeId,
+ int direction, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
+ throws RemoteException {
+ final int resolvedWindowId;
+ IAccessibilityInteractionConnection connection = null;
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(
+ UserHandle.getCallingUserId());
+ if (resolvedUserId != mCurrentUserId) {
+ return false;
+ }
+ mSecurityPolicy.enforceCanRetrieveWindowContent(this);
+ resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
+ final boolean permissionGranted =
+ mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
+ if (!permissionGranted) {
+ return false;
+ } else {
+ connection = getConnectionLocked(resolvedWindowId);
+ if (connection == null) {
+ return false;
+ }
+ }
+ }
+ final int interrogatingPid = Binder.getCallingPid();
+ final long identityToken = Binder.clearCallingIdentity();
+ MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId);
+ try {
+ connection.focusSearch(accessibilityNodeId, direction, interactionId, callback,
+ mFetchFlags, interrogatingPid, interrogatingTid, spec);
+ return true;
+ } catch (RemoteException re) {
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "Error calling accessibilityFocusSearch()");
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int accessibilityWindowId,
+ long accessibilityNodeId, int action, Bundle arguments, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
+ throws RemoteException {
+ final int resolvedWindowId;
+ IAccessibilityInteractionConnection connection = null;
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(
+ UserHandle.getCallingUserId());
+ if (resolvedUserId != mCurrentUserId) {
+ return false;
+ }
+ mSecurityPolicy.enforceCanRetrieveWindowContent(this);
+ resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
+ final boolean permissionGranted = mSecurityPolicy.canPerformActionLocked(this,
+ resolvedWindowId, action, arguments);
+ if (!permissionGranted) {
+ return false;
+ } else {
+ connection = getConnectionLocked(resolvedWindowId);
+ if (connection == null) {
+ return false;
+ }
+ }
+ }
+ final int interrogatingPid = Binder.getCallingPid();
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ connection.performAccessibilityAction(accessibilityNodeId, action, arguments,
+ interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid);
+ } catch (RemoteException re) {
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "Error calling performAccessibilityAction()");
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ return true;
+ }
+
+ public boolean performGlobalAction(int action) {
+ synchronized (mLock) {
+ final int resolvedUserId = mSecurityPolicy
+ .resolveCallingUserIdEnforcingPermissionsLocked(
+ UserHandle.getCallingUserId());
+ if (resolvedUserId != mCurrentUserId) {
+ return false;
+ }
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ switch (action) {
+ case AccessibilityService.GLOBAL_ACTION_BACK: {
+ sendDownAndUpKeyEvents(KeyEvent.KEYCODE_BACK);
+ } return true;
+ case AccessibilityService.GLOBAL_ACTION_HOME: {
+ sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HOME);
+ } return true;
+ case AccessibilityService.GLOBAL_ACTION_RECENTS: {
+ openRecents();
+ } return true;
+ case AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS: {
+ expandNotifications();
+ } return true;
+ case AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS: {
+ expandQuickSettings();
+ } return true;
+ }
+ return false;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
+ mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP);
+ synchronized (mLock) {
+ pw.append("Service[label=" + mAccessibilityServiceInfo.getResolveInfo()
+ .loadLabel(mContext.getPackageManager()));
+ pw.append(", feedbackType"
+ + AccessibilityServiceInfo.feedbackTypeToString(mFeedbackType));
+ pw.append(", capabilities=" + mAccessibilityServiceInfo.getCapabilities());
+ pw.append(", eventTypes="
+ + AccessibilityEvent.eventTypeToString(mEventTypes));
+ pw.append(", notificationTimeout=" + mNotificationTimeout);
+ pw.append("]");
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ /* do nothing - #binderDied takes care */
+ }
+
+ public void linkToOwnDeathLocked() throws RemoteException {
+ mService.linkToDeath(this, 0);
+ }
+
+ public void unlinkToOwnDeathLocked() {
+ mService.unlinkToDeath(this, 0);
+ }
+
+ public void resetLocked() {
+ try {
+ // Clear the proxy in the other process so this
+ // IAccessibilityServiceConnection can be garbage collected.
+ mServiceInterface.setConnection(null, mId);
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ mService = null;
+ mServiceInterface = null;
+ }
+
+ public boolean isConnectedLocked() {
+ return (mService != null);
+ }
+
+ public void binderDied() {
+ synchronized (mLock) {
+ // It is possible that this service's package was force stopped during
+ // whose handling the death recipient is unlinked and still get a call
+ // on binderDied since the call was made before we unlink but was
+ // waiting on the lock we held during the force stop handling.
+ if (!isConnectedLocked()) {
+ return;
+ }
+ mWasConnectedAndDied = true;
+ mKeyEventDispatcher.flush();
+ UserState userState = getUserStateLocked(mUserId);
+ // The death recipient is unregistered in removeServiceLocked
+ removeServiceLocked(this, userState);
+ resetLocked();
+ if (mIsAutomation) {
+ // We no longer have an automation service, so restore
+ // the state based on values in the settings database.
+ userState.mInstalledServices.remove(mAccessibilityServiceInfo);
+ userState.mEnabledServices.remove(mComponentName);
+ userState.destroyUiAutomationService();
+ }
+ }
+ }
+
+ /**
+ * Performs a notification for an {@link AccessibilityEvent}.
+ *
+ * @param event The event.
+ */
+ public void notifyAccessibilityEvent(AccessibilityEvent event) {
+ synchronized (mLock) {
+ final int eventType = event.getEventType();
+ // Make a copy since during dispatch it is possible the event to
+ // be modified to remove its source if the receiving service does
+ // not have permission to access the window content.
+ AccessibilityEvent newEvent = AccessibilityEvent.obtain(event);
+ AccessibilityEvent oldEvent = mPendingEvents.get(eventType);
+ mPendingEvents.put(eventType, newEvent);
+
+ final int what = eventType;
+ if (oldEvent != null) {
+ mEventDispatchHandler.removeMessages(what);
+ oldEvent.recycle();
+ }
+
+ Message message = mEventDispatchHandler.obtainMessage(what);
+ mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout);
+ }
+ }
+
+ /**
+ * Notifies an accessibility service client for a scheduled event given the event type.
+ *
+ * @param eventType The type of the event to dispatch.
+ */
+ private void notifyAccessibilityEventInternal(int eventType) {
+ IAccessibilityServiceClient listener;
+ AccessibilityEvent event;
+
+ synchronized (mLock) {
+ listener = mServiceInterface;
+
+ // If the service died/was disabled while the message for dispatching
+ // the accessibility event was propagating the listener may be null.
+ if (listener == null) {
+ return;
+ }
+
+ event = mPendingEvents.get(eventType);
+
+ // Check for null here because there is a concurrent scenario in which this
+ // happens: 1) A binder thread calls notifyAccessibilityServiceDelayedLocked
+ // which posts a message for dispatching an event. 2) The message is pulled
+ // from the queue by the handler on the service thread and the latter is
+ // just about to acquire the lock and call this method. 3) Now another binder
+ // thread acquires the lock calling notifyAccessibilityServiceDelayedLocked
+ // so the service thread waits for the lock; 4) The binder thread replaces
+ // the event with a more recent one (assume the same event type) and posts a
+ // dispatch request releasing the lock. 5) Now the main thread is unblocked and
+ // dispatches the event which is removed from the pending ones. 6) And ... now
+ // the service thread handles the last message posted by the last binder call
+ // but the event is already dispatched and hence looking it up in the pending
+ // ones yields null. This check is much simpler that keeping count for each
+ // event type of each service to catch such a scenario since only one message
+ // is processed at a time.
+ if (event == null) {
+ return;
+ }
+
+ mPendingEvents.remove(eventType);
+ if (mSecurityPolicy.canRetrieveWindowContent(this)) {
+ event.setConnectionId(mId);
+ } else {
+ event.setSource(null);
+ }
+ event.setSealed(true);
+ }
+
+ try {
+ listener.onAccessibilityEvent(event);
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Event " + event + " sent to " + listener);
+ }
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Error during sending " + event + " to " + listener, re);
+ } finally {
+ event.recycle();
+ }
+ }
+
+ public void notifyGesture(int gestureId) {
+ mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE,
+ gestureId, 0).sendToTarget();
+ }
+
+ public void notifyKeyEvent(KeyEvent event, int policyFlags) {
+ mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_KEY_EVENT,
+ policyFlags, 0, event).sendToTarget();
+ }
+
+ public void notifyClearAccessibilityNodeInfoCache() {
+ mInvocationHandler.sendEmptyMessage(
+ InvocationHandler.MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE);
+ }
+
+ private void notifyGestureInternal(int gestureId) {
+ IAccessibilityServiceClient listener = mServiceInterface;
+ if (listener != null) {
+ try {
+ listener.onGesture(gestureId);
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Error during sending gesture " + gestureId
+ + " to " + mService, re);
+ }
+ }
+ }
+
+ private void notifyKeyEventInternal(KeyEvent event, int policyFlags) {
+ mKeyEventDispatcher.notifyKeyEvent(event, policyFlags);
+ }
+
+ private void notifyClearAccessibilityNodeInfoCacheInternal() {
+ IAccessibilityServiceClient listener = mServiceInterface;
+ if (listener != null) {
+ try {
+ listener.clearAccessibilityNodeInfoCache();
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Error during requesting accessibility info cache"
+ + " to be cleared.", re);
+ }
+ }
+ }
+
+ private void sendDownAndUpKeyEvents(int keyCode) {
+ final long token = Binder.clearCallingIdentity();
+
+ // Inject down.
+ final long downTime = SystemClock.uptimeMillis();
+ KeyEvent down = KeyEvent.obtain(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FROM_SYSTEM,
+ InputDevice.SOURCE_KEYBOARD, null);
+ InputManager.getInstance().injectInputEvent(down,
+ InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
+ down.recycle();
+
+ // Inject up.
+ final long upTime = SystemClock.uptimeMillis();
+ KeyEvent up = KeyEvent.obtain(downTime, upTime, KeyEvent.ACTION_UP, keyCode, 0, 0,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FROM_SYSTEM,
+ InputDevice.SOURCE_KEYBOARD, null);
+ InputManager.getInstance().injectInputEvent(up,
+ InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
+ up.recycle();
+
+ Binder.restoreCallingIdentity(token);
+ }
+
+ private void expandNotifications() {
+ final long token = Binder.clearCallingIdentity();
+
+ StatusBarManager statusBarManager = (StatusBarManager) mContext.getSystemService(
+ android.app.Service.STATUS_BAR_SERVICE);
+ statusBarManager.expandNotificationsPanel();
+
+ Binder.restoreCallingIdentity(token);
+ }
+
+ private void expandQuickSettings() {
+ final long token = Binder.clearCallingIdentity();
+
+ StatusBarManager statusBarManager = (StatusBarManager) mContext.getSystemService(
+ android.app.Service.STATUS_BAR_SERVICE);
+ statusBarManager.expandSettingsPanel();
+
+ Binder.restoreCallingIdentity(token);
+ }
+
+ private void openRecents() {
+ final long token = Binder.clearCallingIdentity();
+
+ IStatusBarService statusBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService("statusbar"));
+ try {
+ statusBarService.toggleRecentApps();
+ } catch (RemoteException e) {
+ Slog.e(LOG_TAG, "Error toggling recent apps.");
+ }
+
+ Binder.restoreCallingIdentity(token);
+ }
+
+ private IAccessibilityInteractionConnection getConnectionLocked(int windowId) {
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId);
+ }
+ AccessibilityConnectionWrapper wrapper = mGlobalInteractionConnections.get(windowId);
+ if (wrapper == null) {
+ wrapper = getCurrentUserStateLocked().mInteractionConnections.get(windowId);
+ }
+ if (wrapper != null && wrapper.mConnection != null) {
+ return wrapper.mConnection;
+ }
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "No interaction connection to window: " + windowId);
+ }
+ return null;
+ }
+
+ private int resolveAccessibilityWindowIdLocked(int accessibilityWindowId) {
+ if (accessibilityWindowId == AccessibilityNodeInfo.ACTIVE_WINDOW_ID) {
+ return mSecurityPolicy.mActiveWindowId;
+ }
+ return accessibilityWindowId;
+ }
+
+ private MagnificationSpec getCompatibleMagnificationSpec(int windowId) {
+ try {
+ IBinder windowToken = mGlobalWindowTokens.get(windowId);
+ if (windowToken == null) {
+ windowToken = getCurrentUserStateLocked().mWindowTokens.get(windowId);
+ }
+ if (windowToken != null) {
+ return mWindowManagerService.getCompatibleMagnificationSpecForWindow(
+ windowToken);
+ }
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ return null;
+ }
+
+ private final class InvocationHandler extends Handler {
+
+ public static final int MSG_ON_GESTURE = 1;
+ public static final int MSG_ON_KEY_EVENT = 2;
+ public static final int MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE = 3;
+ public static final int MSG_ON_KEY_EVENT_TIMEOUT = 4;
+
+ public InvocationHandler(Looper looper) {
+ super(looper, null, true);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ final int type = message.what;
+ switch (type) {
+ case MSG_ON_GESTURE: {
+ final int gestureId = message.arg1;
+ notifyGestureInternal(gestureId);
+ } break;
+ case MSG_ON_KEY_EVENT: {
+ KeyEvent event = (KeyEvent) message.obj;
+ final int policyFlags = message.arg1;
+ notifyKeyEventInternal(event, policyFlags);
+ } break;
+ case MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE: {
+ notifyClearAccessibilityNodeInfoCacheInternal();
+ } break;
+ case MSG_ON_KEY_EVENT_TIMEOUT: {
+ PendingEvent eventState = (PendingEvent) message.obj;
+ setOnKeyEventResult(false, eventState.sequence);
+ } break;
+ default: {
+ throw new IllegalArgumentException("Unknown message: " + type);
+ }
+ }
+ }
+ }
+
+ private final class KeyEventDispatcher {
+
+ private static final long ON_KEY_EVENT_TIMEOUT_MILLIS = 500;
+
+ private PendingEvent mPendingEvents;
+
+ private final InputEventConsistencyVerifier mSentEventsVerifier =
+ InputEventConsistencyVerifier.isInstrumentationEnabled()
+ ? new InputEventConsistencyVerifier(
+ this, 0, KeyEventDispatcher.class.getSimpleName()) : null;
+
+ public void notifyKeyEvent(KeyEvent event, int policyFlags) {
+ final PendingEvent pendingEvent;
+
+ synchronized (mLock) {
+ pendingEvent = addPendingEventLocked(event, policyFlags);
+ }
+
+ Message message = mInvocationHandler.obtainMessage(
+ InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, pendingEvent);
+ mInvocationHandler.sendMessageDelayed(message, ON_KEY_EVENT_TIMEOUT_MILLIS);
+
+ try {
+ // Accessibility services are exclusively not in the system
+ // process, therefore no need to clone the motion event to
+ // prevent tampering. It will be cloned in the IPC call.
+ mServiceInterface.onKeyEvent(pendingEvent.event, pendingEvent.sequence);
+ } catch (RemoteException re) {
+ setOnKeyEventResult(false, pendingEvent.sequence);
+ }
+ }
+
+ public void setOnKeyEventResult(boolean handled, int sequence) {
+ synchronized (mLock) {
+ PendingEvent pendingEvent = removePendingEventLocked(sequence);
+ if (pendingEvent != null) {
+ mInvocationHandler.removeMessages(
+ InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT,
+ pendingEvent);
+ pendingEvent.handled = handled;
+ finishPendingEventLocked(pendingEvent);
+ }
+ }
+ }
+
+ public void flush() {
+ synchronized (mLock) {
+ cancelAllPendingEventsLocked();
+ if (mSentEventsVerifier != null) {
+ mSentEventsVerifier.reset();
+ }
+ }
+ }
+
+ private PendingEvent addPendingEventLocked(KeyEvent event, int policyFlags) {
+ final int sequence = event.getSequenceNumber();
+ PendingEvent pendingEvent = obtainPendingEventLocked(event, policyFlags, sequence);
+ pendingEvent.next = mPendingEvents;
+ mPendingEvents = pendingEvent;
+ return pendingEvent;
+ }
+
+ private PendingEvent removePendingEventLocked(int sequence) {
+ PendingEvent previous = null;
+ PendingEvent current = mPendingEvents;
+
+ while (current != null) {
+ if (current.sequence == sequence) {
+ if (previous != null) {
+ previous.next = current.next;
+ } else {
+ mPendingEvents = current.next;
+ }
+ current.next = null;
+ return current;
+ }
+ previous = current;
+ current = current.next;
+ }
+ return null;
+ }
+
+ private void finishPendingEventLocked(PendingEvent pendingEvent) {
+ if (!pendingEvent.handled) {
+ sendKeyEventToInputFilter(pendingEvent.event, pendingEvent.policyFlags);
+ }
+ // Nullify the event since we do not want it to be
+ // recycled yet. It will be sent to the input filter.
+ pendingEvent.event = null;
+ recyclePendingEventLocked(pendingEvent);
+ }
+
+ private void sendKeyEventToInputFilter(KeyEvent event, int policyFlags) {
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Injecting event: " + event);
+ }
+ if (mSentEventsVerifier != null) {
+ mSentEventsVerifier.onKeyEvent(event, 0);
+ }
+ policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER;
+ mMainHandler.obtainMessage(MainHandler.MSG_SEND_KEY_EVENT_TO_INPUT_FILTER,
+ policyFlags, 0, event).sendToTarget();
+ }
+
+ private void cancelAllPendingEventsLocked() {
+ while (mPendingEvents != null) {
+ PendingEvent pendingEvent = removePendingEventLocked(mPendingEvents.sequence);
+ pendingEvent.handled = false;
+ mInvocationHandler.removeMessages(InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT,
+ pendingEvent);
+ finishPendingEventLocked(pendingEvent);
+ }
+ }
+ }
+ }
+
+ private static final class PendingEvent {
+ PendingEvent next;
+
+ KeyEvent event;
+ int policyFlags;
+ int sequence;
+ boolean handled;
+
+ public void clear() {
+ if (event != null) {
+ event.recycle();
+ event = null;
+ }
+ next = null;
+ policyFlags = 0;
+ sequence = 0;
+ handled = false;
+ }
+ }
+
+ final class SecurityPolicy {
+ private static final int VALID_ACTIONS =
+ AccessibilityNodeInfo.ACTION_CLICK
+ | AccessibilityNodeInfo.ACTION_LONG_CLICK
+ | AccessibilityNodeInfo.ACTION_FOCUS
+ | AccessibilityNodeInfo.ACTION_CLEAR_FOCUS
+ | AccessibilityNodeInfo.ACTION_SELECT
+ | AccessibilityNodeInfo.ACTION_CLEAR_SELECTION
+ | AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS
+ | AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS
+ | AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY
+ | AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
+ | AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT
+ | AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT
+ | AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
+ | AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
+ | AccessibilityNodeInfo.ACTION_COPY
+ | AccessibilityNodeInfo.ACTION_PASTE
+ | AccessibilityNodeInfo.ACTION_CUT
+ | AccessibilityNodeInfo.ACTION_SET_SELECTION
+ | AccessibilityNodeInfo.ACTION_EXPAND
+ | AccessibilityNodeInfo.ACTION_COLLAPSE
+ | AccessibilityNodeInfo.ACTION_DISMISS;
+
+ private static final int RETRIEVAL_ALLOWING_EVENT_TYPES =
+ AccessibilityEvent.TYPE_VIEW_CLICKED
+ | AccessibilityEvent.TYPE_VIEW_FOCUSED
+ | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
+ | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
+ | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
+ | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_SELECTED
+ | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_SCROLLED
+ | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
+ | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
+
+ private int mActiveWindowId;
+ private boolean mTouchInteractionInProgress;
+
+ private boolean canDispatchAccessibilityEvent(AccessibilityEvent event) {
+ final int eventType = event.getEventType();
+ switch (eventType) {
+ // All events that are for changes in a global window
+ // state should *always* be dispatched.
+ case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
+ case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
+ // All events generated by the user touching the
+ // screen should *always* be dispatched.
+ case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
+ case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
+ case AccessibilityEvent.TYPE_GESTURE_DETECTION_START:
+ case AccessibilityEvent.TYPE_GESTURE_DETECTION_END:
+ case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START:
+ case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
+ // These will change the active window, so dispatch.
+ case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
+ case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: {
+ return true;
+ }
+ // All events for changes in window content should be
+ // dispatched *only* if this window is the active one.
+ default:
+ return event.getWindowId() == mActiveWindowId;
+ }
+ }
+
+ public void updateEventSourceLocked(AccessibilityEvent event) {
+ if ((event.getEventType() & RETRIEVAL_ALLOWING_EVENT_TYPES) == 0) {
+ event.setSource(null);
+ }
+ }
+
+ public void updateActiveWindow(int windowId, int eventType) {
+ // The active window is either the window that has input focus or
+ // the window that the user is currently touching. If the user is
+ // touching a window that does not have input focus as soon as the
+ // the user stops touching that window the focused window becomes
+ // the active one.
+ switch (eventType) {
+ case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
+ if (getFocusedWindowId() == windowId) {
+ mActiveWindowId = windowId;
+ }
+ } break;
+ case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: {
+ // Do not allow delayed hover events to confuse us
+ // which the active window is.
+ if (mTouchInteractionInProgress) {
+ mActiveWindowId = windowId;
+ }
+ } break;
+ }
+ }
+
+ public void onTouchInteractionStart() {
+ mTouchInteractionInProgress = true;
+ }
+
+ public void onTouchInteractionEnd() {
+ mTouchInteractionInProgress = false;
+ // We want to set the active window to be current immediately
+ // after the user has stopped touching the screen since if the
+ // user types with the IME he should get a feedback for the
+ // letter typed in the text view which is in the input focused
+ // window. Note that we always deliver hover accessibility events
+ // (they are a result of user touching the screen) so change of
+ // the active window before all hover accessibility events from
+ // the touched window are delivered is fine.
+ mActiveWindowId = getFocusedWindowId();
+ }
+
+ public int getRetrievalAllowingWindowLocked() {
+ return mActiveWindowId;
+ }
+
+ public boolean canGetAccessibilityNodeInfoLocked(Service service, int windowId) {
+ return canRetrieveWindowContent(service) && isRetrievalAllowingWindow(windowId);
+ }
+
+ public boolean canPerformActionLocked(Service service, int windowId, int action,
+ Bundle arguments) {
+ return canRetrieveWindowContent(service)
+ && isRetrievalAllowingWindow(windowId)
+ && isActionPermitted(action);
+ }
+
+ public boolean canRetrieveWindowContent(Service service) {
+ return (service.mAccessibilityServiceInfo.getCapabilities()
+ & AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT) != 0;
+ }
+
+ public void enforceCanRetrieveWindowContent(Service service) throws RemoteException {
+ // This happens due to incorrect registration so make it apparent.
+ if (!canRetrieveWindowContent(service)) {
+ Slog.e(LOG_TAG, "Accessibility serivce " + service.mComponentName + " does not " +
+ "declare android:canRetrieveWindowContent.");
+ throw new RemoteException();
+ }
+ }
+
+ public int resolveCallingUserIdEnforcingPermissionsLocked(int userId) {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid == 0
+ || callingUid == Process.SYSTEM_UID
+ || callingUid == Process.SHELL_UID) {
+ return mCurrentUserId;
+ }
+ final int callingUserId = UserHandle.getUserId(callingUid);
+ if (callingUserId == userId) {
+ return userId;
+ }
+ if (!hasPermission(Manifest.permission.INTERACT_ACROSS_USERS)
+ && !hasPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
+ throw new SecurityException("Call from user " + callingUserId + " as user "
+ + userId + " without permission INTERACT_ACROSS_USERS or "
+ + "INTERACT_ACROSS_USERS_FULL not allowed.");
+ }
+ if (userId == UserHandle.USER_CURRENT
+ || userId == UserHandle.USER_CURRENT_OR_SELF) {
+ return mCurrentUserId;
+ }
+ throw new IllegalArgumentException("Calling user can be changed to only "
+ + "UserHandle.USER_CURRENT or UserHandle.USER_CURRENT_OR_SELF.");
+ }
+
+ public boolean isCallerInteractingAcrossUsers(int userId) {
+ final int callingUid = Binder.getCallingUid();
+ return (Binder.getCallingPid() == android.os.Process.myPid()
+ || callingUid == Process.SHELL_UID
+ || userId == UserHandle.USER_CURRENT
+ || userId == UserHandle.USER_CURRENT_OR_SELF);
+ }
+
+ private boolean isRetrievalAllowingWindow(int windowId) {
+ return (mActiveWindowId == windowId);
+ }
+
+ private boolean isActionPermitted(int action) {
+ return (VALID_ACTIONS & action) != 0;
+ }
+
+ private void enforceCallingPermission(String permission, String function) {
+ if (OWN_PROCESS_ID == Binder.getCallingPid()) {
+ return;
+ }
+ if (!hasPermission(permission)) {
+ throw new SecurityException("You do not have " + permission
+ + " required to call " + function + " from pid="
+ + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
+ }
+ }
+
+ private boolean hasPermission(String permission) {
+ return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private int getFocusedWindowId() {
+ try {
+ // We call this only on window focus change or after touch
+ // exploration gesture end and the shown windows are not that
+ // many, so the linear look up is just fine.
+ IBinder token = mWindowManagerService.getFocusedWindowToken();
+ if (token != null) {
+ synchronized (mLock) {
+ int windowId = getFocusedWindowIdLocked(token, mGlobalWindowTokens);
+ if (windowId < 0) {
+ windowId = getFocusedWindowIdLocked(token,
+ getCurrentUserStateLocked().mWindowTokens);
+ }
+ return windowId;
+ }
+ }
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ return -1;
+ }
+
+ private int getFocusedWindowIdLocked(IBinder token, SparseArray<IBinder> windows) {
+ final int windowCount = windows.size();
+ for (int i = 0; i < windowCount; i++) {
+ if (windows.valueAt(i) == token) {
+ return windows.keyAt(i);
+ }
+ }
+ return -1;
+ }
+ }
+
+ private class UserState {
+ public final int mUserId;
+
+ // Non-transient state.
+
+ public final RemoteCallbackList<IAccessibilityManagerClient> mClients =
+ new RemoteCallbackList<IAccessibilityManagerClient>();
+
+ public final SparseArray<AccessibilityConnectionWrapper> mInteractionConnections =
+ new SparseArray<AccessibilityConnectionWrapper>();
+
+ public final SparseArray<IBinder> mWindowTokens = new SparseArray<IBinder>();
+
+ // Transient state.
+
+ public final CopyOnWriteArrayList<Service> mBoundServices =
+ new CopyOnWriteArrayList<Service>();
+
+ public final Map<ComponentName, Service> mComponentNameToServiceMap =
+ new HashMap<ComponentName, Service>();
+
+ public final List<AccessibilityServiceInfo> mInstalledServices =
+ new ArrayList<AccessibilityServiceInfo>();
+
+ public final Set<ComponentName> mBindingServices = new HashSet<ComponentName>();
+
+ public final Set<ComponentName> mEnabledServices = new HashSet<ComponentName>();
+
+ public final Set<ComponentName> mTouchExplorationGrantedServices =
+ new HashSet<ComponentName>();
+
+ public int mHandledFeedbackTypes = 0;
+
+ public int mLastSentClientState = -1;
+
+ public boolean mIsAccessibilityEnabled;
+ public boolean mIsTouchExplorationEnabled;
+ public boolean mIsEnhancedWebAccessibilityEnabled;
+ public boolean mIsDisplayMagnificationEnabled;
+ public boolean mIsFilterKeyEventsEnabled;
+
+ private Service mUiAutomationService;
+ private IAccessibilityServiceClient mUiAutomationServiceClient;
+
+ private IBinder mUiAutomationServiceOwner;
+ private final DeathRecipient mUiAutomationSerivceOnwerDeathRecipient =
+ new DeathRecipient() {
+ @Override
+ public void binderDied() {
+ mUiAutomationServiceOwner.unlinkToDeath(
+ mUiAutomationSerivceOnwerDeathRecipient, 0);
+ mUiAutomationServiceOwner = null;
+ if (mUiAutomationService != null) {
+ mUiAutomationService.binderDied();
+ }
+ }
+ };
+
+ public UserState(int userId) {
+ mUserId = userId;
+ }
+
+ public int getClientState() {
+ int clientState = 0;
+ if (mIsAccessibilityEnabled) {
+ clientState |= AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED;
+ }
+ // Touch exploration relies on enabled accessibility.
+ if (mIsAccessibilityEnabled && mIsTouchExplorationEnabled) {
+ clientState |= AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED;
+ }
+ return clientState;
+ }
+
+ public void onSwitchToAnotherUser() {
+ // Clear UI test automation state.
+ if (mUiAutomationService != null) {
+ mUiAutomationService.binderDied();
+ }
+
+ // Unbind all services.
+ unbindAllServicesLocked(this);
+
+ // Clear service management state.
+ mBoundServices.clear();
+ mBindingServices.clear();
+
+ // Clear event management state.
+ mHandledFeedbackTypes = 0;
+ mLastSentClientState = -1;
+
+ // Clear state persisted in settings.
+ mEnabledServices.clear();
+ mTouchExplorationGrantedServices.clear();
+ mIsAccessibilityEnabled = false;
+ mIsTouchExplorationEnabled = false;
+ mIsEnhancedWebAccessibilityEnabled = false;
+ mIsDisplayMagnificationEnabled = false;
+ }
+
+ public void destroyUiAutomationService() {
+ mUiAutomationService = null;
+ mUiAutomationServiceClient = null;
+ if (mUiAutomationServiceOwner != null) {
+ mUiAutomationServiceOwner.unlinkToDeath(
+ mUiAutomationSerivceOnwerDeathRecipient, 0);
+ mUiAutomationServiceOwner = null;
+ }
+ }
+ }
+
+ private final class AccessibilityContentObserver extends ContentObserver {
+
+ private final Uri mAccessibilityEnabledUri = Settings.Secure.getUriFor(
+ Settings.Secure.ACCESSIBILITY_ENABLED);
+
+ private final Uri mTouchExplorationEnabledUri = Settings.Secure.getUriFor(
+ Settings.Secure.TOUCH_EXPLORATION_ENABLED);
+
+ private final Uri mDisplayMagnificationEnabledUri = Settings.Secure.getUriFor(
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED);
+
+ private final Uri mEnabledAccessibilityServicesUri = Settings.Secure.getUriFor(
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+
+ private final Uri mTouchExplorationGrantedAccessibilityServicesUri = Settings.Secure
+ .getUriFor(Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES);
+
+ private final Uri mEnhancedWebAccessibilityUri = Settings.Secure
+ .getUriFor(Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION);
+
+ public AccessibilityContentObserver(Handler handler) {
+ super(handler);
+ }
+
+ public void register(ContentResolver contentResolver) {
+ contentResolver.registerContentObserver(mAccessibilityEnabledUri,
+ false, this, UserHandle.USER_ALL);
+ contentResolver.registerContentObserver(mTouchExplorationEnabledUri,
+ false, this, UserHandle.USER_ALL);
+ contentResolver.registerContentObserver(mDisplayMagnificationEnabledUri,
+ false, this, UserHandle.USER_ALL);
+ contentResolver.registerContentObserver(mEnabledAccessibilityServicesUri,
+ false, this, UserHandle.USER_ALL);
+ contentResolver.registerContentObserver(
+ mTouchExplorationGrantedAccessibilityServicesUri,
+ false, this, UserHandle.USER_ALL);
+ contentResolver.registerContentObserver(mEnhancedWebAccessibilityUri,
+ false, this, UserHandle.USER_ALL);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (mAccessibilityEnabledUri.equals(uri)) {
+ synchronized (mLock) {
+ // We will update when the automation service dies.
+ UserState userState = getCurrentUserStateLocked();
+ if (userState.mUiAutomationService == null) {
+ if (readAccessibilityEnabledSettingLocked(userState)) {
+ onUserStateChangedLocked(userState);
+ }
+ }
+ }
+ } else if (mTouchExplorationEnabledUri.equals(uri)) {
+ synchronized (mLock) {
+ // We will update when the automation service dies.
+ UserState userState = getCurrentUserStateLocked();
+ if (userState.mUiAutomationService == null) {
+ if (readTouchExplorationEnabledSettingLocked(userState)) {
+ onUserStateChangedLocked(userState);
+ }
+ }
+ }
+ } else if (mDisplayMagnificationEnabledUri.equals(uri)) {
+ synchronized (mLock) {
+ // We will update when the automation service dies.
+ UserState userState = getCurrentUserStateLocked();
+ if (userState.mUiAutomationService == null) {
+ if (readDisplayMagnificationEnabledSettingLocked(userState)) {
+ onUserStateChangedLocked(userState);
+ }
+ }
+ }
+ } else if (mEnabledAccessibilityServicesUri.equals(uri)) {
+ synchronized (mLock) {
+ // We will update when the automation service dies.
+ UserState userState = getCurrentUserStateLocked();
+ if (userState.mUiAutomationService == null) {
+ if (readEnabledAccessibilityServicesLocked(userState)) {
+ onUserStateChangedLocked(userState);
+ }
+ }
+ }
+ } else if (mTouchExplorationGrantedAccessibilityServicesUri.equals(uri)) {
+ synchronized (mLock) {
+ // We will update when the automation service dies.
+ UserState userState = getCurrentUserStateLocked();
+ if (userState.mUiAutomationService == null) {
+ if (readTouchExplorationGrantedAccessibilityServicesLocked(userState)) {
+ onUserStateChangedLocked(userState);
+ }
+ }
+ }
+ } else if (mEnhancedWebAccessibilityUri.equals(uri)) {
+ synchronized (mLock) {
+ // We will update when the automation service dies.
+ UserState userState = getCurrentUserStateLocked();
+ if (userState.mUiAutomationService == null) {
+ if (readEnhancedWebAccessibilityEnabledChangedLocked(userState)) {
+ onUserStateChangedLocked(userState);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java b/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java
new file mode 100644
index 0000000..8c93e7b
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java
@@ -0,0 +1,93 @@
+/*
+ ** Copyright 2012, 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.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * Interface for classes that can handle and potentially transform a stream of
+ * motion and accessibility events. Instances implementing this interface are
+ * ordered in a sequence to implement a transformation chain. An instance may
+ * consume, modify, and generate events. It is responsible to deliver the
+ * output events to the next transformation in the sequence set via
+ * {@link #setNext(EventStreamTransformation)}.
+ *
+ * Note that since instances implementing this interface are transformations
+ * of the event stream, an instance should work against the event stream
+ * potentially modified by previous ones. Hence, the order of transformations
+ * is important.
+ *
+ * It is a responsibility of each handler that decides to react to an event
+ * sequence and prevent any subsequent ones from performing an action to send
+ * the appropriate cancel event given it has delegated a part of the events
+ * that belong to the current gesture. This will ensure that subsequent
+ * transformations will not be left in an inconsistent state and the applications
+ * see a consistent event stream.
+ *
+ * For example, to cancel a {@link KeyEvent} the handler has to emit an event
+ * with action {@link KeyEvent#ACTION_UP} with the additional flag
+ * {@link KeyEvent#FLAG_CANCELED}. To cancel a {@link MotionEvent} the handler
+ * has to send an event with action {@link MotionEvent#ACTION_CANCEL}.
+ *
+ * It is a responsibility of each handler that received a cancel event to clear its
+ * internal state and to propagate the event to the next one to enable subsequent
+ * transformations to clear their internal state.
+ *
+ * It is a responsibility for each transformation to start handling events only
+ * after an event that designates the start of a well-formed event sequence.
+ * For example, if it received a down motion event followed by a cancel motion
+ * event, it should not handle subsequent move and up events until it gets a down.
+ */
+interface EventStreamTransformation {
+
+ /**
+ * Receives a motion event. Passed are the event transformed by previous
+ * transformations and the raw event to which no transformations have
+ * been applied.
+ *
+ * @param event The transformed motion event.
+ * @param rawEvent The raw motion event.
+ * @param policyFlags Policy flags for the event.
+ */
+ public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags);
+
+ /**
+ * Receives an accessibility event.
+ *
+ * @param event The accessibility event.
+ */
+ public void onAccessibilityEvent(AccessibilityEvent event);
+
+ /**
+ * Sets the next transformation.
+ *
+ * @param next The next transformation.
+ */
+ public void setNext(EventStreamTransformation next);
+
+ /**
+ * Clears the internal state of this transformation.
+ */
+ public void clear();
+
+ /**
+ * Destroys this transformation.
+ */
+ public void onDestroy();
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/GestureUtils.java b/services/accessibility/java/com/android/server/accessibility/GestureUtils.java
new file mode 100644
index 0000000..b68b09f
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/GestureUtils.java
@@ -0,0 +1,102 @@
+package com.android.server.accessibility;
+
+import android.util.MathUtils;
+import android.view.MotionEvent;
+
+/**
+ * Some helper functions for gesture detection.
+ */
+final class GestureUtils {
+
+ private GestureUtils() {
+ /* cannot be instantiated */
+ }
+
+ public static boolean isTap(MotionEvent down, MotionEvent up, int tapTimeSlop,
+ int tapDistanceSlop, int actionIndex) {
+ return eventsWithinTimeAndDistanceSlop(down, up, tapTimeSlop, tapDistanceSlop, actionIndex);
+ }
+
+ public static boolean isMultiTap(MotionEvent firstUp, MotionEvent secondUp,
+ int multiTapTimeSlop, int multiTapDistanceSlop, int actionIndex) {
+ return eventsWithinTimeAndDistanceSlop(firstUp, secondUp, multiTapTimeSlop,
+ multiTapDistanceSlop, actionIndex);
+ }
+
+ private static boolean eventsWithinTimeAndDistanceSlop(MotionEvent first, MotionEvent second,
+ int timeout, int distance, int actionIndex) {
+ if (isTimedOut(first, second, timeout)) {
+ return false;
+ }
+ final double deltaMove = computeDistance(first, second, actionIndex);
+ if (deltaMove >= distance) {
+ return false;
+ }
+ return true;
+ }
+
+ public static double computeDistance(MotionEvent first, MotionEvent second, int pointerIndex) {
+ return MathUtils.dist(first.getX(pointerIndex), first.getY(pointerIndex),
+ second.getX(pointerIndex), second.getY(pointerIndex));
+ }
+
+ public static boolean isTimedOut(MotionEvent firstUp, MotionEvent secondUp, int timeout) {
+ final long deltaTime = secondUp.getEventTime() - firstUp.getEventTime();
+ return (deltaTime >= timeout);
+ }
+
+ public static boolean isSamePointerContext(MotionEvent first, MotionEvent second) {
+ return (first.getPointerIdBits() == second.getPointerIdBits()
+ && first.getPointerId(first.getActionIndex())
+ == second.getPointerId(second.getActionIndex()));
+ }
+
+ /**
+ * Determines whether a two pointer gesture is a dragging one.
+ *
+ * @param event The event with the pointer data.
+ * @return True if the gesture is a dragging one.
+ */
+ public static boolean isDraggingGesture(float firstPtrDownX, float firstPtrDownY,
+ float secondPtrDownX, float secondPtrDownY, float firstPtrX, float firstPtrY,
+ float secondPtrX, float secondPtrY, float maxDraggingAngleCos) {
+
+ // Check if the pointers are moving in the same direction.
+ final float firstDeltaX = firstPtrX - firstPtrDownX;
+ final float firstDeltaY = firstPtrY - firstPtrDownY;
+
+ if (firstDeltaX == 0 && firstDeltaY == 0) {
+ return true;
+ }
+
+ final float firstMagnitude =
+ (float) Math.sqrt(firstDeltaX * firstDeltaX + firstDeltaY * firstDeltaY);
+ final float firstXNormalized =
+ (firstMagnitude > 0) ? firstDeltaX / firstMagnitude : firstDeltaX;
+ final float firstYNormalized =
+ (firstMagnitude > 0) ? firstDeltaY / firstMagnitude : firstDeltaY;
+
+ final float secondDeltaX = secondPtrX - secondPtrDownX;
+ final float secondDeltaY = secondPtrY - secondPtrDownY;
+
+ if (secondDeltaX == 0 && secondDeltaY == 0) {
+ return true;
+ }
+
+ final float secondMagnitude =
+ (float) Math.sqrt(secondDeltaX * secondDeltaX + secondDeltaY * secondDeltaY);
+ final float secondXNormalized =
+ (secondMagnitude > 0) ? secondDeltaX / secondMagnitude : secondDeltaX;
+ final float secondYNormalized =
+ (secondMagnitude > 0) ? secondDeltaY / secondMagnitude : secondDeltaY;
+
+ final float angleCos =
+ firstXNormalized * secondXNormalized + firstYNormalized * secondYNormalized;
+
+ if (angleCos < maxDraggingAngleCos) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java b/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java
new file mode 100644
index 0000000..5f12cf4
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java
@@ -0,0 +1,1177 @@
+/*
+ * Copyright (C) 2012 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.animation.ObjectAnimator;
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Property;
+import android.util.Slog;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.IMagnificationCallbacks;
+import android.view.IWindowManager;
+import android.view.MagnificationSpec;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.internal.os.SomeArgs;
+
+import java.util.Locale;
+
+/**
+ * This class handles the screen magnification when accessibility is enabled.
+ * The behavior is as follows:
+ *
+ * 1. Triple tap toggles permanent screen magnification which is magnifying
+ * the area around the location of the triple tap. One can think of the
+ * location of the triple tap as the center of the magnified viewport.
+ * For example, a triple tap when not magnified would magnify the screen
+ * and leave it in a magnified state. A triple tapping when magnified would
+ * clear magnification and leave the screen in a not magnified state.
+ *
+ * 2. Triple tap and hold would magnify the screen if not magnified and enable
+ * viewport dragging mode until the finger goes up. One can think of this
+ * mode as a way to move the magnified viewport since the area around the
+ * moving finger will be magnified to fit the screen. For example, if the
+ * screen was not magnified and the user triple taps and holds the screen
+ * would magnify and the viewport will follow the user's finger. When the
+ * finger goes up the screen will zoom out. If the same user interaction
+ * is performed when the screen is magnified, the viewport movement will
+ * be the same but when the finger goes up the screen will stay magnified.
+ * In other words, the initial magnified state is sticky.
+ *
+ * 3. Pinching with any number of additional fingers when viewport dragging
+ * is enabled, i.e. the user triple tapped and holds, would adjust the
+ * magnification scale which will become the current default magnification
+ * scale. The next time the user magnifies the same magnification scale
+ * would be used.
+ *
+ * 4. When in a permanent magnified state the user can use two or more fingers
+ * to pan the viewport. Note that in this mode the content is panned as
+ * opposed to the viewport dragging mode in which the viewport is moved.
+ *
+ * 5. When in a permanent magnified state the user can use two or more
+ * fingers to change the magnification scale which will become the current
+ * default magnification scale. The next time the user magnifies the same
+ * magnification scale would be used.
+ *
+ * 6. The magnification scale will be persisted in settings and in the cloud.
+ */
+public final class ScreenMagnifier extends IMagnificationCallbacks.Stub
+ implements EventStreamTransformation {
+
+ private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName();
+
+ private static final boolean DEBUG_STATE_TRANSITIONS = false;
+ private static final boolean DEBUG_DETECTING = false;
+ private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;
+ private static final boolean DEBUG_PANNING = false;
+ private static final boolean DEBUG_SCALING = false;
+ private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false;
+
+ private static final int STATE_DELEGATING = 1;
+ private static final int STATE_DETECTING = 2;
+ private static final int STATE_VIEWPORT_DRAGGING = 3;
+ private static final int STATE_MAGNIFIED_INTERACTION = 4;
+
+ private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
+ private static final int MULTI_TAP_TIME_SLOP_ADJUSTMENT = 50;
+
+ private static final int MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED = 1;
+ private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 2;
+ private static final int MESSAGE_ON_USER_CONTEXT_CHANGED = 3;
+ private static final int MESSAGE_ON_ROTATION_CHANGED = 4;
+
+ private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1;
+
+ private static final int MY_PID = android.os.Process.myPid();
+
+ private final Rect mTempRect = new Rect();
+ private final Rect mTempRect1 = new Rect();
+
+ private final Context mContext;
+ private final IWindowManager mWindowManager;
+ private final MagnificationController mMagnificationController;
+ private final ScreenStateObserver mScreenStateObserver;
+
+ private final DetectingStateHandler mDetectingStateHandler;
+ private final MagnifiedContentInteractonStateHandler mMagnifiedContentInteractonStateHandler;
+ private final StateViewportDraggingHandler mStateViewportDraggingHandler;
+
+ private final AccessibilityManagerService mAms;
+
+ private final int mTapTimeSlop = ViewConfiguration.getTapTimeout();
+ private final int mMultiTapTimeSlop =
+ ViewConfiguration.getDoubleTapTimeout() - MULTI_TAP_TIME_SLOP_ADJUSTMENT;
+ private final int mTapDistanceSlop;
+ private final int mMultiTapDistanceSlop;
+
+ private final long mLongAnimationDuration;
+
+ private final Region mMagnifiedBounds = new Region();
+
+ private EventStreamTransformation mNext;
+
+ private int mCurrentState;
+ private int mPreviousState;
+ private boolean mTranslationEnabledBeforePan;
+
+ private PointerCoords[] mTempPointerCoords;
+ private PointerProperties[] mTempPointerProperties;
+
+ private long mDelegatingStateDownTime;
+
+ private boolean mUpdateMagnificationSpecOnNextBoundsChange;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED: {
+ Region bounds = (Region) message.obj;
+ handleOnMagnifiedBoundsChanged(bounds);
+ bounds.recycle();
+ } break;
+ case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
+ SomeArgs args = (SomeArgs) message.obj;
+ final int left = args.argi1;
+ final int top = args.argi2;
+ final int right = args.argi3;
+ final int bottom = args.argi4;
+ handleOnRectangleOnScreenRequested(left, top, right, bottom);
+ args.recycle();
+ } break;
+ case MESSAGE_ON_USER_CONTEXT_CHANGED: {
+ handleOnUserContextChanged();
+ } break;
+ case MESSAGE_ON_ROTATION_CHANGED: {
+ final int rotation = message.arg1;
+ handleOnRotationChanged(rotation);
+ } break;
+ }
+ }
+ };
+
+ public ScreenMagnifier(Context context, int displayId, AccessibilityManagerService service) {
+ mContext = context;
+ mWindowManager = IWindowManager.Stub.asInterface(
+ ServiceManager.getService("window"));
+ mAms = service;
+
+ mLongAnimationDuration = context.getResources().getInteger(
+ com.android.internal.R.integer.config_longAnimTime);
+ mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
+
+ mDetectingStateHandler = new DetectingStateHandler();
+ mStateViewportDraggingHandler = new StateViewportDraggingHandler();
+ mMagnifiedContentInteractonStateHandler = new MagnifiedContentInteractonStateHandler(
+ context);
+
+ mMagnificationController = new MagnificationController(mLongAnimationDuration);
+ mScreenStateObserver = new ScreenStateObserver(context, mMagnificationController);
+
+ try {
+ mWindowManager.setMagnificationCallbacks(this);
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+
+ transitionToState(STATE_DETECTING);
+ }
+
+ @Override
+ public void onMagnifedBoundsChanged(Region bounds) {
+ Region newBounds = Region.obtain(bounds);
+ mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, newBounds).sendToTarget();
+ if (MY_PID != Binder.getCallingPid()) {
+ bounds.recycle();
+ }
+ }
+
+ private void handleOnMagnifiedBoundsChanged(Region bounds) {
+ // If there was a rotation we have to update the center of the magnified
+ // region since the old offset X/Y may be out of its acceptable range for
+ // the new display width and height.
+ if (mUpdateMagnificationSpecOnNextBoundsChange) {
+ mUpdateMagnificationSpecOnNextBoundsChange = false;
+ MagnificationSpec spec = mMagnificationController.getMagnificationSpec();
+ Rect magnifiedFrame = mTempRect;
+ mMagnifiedBounds.getBounds(magnifiedFrame);
+ final float scale = spec.scale;
+ final float centerX = (-spec.offsetX + magnifiedFrame.width() / 2) / scale;
+ final float centerY = (-spec.offsetY + magnifiedFrame.height() / 2) / scale;
+ mMagnificationController.setScaleAndMagnifiedRegionCenter(scale, centerX,
+ centerY, false);
+ }
+ mMagnifiedBounds.set(bounds);
+ mAms.onMagnificationStateChanged();
+ }
+
+ @Override
+ public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) {
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = left;
+ args.argi2 = top;
+ args.argi3 = right;
+ args.argi4 = bottom;
+ mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, args).sendToTarget();
+ }
+
+ private void handleOnRectangleOnScreenRequested(int left, int top, int right, int bottom) {
+ Rect magnifiedFrame = mTempRect;
+ mMagnifiedBounds.getBounds(magnifiedFrame);
+ if (!magnifiedFrame.intersects(left, top, right, bottom)) {
+ return;
+ }
+ Rect magnifFrameInScreenCoords = mTempRect1;
+ getMagnifiedFrameInContentCoords(magnifFrameInScreenCoords);
+ final float scrollX;
+ final float scrollY;
+ if (right - left > magnifFrameInScreenCoords.width()) {
+ final int direction = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault());
+ if (direction == View.LAYOUT_DIRECTION_LTR) {
+ scrollX = left - magnifFrameInScreenCoords.left;
+ } else {
+ scrollX = right - magnifFrameInScreenCoords.right;
+ }
+ } else if (left < magnifFrameInScreenCoords.left) {
+ scrollX = left - magnifFrameInScreenCoords.left;
+ } else if (right > magnifFrameInScreenCoords.right) {
+ scrollX = right - magnifFrameInScreenCoords.right;
+ } else {
+ scrollX = 0;
+ }
+ if (bottom - top > magnifFrameInScreenCoords.height()) {
+ scrollY = top - magnifFrameInScreenCoords.top;
+ } else if (top < magnifFrameInScreenCoords.top) {
+ scrollY = top - magnifFrameInScreenCoords.top;
+ } else if (bottom > magnifFrameInScreenCoords.bottom) {
+ scrollY = bottom - magnifFrameInScreenCoords.bottom;
+ } else {
+ scrollY = 0;
+ }
+ final float scale = mMagnificationController.getScale();
+ mMagnificationController.offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale);
+ }
+
+ @Override
+ public void onRotationChanged(int rotation) {
+ mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget();
+ }
+
+ private void handleOnRotationChanged(int rotation) {
+ resetMagnificationIfNeeded();
+ if (mMagnificationController.isMagnifying()) {
+ mUpdateMagnificationSpecOnNextBoundsChange = true;
+ }
+ }
+
+ @Override
+ public void onUserContextChanged() {
+ mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED);
+ }
+
+ private void handleOnUserContextChanged() {
+ resetMagnificationIfNeeded();
+ }
+
+ private void getMagnifiedFrameInContentCoords(Rect rect) {
+ MagnificationSpec spec = mMagnificationController.getMagnificationSpec();
+ mMagnifiedBounds.getBounds(rect);
+ rect.offset((int) -spec.offsetX, (int) -spec.offsetY);
+ rect.scale(1.0f / spec.scale);
+ }
+
+ private void resetMagnificationIfNeeded() {
+ if (mMagnificationController.isMagnifying()
+ && isScreenMagnificationAutoUpdateEnabled(mContext)) {
+ mMagnificationController.reset(true);
+ }
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent event, MotionEvent rawEvent,
+ int policyFlags) {
+ mMagnifiedContentInteractonStateHandler.onMotionEvent(event);
+ switch (mCurrentState) {
+ case STATE_DELEGATING: {
+ handleMotionEventStateDelegating(event, rawEvent, policyFlags);
+ } break;
+ case STATE_DETECTING: {
+ mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags);
+ } break;
+ case STATE_VIEWPORT_DRAGGING: {
+ mStateViewportDraggingHandler.onMotionEvent(event, policyFlags);
+ } break;
+ case STATE_MAGNIFIED_INTERACTION: {
+ // mMagnifiedContentInteractonStateHandler handles events only
+ // if this is the current state since it uses ScaleGestureDetecotr
+ // and a GestureDetector which need well formed event stream.
+ } break;
+ default: {
+ throw new IllegalStateException("Unknown state: " + mCurrentState);
+ }
+ }
+ }
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ if (mNext != null) {
+ mNext.onAccessibilityEvent(event);
+ }
+ }
+
+ @Override
+ public void setNext(EventStreamTransformation next) {
+ mNext = next;
+ }
+
+ @Override
+ public void clear() {
+ mCurrentState = STATE_DETECTING;
+ mDetectingStateHandler.clear();
+ mStateViewportDraggingHandler.clear();
+ mMagnifiedContentInteractonStateHandler.clear();
+ if (mNext != null) {
+ mNext.clear();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ mScreenStateObserver.destroy();
+ try {
+ mWindowManager.setMagnificationCallbacks(null);
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ }
+
+ private void handleMotionEventStateDelegating(MotionEvent event,
+ MotionEvent rawEvent, int policyFlags) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ mDelegatingStateDownTime = event.getDownTime();
+ } break;
+ case MotionEvent.ACTION_UP: {
+ if (mDetectingStateHandler.mDelayedEventQueue == null) {
+ transitionToState(STATE_DETECTING);
+ }
+ } break;
+ }
+ if (mNext != null) {
+ // If the event is within the magnified portion of the screen we have
+ // to change its location to be where the user thinks he is poking the
+ // UI which may have been magnified and panned.
+ final float eventX = event.getX();
+ final float eventY = event.getY();
+ if (mMagnificationController.isMagnifying()
+ && mMagnifiedBounds.contains((int) eventX, (int) eventY)) {
+ final float scale = mMagnificationController.getScale();
+ final float scaledOffsetX = mMagnificationController.getOffsetX();
+ final float scaledOffsetY = mMagnificationController.getOffsetY();
+ final int pointerCount = event.getPointerCount();
+ PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
+ PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
+ for (int i = 0; i < pointerCount; i++) {
+ event.getPointerCoords(i, coords[i]);
+ coords[i].x = (coords[i].x - scaledOffsetX) / scale;
+ coords[i].y = (coords[i].y - scaledOffsetY) / scale;
+ event.getPointerProperties(i, properties[i]);
+ }
+ event = MotionEvent.obtain(event.getDownTime(),
+ event.getEventTime(), event.getAction(), pointerCount, properties,
+ coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
+ event.getFlags());
+ }
+ // We cache some events to see if the user wants to trigger magnification.
+ // If no magnification is triggered we inject these events with adjusted
+ // time and down time to prevent subsequent transformations being confused
+ // by stale events. After the cached events, which always have a down, are
+ // injected we need to also update the down time of all subsequent non cached
+ // events. All delegated events cached and non-cached are delivered here.
+ event.setDownTime(mDelegatingStateDownTime);
+ mNext.onMotionEvent(event, rawEvent, policyFlags);
+ }
+ }
+
+ private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
+ final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
+ if (oldSize < size) {
+ PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
+ mTempPointerCoords = new PointerCoords[size];
+ if (oldTempPointerCoords != null) {
+ System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
+ }
+ }
+ for (int i = oldSize; i < size; i++) {
+ mTempPointerCoords[i] = new PointerCoords();
+ }
+ return mTempPointerCoords;
+ }
+
+ private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
+ final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0;
+ if (oldSize < size) {
+ PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
+ mTempPointerProperties = new PointerProperties[size];
+ if (oldTempPointerProperties != null) {
+ System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize);
+ }
+ }
+ for (int i = oldSize; i < size; i++) {
+ mTempPointerProperties[i] = new PointerProperties();
+ }
+ return mTempPointerProperties;
+ }
+
+ private void transitionToState(int state) {
+ if (DEBUG_STATE_TRANSITIONS) {
+ switch (state) {
+ case STATE_DELEGATING: {
+ Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
+ } break;
+ case STATE_DETECTING: {
+ Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
+ } break;
+ case STATE_VIEWPORT_DRAGGING: {
+ Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
+ } break;
+ case STATE_MAGNIFIED_INTERACTION: {
+ Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION");
+ } break;
+ default: {
+ throw new IllegalArgumentException("Unknown state: " + state);
+ }
+ }
+ }
+ mPreviousState = mCurrentState;
+ mCurrentState = state;
+ }
+
+ private final class MagnifiedContentInteractonStateHandler
+ extends SimpleOnGestureListener implements OnScaleGestureListener {
+ private static final float MIN_SCALE = 1.3f;
+ private static final float MAX_SCALE = 5.0f;
+
+ private static final float SCALING_THRESHOLD = 0.3f;
+
+ private final ScaleGestureDetector mScaleGestureDetector;
+ private final GestureDetector mGestureDetector;
+
+ private float mInitialScaleFactor = -1;
+ private boolean mScaling;
+
+ public MagnifiedContentInteractonStateHandler(Context context) {
+ mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ mScaleGestureDetector.setQuickScaleEnabled(false);
+ mGestureDetector = new GestureDetector(context, this);
+ }
+
+ public void onMotionEvent(MotionEvent event) {
+ mScaleGestureDetector.onTouchEvent(event);
+ mGestureDetector.onTouchEvent(event);
+ if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
+ return;
+ }
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ clear();
+ final float scale = Math.min(Math.max(mMagnificationController.getScale(),
+ MIN_SCALE), MAX_SCALE);
+ if (scale != getPersistedScale()) {
+ persistScale(scale);
+ }
+ if (mPreviousState == STATE_VIEWPORT_DRAGGING) {
+ transitionToState(STATE_VIEWPORT_DRAGGING);
+ } else {
+ transitionToState(STATE_DETECTING);
+ }
+ }
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX,
+ float distanceY) {
+ if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
+ return true;
+ }
+ if (DEBUG_PANNING) {
+ Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX
+ + " scrollY: " + distanceY);
+ }
+ mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY);
+ return true;
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ if (!mScaling) {
+ if (mInitialScaleFactor < 0) {
+ mInitialScaleFactor = detector.getScaleFactor();
+ } else {
+ final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
+ if (Math.abs(deltaScale) > SCALING_THRESHOLD) {
+ mScaling = true;
+ return true;
+ }
+ }
+ return false;
+ }
+ final float newScale = mMagnificationController.getScale()
+ * detector.getScaleFactor();
+ final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE);
+ if (DEBUG_SCALING) {
+ Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale);
+ }
+ mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(),
+ detector.getFocusY(), false);
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return (mCurrentState == STATE_MAGNIFIED_INTERACTION);
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ clear();
+ }
+
+ private void clear() {
+ mInitialScaleFactor = -1;
+ mScaling = false;
+ }
+ }
+
+ private final class StateViewportDraggingHandler {
+ private boolean mLastMoveOutsideMagnifiedRegion;
+
+ private void onMotionEvent(MotionEvent event, int policyFlags) {
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN");
+ }
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ clear();
+ transitionToState(STATE_MAGNIFIED_INTERACTION);
+ } break;
+ case MotionEvent.ACTION_MOVE: {
+ if (event.getPointerCount() != 1) {
+ throw new IllegalStateException("Should have one pointer down.");
+ }
+ final float eventX = event.getX();
+ final float eventY = event.getY();
+ if (mMagnifiedBounds.contains((int) eventX, (int) eventY)) {
+ if (mLastMoveOutsideMagnifiedRegion) {
+ mLastMoveOutsideMagnifiedRegion = false;
+ mMagnificationController.setMagnifiedRegionCenter(eventX,
+ eventY, true);
+ } else {
+ mMagnificationController.setMagnifiedRegionCenter(eventX,
+ eventY, false);
+ }
+ } else {
+ mLastMoveOutsideMagnifiedRegion = true;
+ }
+ } break;
+ case MotionEvent.ACTION_UP: {
+ if (!mTranslationEnabledBeforePan) {
+ mMagnificationController.reset(true);
+ }
+ clear();
+ transitionToState(STATE_DETECTING);
+ } break;
+ case MotionEvent.ACTION_POINTER_UP: {
+ throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP");
+ }
+ }
+ }
+
+ public void clear() {
+ mLastMoveOutsideMagnifiedRegion = false;
+ }
+ }
+
+ private final class DetectingStateHandler {
+
+ private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1;
+
+ private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
+
+ private static final int ACTION_TAP_COUNT = 3;
+
+ private MotionEventInfo mDelayedEventQueue;
+
+ private MotionEvent mLastDownEvent;
+ private MotionEvent mLastTapUpEvent;
+ private int mTapCount;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ final int type = message.what;
+ switch (type) {
+ case MESSAGE_ON_ACTION_TAP_AND_HOLD: {
+ MotionEvent event = (MotionEvent) message.obj;
+ final int policyFlags = message.arg1;
+ onActionTapAndHold(event, policyFlags);
+ } break;
+ case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
+ transitionToState(STATE_DELEGATING);
+ sendDelayedMotionEvents();
+ clear();
+ } break;
+ default: {
+ throw new IllegalArgumentException("Unknown message type: " + type);
+ }
+ }
+ }
+ };
+
+ public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cacheDelayedMotionEvent(event, rawEvent, policyFlags);
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
+ if (!mMagnifiedBounds.contains((int) event.getX(),
+ (int) event.getY())) {
+ transitionToDelegatingStateAndClear();
+ return;
+ }
+ if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null
+ && GestureUtils.isMultiTap(mLastDownEvent, event,
+ mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
+ Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD,
+ policyFlags, 0, event);
+ mHandler.sendMessageDelayed(message,
+ ViewConfiguration.getLongPressTimeout());
+ } else if (mTapCount < ACTION_TAP_COUNT) {
+ Message message = mHandler.obtainMessage(
+ MESSAGE_TRANSITION_TO_DELEGATING_STATE);
+ mHandler.sendMessageDelayed(message, mMultiTapTimeSlop);
+ }
+ clearLastDownEvent();
+ mLastDownEvent = MotionEvent.obtain(event);
+ } break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ if (mMagnificationController.isMagnifying()) {
+ transitionToState(STATE_MAGNIFIED_INTERACTION);
+ clear();
+ } else {
+ transitionToDelegatingStateAndClear();
+ }
+ } break;
+ case MotionEvent.ACTION_MOVE: {
+ if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) {
+ final double distance = GestureUtils.computeDistance(mLastDownEvent,
+ event, 0);
+ if (Math.abs(distance) > mTapDistanceSlop) {
+ transitionToDelegatingStateAndClear();
+ }
+ }
+ } break;
+ case MotionEvent.ACTION_UP: {
+ if (mLastDownEvent == null) {
+ return;
+ }
+ mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
+ if (!mMagnifiedBounds.contains((int) event.getX(), (int) event.getY())) {
+ transitionToDelegatingStateAndClear();
+ return;
+ }
+ if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop,
+ mTapDistanceSlop, 0)) {
+ transitionToDelegatingStateAndClear();
+ return;
+ }
+ if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent,
+ event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
+ transitionToDelegatingStateAndClear();
+ return;
+ }
+ mTapCount++;
+ if (DEBUG_DETECTING) {
+ Slog.i(LOG_TAG, "Tap count:" + mTapCount);
+ }
+ if (mTapCount == ACTION_TAP_COUNT) {
+ clear();
+ onActionTap(event, policyFlags);
+ return;
+ }
+ clearLastTapUpEvent();
+ mLastTapUpEvent = MotionEvent.obtain(event);
+ } break;
+ case MotionEvent.ACTION_POINTER_UP: {
+ /* do nothing */
+ } break;
+ }
+ }
+
+ public void clear() {
+ mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
+ mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
+ clearTapDetectionState();
+ clearDelayedMotionEvents();
+ }
+
+ private void clearTapDetectionState() {
+ mTapCount = 0;
+ clearLastTapUpEvent();
+ clearLastDownEvent();
+ }
+
+ private void clearLastTapUpEvent() {
+ if (mLastTapUpEvent != null) {
+ mLastTapUpEvent.recycle();
+ mLastTapUpEvent = null;
+ }
+ }
+
+ private void clearLastDownEvent() {
+ if (mLastDownEvent != null) {
+ mLastDownEvent.recycle();
+ mLastDownEvent = null;
+ }
+ }
+
+ private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
+ int policyFlags) {
+ MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent,
+ policyFlags);
+ if (mDelayedEventQueue == null) {
+ mDelayedEventQueue = info;
+ } else {
+ MotionEventInfo tail = mDelayedEventQueue;
+ while (tail.mNext != null) {
+ tail = tail.mNext;
+ }
+ tail.mNext = info;
+ }
+ }
+
+ private void sendDelayedMotionEvents() {
+ while (mDelayedEventQueue != null) {
+ MotionEventInfo info = mDelayedEventQueue;
+ mDelayedEventQueue = info.mNext;
+ final long offset = SystemClock.uptimeMillis() - info.mCachedTimeMillis;
+ MotionEvent event = obtainEventWithOffsetTimeAndDownTime(info.mEvent, offset);
+ MotionEvent rawEvent = obtainEventWithOffsetTimeAndDownTime(info.mRawEvent, offset);
+ ScreenMagnifier.this.onMotionEvent(event, rawEvent, info.mPolicyFlags);
+ event.recycle();
+ rawEvent.recycle();
+ info.recycle();
+ }
+ }
+
+ private MotionEvent obtainEventWithOffsetTimeAndDownTime(MotionEvent event, long offset) {
+ final int pointerCount = event.getPointerCount();
+ PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
+ PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
+ for (int i = 0; i < pointerCount; i++) {
+ event.getPointerCoords(i, coords[i]);
+ event.getPointerProperties(i, properties[i]);
+ }
+ final long downTime = event.getDownTime() + offset;
+ final long eventTime = event.getEventTime() + offset;
+ return MotionEvent.obtain(downTime, eventTime,
+ event.getAction(), pointerCount, properties, coords,
+ event.getMetaState(), event.getButtonState(),
+ 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(),
+ event.getSource(), event.getFlags());
+ }
+
+ private void clearDelayedMotionEvents() {
+ while (mDelayedEventQueue != null) {
+ MotionEventInfo info = mDelayedEventQueue;
+ mDelayedEventQueue = info.mNext;
+ info.recycle();
+ }
+ }
+
+ private void transitionToDelegatingStateAndClear() {
+ transitionToState(STATE_DELEGATING);
+ sendDelayedMotionEvents();
+ clear();
+ }
+
+ private void onActionTap(MotionEvent up, int policyFlags) {
+ if (DEBUG_DETECTING) {
+ Slog.i(LOG_TAG, "onActionTap()");
+ }
+ if (!mMagnificationController.isMagnifying()) {
+ mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
+ up.getX(), up.getY(), true);
+ } else {
+ mMagnificationController.reset(true);
+ }
+ }
+
+ private void onActionTapAndHold(MotionEvent down, int policyFlags) {
+ if (DEBUG_DETECTING) {
+ Slog.i(LOG_TAG, "onActionTapAndHold()");
+ }
+ clear();
+ mTranslationEnabledBeforePan = mMagnificationController.isMagnifying();
+ mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
+ down.getX(), down.getY(), true);
+ transitionToState(STATE_VIEWPORT_DRAGGING);
+ }
+ }
+
+ private void persistScale(final float scale) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ Settings.Secure.putFloat(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale);
+ return null;
+ }
+ }.execute();
+ }
+
+ private float getPersistedScale() {
+ return Settings.Secure.getFloat(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+ DEFAULT_MAGNIFICATION_SCALE);
+ }
+
+ private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) {
+ return (Settings.Secure.getInt(context.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
+ DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
+ }
+
+ private static final class MotionEventInfo {
+
+ private static final int MAX_POOL_SIZE = 10;
+
+ private static final Object sLock = new Object();
+ private static MotionEventInfo sPool;
+ private static int sPoolSize;
+
+ private MotionEventInfo mNext;
+ private boolean mInPool;
+
+ public MotionEvent mEvent;
+ public MotionEvent mRawEvent;
+ public int mPolicyFlags;
+ public long mCachedTimeMillis;
+
+ public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent,
+ int policyFlags) {
+ synchronized (sLock) {
+ MotionEventInfo info;
+ if (sPoolSize > 0) {
+ sPoolSize--;
+ info = sPool;
+ sPool = info.mNext;
+ info.mNext = null;
+ info.mInPool = false;
+ } else {
+ info = new MotionEventInfo();
+ }
+ info.initialize(event, rawEvent, policyFlags);
+ return info;
+ }
+ }
+
+ private void initialize(MotionEvent event, MotionEvent rawEvent,
+ int policyFlags) {
+ mEvent = MotionEvent.obtain(event);
+ mRawEvent = MotionEvent.obtain(rawEvent);
+ mPolicyFlags = policyFlags;
+ mCachedTimeMillis = SystemClock.uptimeMillis();
+ }
+
+ public void recycle() {
+ synchronized (sLock) {
+ if (mInPool) {
+ throw new IllegalStateException("Already recycled.");
+ }
+ clear();
+ if (sPoolSize < MAX_POOL_SIZE) {
+ sPoolSize++;
+ mNext = sPool;
+ sPool = this;
+ mInPool = true;
+ }
+ }
+ }
+
+ private void clear() {
+ mEvent.recycle();
+ mEvent = null;
+ mRawEvent.recycle();
+ mRawEvent = null;
+ mPolicyFlags = 0;
+ mCachedTimeMillis = 0;
+ }
+ }
+
+ private final class MagnificationController {
+
+ private static final String PROPERTY_NAME_MAGNIFICATION_SPEC =
+ "magnificationSpec";
+
+ private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain();
+
+ private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain();
+
+ private final Rect mTempRect = new Rect();
+
+ private final ValueAnimator mTransformationAnimator;
+
+ public MagnificationController(long animationDuration) {
+ Property<MagnificationController, MagnificationSpec> property =
+ Property.of(MagnificationController.class, MagnificationSpec.class,
+ PROPERTY_NAME_MAGNIFICATION_SPEC);
+ TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() {
+ private final MagnificationSpec mTempTransformationSpec =
+ MagnificationSpec.obtain();
+ @Override
+ public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec,
+ MagnificationSpec toSpec) {
+ MagnificationSpec result = mTempTransformationSpec;
+ result.scale = fromSpec.scale
+ + (toSpec.scale - fromSpec.scale) * fraction;
+ result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX)
+ * fraction;
+ result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY)
+ * fraction;
+ return result;
+ }
+ };
+ mTransformationAnimator = ObjectAnimator.ofObject(this, property,
+ evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec);
+ mTransformationAnimator.setDuration((long) (animationDuration));
+ mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f));
+ }
+
+ public boolean isMagnifying() {
+ return mCurrentMagnificationSpec.scale > 1.0f;
+ }
+
+ public void reset(boolean animate) {
+ if (mTransformationAnimator.isRunning()) {
+ mTransformationAnimator.cancel();
+ }
+ mCurrentMagnificationSpec.clear();
+ if (animate) {
+ animateMangificationSpec(mSentMagnificationSpec,
+ mCurrentMagnificationSpec);
+ } else {
+ setMagnificationSpec(mCurrentMagnificationSpec);
+ }
+ Rect bounds = mTempRect;
+ bounds.setEmpty();
+ mAms.onMagnificationStateChanged();
+ }
+
+ public float getScale() {
+ return mCurrentMagnificationSpec.scale;
+ }
+
+ public float getOffsetX() {
+ return mCurrentMagnificationSpec.offsetX;
+ }
+
+ public float getOffsetY() {
+ return mCurrentMagnificationSpec.offsetY;
+ }
+
+ public void setScale(float scale, float pivotX, float pivotY, boolean animate) {
+ Rect magnifiedFrame = mTempRect;
+ mMagnifiedBounds.getBounds(magnifiedFrame);
+ MagnificationSpec spec = mCurrentMagnificationSpec;
+ final float oldScale = spec.scale;
+ final float oldCenterX = (-spec.offsetX + magnifiedFrame.width() / 2) / oldScale;
+ final float oldCenterY = (-spec.offsetY + magnifiedFrame.height() / 2) / oldScale;
+ final float normPivotX = (-spec.offsetX + pivotX) / oldScale;
+ final float normPivotY = (-spec.offsetY + pivotY) / oldScale;
+ final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
+ final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
+ final float centerX = normPivotX + offsetX;
+ final float centerY = normPivotY + offsetY;
+ setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate);
+ }
+
+ public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) {
+ setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.scale, centerX, centerY,
+ animate);
+ }
+
+ public void offsetMagnifiedRegionCenter(float offsetX, float offsetY) {
+ final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX;
+ mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
+ getMinOffsetX()), 0);
+ final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY;
+ mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
+ getMinOffsetY()), 0);
+ setMagnificationSpec(mCurrentMagnificationSpec);
+ }
+
+ public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY,
+ boolean animate) {
+ if (Float.compare(mCurrentMagnificationSpec.scale, scale) == 0
+ && Float.compare(mCurrentMagnificationSpec.offsetX,
+ centerX) == 0
+ && Float.compare(mCurrentMagnificationSpec.offsetY,
+ centerY) == 0) {
+ return;
+ }
+ if (mTransformationAnimator.isRunning()) {
+ mTransformationAnimator.cancel();
+ }
+ if (DEBUG_MAGNIFICATION_CONTROLLER) {
+ Slog.i(LOG_TAG, "scale: " + scale + " offsetX: " + centerX
+ + " offsetY: " + centerY);
+ }
+ updateMagnificationSpec(scale, centerX, centerY);
+ if (animate) {
+ animateMangificationSpec(mSentMagnificationSpec,
+ mCurrentMagnificationSpec);
+ } else {
+ setMagnificationSpec(mCurrentMagnificationSpec);
+ }
+ mAms.onMagnificationStateChanged();
+ }
+
+ public void updateMagnificationSpec(float scale, float magnifiedCenterX,
+ float magnifiedCenterY) {
+ Rect magnifiedFrame = mTempRect;
+ mMagnifiedBounds.getBounds(magnifiedFrame);
+ mCurrentMagnificationSpec.scale = scale;
+ final int viewportWidth = magnifiedFrame.width();
+ final float nonNormOffsetX = viewportWidth / 2 - magnifiedCenterX * scale;
+ mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
+ getMinOffsetX()), 0);
+ final int viewportHeight = magnifiedFrame.height();
+ final float nonNormOffsetY = viewportHeight / 2 - magnifiedCenterY * scale;
+ mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
+ getMinOffsetY()), 0);
+ }
+
+ private float getMinOffsetX() {
+ Rect magnifiedFrame = mTempRect;
+ mMagnifiedBounds.getBounds(magnifiedFrame);
+ final float viewportWidth = magnifiedFrame.width();
+ return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale;
+ }
+
+ private float getMinOffsetY() {
+ Rect magnifiedFrame = mTempRect;
+ mMagnifiedBounds.getBounds(magnifiedFrame);
+ final float viewportHeight = magnifiedFrame.height();
+ return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale;
+ }
+
+ private void animateMangificationSpec(MagnificationSpec fromSpec,
+ MagnificationSpec toSpec) {
+ mTransformationAnimator.setObjectValues(fromSpec, toSpec);
+ mTransformationAnimator.start();
+ }
+
+ public MagnificationSpec getMagnificationSpec() {
+ return mSentMagnificationSpec;
+ }
+
+ public void setMagnificationSpec(MagnificationSpec spec) {
+ if (DEBUG_SET_MAGNIFICATION_SPEC) {
+ Slog.i(LOG_TAG, "Sending: " + spec);
+ }
+ try {
+ mSentMagnificationSpec.scale = spec.scale;
+ mSentMagnificationSpec.offsetX = spec.offsetX;
+ mSentMagnificationSpec.offsetY = spec.offsetY;
+ mWindowManager.setMagnificationSpec(
+ MagnificationSpec.obtain(spec));
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ }
+ }
+
+ private final class ScreenStateObserver extends BroadcastReceiver {
+ private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1;
+
+ private final Context mContext;
+ private final MagnificationController mMagnificationController;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MESSAGE_ON_SCREEN_STATE_CHANGE: {
+ String action = (String) message.obj;
+ handleOnScreenStateChange(action);
+ } break;
+ }
+ }
+ };
+
+ public ScreenStateObserver(Context context,
+ MagnificationController magnificationController) {
+ mContext = context;
+ mMagnificationController = magnificationController;
+ mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
+ }
+
+ public void destroy() {
+ mContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE,
+ intent.getAction()).sendToTarget();
+ }
+
+ private void handleOnScreenStateChange(String action) {
+ if (mMagnificationController.isMagnifying()
+ && isScreenMagnificationAutoUpdateEnabled(mContext)) {
+ mMagnificationController.reset(false);
+ }
+ }
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
new file mode 100644
index 0000000..43f12eb
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
@@ -0,0 +1,1937 @@
+/*
+ ** Copyright 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.gesture.Gesture;
+import android.gesture.GestureLibraries;
+import android.gesture.GestureLibrary;
+import android.gesture.GesturePoint;
+import android.gesture.GestureStore;
+import android.gesture.GestureStroke;
+import android.gesture.Prediction;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.WindowManagerPolicy;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class is a strategy for performing touch exploration. It
+ * transforms the motion event stream by modifying, adding, replacing,
+ * and consuming certain events. The interaction model is:
+ *
+ * <ol>
+ * <li>1. One finger moving slow around performs touch exploration.</li>
+ * <li>2. One finger moving fast around performs gestures.</li>
+ * <li>3. Two close fingers moving in the same direction perform a drag.</li>
+ * <li>4. Multi-finger gestures are delivered to view hierarchy.</li>
+ * <li>5. Two fingers moving in different directions are considered a multi-finger gesture.</li>
+ * <li>7. Double tapping clicks on the on the last touch explored location if it was in
+ * a window that does not take focus, otherwise the click is within the accessibility
+ * focused rectangle.</li>
+ * <li>7. Tapping and holding for a while performs a long press in a similar fashion
+ * as the click above.</li>
+ * <ol>
+ *
+ * @hide
+ */
+class TouchExplorer implements EventStreamTransformation {
+
+ private static final boolean DEBUG = false;
+
+ // Tag for logging received events.
+ private static final String LOG_TAG = "TouchExplorer";
+
+ // States this explorer can be in.
+ private static final int STATE_TOUCH_EXPLORING = 0x00000001;
+ private static final int STATE_DRAGGING = 0x00000002;
+ private static final int STATE_DELEGATING = 0x00000004;
+ private static final int STATE_GESTURE_DETECTING = 0x00000005;
+
+ // The maximum of the cosine between the vectors of two moving
+ // pointers so they can be considered moving in the same direction.
+ private static final float MAX_DRAGGING_ANGLE_COS = 0.525321989f; // cos(pi/4)
+
+ // Constant referring to the ids bits of all pointers.
+ private static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF;
+
+ // This constant captures the current implementation detail that
+ // pointer IDs are between 0 and 31 inclusive (subject to change).
+ // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h)
+ private static final int MAX_POINTER_COUNT = 32;
+
+ // Invalid pointer ID.
+ private static final int INVALID_POINTER_ID = -1;
+
+ // The velocity above which we detect gestures.
+ private static final int GESTURE_DETECTION_VELOCITY_DIP = 1000;
+
+ // The minimal distance before we take the middle of the distance between
+ // the two dragging pointers as opposed to use the location of the primary one.
+ private static final int MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP = 200;
+
+ // The timeout after which we are no longer trying to detect a gesture.
+ private static final int EXIT_GESTURE_DETECTION_TIMEOUT = 2000;
+
+ // Timeout before trying to decide what the user is trying to do.
+ private final int mDetermineUserIntentTimeout;
+
+ // Timeout within which we try to detect a tap.
+ private final int mTapTimeout;
+
+ // Timeout within which we try to detect a double tap.
+ private final int mDoubleTapTimeout;
+
+ // Slop between the down and up tap to be a tap.
+ private final int mTouchSlop;
+
+ // Slop between the first and second tap to be a double tap.
+ private final int mDoubleTapSlop;
+
+ // The current state of the touch explorer.
+ private int mCurrentState = STATE_TOUCH_EXPLORING;
+
+ // The ID of the pointer used for dragging.
+ private int mDraggingPointerId;
+
+ // Handler for performing asynchronous operations.
+ private final Handler mHandler;
+
+ // Command for delayed sending of a hover enter and move event.
+ private final SendHoverEnterAndMoveDelayed mSendHoverEnterAndMoveDelayed;
+
+ // Command for delayed sending of a hover exit event.
+ private final SendHoverExitDelayed mSendHoverExitDelayed;
+
+ // Command for delayed sending of touch exploration end events.
+ private final SendAccessibilityEventDelayed mSendTouchExplorationEndDelayed;
+
+ // Command for delayed sending of touch interaction end events.
+ private final SendAccessibilityEventDelayed mSendTouchInteractionEndDelayed;
+
+ // Command for delayed sending of a long press.
+ private final PerformLongPressDelayed mPerformLongPressDelayed;
+
+ // Command for exiting gesture detection mode after a timeout.
+ private final ExitGestureDetectionModeDelayed mExitGestureDetectionModeDelayed;
+
+ // Helper to detect and react to double tap in touch explore mode.
+ private final DoubleTapDetector mDoubleTapDetector;
+
+ // The scaled minimal distance before we take the middle of the distance between
+ // the two dragging pointers as opposed to use the location of the primary one.
+ private final int mScaledMinPointerDistanceToUseMiddleLocation;
+
+ // The scaled velocity above which we detect gestures.
+ private final int mScaledGestureDetectionVelocity;
+
+ // The handler to which to delegate events.
+ private EventStreamTransformation mNext;
+
+ // Helper to track gesture velocity.
+ private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+
+ // Helper class to track received pointers.
+ private final ReceivedPointerTracker mReceivedPointerTracker;
+
+ // Helper class to track injected pointers.
+ private final InjectedPointerTracker mInjectedPointerTracker;
+
+ // Handle to the accessibility manager service.
+ private final AccessibilityManagerService mAms;
+
+ // Temporary rectangle to avoid instantiation.
+ private final Rect mTempRect = new Rect();
+
+ // Context in which this explorer operates.
+ private final Context mContext;
+
+ // The X of the previous event.
+ private float mPreviousX;
+
+ // The Y of the previous event.
+ private float mPreviousY;
+
+ // Buffer for storing points for gesture detection.
+ private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100);
+
+ // The minimal delta between moves to add a gesture point.
+ private static final int TOUCH_TOLERANCE = 3;
+
+ // The minimal score for accepting a predicted gesture.
+ private static final float MIN_PREDICTION_SCORE = 2.0f;
+
+ // The library for gesture detection.
+ private GestureLibrary mGestureLibrary;
+
+ // The long pressing pointer id if coordinate remapping is needed.
+ private int mLongPressingPointerId = -1;
+
+ // The long pressing pointer X if coordinate remapping is needed.
+ private int mLongPressingPointerDeltaX;
+
+ // The long pressing pointer Y if coordinate remapping is needed.
+ private int mLongPressingPointerDeltaY;
+
+ // The id of the last touch explored window.
+ private int mLastTouchedWindowId;
+
+ // Whether touch exploration is in progress.
+ private boolean mTouchExplorationInProgress;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param inputFilter The input filter associated with this explorer.
+ * @param context A context handle for accessing resources.
+ */
+ public TouchExplorer(Context context, AccessibilityManagerService service) {
+ mContext = context;
+ mAms = service;
+ mReceivedPointerTracker = new ReceivedPointerTracker();
+ mInjectedPointerTracker = new InjectedPointerTracker();
+ mTapTimeout = ViewConfiguration.getTapTimeout();
+ mDetermineUserIntentTimeout = ViewConfiguration.getDoubleTapTimeout();
+ mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
+ mHandler = new Handler(context.getMainLooper());
+ mPerformLongPressDelayed = new PerformLongPressDelayed();
+ mExitGestureDetectionModeDelayed = new ExitGestureDetectionModeDelayed();
+ mGestureLibrary = GestureLibraries.fromRawResource(context, R.raw.accessibility_gestures);
+ mGestureLibrary.setOrientationStyle(8);
+ mGestureLibrary.setSequenceType(GestureStore.SEQUENCE_SENSITIVE);
+ mGestureLibrary.load();
+ mSendHoverEnterAndMoveDelayed = new SendHoverEnterAndMoveDelayed();
+ mSendHoverExitDelayed = new SendHoverExitDelayed();
+ mSendTouchExplorationEndDelayed = new SendAccessibilityEventDelayed(
+ AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END,
+ mDetermineUserIntentTimeout);
+ mSendTouchInteractionEndDelayed = new SendAccessibilityEventDelayed(
+ AccessibilityEvent.TYPE_TOUCH_INTERACTION_END,
+ mDetermineUserIntentTimeout);
+ mDoubleTapDetector = new DoubleTapDetector();
+ final float density = context.getResources().getDisplayMetrics().density;
+ mScaledMinPointerDistanceToUseMiddleLocation =
+ (int) (MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP * density);
+ mScaledGestureDetectionVelocity = (int) (GESTURE_DETECTION_VELOCITY_DIP * density);
+ }
+
+ public void clear() {
+ // If we have not received an event then we are in initial
+ // state. Therefore, there is not need to clean anything.
+ MotionEvent event = mReceivedPointerTracker.getLastReceivedEvent();
+ if (event != null) {
+ clear(mReceivedPointerTracker.getLastReceivedEvent(), WindowManagerPolicy.FLAG_TRUSTED);
+ }
+ }
+
+ public void onDestroy() {
+ // TODO: Implement
+ }
+
+ private void clear(MotionEvent event, int policyFlags) {
+ switch (mCurrentState) {
+ case STATE_TOUCH_EXPLORING: {
+ // If a touch exploration gesture is in progress send events for its end.
+ sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
+ } break;
+ case STATE_DRAGGING: {
+ mDraggingPointerId = INVALID_POINTER_ID;
+ // Send exit to all pointers that we have delivered.
+ sendUpForInjectedDownPointers(event, policyFlags);
+ } break;
+ case STATE_DELEGATING: {
+ // Send exit to all pointers that we have delivered.
+ sendUpForInjectedDownPointers(event, policyFlags);
+ } break;
+ case STATE_GESTURE_DETECTING: {
+ // Clear the current stroke.
+ mStrokeBuffer.clear();
+ } break;
+ }
+ // Remove all pending callbacks.
+ mSendHoverEnterAndMoveDelayed.cancel();
+ mSendHoverExitDelayed.cancel();
+ mPerformLongPressDelayed.cancel();
+ mExitGestureDetectionModeDelayed.cancel();
+ mSendTouchExplorationEndDelayed.cancel();
+ mSendTouchInteractionEndDelayed.cancel();
+ // Reset the pointer trackers.
+ mReceivedPointerTracker.clear();
+ mInjectedPointerTracker.clear();
+ // Clear the double tap detector
+ mDoubleTapDetector.clear();
+ // Go to initial state.
+ // Clear the long pressing pointer remap data.
+ mLongPressingPointerId = -1;
+ mLongPressingPointerDeltaX = 0;
+ mLongPressingPointerDeltaY = 0;
+ mCurrentState = STATE_TOUCH_EXPLORING;
+ if (mNext != null) {
+ mNext.clear();
+ }
+ mTouchExplorationInProgress = false;
+ mAms.onTouchInteractionEnd();
+ }
+
+ @Override
+ public void setNext(EventStreamTransformation next) {
+ mNext = next;
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "Received event: " + event + ", policyFlags=0x"
+ + Integer.toHexString(policyFlags));
+ Slog.d(LOG_TAG, getStateSymbolicName(mCurrentState));
+ }
+
+ mReceivedPointerTracker.onMotionEvent(rawEvent);
+
+ switch(mCurrentState) {
+ case STATE_TOUCH_EXPLORING: {
+ handleMotionEventStateTouchExploring(event, rawEvent, policyFlags);
+ } break;
+ case STATE_DRAGGING: {
+ handleMotionEventStateDragging(event, policyFlags);
+ } break;
+ case STATE_DELEGATING: {
+ handleMotionEventStateDelegating(event, policyFlags);
+ } break;
+ case STATE_GESTURE_DETECTING: {
+ handleMotionEventGestureDetecting(rawEvent, policyFlags);
+ } break;
+ default:
+ throw new IllegalStateException("Illegal state: " + mCurrentState);
+ }
+ }
+
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ final int eventType = event.getEventType();
+
+ // The event for gesture end should be strictly after the
+ // last hover exit event.
+ if (mSendTouchExplorationEndDelayed.isPending()
+ && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) {
+ mSendTouchExplorationEndDelayed.cancel();
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END);
+ }
+
+ // The event for touch interaction end should be strictly after the
+ // last hover exit and the touch exploration gesture end events.
+ if (mSendTouchInteractionEndDelayed.isPending()
+ && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) {
+ mSendTouchInteractionEndDelayed.cancel();
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
+ }
+
+ // If a new window opens or the accessibility focus moves we no longer
+ // want to click/long press on the last touch explored location.
+ switch (eventType) {
+ case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
+ case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
+ if (mInjectedPointerTracker.mLastInjectedHoverEventForClick != null) {
+ mInjectedPointerTracker.mLastInjectedHoverEventForClick.recycle();
+ mInjectedPointerTracker.mLastInjectedHoverEventForClick = null;
+ }
+ mLastTouchedWindowId = -1;
+ } break;
+ case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
+ case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: {
+ mLastTouchedWindowId = event.getWindowId();
+ } break;
+ }
+ if (mNext != null) {
+ mNext.onAccessibilityEvent(event);
+ }
+ }
+
+ /**
+ * Handles a motion event in touch exploring state.
+ *
+ * @param event The event to be handled.
+ * @param rawEvent The raw (unmodified) motion event.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void handleMotionEventStateTouchExploring(MotionEvent event, MotionEvent rawEvent,
+ int policyFlags) {
+ ReceivedPointerTracker receivedTracker = mReceivedPointerTracker;
+
+ mVelocityTracker.addMovement(rawEvent);
+
+ mDoubleTapDetector.onMotionEvent(event, policyFlags);
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ mAms.onTouchInteractionStart();
+
+ // Pre-feed the motion events to the gesture detector since we
+ // have a distance slop before getting into gesture detection
+ // mode and not using the points within this slop significantly
+ // decreases the quality of gesture recognition.
+ handleMotionEventGestureDetecting(rawEvent, policyFlags);
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_START);
+
+ // If we still have not notified the user for the last
+ // touch, we figure out what to do. If were waiting
+ // we resent the delayed callback and wait again.
+ mSendHoverEnterAndMoveDelayed.cancel();
+ mSendHoverExitDelayed.cancel();
+ mPerformLongPressDelayed.cancel();
+
+ if (mSendTouchExplorationEndDelayed.isPending()) {
+ mSendTouchExplorationEndDelayed.forceSendAndRemove();
+ }
+
+ if (mSendTouchInteractionEndDelayed.isPending()) {
+ mSendTouchInteractionEndDelayed.forceSendAndRemove();
+ }
+
+ // If we have the first tap, schedule a long press and break
+ // since we do not want to schedule hover enter because
+ // the delayed callback will kick in before the long click.
+ // This would lead to a state transition resulting in long
+ // pressing the item below the double taped area which is
+ // not necessary where accessibility focus is.
+ if (mDoubleTapDetector.firstTapDetected()) {
+ // We got a tap now post a long press action.
+ mPerformLongPressDelayed.post(event, policyFlags);
+ break;
+ }
+ if (!mTouchExplorationInProgress) {
+ if (!mSendHoverEnterAndMoveDelayed.isPending()) {
+ // Deliver hover enter with a delay to have a chance
+ // to detect what the user is trying to do.
+ final int pointerId = receivedTracker.getPrimaryPointerId();
+ final int pointerIdBits = (1 << pointerId);
+ mSendHoverEnterAndMoveDelayed.post(event, true, pointerIdBits,
+ policyFlags);
+ } else {
+ // Cache the event until we discern exploration from gesturing.
+ mSendHoverEnterAndMoveDelayed.addEvent(event);
+ }
+ }
+ } break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ // Another finger down means that if we have not started to deliver
+ // hover events, we will not have to. The code for ACTION_MOVE will
+ // decide what we will actually do next.
+ mSendHoverEnterAndMoveDelayed.cancel();
+ mSendHoverExitDelayed.cancel();
+ mPerformLongPressDelayed.cancel();
+ } break;
+ case MotionEvent.ACTION_MOVE: {
+ final int pointerId = receivedTracker.getPrimaryPointerId();
+ final int pointerIndex = event.findPointerIndex(pointerId);
+ final int pointerIdBits = (1 << pointerId);
+ switch (event.getPointerCount()) {
+ case 1: {
+ // We have not started sending events since we try to
+ // figure out what the user is doing.
+ if (mSendHoverEnterAndMoveDelayed.isPending()) {
+ // Pre-feed the motion events to the gesture detector since we
+ // have a distance slop before getting into gesture detection
+ // mode and not using the points within this slop significantly
+ // decreases the quality of gesture recognition.
+ handleMotionEventGestureDetecting(rawEvent, policyFlags);
+
+ // Cache the event until we discern exploration from gesturing.
+ mSendHoverEnterAndMoveDelayed.addEvent(event);
+
+ // It is *important* to use the distance traveled by the pointers
+ // on the screen which may or may not be magnified.
+ final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId)
+ - rawEvent.getX(pointerIndex);
+ final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId)
+ - rawEvent.getY(pointerIndex);
+ final double moveDelta = Math.hypot(deltaX, deltaY);
+ // The user has moved enough for us to decide.
+ if (moveDelta > mDoubleTapSlop) {
+ // Check whether the user is performing a gesture. We
+ // detect gestures if the pointer is moving above a
+ // given velocity.
+ mVelocityTracker.computeCurrentVelocity(1000);
+ final float maxAbsVelocity = Math.max(
+ Math.abs(mVelocityTracker.getXVelocity(pointerId)),
+ Math.abs(mVelocityTracker.getYVelocity(pointerId)));
+ if (maxAbsVelocity > mScaledGestureDetectionVelocity) {
+ // We have to perform gesture detection, so
+ // clear the current state and try to detect.
+ mCurrentState = STATE_GESTURE_DETECTING;
+ mVelocityTracker.clear();
+ mSendHoverEnterAndMoveDelayed.cancel();
+ mSendHoverExitDelayed.cancel();
+ mPerformLongPressDelayed.cancel();
+ mExitGestureDetectionModeDelayed.post();
+ // Send accessibility event to announce the start
+ // of gesture recognition.
+ sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_GESTURE_DETECTION_START);
+ } else {
+ // We have just decided that the user is touch,
+ // exploring so start sending events.
+ mSendHoverEnterAndMoveDelayed.forceSendAndRemove();
+ mSendHoverExitDelayed.cancel();
+ mPerformLongPressDelayed.cancel();
+ sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE,
+ pointerIdBits, policyFlags);
+ }
+ break;
+ }
+ } else {
+ // Cancel the long press if pending and the user
+ // moved more than the slop.
+ if (mPerformLongPressDelayed.isPending()) {
+ final float deltaX =
+ receivedTracker.getReceivedPointerDownX(pointerId)
+ - rawEvent.getX(pointerIndex);
+ final float deltaY =
+ receivedTracker.getReceivedPointerDownY(pointerId)
+ - rawEvent.getY(pointerIndex);
+ final double moveDelta = Math.hypot(deltaX, deltaY);
+ // The user has moved enough for us to decide.
+ if (moveDelta > mTouchSlop) {
+ mPerformLongPressDelayed.cancel();
+ }
+ }
+ if (mTouchExplorationInProgress) {
+ sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags);
+ sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits,
+ policyFlags);
+ }
+ }
+ } break;
+ case 2: {
+ // More than one pointer so the user is not touch exploring
+ // and now we have to decide whether to delegate or drag.
+ if (mSendHoverEnterAndMoveDelayed.isPending()) {
+ // We have not started sending events so cancel
+ // scheduled sending events.
+ mSendHoverEnterAndMoveDelayed.cancel();
+ mSendHoverExitDelayed.cancel();
+ mPerformLongPressDelayed.cancel();
+ } else {
+ mPerformLongPressDelayed.cancel();
+ if (mTouchExplorationInProgress) {
+ // If the user is touch exploring the second pointer may be
+ // performing a double tap to activate an item without need
+ // for the user to lift his exploring finger.
+ // It is *important* to use the distance traveled by the pointers
+ // on the screen which may or may not be magnified.
+ final float deltaX = receivedTracker.getReceivedPointerDownX(
+ pointerId) - rawEvent.getX(pointerIndex);
+ final float deltaY = receivedTracker.getReceivedPointerDownY(
+ pointerId) - rawEvent.getY(pointerIndex);
+ final double moveDelta = Math.hypot(deltaX, deltaY);
+ if (moveDelta < mDoubleTapSlop) {
+ break;
+ }
+ // We are sending events so send exit and gesture
+ // end since we transition to another state.
+ sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
+ }
+ }
+
+ // We know that a new state transition is to happen and the
+ // new state will not be gesture recognition, so clear the
+ // stashed gesture strokes.
+ mStrokeBuffer.clear();
+
+ if (isDraggingGesture(event)) {
+ // Two pointers moving in the same direction within
+ // a given distance perform a drag.
+ mCurrentState = STATE_DRAGGING;
+ mDraggingPointerId = pointerId;
+ event.setEdgeFlags(receivedTracker.getLastReceivedDownEdgeFlags());
+ sendMotionEvent(event, MotionEvent.ACTION_DOWN, pointerIdBits,
+ policyFlags);
+ } else {
+ // Two pointers moving arbitrary are delegated to the view hierarchy.
+ mCurrentState = STATE_DELEGATING;
+ sendDownForAllNotInjectedPointers(event, policyFlags);
+ }
+ mVelocityTracker.clear();
+ } break;
+ default: {
+ // More than one pointer so the user is not touch exploring
+ // and now we have to decide whether to delegate or drag.
+ if (mSendHoverEnterAndMoveDelayed.isPending()) {
+ // We have not started sending events so cancel
+ // scheduled sending events.
+ mSendHoverEnterAndMoveDelayed.cancel();
+ mSendHoverExitDelayed.cancel();
+ mPerformLongPressDelayed.cancel();
+ } else {
+ mPerformLongPressDelayed.cancel();
+ // We are sending events so send exit and gesture
+ // end since we transition to another state.
+ sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
+ }
+
+ // More than two pointers are delegated to the view hierarchy.
+ mCurrentState = STATE_DELEGATING;
+ sendDownForAllNotInjectedPointers(event, policyFlags);
+ mVelocityTracker.clear();
+ }
+ }
+ } break;
+ case MotionEvent.ACTION_UP: {
+ mAms.onTouchInteractionEnd();
+ // We know that we do not need the pre-fed gesture points are not
+ // needed anymore since the last pointer just went up.
+ mStrokeBuffer.clear();
+ final int pointerId = event.getPointerId(event.getActionIndex());
+ final int pointerIdBits = (1 << pointerId);
+
+ mPerformLongPressDelayed.cancel();
+ mVelocityTracker.clear();
+
+ if (mSendHoverEnterAndMoveDelayed.isPending()) {
+ // If we have not delivered the enter schedule an exit.
+ mSendHoverExitDelayed.post(event, pointerIdBits, policyFlags);
+ } else {
+ // The user is touch exploring so we send events for end.
+ sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
+ }
+
+ if (!mSendTouchInteractionEndDelayed.isPending()) {
+ mSendTouchInteractionEndDelayed.post();
+ }
+
+ } break;
+ case MotionEvent.ACTION_CANCEL: {
+ clear(event, policyFlags);
+ } break;
+ }
+ }
+
+ /**
+ * Handles a motion event in dragging state.
+ *
+ * @param event The event to be handled.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void handleMotionEventStateDragging(MotionEvent event, int policyFlags) {
+ final int pointerIdBits = (1 << mDraggingPointerId);
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ throw new IllegalStateException("Dragging state can be reached only if two "
+ + "pointers are already down");
+ }
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ // We are in dragging state so we have two pointers and another one
+ // goes down => delegate the three pointers to the view hierarchy
+ mCurrentState = STATE_DELEGATING;
+ if (mDraggingPointerId != INVALID_POINTER_ID) {
+ sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags);
+ }
+ sendDownForAllNotInjectedPointers(event, policyFlags);
+ } break;
+ case MotionEvent.ACTION_MOVE: {
+ switch (event.getPointerCount()) {
+ case 1: {
+ // do nothing
+ } break;
+ case 2: {
+ if (isDraggingGesture(event)) {
+ final float firstPtrX = event.getX(0);
+ final float firstPtrY = event.getY(0);
+ final float secondPtrX = event.getX(1);
+ final float secondPtrY = event.getY(1);
+
+ final float deltaX = firstPtrX - secondPtrX;
+ final float deltaY = firstPtrY - secondPtrY;
+ final double distance = Math.hypot(deltaX, deltaY);
+
+ if (distance > mScaledMinPointerDistanceToUseMiddleLocation) {
+ event.setLocation(deltaX / 2, deltaY / 2);
+ }
+
+ // If still dragging send a drag event.
+ sendMotionEvent(event, MotionEvent.ACTION_MOVE, pointerIdBits,
+ policyFlags);
+ } else {
+ // The two pointers are moving either in different directions or
+ // no close enough => delegate the gesture to the view hierarchy.
+ mCurrentState = STATE_DELEGATING;
+ // Send an event to the end of the drag gesture.
+ sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits,
+ policyFlags);
+ // Deliver all pointers to the view hierarchy.
+ sendDownForAllNotInjectedPointers(event, policyFlags);
+ }
+ } break;
+ default: {
+ mCurrentState = STATE_DELEGATING;
+ // Send an event to the end of the drag gesture.
+ sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits,
+ policyFlags);
+ // Deliver all pointers to the view hierarchy.
+ sendDownForAllNotInjectedPointers(event, policyFlags);
+ }
+ }
+ } break;
+ case MotionEvent.ACTION_POINTER_UP: {
+ final int pointerId = event.getPointerId(event.getActionIndex());
+ if (pointerId == mDraggingPointerId) {
+ mDraggingPointerId = INVALID_POINTER_ID;
+ // Send an event to the end of the drag gesture.
+ sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags);
+ }
+ } break;
+ case MotionEvent.ACTION_UP: {
+ mAms.onTouchInteractionEnd();
+ // Announce the end of a new touch interaction.
+ sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
+ final int pointerId = event.getPointerId(event.getActionIndex());
+ if (pointerId == mDraggingPointerId) {
+ mDraggingPointerId = INVALID_POINTER_ID;
+ // Send an event to the end of the drag gesture.
+ sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags);
+ }
+ mCurrentState = STATE_TOUCH_EXPLORING;
+ } break;
+ case MotionEvent.ACTION_CANCEL: {
+ clear(event, policyFlags);
+ } break;
+ }
+ }
+
+ /**
+ * Handles a motion event in delegating state.
+ *
+ * @param event The event to be handled.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void handleMotionEventStateDelegating(MotionEvent event, int policyFlags) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ throw new IllegalStateException("Delegating state can only be reached if "
+ + "there is at least one pointer down!");
+ }
+ case MotionEvent.ACTION_UP: {
+ // Offset the event if we are doing a long press as the
+ // target is not necessarily under the user's finger.
+ if (mLongPressingPointerId >= 0) {
+ event = offsetEvent(event, - mLongPressingPointerDeltaX,
+ - mLongPressingPointerDeltaY);
+ // Clear the long press state.
+ mLongPressingPointerId = -1;
+ mLongPressingPointerDeltaX = 0;
+ mLongPressingPointerDeltaY = 0;
+ }
+
+ // Deliver the event.
+ sendMotionEvent(event, event.getAction(), ALL_POINTER_ID_BITS, policyFlags);
+
+ // Announce the end of a the touch interaction.
+ mAms.onTouchInteractionEnd();
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
+
+ mCurrentState = STATE_TOUCH_EXPLORING;
+ } break;
+ case MotionEvent.ACTION_CANCEL: {
+ clear(event, policyFlags);
+ } break;
+ default: {
+ // Deliver the event.
+ sendMotionEvent(event, event.getAction(), ALL_POINTER_ID_BITS, policyFlags);
+ }
+ }
+ }
+
+ private void handleMotionEventGestureDetecting(MotionEvent event, int policyFlags) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = event.getX();
+ final float y = event.getY();
+ mPreviousX = x;
+ mPreviousY = y;
+ mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
+ } break;
+ case MotionEvent.ACTION_MOVE: {
+ final float x = event.getX();
+ final float y = event.getY();
+ final float dX = Math.abs(x - mPreviousX);
+ final float dY = Math.abs(y - mPreviousY);
+ if (dX >= TOUCH_TOLERANCE || dY >= TOUCH_TOLERANCE) {
+ mPreviousX = x;
+ mPreviousY = y;
+ mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
+ }
+ } break;
+ case MotionEvent.ACTION_UP: {
+ mAms.onTouchInteractionEnd();
+ // Announce the end of the gesture recognition.
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END);
+ // Announce the end of a the touch interaction.
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
+
+ float x = event.getX();
+ float y = event.getY();
+ mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
+
+ Gesture gesture = new Gesture();
+ gesture.addStroke(new GestureStroke(mStrokeBuffer));
+
+ ArrayList<Prediction> predictions = mGestureLibrary.recognize(gesture);
+ if (!predictions.isEmpty()) {
+ Prediction bestPrediction = predictions.get(0);
+ if (bestPrediction.score >= MIN_PREDICTION_SCORE) {
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "gesture: " + bestPrediction.name + " score: "
+ + bestPrediction.score);
+ }
+ try {
+ final int gestureId = Integer.parseInt(bestPrediction.name);
+ mAms.onGesture(gestureId);
+ } catch (NumberFormatException nfe) {
+ Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name);
+ }
+ }
+ }
+
+ mStrokeBuffer.clear();
+ mExitGestureDetectionModeDelayed.cancel();
+ mCurrentState = STATE_TOUCH_EXPLORING;
+ } break;
+ case MotionEvent.ACTION_CANCEL: {
+ clear(event, policyFlags);
+ } break;
+ }
+ }
+
+ /**
+ * Sends an accessibility event of the given type.
+ *
+ * @param type The event type.
+ */
+ private void sendAccessibilityEvent(int type) {
+ AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mContext);
+ if (accessibilityManager.isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(type);
+ event.setWindowId(mAms.getActiveWindowId());
+ accessibilityManager.sendAccessibilityEvent(event);
+ switch (type) {
+ case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START: {
+ mTouchExplorationInProgress = true;
+ } break;
+ case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: {
+ mTouchExplorationInProgress = false;
+ } break;
+ }
+ }
+ }
+
+ /**
+ * Sends down events to the view hierarchy for all pointers which are
+ * not already being delivered i.e. pointers that are not yet injected.
+ *
+ * @param prototype The prototype from which to create the injected events.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendDownForAllNotInjectedPointers(MotionEvent prototype, int policyFlags) {
+ InjectedPointerTracker injectedPointers = mInjectedPointerTracker;
+
+ // Inject the injected pointers.
+ int pointerIdBits = 0;
+ final int pointerCount = prototype.getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ final int pointerId = prototype.getPointerId(i);
+ // Do not send event for already delivered pointers.
+ if (!injectedPointers.isInjectedPointerDown(pointerId)) {
+ pointerIdBits |= (1 << pointerId);
+ final int action = computeInjectionAction(MotionEvent.ACTION_DOWN, i);
+ sendMotionEvent(prototype, action, pointerIdBits, policyFlags);
+ }
+ }
+ }
+
+ /**
+ * Sends the exit events if needed. Such events are hover exit and touch explore
+ * gesture end.
+ *
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendHoverExitAndTouchExplorationGestureEndIfNeeded(int policyFlags) {
+ MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent();
+ if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) {
+ final int pointerIdBits = event.getPointerIdBits();
+ if (!mSendTouchExplorationEndDelayed.isPending()) {
+ mSendTouchExplorationEndDelayed.post();
+ }
+ sendMotionEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, policyFlags);
+ }
+ }
+
+ /**
+ * Sends the enter events if needed. Such events are hover enter and touch explore
+ * gesture start.
+ *
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendTouchExplorationGestureStartAndHoverEnterIfNeeded(int policyFlags) {
+ MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent();
+ if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
+ final int pointerIdBits = event.getPointerIdBits();
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START);
+ sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags);
+ }
+ }
+
+ /**
+ * Sends up events to the view hierarchy for all pointers which are
+ * already being delivered i.e. pointers that are injected.
+ *
+ * @param prototype The prototype from which to create the injected events.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendUpForInjectedDownPointers(MotionEvent prototype, int policyFlags) {
+ final InjectedPointerTracker injectedTracked = mInjectedPointerTracker;
+ int pointerIdBits = 0;
+ final int pointerCount = prototype.getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ final int pointerId = prototype.getPointerId(i);
+ // Skip non injected down pointers.
+ if (!injectedTracked.isInjectedPointerDown(pointerId)) {
+ continue;
+ }
+ pointerIdBits |= (1 << pointerId);
+ final int action = computeInjectionAction(MotionEvent.ACTION_UP, i);
+ sendMotionEvent(prototype, action, pointerIdBits, policyFlags);
+ }
+ }
+
+ /**
+ * Sends an up and down events.
+ *
+ * @param prototype The prototype from which to create the injected events.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendActionDownAndUp(MotionEvent prototype, int policyFlags) {
+ // Tap with the pointer that last explored.
+ final int pointerId = prototype.getPointerId(prototype.getActionIndex());
+ final int pointerIdBits = (1 << pointerId);
+ sendMotionEvent(prototype, MotionEvent.ACTION_DOWN, pointerIdBits, policyFlags);
+ sendMotionEvent(prototype, MotionEvent.ACTION_UP, pointerIdBits, policyFlags);
+ }
+
+ /**
+ * Sends an event.
+ *
+ * @param prototype The prototype from which to create the injected events.
+ * @param action The action of the event.
+ * @param pointerIdBits The bits of the pointers to send.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendMotionEvent(MotionEvent prototype, int action, int pointerIdBits,
+ int policyFlags) {
+ prototype.setAction(action);
+
+ MotionEvent event = null;
+ if (pointerIdBits == ALL_POINTER_ID_BITS) {
+ event = prototype;
+ } else {
+ event = prototype.split(pointerIdBits);
+ }
+ if (action == MotionEvent.ACTION_DOWN) {
+ event.setDownTime(event.getEventTime());
+ } else {
+ event.setDownTime(mInjectedPointerTracker.getLastInjectedDownEventTime());
+ }
+
+ // If the user is long pressing but the long pressing pointer
+ // was not exactly over the accessibility focused item we need
+ // to remap the location of that pointer so the user does not
+ // have to explicitly touch explore something to be able to
+ // long press it, or even worse to avoid the user long pressing
+ // on the wrong item since click and long press behave differently.
+ if (mLongPressingPointerId >= 0) {
+ event = offsetEvent(event, - mLongPressingPointerDeltaX,
+ - mLongPressingPointerDeltaY);
+ }
+
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "Injecting event: " + event + ", policyFlags=0x"
+ + Integer.toHexString(policyFlags));
+ }
+
+ // Make sure that the user will see the event.
+ policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER;
+ if (mNext != null) {
+ // TODO: For now pass null for the raw event since the touch
+ // explorer is the last event transformation and it does
+ // not care about the raw event.
+ mNext.onMotionEvent(event, null, policyFlags);
+ }
+
+ mInjectedPointerTracker.onMotionEvent(event);
+
+ if (event != prototype) {
+ event.recycle();
+ }
+ }
+
+ /**
+ * Offsets all pointers in the given event by adding the specified X and Y
+ * offsets.
+ *
+ * @param event The event to offset.
+ * @param offsetX The X offset.
+ * @param offsetY The Y offset.
+ * @return An event with the offset pointers or the original event if both
+ * offsets are zero.
+ */
+ private MotionEvent offsetEvent(MotionEvent event, int offsetX, int offsetY) {
+ if (offsetX == 0 && offsetY == 0) {
+ return event;
+ }
+ final int remappedIndex = event.findPointerIndex(mLongPressingPointerId);
+ final int pointerCount = event.getPointerCount();
+ PointerProperties[] props = PointerProperties.createArray(pointerCount);
+ PointerCoords[] coords = PointerCoords.createArray(pointerCount);
+ for (int i = 0; i < pointerCount; i++) {
+ event.getPointerProperties(i, props[i]);
+ event.getPointerCoords(i, coords[i]);
+ if (i == remappedIndex) {
+ coords[i].x += offsetX;
+ coords[i].y += offsetY;
+ }
+ }
+ return MotionEvent.obtain(event.getDownTime(),
+ event.getEventTime(), event.getAction(), event.getPointerCount(),
+ props, coords, event.getMetaState(), event.getButtonState(),
+ 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(),
+ event.getSource(), event.getFlags());
+ }
+
+ /**
+ * Computes the action for an injected event based on a masked action
+ * and a pointer index.
+ *
+ * @param actionMasked The masked action.
+ * @param pointerIndex The index of the pointer which has changed.
+ * @return The action to be used for injection.
+ */
+ private int computeInjectionAction(int actionMasked, int pointerIndex) {
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ InjectedPointerTracker injectedTracker = mInjectedPointerTracker;
+ // Compute the action based on how many down pointers are injected.
+ if (injectedTracker.getInjectedPointerDownCount() == 0) {
+ return MotionEvent.ACTION_DOWN;
+ } else {
+ return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT)
+ | MotionEvent.ACTION_POINTER_DOWN;
+ }
+ }
+ case MotionEvent.ACTION_POINTER_UP: {
+ InjectedPointerTracker injectedTracker = mInjectedPointerTracker;
+ // Compute the action based on how many down pointers are injected.
+ if (injectedTracker.getInjectedPointerDownCount() == 1) {
+ return MotionEvent.ACTION_UP;
+ } else {
+ return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT)
+ | MotionEvent.ACTION_POINTER_UP;
+ }
+ }
+ default:
+ return actionMasked;
+ }
+ }
+
+ private class DoubleTapDetector {
+ private MotionEvent mDownEvent;
+ private MotionEvent mFirstTapEvent;
+
+ public void onMotionEvent(MotionEvent event, int policyFlags) {
+ final int actionIndex = event.getActionIndex();
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ if (mFirstTapEvent != null
+ && !GestureUtils.isSamePointerContext(mFirstTapEvent, event)) {
+ clear();
+ }
+ mDownEvent = MotionEvent.obtain(event);
+ } break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP: {
+ if (mDownEvent == null) {
+ return;
+ }
+ if (!GestureUtils.isSamePointerContext(mDownEvent, event)) {
+ clear();
+ return;
+ }
+ if (GestureUtils.isTap(mDownEvent, event, mTapTimeout, mTouchSlop,
+ actionIndex)) {
+ if (mFirstTapEvent == null || GestureUtils.isTimedOut(mFirstTapEvent,
+ event, mDoubleTapTimeout)) {
+ mFirstTapEvent = MotionEvent.obtain(event);
+ mDownEvent.recycle();
+ mDownEvent = null;
+ return;
+ }
+ if (GestureUtils.isMultiTap(mFirstTapEvent, event, mDoubleTapTimeout,
+ mDoubleTapSlop, actionIndex)) {
+ onDoubleTap(event, policyFlags);
+ mFirstTapEvent.recycle();
+ mFirstTapEvent = null;
+ mDownEvent.recycle();
+ mDownEvent = null;
+ return;
+ }
+ mFirstTapEvent.recycle();
+ mFirstTapEvent = null;
+ } else {
+ if (mFirstTapEvent != null) {
+ mFirstTapEvent.recycle();
+ mFirstTapEvent = null;
+ }
+ }
+ mDownEvent.recycle();
+ mDownEvent = null;
+ } break;
+ }
+ }
+
+ public void onDoubleTap(MotionEvent secondTapUp, int policyFlags) {
+ // This should never be called when more than two pointers are down.
+ if (secondTapUp.getPointerCount() > 2) {
+ return;
+ }
+
+ // Remove pending event deliveries.
+ mSendHoverEnterAndMoveDelayed.cancel();
+ mSendHoverExitDelayed.cancel();
+ mPerformLongPressDelayed.cancel();
+
+ if (mSendTouchExplorationEndDelayed.isPending()) {
+ mSendTouchExplorationEndDelayed.forceSendAndRemove();
+ }
+ if (mSendTouchInteractionEndDelayed.isPending()) {
+ mSendTouchInteractionEndDelayed.forceSendAndRemove();
+ }
+
+ int clickLocationX;
+ int clickLocationY;
+
+ final int pointerId = secondTapUp.getPointerId(secondTapUp.getActionIndex());
+ final int pointerIndex = secondTapUp.findPointerIndex(pointerId);
+
+ MotionEvent lastExploreEvent =
+ mInjectedPointerTracker.getLastInjectedHoverEventForClick();
+ if (lastExploreEvent == null) {
+ // No last touch explored event but there is accessibility focus in
+ // the active window. We click in the middle of the focus bounds.
+ Rect focusBounds = mTempRect;
+ if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) {
+ clickLocationX = focusBounds.centerX();
+ clickLocationY = focusBounds.centerY();
+ } else {
+ // Out of luck - do nothing.
+ return;
+ }
+ } else {
+ // If the click is within the active window but not within the
+ // accessibility focus bounds we click in the focus center.
+ final int lastExplorePointerIndex = lastExploreEvent.getActionIndex();
+ clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex);
+ clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex);
+ Rect activeWindowBounds = mTempRect;
+ if (mLastTouchedWindowId == mAms.getActiveWindowId()) {
+ mAms.getActiveWindowBounds(activeWindowBounds);
+ if (activeWindowBounds.contains(clickLocationX, clickLocationY)) {
+ Rect focusBounds = mTempRect;
+ if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) {
+ if (!focusBounds.contains(clickLocationX, clickLocationY)) {
+ clickLocationX = focusBounds.centerX();
+ clickLocationY = focusBounds.centerY();
+ }
+ }
+ }
+ }
+ }
+
+ // Do the click.
+ PointerProperties[] properties = new PointerProperties[1];
+ properties[0] = new PointerProperties();
+ secondTapUp.getPointerProperties(pointerIndex, properties[0]);
+ PointerCoords[] coords = new PointerCoords[1];
+ coords[0] = new PointerCoords();
+ coords[0].x = clickLocationX;
+ coords[0].y = clickLocationY;
+ MotionEvent event = MotionEvent.obtain(secondTapUp.getDownTime(),
+ secondTapUp.getEventTime(), MotionEvent.ACTION_DOWN, 1, properties,
+ coords, 0, 0, 1.0f, 1.0f, secondTapUp.getDeviceId(), 0,
+ secondTapUp.getSource(), secondTapUp.getFlags());
+ sendActionDownAndUp(event, policyFlags);
+ event.recycle();
+ }
+
+ public void clear() {
+ if (mDownEvent != null) {
+ mDownEvent.recycle();
+ mDownEvent = null;
+ }
+ if (mFirstTapEvent != null) {
+ mFirstTapEvent.recycle();
+ mFirstTapEvent = null;
+ }
+ }
+
+ public boolean firstTapDetected() {
+ return mFirstTapEvent != null
+ && SystemClock.uptimeMillis() - mFirstTapEvent.getEventTime() < mDoubleTapTimeout;
+ }
+ }
+
+ /**
+ * Determines whether a two pointer gesture is a dragging one.
+ *
+ * @param event The event with the pointer data.
+ * @return True if the gesture is a dragging one.
+ */
+ private boolean isDraggingGesture(MotionEvent event) {
+ ReceivedPointerTracker receivedTracker = mReceivedPointerTracker;
+
+ final float firstPtrX = event.getX(0);
+ final float firstPtrY = event.getY(0);
+ final float secondPtrX = event.getX(1);
+ final float secondPtrY = event.getY(1);
+
+ final float firstPtrDownX = receivedTracker.getReceivedPointerDownX(0);
+ final float firstPtrDownY = receivedTracker.getReceivedPointerDownY(0);
+ final float secondPtrDownX = receivedTracker.getReceivedPointerDownX(1);
+ final float secondPtrDownY = receivedTracker.getReceivedPointerDownY(1);
+
+ return GestureUtils.isDraggingGesture(firstPtrDownX, firstPtrDownY, secondPtrDownX,
+ secondPtrDownY, firstPtrX, firstPtrY, secondPtrX, secondPtrY,
+ MAX_DRAGGING_ANGLE_COS);
+ }
+
+ /**
+ * Gets the symbolic name of a state.
+ *
+ * @param state A state.
+ * @return The state symbolic name.
+ */
+ private static String getStateSymbolicName(int state) {
+ switch (state) {
+ case STATE_TOUCH_EXPLORING:
+ return "STATE_TOUCH_EXPLORING";
+ case STATE_DRAGGING:
+ return "STATE_DRAGGING";
+ case STATE_DELEGATING:
+ return "STATE_DELEGATING";
+ case STATE_GESTURE_DETECTING:
+ return "STATE_GESTURE_DETECTING";
+ default:
+ throw new IllegalArgumentException("Unknown state: " + state);
+ }
+ }
+
+ /**
+ * Class for delayed exiting from gesture detecting mode.
+ */
+ private final class ExitGestureDetectionModeDelayed implements Runnable {
+
+ public void post() {
+ mHandler.postDelayed(this, EXIT_GESTURE_DETECTION_TIMEOUT);
+ }
+
+ public void cancel() {
+ mHandler.removeCallbacks(this);
+ }
+
+ @Override
+ public void run() {
+ // Announce the end of gesture recognition.
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END);
+ // Clearing puts is in touch exploration state with a finger already
+ // down, so announce the transition to exploration state.
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START);
+ clear();
+ }
+ }
+
+ /**
+ * Class for delayed sending of long press.
+ */
+ private final class PerformLongPressDelayed implements Runnable {
+ private MotionEvent mEvent;
+ private int mPolicyFlags;
+
+ public void post(MotionEvent prototype, int policyFlags) {
+ mEvent = MotionEvent.obtain(prototype);
+ mPolicyFlags = policyFlags;
+ mHandler.postDelayed(this, ViewConfiguration.getLongPressTimeout());
+ }
+
+ public void cancel() {
+ if (mEvent != null) {
+ mHandler.removeCallbacks(this);
+ clear();
+ }
+ }
+
+ private boolean isPending() {
+ return mHandler.hasCallbacks(this);
+ }
+
+ @Override
+ public void run() {
+ // Pointers should not be zero when running this command.
+ if (mReceivedPointerTracker.getLastReceivedEvent().getPointerCount() == 0) {
+ return;
+ }
+
+ int clickLocationX;
+ int clickLocationY;
+
+ final int pointerId = mEvent.getPointerId(mEvent.getActionIndex());
+ final int pointerIndex = mEvent.findPointerIndex(pointerId);
+
+ MotionEvent lastExploreEvent =
+ mInjectedPointerTracker.getLastInjectedHoverEventForClick();
+ if (lastExploreEvent == null) {
+ // No last touch explored event but there is accessibility focus in
+ // the active window. We click in the middle of the focus bounds.
+ Rect focusBounds = mTempRect;
+ if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) {
+ clickLocationX = focusBounds.centerX();
+ clickLocationY = focusBounds.centerY();
+ } else {
+ // Out of luck - do nothing.
+ return;
+ }
+ } else {
+ // If the click is within the active window but not within the
+ // accessibility focus bounds we click in the focus center.
+ final int lastExplorePointerIndex = lastExploreEvent.getActionIndex();
+ clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex);
+ clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex);
+ Rect activeWindowBounds = mTempRect;
+ if (mLastTouchedWindowId == mAms.getActiveWindowId()) {
+ mAms.getActiveWindowBounds(activeWindowBounds);
+ if (activeWindowBounds.contains(clickLocationX, clickLocationY)) {
+ Rect focusBounds = mTempRect;
+ if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) {
+ if (!focusBounds.contains(clickLocationX, clickLocationY)) {
+ clickLocationX = focusBounds.centerX();
+ clickLocationY = focusBounds.centerY();
+ }
+ }
+ }
+ }
+ }
+
+ mLongPressingPointerId = pointerId;
+ mLongPressingPointerDeltaX = (int) mEvent.getX(pointerIndex) - clickLocationX;
+ mLongPressingPointerDeltaY = (int) mEvent.getY(pointerIndex) - clickLocationY;
+
+ sendHoverExitAndTouchExplorationGestureEndIfNeeded(mPolicyFlags);
+
+ mCurrentState = STATE_DELEGATING;
+ sendDownForAllNotInjectedPointers(mEvent, mPolicyFlags);
+ clear();
+ }
+
+ private void clear() {
+ mEvent.recycle();
+ mEvent = null;
+ mPolicyFlags = 0;
+ }
+ }
+
+ /**
+ * Class for delayed sending of hover enter and move events.
+ */
+ class SendHoverEnterAndMoveDelayed implements Runnable {
+ private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverEnterAndMoveDelayed";
+
+ private final List<MotionEvent> mEvents = new ArrayList<MotionEvent>();
+
+ private int mPointerIdBits;
+ private int mPolicyFlags;
+
+ public void post(MotionEvent event, boolean touchExplorationInProgress,
+ int pointerIdBits, int policyFlags) {
+ cancel();
+ addEvent(event);
+ mPointerIdBits = pointerIdBits;
+ mPolicyFlags = policyFlags;
+ mHandler.postDelayed(this, mDetermineUserIntentTimeout);
+ }
+
+ public void addEvent(MotionEvent event) {
+ mEvents.add(MotionEvent.obtain(event));
+ }
+
+ public void cancel() {
+ if (isPending()) {
+ mHandler.removeCallbacks(this);
+ clear();
+ }
+ }
+
+ private boolean isPending() {
+ return mHandler.hasCallbacks(this);
+ }
+
+ private void clear() {
+ mPointerIdBits = -1;
+ mPolicyFlags = 0;
+ final int eventCount = mEvents.size();
+ for (int i = eventCount - 1; i >= 0; i--) {
+ mEvents.remove(i).recycle();
+ }
+ }
+
+ public void forceSendAndRemove() {
+ if (isPending()) {
+ run();
+ cancel();
+ }
+ }
+
+ public void run() {
+ // Send an accessibility event to announce the touch exploration start.
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START);
+
+ if (!mEvents.isEmpty()) {
+ // Deliver a down event.
+ sendMotionEvent(mEvents.get(0), MotionEvent.ACTION_HOVER_ENTER,
+ mPointerIdBits, mPolicyFlags);
+ if (DEBUG) {
+ Slog.d(LOG_TAG_SEND_HOVER_DELAYED,
+ "Injecting motion event: ACTION_HOVER_ENTER");
+ }
+
+ // Deliver move events.
+ final int eventCount = mEvents.size();
+ for (int i = 1; i < eventCount; i++) {
+ sendMotionEvent(mEvents.get(i), MotionEvent.ACTION_HOVER_MOVE,
+ mPointerIdBits, mPolicyFlags);
+ if (DEBUG) {
+ Slog.d(LOG_TAG_SEND_HOVER_DELAYED,
+ "Injecting motion event: ACTION_HOVER_MOVE");
+ }
+ }
+ }
+ clear();
+ }
+ }
+
+ /**
+ * Class for delayed sending of hover exit events.
+ */
+ class SendHoverExitDelayed implements Runnable {
+ private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverExitDelayed";
+
+ private MotionEvent mPrototype;
+ private int mPointerIdBits;
+ private int mPolicyFlags;
+
+ public void post(MotionEvent prototype, int pointerIdBits, int policyFlags) {
+ cancel();
+ mPrototype = MotionEvent.obtain(prototype);
+ mPointerIdBits = pointerIdBits;
+ mPolicyFlags = policyFlags;
+ mHandler.postDelayed(this, mDetermineUserIntentTimeout);
+ }
+
+ public void cancel() {
+ if (isPending()) {
+ mHandler.removeCallbacks(this);
+ clear();
+ }
+ }
+
+ private boolean isPending() {
+ return mHandler.hasCallbacks(this);
+ }
+
+ private void clear() {
+ mPrototype.recycle();
+ mPrototype = null;
+ mPointerIdBits = -1;
+ mPolicyFlags = 0;
+ }
+
+ public void forceSendAndRemove() {
+ if (isPending()) {
+ run();
+ cancel();
+ }
+ }
+
+ public void run() {
+ if (DEBUG) {
+ Slog.d(LOG_TAG_SEND_HOVER_DELAYED, "Injecting motion event:"
+ + " ACTION_HOVER_EXIT");
+ }
+ sendMotionEvent(mPrototype, MotionEvent.ACTION_HOVER_EXIT,
+ mPointerIdBits, mPolicyFlags);
+ if (!mSendTouchExplorationEndDelayed.isPending()) {
+ mSendTouchExplorationEndDelayed.cancel();
+ mSendTouchExplorationEndDelayed.post();
+ }
+ if (mSendTouchInteractionEndDelayed.isPending()) {
+ mSendTouchInteractionEndDelayed.cancel();
+ mSendTouchInteractionEndDelayed.post();
+ }
+ clear();
+ }
+ }
+
+ private class SendAccessibilityEventDelayed implements Runnable {
+ private final int mEventType;
+ private final int mDelay;
+
+ public SendAccessibilityEventDelayed(int eventType, int delay) {
+ mEventType = eventType;
+ mDelay = delay;
+ }
+
+ public void cancel() {
+ mHandler.removeCallbacks(this);
+ }
+
+ public void post() {
+ mHandler.postDelayed(this, mDelay);
+ }
+
+ public boolean isPending() {
+ return mHandler.hasCallbacks(this);
+ }
+
+ public void forceSendAndRemove() {
+ if (isPending()) {
+ run();
+ cancel();
+ }
+ }
+
+ @Override
+ public void run() {
+ sendAccessibilityEvent(mEventType);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return LOG_TAG;
+ }
+
+ class InjectedPointerTracker {
+ private static final String LOG_TAG_INJECTED_POINTER_TRACKER = "InjectedPointerTracker";
+
+ // Keep track of which pointers sent to the system are down.
+ private int mInjectedPointersDown;
+
+ // The time of the last injected down.
+ private long mLastInjectedDownEventTime;
+
+ // The last injected hover event.
+ private MotionEvent mLastInjectedHoverEvent;
+
+ // The last injected hover event used for performing clicks.
+ private MotionEvent mLastInjectedHoverEventForClick;
+
+ /**
+ * Processes an injected {@link MotionEvent} event.
+ *
+ * @param event The event to process.
+ */
+ public void onMotionEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int pointerId = event.getPointerId(event.getActionIndex());
+ final int pointerFlag = (1 << pointerId);
+ mInjectedPointersDown |= pointerFlag;
+ mLastInjectedDownEventTime = event.getDownTime();
+ } break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP: {
+ final int pointerId = event.getPointerId(event.getActionIndex());
+ final int pointerFlag = (1 << pointerId);
+ mInjectedPointersDown &= ~pointerFlag;
+ if (mInjectedPointersDown == 0) {
+ mLastInjectedDownEventTime = 0;
+ }
+ } break;
+ case MotionEvent.ACTION_HOVER_ENTER:
+ case MotionEvent.ACTION_HOVER_MOVE:
+ case MotionEvent.ACTION_HOVER_EXIT: {
+ if (mLastInjectedHoverEvent != null) {
+ mLastInjectedHoverEvent.recycle();
+ }
+ mLastInjectedHoverEvent = MotionEvent.obtain(event);
+ if (mLastInjectedHoverEventForClick != null) {
+ mLastInjectedHoverEventForClick.recycle();
+ }
+ mLastInjectedHoverEventForClick = MotionEvent.obtain(event);
+ } break;
+ }
+ if (DEBUG) {
+ Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer:\n" + toString());
+ }
+ }
+
+ /**
+ * Clears the internals state.
+ */
+ public void clear() {
+ mInjectedPointersDown = 0;
+ }
+
+ /**
+ * @return The time of the last injected down event.
+ */
+ public long getLastInjectedDownEventTime() {
+ return mLastInjectedDownEventTime;
+ }
+
+ /**
+ * @return The number of down pointers injected to the view hierarchy.
+ */
+ public int getInjectedPointerDownCount() {
+ return Integer.bitCount(mInjectedPointersDown);
+ }
+
+ /**
+ * @return The bits of the injected pointers that are down.
+ */
+ public int getInjectedPointersDown() {
+ return mInjectedPointersDown;
+ }
+
+ /**
+ * Whether an injected pointer is down.
+ *
+ * @param pointerId The unique pointer id.
+ * @return True if the pointer is down.
+ */
+ public boolean isInjectedPointerDown(int pointerId) {
+ final int pointerFlag = (1 << pointerId);
+ return (mInjectedPointersDown & pointerFlag) != 0;
+ }
+
+ /**
+ * @return The the last injected hover event.
+ */
+ public MotionEvent getLastInjectedHoverEvent() {
+ return mLastInjectedHoverEvent;
+ }
+
+ /**
+ * @return The the last injected hover event.
+ */
+ public MotionEvent getLastInjectedHoverEventForClick() {
+ return mLastInjectedHoverEventForClick;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("=========================");
+ builder.append("\nDown pointers #");
+ builder.append(Integer.bitCount(mInjectedPointersDown));
+ builder.append(" [ ");
+ for (int i = 0; i < MAX_POINTER_COUNT; i++) {
+ if ((mInjectedPointersDown & i) != 0) {
+ builder.append(i);
+ builder.append(" ");
+ }
+ }
+ builder.append("]");
+ builder.append("\n=========================");
+ return builder.toString();
+ }
+ }
+
+ class ReceivedPointerTracker {
+ private static final String LOG_TAG_RECEIVED_POINTER_TRACKER = "ReceivedPointerTracker";
+
+ // Keep track of where and when a pointer went down.
+ private final float[] mReceivedPointerDownX = new float[MAX_POINTER_COUNT];
+ private final float[] mReceivedPointerDownY = new float[MAX_POINTER_COUNT];
+ private final long[] mReceivedPointerDownTime = new long[MAX_POINTER_COUNT];
+
+ // Which pointers are down.
+ private int mReceivedPointersDown;
+
+ // The edge flags of the last received down event.
+ private int mLastReceivedDownEdgeFlags;
+
+ // Primary pointer which is either the first that went down
+ // or if it goes up the next one that most recently went down.
+ private int mPrimaryPointerId;
+
+ // Keep track of the last up pointer data.
+ private long mLastReceivedUpPointerDownTime;
+ private float mLastReceivedUpPointerDownX;
+ private float mLastReceivedUpPointerDownY;
+
+ private MotionEvent mLastReceivedEvent;
+
+ /**
+ * Clears the internals state.
+ */
+ public void clear() {
+ Arrays.fill(mReceivedPointerDownX, 0);
+ Arrays.fill(mReceivedPointerDownY, 0);
+ Arrays.fill(mReceivedPointerDownTime, 0);
+ mReceivedPointersDown = 0;
+ mPrimaryPointerId = 0;
+ mLastReceivedUpPointerDownTime = 0;
+ mLastReceivedUpPointerDownX = 0;
+ mLastReceivedUpPointerDownY = 0;
+ }
+
+ /**
+ * Processes a received {@link MotionEvent} event.
+ *
+ * @param event The event to process.
+ */
+ public void onMotionEvent(MotionEvent event) {
+ if (mLastReceivedEvent != null) {
+ mLastReceivedEvent.recycle();
+ }
+ mLastReceivedEvent = MotionEvent.obtain(event);
+
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ handleReceivedPointerDown(event.getActionIndex(), event);
+ } break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ handleReceivedPointerDown(event.getActionIndex(), event);
+ } break;
+ case MotionEvent.ACTION_UP: {
+ handleReceivedPointerUp(event.getActionIndex(), event);
+ } break;
+ case MotionEvent.ACTION_POINTER_UP: {
+ handleReceivedPointerUp(event.getActionIndex(), event);
+ } break;
+ }
+ if (DEBUG) {
+ Slog.i(LOG_TAG_RECEIVED_POINTER_TRACKER, "Received pointer:\n" + toString());
+ }
+ }
+
+ /**
+ * @return The last received event.
+ */
+ public MotionEvent getLastReceivedEvent() {
+ return mLastReceivedEvent;
+ }
+
+ /**
+ * @return The number of received pointers that are down.
+ */
+ public int getReceivedPointerDownCount() {
+ return Integer.bitCount(mReceivedPointersDown);
+ }
+
+ /**
+ * Whether an received pointer is down.
+ *
+ * @param pointerId The unique pointer id.
+ * @return True if the pointer is down.
+ */
+ public boolean isReceivedPointerDown(int pointerId) {
+ final int pointerFlag = (1 << pointerId);
+ return (mReceivedPointersDown & pointerFlag) != 0;
+ }
+
+ /**
+ * @param pointerId The unique pointer id.
+ * @return The X coordinate where the pointer went down.
+ */
+ public float getReceivedPointerDownX(int pointerId) {
+ return mReceivedPointerDownX[pointerId];
+ }
+
+ /**
+ * @param pointerId The unique pointer id.
+ * @return The Y coordinate where the pointer went down.
+ */
+ public float getReceivedPointerDownY(int pointerId) {
+ return mReceivedPointerDownY[pointerId];
+ }
+
+ /**
+ * @param pointerId The unique pointer id.
+ * @return The time when the pointer went down.
+ */
+ public long getReceivedPointerDownTime(int pointerId) {
+ return mReceivedPointerDownTime[pointerId];
+ }
+
+ /**
+ * @return The id of the primary pointer.
+ */
+ public int getPrimaryPointerId() {
+ if (mPrimaryPointerId == INVALID_POINTER_ID) {
+ mPrimaryPointerId = findPrimaryPointerId();
+ }
+ return mPrimaryPointerId;
+ }
+
+ /**
+ * @return The time when the last up received pointer went down.
+ */
+ public long getLastReceivedUpPointerDownTime() {
+ return mLastReceivedUpPointerDownTime;
+ }
+
+ /**
+ * @return The down X of the last received pointer that went up.
+ */
+ public float getLastReceivedUpPointerDownX() {
+ return mLastReceivedUpPointerDownX;
+ }
+
+ /**
+ * @return The down Y of the last received pointer that went up.
+ */
+ public float getLastReceivedUpPointerDownY() {
+ return mLastReceivedUpPointerDownY;
+ }
+
+ /**
+ * @return The edge flags of the last received down event.
+ */
+ public int getLastReceivedDownEdgeFlags() {
+ return mLastReceivedDownEdgeFlags;
+ }
+
+ /**
+ * Handles a received pointer down event.
+ *
+ * @param pointerIndex The index of the pointer that has changed.
+ * @param event The event to be handled.
+ */
+ private void handleReceivedPointerDown(int pointerIndex, MotionEvent event) {
+ final int pointerId = event.getPointerId(pointerIndex);
+ final int pointerFlag = (1 << pointerId);
+
+ mLastReceivedUpPointerDownTime = 0;
+ mLastReceivedUpPointerDownX = 0;
+ mLastReceivedUpPointerDownX = 0;
+
+ mLastReceivedDownEdgeFlags = event.getEdgeFlags();
+
+ mReceivedPointersDown |= pointerFlag;
+ mReceivedPointerDownX[pointerId] = event.getX(pointerIndex);
+ mReceivedPointerDownY[pointerId] = event.getY(pointerIndex);
+ mReceivedPointerDownTime[pointerId] = event.getEventTime();
+
+ mPrimaryPointerId = pointerId;
+ }
+
+ /**
+ * Handles a received pointer up event.
+ *
+ * @param pointerIndex The index of the pointer that has changed.
+ * @param event The event to be handled.
+ */
+ private void handleReceivedPointerUp(int pointerIndex, MotionEvent event) {
+ final int pointerId = event.getPointerId(pointerIndex);
+ final int pointerFlag = (1 << pointerId);
+
+ mLastReceivedUpPointerDownTime = getReceivedPointerDownTime(pointerId);
+ mLastReceivedUpPointerDownX = mReceivedPointerDownX[pointerId];
+ mLastReceivedUpPointerDownY = mReceivedPointerDownY[pointerId];
+
+ mReceivedPointersDown &= ~pointerFlag;
+ mReceivedPointerDownX[pointerId] = 0;
+ mReceivedPointerDownY[pointerId] = 0;
+ mReceivedPointerDownTime[pointerId] = 0;
+
+ if (mPrimaryPointerId == pointerId) {
+ mPrimaryPointerId = INVALID_POINTER_ID;
+ }
+ }
+
+ /**
+ * @return The primary pointer id.
+ */
+ private int findPrimaryPointerId() {
+ int primaryPointerId = INVALID_POINTER_ID;
+ long minDownTime = Long.MAX_VALUE;
+
+ // Find the pointer that went down first.
+ int pointerIdBits = mReceivedPointersDown;
+ while (pointerIdBits > 0) {
+ final int pointerId = Integer.numberOfTrailingZeros(pointerIdBits);
+ pointerIdBits &= ~(1 << pointerId);
+ final long downPointerTime = mReceivedPointerDownTime[pointerId];
+ if (downPointerTime < minDownTime) {
+ minDownTime = downPointerTime;
+ primaryPointerId = pointerId;
+ }
+ }
+ return primaryPointerId;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("=========================");
+ builder.append("\nDown pointers #");
+ builder.append(getReceivedPointerDownCount());
+ builder.append(" [ ");
+ for (int i = 0; i < MAX_POINTER_COUNT; i++) {
+ if (isReceivedPointerDown(i)) {
+ builder.append(i);
+ builder.append(" ");
+ }
+ }
+ builder.append("]");
+ builder.append("\nPrimary pointer id [ ");
+ builder.append(getPrimaryPointerId());
+ builder.append(" ]");
+ builder.append("\n=========================");
+ return builder.toString();
+ }
+ }
+}
diff --git a/services/accessibility/java/service.mk b/services/accessibility/java/service.mk
new file mode 100644
index 0000000..5e8f375
--- /dev/null
+++ b/services/accessibility/java/service.mk
@@ -0,0 +1,11 @@
+# Include only if the service is required
+ifneq ($(findstring accessibility,$(REQUIRED_SERVICES)),)
+
+SUB_DIR := accessibility/java
+
+LOCAL_SRC_FILES += \
+ $(call all-java-files-under,$(SUB_DIR))
+
+#DEFINED_SERVICES += com.android.server.accessibility.AccessibilityManagerService
+
+endif