From fefd20e927b7252d63acb7bb1852c5188e3c1b2e Mon Sep 17 00:00:00 2001
From: Svetoslav Ganov
Date: Thu, 19 Apr 2012 21:44:35 -0700
Subject: [PATCH] Adding an opt-in mechanism for gesture detection in
AccessibilityService.
1. An accessibility service has to explicitly opt in to be notified
for gestures by the system. There is only one accessibility service
that handles gestures and in case it does not handle a gesture
the system performs default handling. This default handling ensures
that we have gesture navigation even if no accessibility service
would like to participate/customize the interaction model.
bug:5932640
Change-Id: Id8194293bd94097b455e9388b68134a45dc3b8fa
---
Android.mk | 1 +
api/current.txt | 6 +-
.../AccessibilityService.java | 162 ++++------
.../AccessibilityServiceInfo.java | 21 +-
.../IAccessibilityServiceClient.aidl | 3 +-
.../IAccessibilityServiceClientCallback.aidl | 30 ++
.../IAccessibilityServiceConnection.aidl | 2 +-
.../UiTestAutomationBridge.java | 4 +-
.../AccessibilityInteractionClient.java | 14 +-
core/res/res/values/attrs.xml | 2 +
core/res/res/values/public.xml | 1 +
.../AccessibilityManagerService.java | 287 +++++++++++++++++-
.../server/accessibility/TouchExplorer.java | 2 +-
13 files changed, 402 insertions(+), 133 deletions(-)
create mode 100644 core/java/android/accessibilityservice/IAccessibilityServiceClientCallback.aidl
diff --git a/Android.mk b/Android.mk
index 3b2d32d3f32f7..a98b1c2eaef7f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -61,6 +61,7 @@ LOCAL_SRC_FILES := $(filter-out \
LOCAL_SRC_FILES += \
core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl \
core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl \
+ core/java/android/accessibilityservice/IAccessibilityServiceClientCallback.aidl \
core/java/android/accounts/IAccountManager.aidl \
core/java/android/accounts/IAccountManagerResponse.aidl \
core/java/android/accounts/IAccountAuthenticator.aidl \
diff --git a/api/current.txt b/api/current.txt
index 8dbb577a1fc7d..5c53ab78c9c5f 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -294,6 +294,7 @@ package android {
field public static final int cacheColorHint = 16843009; // 0x1010101
field public static final int calendarViewShown = 16843596; // 0x101034c
field public static final int calendarViewStyle = 16843613; // 0x101035d
+ field public static final int canHandleGestures = 16843699; // 0x10103b3
field public static final int canRetrieveWindowContent = 16843653; // 0x1010385
field public static final int candidatesTextStyleSpans = 16843312; // 0x1010230
field public static final deprecated int capitalize = 16843113; // 0x1010169
@@ -1997,7 +1998,7 @@ package android.accessibilityservice {
method public final android.accessibilityservice.AccessibilityServiceInfo getServiceInfo();
method public abstract void onAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
method public final android.os.IBinder onBind(android.content.Intent);
- method protected void onGesture(int);
+ method protected boolean onGesture(int);
method public abstract void onInterrupt();
method protected void onServiceConnected();
method public final boolean performGlobalAction(int);
@@ -2020,6 +2021,8 @@ package android.accessibilityservice {
field public static final int GESTURE_SWIPE_UP_AND_DOWN = 7; // 0x7
field public static final int GESTURE_SWIPE_UP_AND_LEFT = 15; // 0xf
field public static final int GESTURE_SWIPE_UP_AND_RIGHT = 16; // 0x10
+ field public static final int GESTURE_TWO_FINGER_LONG_PRESS = 20; // 0x14
+ field public static final int GESTURE_TWO_FINGER_TAP = 19; // 0x13
field public static final int GLOBAL_ACTION_BACK = 1; // 0x1
field public static final int GLOBAL_ACTION_HOME = 2; // 0x2
field public static final int GLOBAL_ACTION_NOTIFICATIONS = 4; // 0x4
@@ -2033,6 +2036,7 @@ package android.accessibilityservice {
method public int describeContents();
method public static java.lang.String feedbackTypeToString(int);
method public static java.lang.String flagToString(int);
+ method public boolean getCanHandleGestures();
method public boolean getCanRetrieveWindowContent();
method public deprecated java.lang.String getDescription();
method public java.lang.String getId();
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 4e340c01ebb16..c858e3ca233c2 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -19,22 +19,17 @@ package android.accessibilityservice;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Configuration;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
-import android.util.LocaleUtil;
import android.util.Log;
-import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityInteractionClient;
import android.view.accessibility.AccessibilityNodeInfo;
import com.android.internal.os.HandlerCaller;
-import java.util.Locale;
-
/**
* An accessibility service runs in the background and receives callbacks by the system
* when {@link AccessibilityEvent}s are fired. Such events denote some state transition
@@ -298,6 +293,16 @@ public abstract class AccessibilityService extends Service {
*/
public static final int GESTURE_SWIPE_DOWN_AND_RIGHT = 18;
+ /**
+ * The user has performed a two finger tap gesture on the touch screen.
+ */
+ public static final int GESTURE_TWO_FINGER_TAP = 19;
+
+ /**
+ * The user has performed a two finger long press gesture on the touch screen.
+ */
+ public static final int GESTURE_TWO_FINGER_LONG_PRESS = 20;
+
/**
* The {@link Intent} that must be declared as handled by the service.
*/
@@ -342,8 +347,6 @@ public abstract class AccessibilityService extends Service {
*/
public static final int GLOBAL_ACTION_NOTIFICATIONS = 4;
- private static final int UNDEFINED = -1;
-
private static final String LOG_TAG = "AccessibilityService";
interface Callbacks {
@@ -351,15 +354,13 @@ public abstract class AccessibilityService extends Service {
public void onInterrupt();
public void onServiceConnected();
public void onSetConnectionId(int connectionId);
- public void onGesture(int gestureId);
+ public boolean onGesture(int gestureId);
}
private int mConnectionId;
private AccessibilityServiceInfo mInfo;
- private int mLayoutDirection;
-
/**
* Callback for {@link android.view.accessibility.AccessibilityEvent}s.
*
@@ -386,95 +387,43 @@ public abstract class AccessibilityService extends Service {
/**
* Called by the system when the user performs a specific gesture on the
- * touch screen.
+ * touch screen. If the gesture is not handled in this callback the system
+ * may provide default handing. Therefore, one should return true from this
+ * function if overriding of default behavior is desired.
+ *
+ * Note: To receive gestures an accessibility service
+ * must declare that it can handle such by specifying the
+ * <{@link android.R.styleable#AccessibilityService_canHandleGestures
+ * canHandleGestures}> attribute.
*
* @param gestureId The unique id of the performed gesture.
*
+ * @return Whether the gesture was handled.
+ *
* @see #GESTURE_SWIPE_UP
- * @see #GESTURE_SWIPE_DOWN
- * @see #GESTURE_SWIPE_LEFT
- * @see #GESTURE_SWIPE_RIGHT
+ * @see #GESTURE_SWIPE_UP_AND_LEFT
* @see #GESTURE_SWIPE_UP_AND_DOWN
+ * @see #GESTURE_SWIPE_UP_AND_RIGHT
+ * @see #GESTURE_SWIPE_DOWN
+ * @see #GESTURE_SWIPE_DOWN_AND_LEFT
* @see #GESTURE_SWIPE_DOWN_AND_UP
+ * @see #GESTURE_SWIPE_DOWN_AND_RIGHT
+ * @see #GESTURE_SWIPE_LEFT
+ * @see #GESTURE_SWIPE_LEFT_AND_UP
* @see #GESTURE_SWIPE_LEFT_AND_RIGHT
+ * @see #GESTURE_SWIPE_LEFT_AND_DOWN
+ * @see #GESTURE_SWIPE_RIGHT
+ * @see #GESTURE_SWIPE_RIGHT_AND_UP
* @see #GESTURE_SWIPE_RIGHT_AND_LEFT
+ * @see #GESTURE_SWIPE_RIGHT_AND_DOWN
* @see #GESTURE_CLOCKWISE_CIRCLE
* @see #GESTURE_COUNTER_CLOCKWISE_CIRCLE
+ * @see #GESTURE_TWO_FINGER_TAP
+ * @see #GESTURE_TWO_FINGER_LONG_PRESS
*/
- protected void onGesture(int gestureId) {
+ protected boolean onGesture(int gestureId) {
// TODO: Describe the default gesture processing in the javaDoc once it is finalized.
-
- // Global actions.
- switch (gestureId) {
- case GESTURE_SWIPE_DOWN_AND_LEFT: {
- performGlobalAction(GLOBAL_ACTION_BACK);
- } return;
- case GESTURE_SWIPE_DOWN_AND_RIGHT: {
- performGlobalAction(GLOBAL_ACTION_HOME);
- } return;
- case GESTURE_SWIPE_UP_AND_LEFT: {
- performGlobalAction(GLOBAL_ACTION_RECENTS);
- } return;
- case GESTURE_SWIPE_UP_AND_RIGHT: {
- performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS);
- } return;
- }
-
- // Cache the id to avoid locking
- final int connectionId = mConnectionId;
- if (connectionId == UNDEFINED) {
- throw new IllegalStateException("AccessibilityService not connected."
- + " Did you receive a call of onServiceConnected()?");
- }
- AccessibilityNodeInfo root = getRootInActiveWindow();
- if (root == null) {
- return;
- }
-
- AccessibilityNodeInfo current = root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY);
- if (current == null) {
- current = root;
- }
-
- // Local actions.
- AccessibilityNodeInfo next = null;
- switch (gestureId) {
- case GESTURE_SWIPE_UP: {
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_OUT);
- } break;
- case GESTURE_SWIPE_DOWN: {
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_IN);
- } break;
- case GESTURE_SWIPE_LEFT: {
- if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) {
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_BACKWARD);
- } else { // LAYOUT_DIRECTION_RTL
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_FORWARD);
- }
- } break;
- case GESTURE_SWIPE_RIGHT: {
- if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) {
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_FORWARD);
- } else { // LAYOUT_DIRECTION_RTL
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_BACKWARD);
- }
- } break;
- case GESTURE_SWIPE_UP_AND_DOWN: {
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_UP);
- } break;
- case GESTURE_SWIPE_DOWN_AND_UP: {
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_DOWN);
- } break;
- case GESTURE_SWIPE_LEFT_AND_RIGHT: {
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_LEFT);
- } break;
- case GESTURE_SWIPE_RIGHT_AND_LEFT: {
- next = current.focusSearch(View.ACCESSIBILITY_FOCUS_RIGHT);
- } break;
- }
- if (next != null && !next.equals(current)) {
- next.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
- }
+ return false;
}
/**
@@ -484,10 +433,7 @@ public abstract class AccessibilityService extends Service {
* @return The root node if this service can retrieve window content.
*/
public AccessibilityNodeInfo getRootInActiveWindow() {
- return AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
- AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
- AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);
+ return AccessibilityInteractionClient.getInstance().getRootInActiveWindow(mConnectionId);
}
/**
@@ -509,7 +455,7 @@ public abstract class AccessibilityService extends Service {
AccessibilityInteractionClient.getInstance().getConnection(mConnectionId);
if (connection != null) {
try {
- return connection.perfromGlobalAction(action);
+ return connection.performGlobalAction(action);
} catch (RemoteException re) {
Log.w(LOG_TAG, "Error while calling performGlobalAction", re);
}
@@ -572,18 +518,6 @@ public abstract class AccessibilityService extends Service {
}
}
- @Override
- public void onCreate() {
- Locale locale = getResources().getConfiguration().locale;
- mLayoutDirection = LocaleUtil.getLayoutDirectionFromLocale(locale);
- }
-
- @Override
- public void onConfigurationChanged(Configuration configuration) {
- super.onConfigurationChanged(configuration);
- mLayoutDirection = LocaleUtil.getLayoutDirectionFromLocale(configuration.locale);
- }
-
/**
* Implement to return the implementation of the internal accessibility
* service interface.
@@ -612,8 +546,8 @@ public abstract class AccessibilityService extends Service {
}
@Override
- public void onGesture(int gestureId) {
- AccessibilityService.this.onGesture(gestureId);
+ public boolean onGesture(int gestureId) {
+ return AccessibilityService.this.onGesture(gestureId);
}
});
}
@@ -658,8 +592,10 @@ public abstract class AccessibilityService extends Service {
mCaller.sendMessage(message);
}
- public void onGesture(int gestureId) {
- Message message = mCaller.obtainMessageI(DO_ON_GESTURE, gestureId);
+ public void onGesture(int gestureId, IAccessibilityServiceClientCallback callback,
+ int interactionId) {
+ Message message = mCaller.obtainMessageIIO(DO_ON_GESTURE, gestureId, interactionId,
+ callback);
mCaller.sendMessage(message);
}
@@ -692,7 +628,15 @@ public abstract class AccessibilityService extends Service {
return;
case DO_ON_GESTURE :
final int gestureId = message.arg1;
- mCallback.onGesture(gestureId);
+ final int interactionId = message.arg2;
+ IAccessibilityServiceClientCallback callback =
+ (IAccessibilityServiceClientCallback) message.obj;
+ final boolean handled = mCallback.onGesture(gestureId);
+ try {
+ callback.setGestureResult(gestureId, handled, interactionId);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error calling back with the gesture resut.", re);
+ }
return;
default :
Log.w(LOG_TAG, "Unknown message type " + message.what);
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index e77ed9ab9065b..7e6786b16a12e 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -223,6 +223,11 @@ public class AccessibilityServiceInfo implements Parcelable {
*/
private boolean mCanRetrieveWindowContent;
+ /**
+ * Flag whether this accessibility service can handle gestures.
+ */
+ private boolean mCanHandleGestures;
+
/**
* Resource id of the description of the accessibility service.
*/
@@ -303,6 +308,8 @@ public class AccessibilityServiceInfo implements Parcelable {
mCanRetrieveWindowContent = asAttributes.getBoolean(
com.android.internal.R.styleable.AccessibilityService_canRetrieveWindowContent,
false);
+ mCanHandleGestures = asAttributes.getBoolean(
+ com.android.internal.R.styleable.AccessibilityService_canHandleGestures, false);
TypedValue peekedValue = asAttributes.peekValue(
com.android.internal.R.styleable.AccessibilityService_description);
if (peekedValue != null) {
@@ -378,12 +385,24 @@ public class AccessibilityServiceInfo implements Parcelable {
* Statically set from
* {@link AccessibilityService#SERVICE_META_DATA meta-data}.
*
- * @return True window content can be retrieved.
+ * @return True if window content can be retrieved.
*/
public boolean getCanRetrieveWindowContent() {
return mCanRetrieveWindowContent;
}
+ /**
+ * Whether this service can handle gestures.
+ *
+ * Statically set from
+ * {@link AccessibilityService#SERVICE_META_DATA meta-data}.
+ *
+ * @return True if the service can handle gestures.
+ */
+ public boolean getCanHandleGestures() {
+ return mCanHandleGestures;
+ }
+
/**
* Gets the non-localized description of the accessibility service.
*
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
index 588728c9ce7fa..0257aa463dfd2 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
@@ -16,6 +16,7 @@
package android.accessibilityservice;
+import android.accessibilityservice.IAccessibilityServiceClientCallback;
import android.accessibilityservice.IAccessibilityServiceConnection;
import android.view.accessibility.AccessibilityEvent;
@@ -32,5 +33,5 @@ import android.view.accessibility.AccessibilityEvent;
void onInterrupt();
- void onGesture(int gestureId);
+ void onGesture(int gesture, in IAccessibilityServiceClientCallback callback, int interactionId);
}
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClientCallback.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClientCallback.aidl
new file mode 100644
index 0000000000000..90613981f012f
--- /dev/null
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceClientCallback.aidl
@@ -0,0 +1,30 @@
+/*
+** 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 android.accessibilityservice;
+
+import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * Callback for IAccessibilityServiceClient.
+ *
+ * @hide
+ */
+ oneway interface IAccessibilityServiceClientCallback {
+
+ void setGestureResult(int gestureId, boolean handled, int interactionId);
+}
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 1bd53877356b9..6e856659d7d65 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -167,5 +167,5 @@ interface IAccessibilityServiceConnection {
* @param action The action to perform.
* @return Whether the action was performed.
*/
- boolean perfromGlobalAction(int action);
+ boolean performGlobalAction(int action);
}
diff --git a/core/java/android/accessibilityservice/UiTestAutomationBridge.java b/core/java/android/accessibilityservice/UiTestAutomationBridge.java
index c840bd659eafd..1697df0e1015d 100644
--- a/core/java/android/accessibilityservice/UiTestAutomationBridge.java
+++ b/core/java/android/accessibilityservice/UiTestAutomationBridge.java
@@ -177,8 +177,8 @@ public class UiTestAutomationBridge {
}
@Override
- public void onGesture(int gestureId) {
- /* do nothing */
+ public boolean onGesture(int gestureId) {
+ return false;
}
});
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index 35f0d9d6fd5ae..f73faf31090fa 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -84,7 +84,7 @@ public final class AccessibilityInteractionClient
private final Object mInstanceLock = new Object();
- private int mInteractionId = -1;
+ private volatile int mInteractionId = -1;
private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
@@ -149,6 +149,18 @@ public final class AccessibilityInteractionClient
}
}
+ /**
+ * Gets the root {@link AccessibilityNodeInfo} in the currently active window.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @return The root {@link AccessibilityNodeInfo} if found, null otherwise.
+ */
+ public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) {
+ return findAccessibilityNodeInfoByAccessibilityId(connectionId,
+ AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
+ AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);
+ }
+
/**
* Finds an {@link AccessibilityNodeInfo} by accessibility id.
*
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 6f489d4d3512e..aa47993563536 100755
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2470,6 +2470,8 @@
+
+
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 03ba08c026fe4..3f8036b605e2d 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3599,5 +3599,6 @@
+
diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
index 66758164a5f60..73610629b9078 100644
--- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -23,6 +23,7 @@ import android.Manifest;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IAccessibilityServiceClientCallback;
import android.accessibilityservice.IAccessibilityServiceConnection;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -42,6 +43,7 @@ import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -55,7 +57,9 @@ import android.view.IWindow;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.View;
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;
@@ -64,6 +68,8 @@ import android.view.accessibility.IAccessibilityManager;
import android.view.accessibility.IAccessibilityManagerClient;
import com.android.internal.content.PackageMonitor;
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.HandlerCaller.Callback;
import com.android.server.accessibility.TouchExplorer.GestureListener;
import com.android.server.wm.WindowManagerService;
@@ -78,6 +84,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* This class is instantiated by the system as a system level service and can be
@@ -97,10 +104,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private static final String FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE =
"registerUiTestAutomationService";
- private static int sIdCounter = 0;
-
private static final int OWN_PROCESS_ID = android.os.Process.myPid();
+ private static final int UNDEFINED = -1;
+
+ private static int sIdCounter = 0;
+
private static int sNextWindowId;
final Context mContext;
@@ -145,6 +154,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private Service mUiAutomationService;
+ private GestureHandler mGestureHandler;
+
+ private int mDefaultGestureHandlingHelperServiceId = UNDEFINED;
+
/**
* Handler for delayed event dispatch.
*/
@@ -471,26 +484,60 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
@Override
- public void onGesture(int gestureId) {
+ public boolean onGesture(int gestureId) {
+ // Lazily instantiate the gesture handler.
+ if (mGestureHandler == null) {
+ mGestureHandler = new GestureHandler();
+ }
synchronized (mLock) {
- final boolean dispatched = notifyGestureLocked(gestureId, false);
- if (!dispatched) {
- notifyGestureLocked(gestureId, true);
+ boolean handled = notifyGestureLocked(gestureId, false);
+ if (!handled) {
+ handled = notifyGestureLocked(gestureId, true);
}
+ if (!handled) {
+ mGestureHandler.scheduleHandleGestureDefault(gestureId);
+ }
+ return handled;
+ }
+ }
+
+ private Service getDefaultGestureHandlingHelperService() {
+ // Since querying of screen content is done through the
+ // AccessibilityInteractionClient which talks to an
+ // IAccessibilityServiceConnection implementation we create a proxy
+ // Service when necessary to enable interaction with the remote
+ // view tree. Note that this service is just a stateless proxy
+ // that does not get any events or interrupts.
+ if (mDefaultGestureHandlingHelperServiceId == UNDEFINED) {
+ ComponentName name = new ComponentName("android",
+ "DefaultGestureHandlingHelperService");
+ AccessibilityServiceInfo info = new AccessibilityServiceInfo();
+ Service service = new Service(name, info, true);
+ mDefaultGestureHandlingHelperServiceId = service.mId;
+ AccessibilityInteractionClient.getInstance().addConnection(
+ mDefaultGestureHandlingHelperServiceId, service);
+ return service;
+ } else {
+ return (Service) AccessibilityInteractionClient.getInstance()
+ .getConnection(mDefaultGestureHandlingHelperServiceId);
}
}
private boolean notifyGestureLocked(int gestureId, boolean isDefault) {
- final int serviceCount = mServices.size();
- for (int i = 0; i < serviceCount; i++) {
+ // 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
+ // gestrues to avoid user frustration when different
+ // bahiour is observed from different combinations of
+ // enabled accessibility services.
+ for (int i = mServices.size() - 1; i >= 0; i--) {
Service service = mServices.get(i);
- if (service.mIsDefault == isDefault) {
- try {
- service.mServiceInterface.onGesture(gestureId);
- return true;
- } catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error dispatching gesture.");
- }
+ if (service.mCanHandleGestures && service.mIsDefault == isDefault) {
+ mGestureHandler.scheduleHandleGesture(gestureId, service.mServiceInterface);
+ return true;
}
}
return false;
@@ -935,6 +982,210 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
}
+ class GestureHandler extends IAccessibilityServiceClientCallback.Stub
+ implements Runnable, Callback {
+
+ private static final String THREAD_NAME = "AccessibilityGestureHandler";
+
+ private static final long TIMEOUT_INTERACTION_MILLIS = 5000;
+
+ private static final int MSG_HANDLE_GESTURE = 1;
+
+ private static final int MSG_HANDLE_GESTURE_DEFAULT = 2;
+
+ private final AtomicInteger mInteractionCounter = new AtomicInteger();
+
+ private final Object mGestureLock = new Object();
+
+ private HandlerCaller mHandlerCaller;
+
+ private volatile int mInteractionId = -1;
+
+ private volatile boolean mGestureResult;
+
+ public GestureHandler() {
+ synchronized (mGestureLock) {
+ Thread worker = new Thread(this, THREAD_NAME);
+ worker.start();
+ try {
+ mGestureLock.wait();
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ synchronized (mGestureLock) {
+ mHandlerCaller = new HandlerCaller(mContext, Looper.myLooper(), this);
+ mGestureLock.notifyAll();
+ }
+ Looper.loop();
+ }
+
+ @Override
+ public void setGestureResult(int gestureId, boolean handled, int interactionId) {
+ synchronized (mGestureLock) {
+ if (interactionId > mInteractionId) {
+ mGestureResult = handled;
+ mInteractionId = interactionId;
+ }
+ mGestureLock.notifyAll();
+ }
+ }
+
+ @Override
+ public void executeMessage(Message message) {
+ final int type = message.what;
+ switch (type) {
+ case MSG_HANDLE_GESTURE: {
+ IAccessibilityServiceClient service =
+ (IAccessibilityServiceClient) message.obj;
+ final int gestureId = message.arg1;
+ final int interactionId = message.arg1;
+
+ try {
+ service.onGesture(gestureId, this, interactionId);
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Error dispatching a gesture to a client.", re);
+ return;
+ }
+
+ long waitTimeMillis = 0;
+ final long startTimeMillis = SystemClock.uptimeMillis();
+ synchronized (mGestureLock) {
+ while (true) {
+ try {
+ // Did we get the expected callback?
+ if (mInteractionId == interactionId) {
+ break;
+ }
+ // Did we get an obsolete callback?
+ if (mInteractionId > interactionId) {
+ break;
+ }
+ // Did we time out?
+ final long elapsedTimeMillis =
+ SystemClock.uptimeMillis() - startTimeMillis;
+ waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
+ if (waitTimeMillis <= 0) {
+ break;
+ }
+ mGestureLock.wait(waitTimeMillis);
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ }
+ handleGestureIfNeededAndResetLocked(gestureId);
+ }
+ } break;
+ case MSG_HANDLE_GESTURE_DEFAULT: {
+ final int gestureId = message.arg1;
+ handleGestureDefault(gestureId);
+ } break;
+ default: {
+ throw new IllegalArgumentException("Unknown message type: " + type);
+ }
+ }
+ }
+
+ private void handleGestureIfNeededAndResetLocked(int gestureId) {
+ if (!mGestureResult) {
+ handleGestureDefault(gestureId);
+ }
+ mGestureResult = false;
+ mInteractionId = -1;
+ }
+
+ public void scheduleHandleGesture(int gestureId, IAccessibilityServiceClient service) {
+ final int interactionId = mInteractionCounter.incrementAndGet();
+ mHandlerCaller.obtainMessageIIO(MSG_HANDLE_GESTURE, gestureId, interactionId,
+ service).sendToTarget();
+ }
+
+ public void scheduleHandleGestureDefault(int gestureId) {
+ final int interactionId = mInteractionCounter.incrementAndGet();
+ mHandlerCaller.obtainMessageI(MSG_HANDLE_GESTURE_DEFAULT, gestureId).sendToTarget();
+ }
+
+ private void handleGestureDefault(int gestureId) {
+ Service service = getDefaultGestureHandlingHelperService();
+
+ // Global actions.
+ switch (gestureId) {
+ case AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT: {
+ service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
+ } return;
+ case AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT: {
+ service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);
+ } return;
+ case AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT: {
+ service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
+ } return;
+ case AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT: {
+ service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
+ } return;
+ }
+
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+
+ AccessibilityNodeInfo root = client.getRootInActiveWindow(service.mId);
+ if (root == null) {
+ return;
+ }
+
+ AccessibilityNodeInfo current = root.findFocus(
+ AccessibilityNodeInfo.FOCUS_ACCESSIBILITY);
+ if (current == null) {
+ current = root;
+ }
+
+ // Local actions.
+ AccessibilityNodeInfo next = null;
+ switch (gestureId) {
+ case AccessibilityService.GESTURE_SWIPE_UP: {
+ next = current.focusSearch(View.ACCESSIBILITY_FOCUS_OUT);
+ } break;
+ case AccessibilityService.GESTURE_SWIPE_DOWN: {
+ next = current.focusSearch(View.ACCESSIBILITY_FOCUS_IN);
+ } break;
+ case AccessibilityService.GESTURE_SWIPE_LEFT: {
+ // TODO: Implement the RTL support.
+// if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) {
+ next = current.focusSearch(View.ACCESSIBILITY_FOCUS_BACKWARD);
+// } else { // LAYOUT_DIRECTION_RTL
+// next = current.focusSearch(View.ACCESSIBILITY_FOCUS_FORWARD);
+// }
+ } break;
+ case AccessibilityService.GESTURE_SWIPE_RIGHT: {
+ // TODO: Implement the RTL support.
+// if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) {
+ next = current.focusSearch(View.ACCESSIBILITY_FOCUS_FORWARD);
+// } else { // LAYOUT_DIRECTION_RTL
+// next = current.focusSearch(View.ACCESSIBILITY_FOCUS_BACKWARD);
+// }
+ } break;
+ case AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN: {
+ next = current.focusSearch(View.ACCESSIBILITY_FOCUS_UP);
+ } break;
+ case AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP: {
+ next = current.focusSearch(View.ACCESSIBILITY_FOCUS_DOWN);
+ } break;
+ case AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT: {
+ next = current.focusSearch(View.ACCESSIBILITY_FOCUS_LEFT);
+ } break;
+ case AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT: {
+ next = current.focusSearch(View.ACCESSIBILITY_FOCUS_RIGHT);
+ } break;
+ }
+ if (next != null && !next.equals(current)) {
+ next.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ }
+ }
+ }
+
/**
* This class represents an accessibility service. It stores all per service
* data required for the service management, provides API for starting/stopping the
@@ -971,6 +1222,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
boolean mCanRetrieveScreenContent;
+ boolean mCanHandleGestures;
+
boolean mIsAutomation;
final Rect mTempBounds = new Rect();
@@ -987,6 +1240,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mIsAutomation = isAutomation;
if (!isAutomation) {
mCanRetrieveScreenContent = accessibilityServiceInfo.getCanRetrieveWindowContent();
+ mCanHandleGestures = accessibilityServiceInfo.getCanHandleGestures();
mIntent = new Intent().setComponent(mComponentName);
mIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.accessibility_binding_label);
@@ -995,6 +1249,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
} else {
mCanRetrieveScreenContent = true;
mIncludeNotImportantViews = true;
+ mCanHandleGestures = true;
}
setDynamicallyConfigurableProperties(accessibilityServiceInfo);
}
@@ -1323,7 +1578,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
return true;
}
- public boolean perfromGlobalAction(int action) {
+ public boolean performGlobalAction(int action) {
switch (action) {
case AccessibilityService.GLOBAL_ACTION_BACK: {
sendDownAndUpKeyEvents(KeyEvent.KEYCODE_BACK);
diff --git a/services/java/com/android/server/accessibility/TouchExplorer.java b/services/java/com/android/server/accessibility/TouchExplorer.java
index 5d01c7710bf95..39012e6ee2530 100644
--- a/services/java/com/android/server/accessibility/TouchExplorer.java
+++ b/services/java/com/android/server/accessibility/TouchExplorer.java
@@ -152,7 +152,7 @@ public class TouchExplorer {
*
* @param gestureId The gesture id.
*/
- public void onGesture(int gestureId);
+ public boolean onGesture(int gestureId);
}
/**