summaryrefslogtreecommitdiff
path: root/android/accessibilityservice
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
committerJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
commit10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch)
tree8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/accessibilityservice
parent677516fb6b6f207d373984757d3d9450474b6b00 (diff)
downloadandroid-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \ --bid 4335822 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4335822.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/accessibilityservice')
-rw-r--r--android/accessibilityservice/AccessibilityButtonController.java218
-rw-r--r--android/accessibilityservice/AccessibilityService.java1869
-rw-r--r--android/accessibilityservice/AccessibilityServiceInfo.java1111
-rw-r--r--android/accessibilityservice/FingerprintGestureController.java185
-rw-r--r--android/accessibilityservice/GestureDescription.java556
5 files changed, 3939 insertions, 0 deletions
diff --git a/android/accessibilityservice/AccessibilityButtonController.java b/android/accessibilityservice/AccessibilityButtonController.java
new file mode 100644
index 00000000..a70085cb
--- /dev/null
+++ b/android/accessibilityservice/AccessibilityButtonController.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2017 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Controller for the accessibility button within the system's navigation area
+ * <p>
+ * This class may be used to query the accessibility button's state and register
+ * callbacks for interactions with and state changes to the accessibility button when
+ * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} is set.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This class and
+ * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} should not be used as
+ * the sole means for offering functionality to users via an {@link AccessibilityService}.
+ * Some device implementations may choose not to provide a software-rendered system
+ * navigation area, making this affordance permanently unavailable.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> On device implementations where the accessibility button is
+ * supported, it may not be available at all times, such as when a foreground application uses
+ * {@link android.view.View#SYSTEM_UI_FLAG_HIDE_NAVIGATION}. A user may also choose to assign
+ * this button to another accessibility service or feature. In each of these cases, a
+ * registered {@link AccessibilityButtonCallback}'s
+ * {@link AccessibilityButtonCallback#onAvailabilityChanged(AccessibilityButtonController, boolean)}
+ * method will be invoked to provide notifications of changes in the accessibility button's
+ * availability to the registering service.
+ * </p>
+ */
+public final class AccessibilityButtonController {
+ private static final String LOG_TAG = "A11yButtonController";
+
+ private final IAccessibilityServiceConnection mServiceConnection;
+ private final Object mLock;
+ private ArrayMap<AccessibilityButtonCallback, Handler> mCallbacks;
+
+ AccessibilityButtonController(@NonNull IAccessibilityServiceConnection serviceConnection) {
+ mServiceConnection = serviceConnection;
+ mLock = new Object();
+ }
+
+ /**
+ * Retrieves whether the accessibility button in the system's navigation area is
+ * available to the calling service.
+ * <p>
+ * <strong>Note:</strong> If the service is not yet connected (e.g.
+ * {@link AccessibilityService#onServiceConnected()} has not yet been called) or the
+ * service has been disconnected, this method will have no effect and return {@code false}.
+ * </p>
+ *
+ * @return {@code true} if the accessibility button in the system's navigation area is
+ * available to the calling service, {@code false} otherwise
+ */
+ public boolean isAccessibilityButtonAvailable() {
+ try {
+ return mServiceConnection.isAccessibilityButtonAvailable();
+ } catch (RemoteException re) {
+ Slog.w(LOG_TAG, "Failed to get accessibility button availability.", re);
+ re.rethrowFromSystemServer();
+ return false;
+ }
+ }
+
+ /**
+ * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
+ * changes callbacks related to the accessibility button.
+ *
+ * @param callback the callback to add, must be non-null
+ */
+ public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) {
+ registerAccessibilityButtonCallback(callback, new Handler(Looper.getMainLooper()));
+ }
+
+ /**
+ * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
+ * change callbacks related to the accessibility button. The callback will occur on the
+ * specified {@link Handler}'s thread, or on the services's main thread if the handler is
+ * {@code null}.
+ *
+ * @param callback the callback to add, must be non-null
+ * @param handler the handler on which the callback should execute, must be non-null
+ */
+ public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback,
+ @NonNull Handler handler) {
+ Preconditions.checkNotNull(callback);
+ Preconditions.checkNotNull(handler);
+ synchronized (mLock) {
+ if (mCallbacks == null) {
+ mCallbacks = new ArrayMap<>();
+ }
+
+ mCallbacks.put(callback, handler);
+ }
+ }
+
+ /**
+ * Unregisters the provided {@link AccessibilityButtonCallback} for interaction and state
+ * change callbacks related to the accessibility button.
+ *
+ * @param callback the callback to remove, must be non-null
+ */
+ public void unregisterAccessibilityButtonCallback(
+ @NonNull AccessibilityButtonCallback callback) {
+ Preconditions.checkNotNull(callback);
+ synchronized (mLock) {
+ if (mCallbacks == null) {
+ return;
+ }
+
+ final int keyIndex = mCallbacks.indexOfKey(callback);
+ final boolean hasKey = keyIndex >= 0;
+ if (hasKey) {
+ mCallbacks.removeAt(keyIndex);
+ }
+ }
+ }
+
+ /**
+ * Dispatches the accessibility button click to any registered callbacks. This should
+ * be called on the service's main thread.
+ */
+ void dispatchAccessibilityButtonClicked() {
+ final ArrayMap<AccessibilityButtonCallback, Handler> entries;
+ synchronized (mLock) {
+ if (mCallbacks == null || mCallbacks.isEmpty()) {
+ Slog.w(LOG_TAG, "Received accessibility button click with no callbacks!");
+ return;
+ }
+
+ // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent
+ // modification.
+ entries = new ArrayMap<>(mCallbacks);
+ }
+
+ for (int i = 0, count = entries.size(); i < count; i++) {
+ final AccessibilityButtonCallback callback = entries.keyAt(i);
+ final Handler handler = entries.valueAt(i);
+ handler.post(() -> callback.onClicked(this));
+ }
+ }
+
+ /**
+ * Dispatches the accessibility button availability changes to any registered callbacks.
+ * This should be called on the service's main thread.
+ */
+ void dispatchAccessibilityButtonAvailabilityChanged(boolean available) {
+ final ArrayMap<AccessibilityButtonCallback, Handler> entries;
+ synchronized (mLock) {
+ if (mCallbacks == null || mCallbacks.isEmpty()) {
+ Slog.w(LOG_TAG,
+ "Received accessibility button availability change with no callbacks!");
+ return;
+ }
+
+ // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent
+ // modification.
+ entries = new ArrayMap<>(mCallbacks);
+ }
+
+ for (int i = 0, count = entries.size(); i < count; i++) {
+ final AccessibilityButtonCallback callback = entries.keyAt(i);
+ final Handler handler = entries.valueAt(i);
+ handler.post(() -> callback.onAvailabilityChanged(this, available));
+ }
+ }
+
+ /**
+ * Callback for interaction with and changes to state of the accessibility button
+ * within the system's navigation area.
+ */
+ public static abstract class AccessibilityButtonCallback {
+
+ /**
+ * Called when the accessibility button in the system's navigation area is clicked.
+ *
+ * @param controller the controller used to register for this callback
+ */
+ public void onClicked(AccessibilityButtonController controller) {}
+
+ /**
+ * Called when the availability of the accessibility button in the system's
+ * navigation area has changed. The accessibility button may become unavailable
+ * because the device shopped showing the button, the button was assigned to another
+ * service, or for other reasons.
+ *
+ * @param controller the controller used to register for this callback
+ * @param available {@code true} if the accessibility button is available to this
+ * service, {@code false} otherwise
+ */
+ public void onAvailabilityChanged(AccessibilityButtonController controller,
+ boolean available) {
+ }
+ }
+}
diff --git a/android/accessibilityservice/AccessibilityService.java b/android/accessibilityservice/AccessibilityService.java
new file mode 100644
index 00000000..a558d685
--- /dev/null
+++ b/android/accessibilityservice/AccessibilityService.java
@@ -0,0 +1,1869 @@
+/*
+ * Copyright (C) 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 android.accessibilityservice;
+
+import android.accessibilityservice.GestureDescription.MotionEventGenerator;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Region;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.WindowManager;
+import android.view.WindowManagerImpl;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Accessibility services should only be used to assist users with disabilities in using
+ * Android devices and apps. They run in the background and receive callbacks by the system
+ * when {@link AccessibilityEvent}s are fired. Such events denote some state transition
+ * in the user interface, for example, the focus has changed, a button has been clicked,
+ * etc. Such a service can optionally request the capability for querying the content
+ * of the active window. Development of an accessibility service requires extending this
+ * class and implementing its abstract methods.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about creating AccessibilityServices, read the
+ * <a href="{@docRoot}guide/topics/ui/accessibility/index.html">Accessibility</a>
+ * developer guide.</p>
+ * </div>
+ *
+ * <h3>Lifecycle</h3>
+ * <p>
+ * The lifecycle of an accessibility service is managed exclusively by the system and
+ * follows the established service life cycle. Starting an accessibility service is triggered
+ * exclusively by the user explicitly turning the service on in device settings. After the system
+ * binds to a service, it calls {@link AccessibilityService#onServiceConnected()}. This method can
+ * be overriden by clients that want to perform post binding setup.
+ * </p>
+ * <p>
+ * An accessibility service stops either when the user turns it off in device settings or when
+ * it calls {@link AccessibilityService#disableSelf()}.
+ * </p>
+ * <h3>Declaration</h3>
+ * <p>
+ * An accessibility is declared as any other service in an AndroidManifest.xml, but it
+ * must do two things:
+ * <ul>
+ * <ol>
+ * Specify that it handles the "android.accessibilityservice.AccessibilityService"
+ * {@link android.content.Intent}.
+ * </ol>
+ * <ol>
+ * Request the {@link android.Manifest.permission#BIND_ACCESSIBILITY_SERVICE} permission to
+ * ensure that only the system can bind to it.
+ * </ol>
+ * </ul>
+ * If either of these items is missing, the system will ignore the accessibility service.
+ * Following is an example declaration:
+ * </p>
+ * <pre> &lt;service android:name=".MyAccessibilityService"
+ * android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"&gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.accessibilityservice.AccessibilityService" /&gt;
+ * &lt;/intent-filter&gt;
+ * . . .
+ * &lt;/service&gt;</pre>
+ * <h3>Configuration</h3>
+ * <p>
+ * An accessibility service can be configured to receive specific types of accessibility events,
+ * listen only to specific packages, get events from each type only once in a given time frame,
+ * retrieve window content, specify a settings activity, etc.
+ * </p>
+ * <p>
+ * There are two approaches for configuring an accessibility service:
+ * </p>
+ * <ul>
+ * <li>
+ * Providing a {@link #SERVICE_META_DATA meta-data} entry in the manifest when declaring
+ * the service. A service declaration with a meta-data tag is presented below:
+ * <pre> &lt;service android:name=".MyAccessibilityService"&gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.accessibilityservice.AccessibilityService" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibilityservice" /&gt;
+ * &lt;/service&gt;</pre>
+ * <p class="note">
+ * <strong>Note:</strong> This approach enables setting all properties.
+ * </p>
+ * <p>
+ * For more details refer to {@link #SERVICE_META_DATA} and
+ * <code>&lt;{@link android.R.styleable#AccessibilityService accessibility-service}&gt;</code>.
+ * </p>
+ * </li>
+ * <li>
+ * Calling {@link AccessibilityService#setServiceInfo(AccessibilityServiceInfo)}. Note
+ * that this method can be called any time to dynamically change the service configuration.
+ * <p class="note">
+ * <strong>Note:</strong> This approach enables setting only dynamically configurable properties:
+ * {@link AccessibilityServiceInfo#eventTypes},
+ * {@link AccessibilityServiceInfo#feedbackType},
+ * {@link AccessibilityServiceInfo#flags},
+ * {@link AccessibilityServiceInfo#notificationTimeout},
+ * {@link AccessibilityServiceInfo#packageNames}
+ * </p>
+ * <p>
+ * For more details refer to {@link AccessibilityServiceInfo}.
+ * </p>
+ * </li>
+ * </ul>
+ * <h3>Retrieving window content</h3>
+ * <p>
+ * A service can specify in its declaration that it can retrieve window
+ * content which is represented as a tree of {@link AccessibilityWindowInfo} and
+ * {@link AccessibilityNodeInfo} objects. Note that
+ * declaring this capability requires that the service declares its configuration via
+ * an XML resource referenced by {@link #SERVICE_META_DATA}.
+ * </p>
+ * <p>
+ * Window content may be retrieved with
+ * {@link AccessibilityEvent#getSource() AccessibilityEvent.getSource()},
+ * {@link AccessibilityService#findFocus(int)},
+ * {@link AccessibilityService#getWindows()}, or
+ * {@link AccessibilityService#getRootInActiveWindow()}.
+ * </p>
+ * <p class="note">
+ * <strong>Note</strong> An accessibility service may have requested to be notified for
+ * a subset of the event types, and thus be unaware when the node hierarchy has changed. It is also
+ * possible for a node to contain outdated information because the window content may change at any
+ * time.
+ * </p>
+ * <h3>Notification strategy</h3>
+ * <p>
+ * All accessibility services are notified of all events they have requested, regardless of their
+ * feedback type.
+ * </p>
+ * <p class="note">
+ * <strong>Note:</strong> The event notification timeout is useful to avoid propagating
+ * events to the client too frequently since this is accomplished via an expensive
+ * interprocess call. One can think of the timeout as a criteria to determine when
+ * event generation has settled down.</p>
+ * <h3>Event types</h3>
+ * <ul>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_CLICKED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_LONG_CLICKED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_FOCUSED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_SELECTED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_START}</li>
+ * <li>{@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_HOVER_ENTER}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_SCROLLED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_TEXT_SELECTION_CHANGED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_ANNOUNCEMENT}</li>
+ * <li>{@link AccessibilityEvent#TYPE_GESTURE_DETECTION_START}</li>
+ * <li>{@link AccessibilityEvent#TYPE_GESTURE_DETECTION_END}</li>
+ * <li>{@link AccessibilityEvent#TYPE_TOUCH_INTERACTION_START}</li>
+ * <li>{@link AccessibilityEvent#TYPE_TOUCH_INTERACTION_END}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUSED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_WINDOWS_CHANGED}</li>
+ * <li>{@link AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED}</li>
+ * </ul>
+ * <h3>Feedback types</h3>
+ * <ul>
+ * <li>{@link AccessibilityServiceInfo#FEEDBACK_AUDIBLE}</li>
+ * <li>{@link AccessibilityServiceInfo#FEEDBACK_HAPTIC}</li>
+ * <li>{@link AccessibilityServiceInfo#FEEDBACK_AUDIBLE}</li>
+ * <li>{@link AccessibilityServiceInfo#FEEDBACK_VISUAL}</li>
+ * <li>{@link AccessibilityServiceInfo#FEEDBACK_GENERIC}</li>
+ * <li>{@link AccessibilityServiceInfo#FEEDBACK_BRAILLE}</li>
+ * </ul>
+ * @see AccessibilityEvent
+ * @see AccessibilityServiceInfo
+ * @see android.view.accessibility.AccessibilityManager
+ */
+public abstract class AccessibilityService extends Service {
+
+ /**
+ * The user has performed a swipe up gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_UP = 1;
+
+ /**
+ * The user has performed a swipe down gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_DOWN = 2;
+
+ /**
+ * The user has performed a swipe left gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_LEFT = 3;
+
+ /**
+ * The user has performed a swipe right gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_RIGHT = 4;
+
+ /**
+ * The user has performed a swipe left and right gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_LEFT_AND_RIGHT = 5;
+
+ /**
+ * The user has performed a swipe right and left gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_RIGHT_AND_LEFT = 6;
+
+ /**
+ * The user has performed a swipe up and down gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_UP_AND_DOWN = 7;
+
+ /**
+ * The user has performed a swipe down and up gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_DOWN_AND_UP = 8;
+
+ /**
+ * The user has performed a left and up gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_LEFT_AND_UP = 9;
+
+ /**
+ * The user has performed a left and down gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_LEFT_AND_DOWN = 10;
+
+ /**
+ * The user has performed a right and up gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_RIGHT_AND_UP = 11;
+
+ /**
+ * The user has performed a right and down gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_RIGHT_AND_DOWN = 12;
+
+ /**
+ * The user has performed an up and left gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_UP_AND_LEFT = 13;
+
+ /**
+ * The user has performed an up and right gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_UP_AND_RIGHT = 14;
+
+ /**
+ * The user has performed an down and left gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_DOWN_AND_LEFT = 15;
+
+ /**
+ * The user has performed an down and right gesture on the touch screen.
+ */
+ public static final int GESTURE_SWIPE_DOWN_AND_RIGHT = 16;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.accessibilityservice.AccessibilityService";
+
+ /**
+ * Name under which an AccessibilityService component publishes information
+ * about itself. This meta-data must reference an XML resource containing an
+ * <code>&lt;{@link android.R.styleable#AccessibilityService accessibility-service}&gt;</code>
+ * tag. This is a a sample XML file configuring an accessibility service:
+ * <pre> &lt;accessibility-service
+ * android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
+ * android:packageNames="foo.bar, foo.baz"
+ * android:accessibilityFeedbackType="feedbackSpoken"
+ * android:notificationTimeout="100"
+ * android:accessibilityFlags="flagDefault"
+ * android:settingsActivity="foo.bar.TestBackActivity"
+ * android:canRetrieveWindowContent="true"
+ * android:canRequestTouchExplorationMode="true"
+ * . . .
+ * /&gt;</pre>
+ */
+ public static final String SERVICE_META_DATA = "android.accessibilityservice";
+
+ /**
+ * Action to go back.
+ */
+ public static final int GLOBAL_ACTION_BACK = 1;
+
+ /**
+ * Action to go home.
+ */
+ public static final int GLOBAL_ACTION_HOME = 2;
+
+ /**
+ * Action to toggle showing the overview of recent apps. Will fail on platforms that don't
+ * show recent apps.
+ */
+ public static final int GLOBAL_ACTION_RECENTS = 3;
+
+ /**
+ * Action to open the notifications.
+ */
+ public static final int GLOBAL_ACTION_NOTIFICATIONS = 4;
+
+ /**
+ * Action to open the quick settings.
+ */
+ public static final int GLOBAL_ACTION_QUICK_SETTINGS = 5;
+
+ /**
+ * Action to open the power long-press dialog.
+ */
+ public static final int GLOBAL_ACTION_POWER_DIALOG = 6;
+
+ /**
+ * Action to toggle docking the current app's window
+ */
+ public static final int GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN = 7;
+
+ private static final String LOG_TAG = "AccessibilityService";
+
+ /**
+ * Interface used by IAccessibilityServiceWrapper to call the service from its main thread.
+ * @hide
+ */
+ public interface Callbacks {
+ void onAccessibilityEvent(AccessibilityEvent event);
+ void onInterrupt();
+ void onServiceConnected();
+ void init(int connectionId, IBinder windowToken);
+ boolean onGesture(int gestureId);
+ boolean onKeyEvent(KeyEvent event);
+ void onMagnificationChanged(@NonNull Region region,
+ float scale, float centerX, float centerY);
+ void onSoftKeyboardShowModeChanged(int showMode);
+ void onPerformGestureResult(int sequence, boolean completedSuccessfully);
+ void onFingerprintCapturingGesturesChanged(boolean active);
+ void onFingerprintGesture(int gesture);
+ void onAccessibilityButtonClicked();
+ void onAccessibilityButtonAvailabilityChanged(boolean available);
+ }
+
+ /**
+ * Annotations for Soft Keyboard show modes so tools can catch invalid show modes.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SHOW_MODE_AUTO, SHOW_MODE_HIDDEN})
+ public @interface SoftKeyboardShowMode {};
+ public static final int SHOW_MODE_AUTO = 0;
+ public static final int SHOW_MODE_HIDDEN = 1;
+
+ private int mConnectionId = AccessibilityInteractionClient.NO_ID;
+
+ private AccessibilityServiceInfo mInfo;
+
+ private IBinder mWindowToken;
+
+ private WindowManager mWindowManager;
+
+ private MagnificationController mMagnificationController;
+ private SoftKeyboardController mSoftKeyboardController;
+ private AccessibilityButtonController mAccessibilityButtonController;
+
+ private int mGestureStatusCallbackSequence;
+
+ private SparseArray<GestureResultCallbackInfo> mGestureStatusCallbackInfos;
+
+ private final Object mLock = new Object();
+
+ private FingerprintGestureController mFingerprintGestureController;
+
+ /**
+ * Callback for {@link android.view.accessibility.AccessibilityEvent}s.
+ *
+ * @param event The new event. This event is owned by the caller and cannot be used after
+ * this method returns. Services wishing to use the event after this method returns should
+ * make a copy.
+ */
+ public abstract void onAccessibilityEvent(AccessibilityEvent event);
+
+ /**
+ * Callback for interrupting the accessibility feedback.
+ */
+ public abstract void onInterrupt();
+
+ /**
+ * Dispatches service connection to internal components first, then the
+ * client code.
+ */
+ private void dispatchServiceConnected() {
+ if (mMagnificationController != null) {
+ mMagnificationController.onServiceConnected();
+ }
+ if (mSoftKeyboardController != null) {
+ mSoftKeyboardController.onServiceConnected();
+ }
+
+ // The client gets to handle service connection last, after we've set
+ // up any state upon which their code may rely.
+ onServiceConnected();
+ }
+
+ /**
+ * This method is a part of the {@link AccessibilityService} lifecycle and is
+ * called after the system has successfully bound to the service. If is
+ * convenient to use this method for setting the {@link AccessibilityServiceInfo}.
+ *
+ * @see AccessibilityServiceInfo
+ * @see #setServiceInfo(AccessibilityServiceInfo)
+ */
+ protected void onServiceConnected() {
+
+ }
+
+ /**
+ * Called by the system when the user performs a specific gesture on the
+ * touch screen.
+ *
+ * <strong>Note:</strong> To receive gestures an accessibility service must
+ * request that the device is in touch exploration mode by setting the
+ * {@link android.accessibilityservice.AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE}
+ * flag.
+ *
+ * @param gestureId The unique id of the performed gesture.
+ *
+ * @return Whether the gesture was handled.
+ *
+ * @see #GESTURE_SWIPE_UP
+ * @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
+ */
+ protected boolean onGesture(int gestureId) {
+ return false;
+ }
+
+ /**
+ * Callback that allows an accessibility service to observe the key events
+ * before they are passed to the rest of the system. This means that the events
+ * are first delivered here before they are passed to the device policy, the
+ * input method, or applications.
+ * <p>
+ * <strong>Note:</strong> It is important that key events are handled in such
+ * a way that the event stream that would be passed to the rest of the system
+ * is well-formed. For example, handling the down event but not the up event
+ * and vice versa would generate an inconsistent event stream.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> The key events delivered in this method are copies
+ * and modifying them will have no effect on the events that will be passed
+ * to the system. This method is intended to perform purely filtering
+ * functionality.
+ * <p>
+ *
+ * @param event The event to be processed. This event is owned by the caller and cannot be used
+ * after this method returns. Services wishing to use the event after this method returns should
+ * make a copy.
+ * @return If true then the event will be consumed and not delivered to
+ * applications, otherwise it will be delivered as usual.
+ */
+ protected boolean onKeyEvent(KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Gets the windows on the screen. This method returns only the windows
+ * that a sighted user can interact with, as opposed to all windows.
+ * For example, if there is a modal dialog shown and the user cannot touch
+ * anything behind it, then only the modal window will be reported
+ * (assuming it is the top one). For convenience the returned windows
+ * are ordered in a descending layer order, which is the windows that
+ * are higher in the Z-order are reported first. Since the user can always
+ * interact with the window that has input focus by typing, the focused
+ * window is always returned (even if covered by a modal window).
+ * <p>
+ * <strong>Note:</strong> In order to access the windows your service has
+ * to declare the capability to retrieve window content by setting the
+ * {@link android.R.styleable#AccessibilityService_canRetrieveWindowContent}
+ * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}.
+ * Also the service has to opt-in to retrieve the interactive windows by
+ * setting the {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS}
+ * flag.
+ * </p>
+ *
+ * @return The windows if there are windows and the service is can retrieve
+ * them, otherwise an empty list.
+ */
+ public List<AccessibilityWindowInfo> getWindows() {
+ return AccessibilityInteractionClient.getInstance().getWindows(mConnectionId);
+ }
+
+ /**
+ * Gets the root node in the currently active window if this service
+ * can retrieve window content. The active window is the one that the user
+ * is currently touching or the window with input focus, if the user is not
+ * touching any window.
+ * <p>
+ * The currently active window is defined as the window that most recently fired one
+ * of the following events:
+ * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED},
+ * {@link AccessibilityEvent#TYPE_VIEW_HOVER_ENTER},
+ * {@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT}.
+ * In other words, the last window shown that also has input focus.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> In order to access the root node your service has
+ * to declare the capability to retrieve window content by setting the
+ * {@link android.R.styleable#AccessibilityService_canRetrieveWindowContent}
+ * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}.
+ * </p>
+ *
+ * @return The root node if this service can retrieve window content.
+ */
+ public AccessibilityNodeInfo getRootInActiveWindow() {
+ return AccessibilityInteractionClient.getInstance().getRootInActiveWindow(mConnectionId);
+ }
+
+ /**
+ * Disables the service. After calling this method, the service will be disabled and settings
+ * will show that it is turned off.
+ */
+ public final void disableSelf() {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId);
+ if (connection != null) {
+ try {
+ connection.disableSelf();
+ } catch (RemoteException re) {
+ throw new RuntimeException(re);
+ }
+ }
+ }
+
+ /**
+ * Returns the magnification controller, which may be used to query and
+ * modify the state of display magnification.
+ * <p>
+ * <strong>Note:</strong> In order to control magnification, your service
+ * must declare the capability by setting the
+ * {@link android.R.styleable#AccessibilityService_canControlMagnification}
+ * property in its meta-data. For more information, see
+ * {@link #SERVICE_META_DATA}.
+ *
+ * @return the magnification controller
+ */
+ @NonNull
+ public final MagnificationController getMagnificationController() {
+ synchronized (mLock) {
+ if (mMagnificationController == null) {
+ mMagnificationController = new MagnificationController(this, mLock);
+ }
+ return mMagnificationController;
+ }
+ }
+
+ /**
+ * Get the controller for fingerprint gestures. This feature requires {@link
+ * AccessibilityServiceInfo#CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES}.
+ *
+ *<strong>Note: </strong> The service must be connected before this method is called.
+ *
+ * @return The controller for fingerprint gestures, or {@code null} if gestures are unavailable.
+ */
+ @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT)
+ public final @NonNull FingerprintGestureController getFingerprintGestureController() {
+ if (mFingerprintGestureController == null) {
+ mFingerprintGestureController = new FingerprintGestureController(
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId));
+ }
+ return mFingerprintGestureController;
+ }
+
+ /**
+ * Dispatch a gesture to the touch screen. Any gestures currently in progress, whether from
+ * the user, this service, or another service, will be cancelled.
+ * <p>
+ * The gesture will be dispatched as if it were performed directly on the screen by a user, so
+ * the events may be affected by features such as magnification and explore by touch.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> In order to dispatch gestures, your service
+ * must declare the capability by setting the
+ * {@link android.R.styleable#AccessibilityService_canPerformGestures}
+ * property in its meta-data. For more information, see
+ * {@link #SERVICE_META_DATA}.
+ * </p>
+ *
+ * @param gesture The gesture to dispatch
+ * @param callback The object to call back when the status of the gesture is known. If
+ * {@code null}, no status is reported.
+ * @param handler The handler on which to call back the {@code callback} object. If
+ * {@code null}, the object is called back on the service's main thread.
+ *
+ * @return {@code true} if the gesture is dispatched, {@code false} if not.
+ */
+ public final boolean dispatchGesture(@NonNull GestureDescription gesture,
+ @Nullable GestureResultCallback callback,
+ @Nullable Handler handler) {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mConnectionId);
+ if (connection == null) {
+ return false;
+ }
+ List<GestureDescription.GestureStep> steps =
+ MotionEventGenerator.getGestureStepsFromGestureDescription(gesture, 100);
+ try {
+ synchronized (mLock) {
+ mGestureStatusCallbackSequence++;
+ if (callback != null) {
+ if (mGestureStatusCallbackInfos == null) {
+ mGestureStatusCallbackInfos = new SparseArray<>();
+ }
+ GestureResultCallbackInfo callbackInfo = new GestureResultCallbackInfo(gesture,
+ callback, handler);
+ mGestureStatusCallbackInfos.put(mGestureStatusCallbackSequence, callbackInfo);
+ }
+ connection.sendGesture(mGestureStatusCallbackSequence,
+ new ParceledListSlice<>(steps));
+ }
+ } catch (RemoteException re) {
+ throw new RuntimeException(re);
+ }
+ return true;
+ }
+
+ void onPerformGestureResult(int sequence, final boolean completedSuccessfully) {
+ if (mGestureStatusCallbackInfos == null) {
+ return;
+ }
+ GestureResultCallbackInfo callbackInfo;
+ synchronized (mLock) {
+ callbackInfo = mGestureStatusCallbackInfos.get(sequence);
+ }
+ final GestureResultCallbackInfo finalCallbackInfo = callbackInfo;
+ if ((callbackInfo != null) && (callbackInfo.gestureDescription != null)
+ && (callbackInfo.callback != null)) {
+ if (callbackInfo.handler != null) {
+ callbackInfo.handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (completedSuccessfully) {
+ finalCallbackInfo.callback
+ .onCompleted(finalCallbackInfo.gestureDescription);
+ } else {
+ finalCallbackInfo.callback
+ .onCancelled(finalCallbackInfo.gestureDescription);
+ }
+ }
+ });
+ return;
+ }
+ if (completedSuccessfully) {
+ callbackInfo.callback.onCompleted(callbackInfo.gestureDescription);
+ } else {
+ callbackInfo.callback.onCancelled(callbackInfo.gestureDescription);
+ }
+ }
+ }
+
+ private void onMagnificationChanged(@NonNull Region region, float scale,
+ float centerX, float centerY) {
+ if (mMagnificationController != null) {
+ mMagnificationController.dispatchMagnificationChanged(
+ region, scale, centerX, centerY);
+ }
+ }
+
+ /**
+ * Callback for fingerprint gesture handling
+ * @param active If gesture detection is active
+ */
+ private void onFingerprintCapturingGesturesChanged(boolean active) {
+ getFingerprintGestureController().onGestureDetectionActiveChanged(active);
+ }
+
+ /**
+ * Callback for fingerprint gesture handling
+ * @param gesture The identifier for the gesture performed
+ */
+ private void onFingerprintGesture(int gesture) {
+ getFingerprintGestureController().onGesture(gesture);
+ }
+
+ /**
+ * Used to control and query the state of display magnification.
+ */
+ public static final class MagnificationController {
+ private final AccessibilityService mService;
+
+ /**
+ * Map of listeners to their handlers. Lazily created when adding the
+ * first magnification listener.
+ */
+ private ArrayMap<OnMagnificationChangedListener, Handler> mListeners;
+ private final Object mLock;
+
+ MagnificationController(@NonNull AccessibilityService service, @NonNull Object lock) {
+ mService = service;
+ mLock = lock;
+ }
+
+ /**
+ * Called when the service is connected.
+ */
+ void onServiceConnected() {
+ synchronized (mLock) {
+ if (mListeners != null && !mListeners.isEmpty()) {
+ setMagnificationCallbackEnabled(true);
+ }
+ }
+ }
+
+ /**
+ * Adds the specified change listener to the list of magnification
+ * change listeners. The callback will occur on the service's main
+ * thread.
+ *
+ * @param listener the listener to add, must be non-{@code null}
+ */
+ public void addListener(@NonNull OnMagnificationChangedListener listener) {
+ addListener(listener, null);
+ }
+
+ /**
+ * Adds the specified change listener to the list of magnification
+ * change listeners. The callback will occur on the specified
+ * {@link Handler}'s thread, or on the service's main thread if the
+ * handler is {@code null}.
+ *
+ * @param listener the listener to add, must be non-null
+ * @param handler the handler on which the callback should execute, or
+ * {@code null} to execute on the service's main thread
+ */
+ public void addListener(@NonNull OnMagnificationChangedListener listener,
+ @Nullable Handler handler) {
+ synchronized (mLock) {
+ if (mListeners == null) {
+ mListeners = new ArrayMap<>();
+ }
+
+ final boolean shouldEnableCallback = mListeners.isEmpty();
+ mListeners.put(listener, handler);
+
+ if (shouldEnableCallback) {
+ // This may fail if the service is not connected yet, but if we
+ // still have listeners when it connects then we can try again.
+ setMagnificationCallbackEnabled(true);
+ }
+ }
+ }
+
+ /**
+ * Removes the specified change listener from the list of magnification change listeners.
+ *
+ * @param listener the listener to remove, must be non-null
+ * @return {@code true} if the listener was removed, {@code false} otherwise
+ */
+ public boolean removeListener(@NonNull OnMagnificationChangedListener listener) {
+ if (mListeners == null) {
+ return false;
+ }
+
+ synchronized (mLock) {
+ final int keyIndex = mListeners.indexOfKey(listener);
+ final boolean hasKey = keyIndex >= 0;
+ if (hasKey) {
+ mListeners.removeAt(keyIndex);
+ }
+
+ if (hasKey && mListeners.isEmpty()) {
+ // We just removed the last listener, so we don't need
+ // callbacks from the service anymore.
+ setMagnificationCallbackEnabled(false);
+ }
+
+ return hasKey;
+ }
+ }
+
+ private void setMagnificationCallbackEnabled(boolean enabled) {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ connection.setMagnificationCallbackEnabled(enabled);
+ } catch (RemoteException re) {
+ throw new RuntimeException(re);
+ }
+ }
+ }
+
+ /**
+ * Dispatches magnification changes to any registered listeners. This
+ * should be called on the service's main thread.
+ */
+ void dispatchMagnificationChanged(final @NonNull Region region, final float scale,
+ final float centerX, final float centerY) {
+ final ArrayMap<OnMagnificationChangedListener, Handler> entries;
+ synchronized (mLock) {
+ if (mListeners == null || mListeners.isEmpty()) {
+ Slog.d(LOG_TAG, "Received magnification changed "
+ + "callback with no listeners registered!");
+ setMagnificationCallbackEnabled(false);
+ return;
+ }
+
+ // Listeners may remove themselves. Perform a shallow copy to avoid concurrent
+ // modification.
+ entries = new ArrayMap<>(mListeners);
+ }
+
+ for (int i = 0, count = entries.size(); i < count; i++) {
+ final OnMagnificationChangedListener listener = entries.keyAt(i);
+ final Handler handler = entries.valueAt(i);
+ if (handler != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onMagnificationChanged(MagnificationController.this,
+ region, scale, centerX, centerY);
+ }
+ });
+ } else {
+ // We're already on the main thread, just run the listener.
+ listener.onMagnificationChanged(this, region, scale, centerX, centerY);
+ }
+ }
+ }
+
+ /**
+ * Returns the current magnification scale.
+ * <p>
+ * <strong>Note:</strong> If the service is not yet connected (e.g.
+ * {@link AccessibilityService#onServiceConnected()} has not yet been
+ * called) or the service has been disconnected, this method will
+ * return a default value of {@code 1.0f}.
+ *
+ * @return the current magnification scale
+ */
+ public float getScale() {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.getMagnificationScale();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to obtain scale", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return 1.0f;
+ }
+
+ /**
+ * Returns the unscaled screen-relative X coordinate of the focal
+ * center of the magnified region. This is the point around which
+ * zooming occurs and is guaranteed to lie within the magnified
+ * region.
+ * <p>
+ * <strong>Note:</strong> If the service is not yet connected (e.g.
+ * {@link AccessibilityService#onServiceConnected()} has not yet been
+ * called) or the service has been disconnected, this method will
+ * return a default value of {@code 0.0f}.
+ *
+ * @return the unscaled screen-relative X coordinate of the center of
+ * the magnified region
+ */
+ public float getCenterX() {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.getMagnificationCenterX();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to obtain center X", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return 0.0f;
+ }
+
+ /**
+ * Returns the unscaled screen-relative Y coordinate of the focal
+ * center of the magnified region. This is the point around which
+ * zooming occurs and is guaranteed to lie within the magnified
+ * region.
+ * <p>
+ * <strong>Note:</strong> If the service is not yet connected (e.g.
+ * {@link AccessibilityService#onServiceConnected()} has not yet been
+ * called) or the service has been disconnected, this method will
+ * return a default value of {@code 0.0f}.
+ *
+ * @return the unscaled screen-relative Y coordinate of the center of
+ * the magnified region
+ */
+ public float getCenterY() {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.getMagnificationCenterY();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to obtain center Y", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return 0.0f;
+ }
+
+ /**
+ * Returns the region of the screen currently active for magnification. Changes to
+ * magnification scale and center only affect this portion of the screen. The rest of the
+ * screen, for example input methods, cannot be magnified. This region is relative to the
+ * unscaled screen and is independent of the scale and center point.
+ * <p>
+ * The returned region will be empty if magnification is not active. Magnification is active
+ * if magnification gestures are enabled or if a service is running that can control
+ * magnification.
+ * <p>
+ * <strong>Note:</strong> If the service is not yet connected (e.g.
+ * {@link AccessibilityService#onServiceConnected()} has not yet been
+ * called) or the service has been disconnected, this method will
+ * return an empty region.
+ *
+ * @return the region of the screen currently active for magnification, or an empty region
+ * if magnification is not active.
+ */
+ @NonNull
+ public Region getMagnificationRegion() {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.getMagnificationRegion();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to obtain magnified region", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return Region.obtain();
+ }
+
+ /**
+ * Resets magnification scale and center to their default (e.g. no
+ * magnification) values.
+ * <p>
+ * <strong>Note:</strong> If the service is not yet connected (e.g.
+ * {@link AccessibilityService#onServiceConnected()} has not yet been
+ * called) or the service has been disconnected, this method will have
+ * no effect and return {@code false}.
+ *
+ * @param animate {@code true} to animate from the current scale and
+ * center or {@code false} to reset the scale and center
+ * immediately
+ * @return {@code true} on success, {@code false} on failure
+ */
+ public boolean reset(boolean animate) {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.resetMagnification(animate);
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to reset", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the magnification scale.
+ * <p>
+ * <strong>Note:</strong> If the service is not yet connected (e.g.
+ * {@link AccessibilityService#onServiceConnected()} has not yet been
+ * called) or the service has been disconnected, this method will have
+ * no effect and return {@code false}.
+ *
+ * @param scale the magnification scale to set, must be >= 1 and <= 5
+ * @param animate {@code true} to animate from the current scale or
+ * {@code false} to set the scale immediately
+ * @return {@code true} on success, {@code false} on failure
+ */
+ public boolean setScale(float scale, boolean animate) {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.setMagnificationScaleAndCenter(
+ scale, Float.NaN, Float.NaN, animate);
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to set scale", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the center of the magnified viewport.
+ * <p>
+ * <strong>Note:</strong> If the service is not yet connected (e.g.
+ * {@link AccessibilityService#onServiceConnected()} has not yet been
+ * called) or the service has been disconnected, this method will have
+ * no effect and return {@code false}.
+ *
+ * @param centerX the unscaled screen-relative X coordinate on which to
+ * center the viewport
+ * @param centerY the unscaled screen-relative Y coordinate on which to
+ * center the viewport
+ * @param animate {@code true} to animate from the current viewport
+ * center or {@code false} to set the center immediately
+ * @return {@code true} on success, {@code false} on failure
+ */
+ public boolean setCenter(float centerX, float centerY, boolean animate) {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.setMagnificationScaleAndCenter(
+ Float.NaN, centerX, centerY, animate);
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to set center", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Listener for changes in the state of magnification.
+ */
+ public interface OnMagnificationChangedListener {
+ /**
+ * Called when the magnified region, scale, or center changes.
+ *
+ * @param controller the magnification controller
+ * @param region the magnification region
+ * @param scale the new scale
+ * @param centerX the new X coordinate, in unscaled coordinates, around which
+ * magnification is focused
+ * @param centerY the new Y coordinate, in unscaled coordinates, around which
+ * magnification is focused
+ */
+ void onMagnificationChanged(@NonNull MagnificationController controller,
+ @NonNull Region region, float scale, float centerX, float centerY);
+ }
+ }
+
+ /**
+ * Returns the soft keyboard controller, which may be used to query and modify the soft keyboard
+ * show mode.
+ *
+ * @return the soft keyboard controller
+ */
+ @NonNull
+ public final SoftKeyboardController getSoftKeyboardController() {
+ synchronized (mLock) {
+ if (mSoftKeyboardController == null) {
+ mSoftKeyboardController = new SoftKeyboardController(this, mLock);
+ }
+ return mSoftKeyboardController;
+ }
+ }
+
+ private void onSoftKeyboardShowModeChanged(int showMode) {
+ if (mSoftKeyboardController != null) {
+ mSoftKeyboardController.dispatchSoftKeyboardShowModeChanged(showMode);
+ }
+ }
+
+ /**
+ * Used to control and query the soft keyboard show mode.
+ */
+ public static final class SoftKeyboardController {
+ private final AccessibilityService mService;
+
+ /**
+ * Map of listeners to their handlers. Lazily created when adding the first
+ * soft keyboard change listener.
+ */
+ private ArrayMap<OnShowModeChangedListener, Handler> mListeners;
+ private final Object mLock;
+
+ SoftKeyboardController(@NonNull AccessibilityService service, @NonNull Object lock) {
+ mService = service;
+ mLock = lock;
+ }
+
+ /**
+ * Called when the service is connected.
+ */
+ void onServiceConnected() {
+ synchronized(mLock) {
+ if (mListeners != null && !mListeners.isEmpty()) {
+ setSoftKeyboardCallbackEnabled(true);
+ }
+ }
+ }
+
+ /**
+ * Adds the specified change listener to the list of show mode change listeners. The
+ * callback will occur on the service's main thread. Listener is not called on registration.
+ */
+ public void addOnShowModeChangedListener(@NonNull OnShowModeChangedListener listener) {
+ addOnShowModeChangedListener(listener, null);
+ }
+
+ /**
+ * Adds the specified change listener to the list of soft keyboard show mode change
+ * listeners. The callback will occur on the specified {@link Handler}'s thread, or on the
+ * services's main thread if the handler is {@code null}.
+ *
+ * @param listener the listener to add, must be non-null
+ * @param handler the handler on which to callback should execute, or {@code null} to
+ * execute on the service's main thread
+ */
+ public void addOnShowModeChangedListener(@NonNull OnShowModeChangedListener listener,
+ @Nullable Handler handler) {
+ synchronized (mLock) {
+ if (mListeners == null) {
+ mListeners = new ArrayMap<>();
+ }
+
+ final boolean shouldEnableCallback = mListeners.isEmpty();
+ mListeners.put(listener, handler);
+
+ if (shouldEnableCallback) {
+ // This may fail if the service is not connected yet, but if we still have
+ // listeners when it connects, we can try again.
+ setSoftKeyboardCallbackEnabled(true);
+ }
+ }
+ }
+
+ /**
+ * Removes the specified change listener from the list of keyboard show mode change
+ * listeners.
+ *
+ * @param listener the listener to remove, must be non-null
+ * @return {@code true} if the listener was removed, {@code false} otherwise
+ */
+ public boolean removeOnShowModeChangedListener(@NonNull OnShowModeChangedListener listener) {
+ if (mListeners == null) {
+ return false;
+ }
+
+ synchronized (mLock) {
+ final int keyIndex = mListeners.indexOfKey(listener);
+ final boolean hasKey = keyIndex >= 0;
+ if (hasKey) {
+ mListeners.removeAt(keyIndex);
+ }
+
+ if (hasKey && mListeners.isEmpty()) {
+ // We just removed the last listener, so we don't need callbacks from the
+ // service anymore.
+ setSoftKeyboardCallbackEnabled(false);
+ }
+
+ return hasKey;
+ }
+ }
+
+ private void setSoftKeyboardCallbackEnabled(boolean enabled) {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ connection.setSoftKeyboardCallbackEnabled(enabled);
+ } catch (RemoteException re) {
+ throw new RuntimeException(re);
+ }
+ }
+ }
+
+ /**
+ * Dispatches the soft keyboard show mode change to any registered listeners. This should
+ * be called on the service's main thread.
+ */
+ void dispatchSoftKeyboardShowModeChanged(final int showMode) {
+ final ArrayMap<OnShowModeChangedListener, Handler> entries;
+ synchronized (mLock) {
+ if (mListeners == null || mListeners.isEmpty()) {
+ Slog.w(LOG_TAG, "Received soft keyboard show mode changed callback"
+ + " with no listeners registered!");
+ setSoftKeyboardCallbackEnabled(false);
+ return;
+ }
+
+ // Listeners may remove themselves. Perform a shallow copy to avoid concurrent
+ // modification.
+ entries = new ArrayMap<>(mListeners);
+ }
+
+ for (int i = 0, count = entries.size(); i < count; i++) {
+ final OnShowModeChangedListener listener = entries.keyAt(i);
+ final Handler handler = entries.valueAt(i);
+ if (handler != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onShowModeChanged(SoftKeyboardController.this, showMode);
+ }
+ });
+ } else {
+ // We're already on the main thread, just run the listener.
+ listener.onShowModeChanged(this, showMode);
+ }
+ }
+ }
+
+ /**
+ * Returns the show mode of the soft keyboard. The default show mode is
+ * {@code SHOW_MODE_AUTO}, where the soft keyboard is shown when a text input field is
+ * focused. An AccessibilityService can also request the show mode
+ * {@code SHOW_MODE_HIDDEN}, where the soft keyboard is never shown.
+ *
+ * @return the current soft keyboard show mode
+ */
+ @SoftKeyboardShowMode
+ public int getShowMode() {
+ try {
+ return Settings.Secure.getInt(mService.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE);
+ } catch (Settings.SettingNotFoundException e) {
+ Log.v(LOG_TAG, "Failed to obtain the soft keyboard mode", e);
+ // The settings hasn't been changed yet, so it's value is null. Return the default.
+ return 0;
+ }
+ }
+
+ /**
+ * Sets the soft keyboard show mode. The default show mode is
+ * {@code SHOW_MODE_AUTO}, where the soft keyboard is shown when a text input field is
+ * focused. An AccessibilityService can also request the show mode
+ * {@code SHOW_MODE_HIDDEN}, where the soft keyboard is never shown. The
+ * The lastto this method will be honored, regardless of any previous calls (including those
+ * made by other AccessibilityServices).
+ * <p>
+ * <strong>Note:</strong> If the service is not yet connected (e.g.
+ * {@link AccessibilityService#onServiceConnected()} has not yet been called) or the
+ * service has been disconnected, this method will have no effect and return {@code false}.
+ *
+ * @param showMode the new show mode for the soft keyboard
+ * @return {@code true} on success
+ */
+ public boolean setShowMode(@SoftKeyboardShowMode int showMode) {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.setSoftKeyboardShowMode(showMode);
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to set soft keyboard behavior", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Listener for changes in the soft keyboard show mode.
+ */
+ public interface OnShowModeChangedListener {
+ /**
+ * Called when the soft keyboard behavior changes. The default show mode is
+ * {@code SHOW_MODE_AUTO}, where the soft keyboard is shown when a text input field is
+ * focused. An AccessibilityService can also request the show mode
+ * {@code SHOW_MODE_HIDDEN}, where the soft keyboard is never shown.
+ *
+ * @param controller the soft keyboard controller
+ * @param showMode the current soft keyboard show mode
+ */
+ void onShowModeChanged(@NonNull SoftKeyboardController controller,
+ @SoftKeyboardShowMode int showMode);
+ }
+ }
+
+ /**
+ * Returns the controller for the accessibility button within the system's navigation area.
+ * This instance may be used to query the accessibility button's state and register listeners
+ * for interactions with and state changes for the accessibility button when
+ * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} is set.
+ * <p>
+ * <strong>Note:</strong> Not all devices are capable of displaying the accessibility button
+ * within a navigation area, and as such, use of this class should be considered only as an
+ * optional feature or shortcut on supported device implementations.
+ * </p>
+ *
+ * @return the accessibility button controller for this {@link AccessibilityService}
+ */
+ @NonNull
+ public final AccessibilityButtonController getAccessibilityButtonController() {
+ synchronized (mLock) {
+ if (mAccessibilityButtonController == null) {
+ mAccessibilityButtonController = new AccessibilityButtonController(
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId));
+ }
+ return mAccessibilityButtonController;
+ }
+ }
+
+ private void onAccessibilityButtonClicked() {
+ getAccessibilityButtonController().dispatchAccessibilityButtonClicked();
+ }
+
+ private void onAccessibilityButtonAvailabilityChanged(boolean available) {
+ getAccessibilityButtonController().dispatchAccessibilityButtonAvailabilityChanged(
+ available);
+ }
+
+ /**
+ * Performs a global action. Such an action can be performed
+ * at any moment regardless of the current application or user
+ * location in that application. For example going back, going
+ * home, opening recents, etc.
+ *
+ * @param action The action to perform.
+ * @return Whether the action was successfully performed.
+ *
+ * @see #GLOBAL_ACTION_BACK
+ * @see #GLOBAL_ACTION_HOME
+ * @see #GLOBAL_ACTION_NOTIFICATIONS
+ * @see #GLOBAL_ACTION_RECENTS
+ */
+ public final boolean performGlobalAction(int action) {
+ IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.performGlobalAction(action);
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while calling performGlobalAction", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Find the view that has the specified focus type. The search is performed
+ * across all windows.
+ * <p>
+ * <strong>Note:</strong> In order to access the windows your service has
+ * to declare the capability to retrieve window content by setting the
+ * {@link android.R.styleable#AccessibilityService_canRetrieveWindowContent}
+ * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}.
+ * Also the service has to opt-in to retrieve the interactive windows by
+ * setting the {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS}
+ * flag. Otherwise, the search will be performed only in the active window.
+ * </p>
+ *
+ * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or
+ * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}.
+ * @return The node info of the focused view or null.
+ *
+ * @see AccessibilityNodeInfo#FOCUS_INPUT
+ * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY
+ */
+ public AccessibilityNodeInfo findFocus(int focus) {
+ return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId,
+ AccessibilityWindowInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, focus);
+ }
+
+ /**
+ * Gets the an {@link AccessibilityServiceInfo} describing this
+ * {@link AccessibilityService}. This method is useful if one wants
+ * to change some of the dynamically configurable properties at
+ * runtime.
+ *
+ * @return The accessibility service info.
+ *
+ * @see AccessibilityServiceInfo
+ */
+ public final AccessibilityServiceInfo getServiceInfo() {
+ IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.getServiceInfo();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the {@link AccessibilityServiceInfo} that describes this service.
+ * <p>
+ * Note: You can call this method any time but the info will be picked up after
+ * the system has bound to this service and when this method is called thereafter.
+ *
+ * @param info The info.
+ */
+ public final void setServiceInfo(AccessibilityServiceInfo info) {
+ mInfo = info;
+ sendServiceInfo();
+ }
+
+ /**
+ * Sets the {@link AccessibilityServiceInfo} for this service if the latter is
+ * properly set and there is an {@link IAccessibilityServiceConnection} to the
+ * AccessibilityManagerService.
+ */
+ private void sendServiceInfo() {
+ IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId);
+ if (mInfo != null && connection != null) {
+ try {
+ connection.setServiceInfo(mInfo);
+ mInfo = null;
+ AccessibilityInteractionClient.getInstance().clearCache();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
+ public Object getSystemService(@ServiceName @NonNull String name) {
+ if (getBaseContext() == null) {
+ throw new IllegalStateException(
+ "System services not available to Activities before onCreate()");
+ }
+
+ // Guarantee that we always return the same window manager instance.
+ if (WINDOW_SERVICE.equals(name)) {
+ if (mWindowManager == null) {
+ mWindowManager = (WindowManager) getBaseContext().getSystemService(name);
+ }
+ return mWindowManager;
+ }
+ return super.getSystemService(name);
+ }
+
+ /**
+ * Implement to return the implementation of the internal accessibility
+ * service interface.
+ */
+ @Override
+ public final IBinder onBind(Intent intent) {
+ return new IAccessibilityServiceClientWrapper(this, getMainLooper(), new Callbacks() {
+ @Override
+ public void onServiceConnected() {
+ AccessibilityService.this.dispatchServiceConnected();
+ }
+
+ @Override
+ public void onInterrupt() {
+ AccessibilityService.this.onInterrupt();
+ }
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ AccessibilityService.this.onAccessibilityEvent(event);
+ }
+
+ @Override
+ public void init(int connectionId, IBinder windowToken) {
+ mConnectionId = connectionId;
+ mWindowToken = windowToken;
+
+ // The client may have already obtained the window manager, so
+ // update the default token on whatever manager we gave them.
+ final WindowManagerImpl wm = (WindowManagerImpl) getSystemService(WINDOW_SERVICE);
+ wm.setDefaultToken(windowToken);
+ }
+
+ @Override
+ public boolean onGesture(int gestureId) {
+ return AccessibilityService.this.onGesture(gestureId);
+ }
+
+ @Override
+ public boolean onKeyEvent(KeyEvent event) {
+ return AccessibilityService.this.onKeyEvent(event);
+ }
+
+ @Override
+ public void onMagnificationChanged(@NonNull Region region,
+ float scale, float centerX, float centerY) {
+ AccessibilityService.this.onMagnificationChanged(region, scale, centerX, centerY);
+ }
+
+ @Override
+ public void onSoftKeyboardShowModeChanged(int showMode) {
+ AccessibilityService.this.onSoftKeyboardShowModeChanged(showMode);
+ }
+
+ @Override
+ public void onPerformGestureResult(int sequence, boolean completedSuccessfully) {
+ AccessibilityService.this.onPerformGestureResult(sequence, completedSuccessfully);
+ }
+
+ @Override
+ public void onFingerprintCapturingGesturesChanged(boolean active) {
+ AccessibilityService.this.onFingerprintCapturingGesturesChanged(active);
+ }
+
+ @Override
+ public void onFingerprintGesture(int gesture) {
+ AccessibilityService.this.onFingerprintGesture(gesture);
+ }
+
+ @Override
+ public void onAccessibilityButtonClicked() {
+ AccessibilityService.this.onAccessibilityButtonClicked();
+ }
+
+ @Override
+ public void onAccessibilityButtonAvailabilityChanged(boolean available) {
+ AccessibilityService.this.onAccessibilityButtonAvailabilityChanged(available);
+ }
+ });
+ }
+
+ /**
+ * Implements the internal {@link IAccessibilityServiceClient} interface to convert
+ * incoming calls to it back to calls on an {@link AccessibilityService}.
+ *
+ * @hide
+ */
+ public static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub
+ implements HandlerCaller.Callback {
+ private static final int DO_INIT = 1;
+ private static final int DO_ON_INTERRUPT = 2;
+ private static final int DO_ON_ACCESSIBILITY_EVENT = 3;
+ private static final int DO_ON_GESTURE = 4;
+ private static final int DO_CLEAR_ACCESSIBILITY_CACHE = 5;
+ private static final int DO_ON_KEY_EVENT = 6;
+ private static final int DO_ON_MAGNIFICATION_CHANGED = 7;
+ private static final int DO_ON_SOFT_KEYBOARD_SHOW_MODE_CHANGED = 8;
+ private static final int DO_GESTURE_COMPLETE = 9;
+ private static final int DO_ON_FINGERPRINT_ACTIVE_CHANGED = 10;
+ private static final int DO_ON_FINGERPRINT_GESTURE = 11;
+ private static final int DO_ACCESSIBILITY_BUTTON_CLICKED = 12;
+ private static final int DO_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED = 13;
+
+ private final HandlerCaller mCaller;
+
+ private final Callbacks mCallback;
+
+ private int mConnectionId = AccessibilityInteractionClient.NO_ID;
+
+ public IAccessibilityServiceClientWrapper(Context context, Looper looper,
+ Callbacks callback) {
+ mCallback = callback;
+ mCaller = new HandlerCaller(context, looper, this, true /*asyncHandler*/);
+ }
+
+ public void init(IAccessibilityServiceConnection connection, int connectionId,
+ IBinder windowToken) {
+ Message message = mCaller.obtainMessageIOO(DO_INIT, connectionId,
+ connection, windowToken);
+ mCaller.sendMessage(message);
+ }
+
+ public void onInterrupt() {
+ Message message = mCaller.obtainMessage(DO_ON_INTERRUPT);
+ mCaller.sendMessage(message);
+ }
+
+ public void onAccessibilityEvent(AccessibilityEvent event, boolean serviceWantsEvent) {
+ Message message = mCaller.obtainMessageBO(
+ DO_ON_ACCESSIBILITY_EVENT, serviceWantsEvent, event);
+ mCaller.sendMessage(message);
+ }
+
+ public void onGesture(int gestureId) {
+ Message message = mCaller.obtainMessageI(DO_ON_GESTURE, gestureId);
+ mCaller.sendMessage(message);
+ }
+
+ public void clearAccessibilityCache() {
+ Message message = mCaller.obtainMessage(DO_CLEAR_ACCESSIBILITY_CACHE);
+ mCaller.sendMessage(message);
+ }
+
+ @Override
+ public void onKeyEvent(KeyEvent event, int sequence) {
+ Message message = mCaller.obtainMessageIO(DO_ON_KEY_EVENT, sequence, event);
+ mCaller.sendMessage(message);
+ }
+
+ public void onMagnificationChanged(@NonNull Region region,
+ float scale, float centerX, float centerY) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = region;
+ args.arg2 = scale;
+ args.arg3 = centerX;
+ args.arg4 = centerY;
+
+ final Message message = mCaller.obtainMessageO(DO_ON_MAGNIFICATION_CHANGED, args);
+ mCaller.sendMessage(message);
+ }
+
+ public void onSoftKeyboardShowModeChanged(int showMode) {
+ final Message message =
+ mCaller.obtainMessageI(DO_ON_SOFT_KEYBOARD_SHOW_MODE_CHANGED, showMode);
+ mCaller.sendMessage(message);
+ }
+
+ public void onPerformGestureResult(int sequence, boolean successfully) {
+ Message message = mCaller.obtainMessageII(DO_GESTURE_COMPLETE, sequence,
+ successfully ? 1 : 0);
+ mCaller.sendMessage(message);
+ }
+
+ public void onFingerprintCapturingGesturesChanged(boolean active) {
+ mCaller.sendMessage(mCaller.obtainMessageI(
+ DO_ON_FINGERPRINT_ACTIVE_CHANGED, active ? 1 : 0));
+ }
+
+ public void onFingerprintGesture(int gesture) {
+ mCaller.sendMessage(mCaller.obtainMessageI(DO_ON_FINGERPRINT_GESTURE, gesture));
+ }
+
+ public void onAccessibilityButtonClicked() {
+ final Message message = mCaller.obtainMessage(DO_ACCESSIBILITY_BUTTON_CLICKED);
+ mCaller.sendMessage(message);
+ }
+
+ public void onAccessibilityButtonAvailabilityChanged(boolean available) {
+ final Message message = mCaller.obtainMessageI(
+ DO_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED, (available ? 1 : 0));
+ mCaller.sendMessage(message);
+ }
+
+ @Override
+ public void executeMessage(Message message) {
+ switch (message.what) {
+ case DO_ON_ACCESSIBILITY_EVENT: {
+ AccessibilityEvent event = (AccessibilityEvent) message.obj;
+ boolean serviceWantsEvent = message.arg1 != 0;
+ if (event != null) {
+ // Send the event to AccessibilityCache via AccessibilityInteractionClient
+ AccessibilityInteractionClient.getInstance().onAccessibilityEvent(event);
+ if (serviceWantsEvent
+ && (mConnectionId != AccessibilityInteractionClient.NO_ID)) {
+ // Send the event to AccessibilityService
+ mCallback.onAccessibilityEvent(event);
+ }
+ // Make sure the event is recycled.
+ try {
+ event.recycle();
+ } catch (IllegalStateException ise) {
+ /* ignore - best effort */
+ }
+ }
+ } return;
+
+ case DO_ON_INTERRUPT: {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ mCallback.onInterrupt();
+ }
+ } return;
+
+ case DO_INIT: {
+ mConnectionId = message.arg1;
+ SomeArgs args = (SomeArgs) message.obj;
+ IAccessibilityServiceConnection connection =
+ (IAccessibilityServiceConnection) args.arg1;
+ IBinder windowToken = (IBinder) args.arg2;
+ args.recycle();
+ if (connection != null) {
+ AccessibilityInteractionClient.getInstance().addConnection(mConnectionId,
+ connection);
+ mCallback.init(mConnectionId, windowToken);
+ mCallback.onServiceConnected();
+ } else {
+ AccessibilityInteractionClient.getInstance().removeConnection(
+ mConnectionId);
+ mConnectionId = AccessibilityInteractionClient.NO_ID;
+ AccessibilityInteractionClient.getInstance().clearCache();
+ mCallback.init(AccessibilityInteractionClient.NO_ID, null);
+ }
+ } return;
+
+ case DO_ON_GESTURE: {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ final int gestureId = message.arg1;
+ mCallback.onGesture(gestureId);
+ }
+ } return;
+
+ case DO_CLEAR_ACCESSIBILITY_CACHE: {
+ AccessibilityInteractionClient.getInstance().clearCache();
+ } return;
+
+ case DO_ON_KEY_EVENT: {
+ KeyEvent event = (KeyEvent) message.obj;
+ try {
+ IAccessibilityServiceConnection connection = AccessibilityInteractionClient
+ .getInstance().getConnection(mConnectionId);
+ if (connection != null) {
+ final boolean result = mCallback.onKeyEvent(event);
+ final int sequence = message.arg1;
+ try {
+ connection.setOnKeyEventResult(result, sequence);
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ }
+ } finally {
+ // Make sure the event is recycled.
+ try {
+ event.recycle();
+ } catch (IllegalStateException ise) {
+ /* ignore - best effort */
+ }
+ }
+ } return;
+
+ case DO_ON_MAGNIFICATION_CHANGED: {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ final SomeArgs args = (SomeArgs) message.obj;
+ final Region region = (Region) args.arg1;
+ final float scale = (float) args.arg2;
+ final float centerX = (float) args.arg3;
+ final float centerY = (float) args.arg4;
+ mCallback.onMagnificationChanged(region, scale, centerX, centerY);
+ }
+ } return;
+
+ case DO_ON_SOFT_KEYBOARD_SHOW_MODE_CHANGED: {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ final int showMode = (int) message.arg1;
+ mCallback.onSoftKeyboardShowModeChanged(showMode);
+ }
+ } return;
+
+ case DO_GESTURE_COMPLETE: {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ final boolean successfully = message.arg2 == 1;
+ mCallback.onPerformGestureResult(message.arg1, successfully);
+ }
+ } return;
+ case DO_ON_FINGERPRINT_ACTIVE_CHANGED: {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ mCallback.onFingerprintCapturingGesturesChanged(message.arg1 == 1);
+ }
+ } return;
+ case DO_ON_FINGERPRINT_GESTURE: {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ mCallback.onFingerprintGesture(message.arg1);
+ }
+ } return;
+
+ case (DO_ACCESSIBILITY_BUTTON_CLICKED): {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ mCallback.onAccessibilityButtonClicked();
+ }
+ } return;
+
+ case (DO_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED): {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ final boolean available = (message.arg1 != 0);
+ mCallback.onAccessibilityButtonAvailabilityChanged(available);
+ }
+ } return;
+
+ default :
+ Log.w(LOG_TAG, "Unknown message type " + message.what);
+ }
+ }
+ }
+
+ /**
+ * Class used to report status of dispatched gestures
+ */
+ public static abstract class GestureResultCallback {
+ /** Called when the gesture has completed successfully
+ *
+ * @param gestureDescription The description of the gesture that completed.
+ */
+ public void onCompleted(GestureDescription gestureDescription) {
+ }
+
+ /** Called when the gesture was cancelled
+ *
+ * @param gestureDescription The description of the gesture that was cancelled.
+ */
+ public void onCancelled(GestureDescription gestureDescription) {
+ }
+ }
+
+ /* Object to keep track of gesture result callbacks */
+ private static class GestureResultCallbackInfo {
+ GestureDescription gestureDescription;
+ GestureResultCallback callback;
+ Handler handler;
+
+ GestureResultCallbackInfo(GestureDescription gestureDescription,
+ GestureResultCallback callback, Handler handler) {
+ this.gestureDescription = gestureDescription;
+ this.callback = callback;
+ this.handler = handler;
+ }
+ }
+}
diff --git a/android/accessibilityservice/AccessibilityServiceInfo.java b/android/accessibilityservice/AccessibilityServiceInfo.java
new file mode 100644
index 00000000..06a9b067
--- /dev/null
+++ b/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -0,0 +1,1111 @@
+/*
+ * Copyright (C) 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 android.accessibilityservice;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static android.content.pm.PackageManager.FEATURE_FINGERPRINT;
+
+/**
+ * This class describes an {@link AccessibilityService}. The system notifies an
+ * {@link AccessibilityService} for {@link android.view.accessibility.AccessibilityEvent}s
+ * according to the information encapsulated in this class.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about creating AccessibilityServices, read the
+ * <a href="{@docRoot}guide/topics/ui/accessibility/index.html">Accessibility</a>
+ * developer guide.</p>
+ * </div>
+ *
+ * @attr ref android.R.styleable#AccessibilityService_accessibilityEventTypes
+ * @attr ref android.R.styleable#AccessibilityService_accessibilityFeedbackType
+ * @attr ref android.R.styleable#AccessibilityService_accessibilityFlags
+ * @attr ref android.R.styleable#AccessibilityService_canRequestEnhancedWebAccessibility
+ * @attr ref android.R.styleable#AccessibilityService_canRequestFilterKeyEvents
+ * @attr ref android.R.styleable#AccessibilityService_canRequestTouchExplorationMode
+ * @attr ref android.R.styleable#AccessibilityService_canRetrieveWindowContent
+ * @attr ref android.R.styleable#AccessibilityService_description
+ * @attr ref android.R.styleable#AccessibilityService_summary
+ * @attr ref android.R.styleable#AccessibilityService_notificationTimeout
+ * @attr ref android.R.styleable#AccessibilityService_packageNames
+ * @attr ref android.R.styleable#AccessibilityService_settingsActivity
+ * @see AccessibilityService
+ * @see android.view.accessibility.AccessibilityEvent
+ * @see android.view.accessibility.AccessibilityManager
+ */
+public class AccessibilityServiceInfo implements Parcelable {
+
+ private static final String TAG_ACCESSIBILITY_SERVICE = "accessibility-service";
+
+ /**
+ * Capability: This accessibility service can retrieve the active window content.
+ * @see android.R.styleable#AccessibilityService_canRetrieveWindowContent
+ */
+ public static final int CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT = 0x00000001;
+
+ /**
+ * Capability: This accessibility service can request touch exploration mode in which
+ * touched items are spoken aloud and the UI can be explored via gestures.
+ * @see android.R.styleable#AccessibilityService_canRequestTouchExplorationMode
+ */
+ public static final int CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = 0x00000002;
+
+ /**
+ * @deprecated No longer used
+ */
+ public static final int CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 0x00000004;
+
+ /**
+ * Capability: This accessibility service can request to filter the key event stream.
+ * @see android.R.styleable#AccessibilityService_canRequestFilterKeyEvents
+ */
+ public static final int CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS = 0x00000008;
+
+ /**
+ * Capability: This accessibility service can control display magnification.
+ * @see android.R.styleable#AccessibilityService_canControlMagnification
+ */
+ public static final int CAPABILITY_CAN_CONTROL_MAGNIFICATION = 0x00000010;
+
+ /**
+ * Capability: This accessibility service can perform gestures.
+ * @see android.R.styleable#AccessibilityService_canPerformGestures
+ */
+ public static final int CAPABILITY_CAN_PERFORM_GESTURES = 0x00000020;
+
+ /**
+ * Capability: This accessibility service can capture gestures from the fingerprint sensor
+ * @see android.R.styleable#AccessibilityService_canRequestFingerprintGestures
+ */
+ public static final int CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES = 0x00000040;
+
+ private static SparseArray<CapabilityInfo> sAvailableCapabilityInfos;
+
+ /**
+ * Denotes spoken feedback.
+ */
+ public static final int FEEDBACK_SPOKEN = 0x0000001;
+
+ /**
+ * Denotes haptic feedback.
+ */
+ public static final int FEEDBACK_HAPTIC = 0x0000002;
+
+ /**
+ * Denotes audible (not spoken) feedback.
+ */
+ public static final int FEEDBACK_AUDIBLE = 0x0000004;
+
+ /**
+ * Denotes visual feedback.
+ */
+ public static final int FEEDBACK_VISUAL = 0x0000008;
+
+ /**
+ * Denotes generic feedback.
+ */
+ public static final int FEEDBACK_GENERIC = 0x0000010;
+
+ /**
+ * Denotes braille feedback.
+ */
+ public static final int FEEDBACK_BRAILLE = 0x0000020;
+
+ /**
+ * Mask for all feedback types.
+ *
+ * @see #FEEDBACK_SPOKEN
+ * @see #FEEDBACK_HAPTIC
+ * @see #FEEDBACK_AUDIBLE
+ * @see #FEEDBACK_VISUAL
+ * @see #FEEDBACK_GENERIC
+ * @see #FEEDBACK_BRAILLE
+ */
+ public static final int FEEDBACK_ALL_MASK = 0xFFFFFFFF;
+
+ /**
+ * If an {@link AccessibilityService} is the default for a given type.
+ * Default service is invoked only if no package specific one exists. In case of
+ * more than one package specific service only the earlier registered is notified.
+ */
+ public static final int DEFAULT = 0x0000001;
+
+ /**
+ * If this flag is set the system will regard views that are not important
+ * for accessibility in addition to the ones that are important for accessibility.
+ * That is, views that are marked as not important for accessibility via
+ * {@link View#IMPORTANT_FOR_ACCESSIBILITY_NO} or
+ * {@link View#IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS} and views that are
+ * marked as potentially important for accessibility via
+ * {@link View#IMPORTANT_FOR_ACCESSIBILITY_AUTO} for which the system has determined
+ * that are not important for accessibility, are reported while querying the window
+ * content and also the accessibility service will receive accessibility events from
+ * them.
+ * <p>
+ * <strong>Note:</strong> For accessibility services targeting API version
+ * {@link Build.VERSION_CODES#JELLY_BEAN} or higher this flag has to be explicitly
+ * set for the system to regard views that are not important for accessibility. For
+ * accessibility services targeting API version lower than
+ * {@link Build.VERSION_CODES#JELLY_BEAN} this flag is ignored and all views are
+ * regarded for accessibility purposes.
+ * </p>
+ * <p>
+ * Usually views not important for accessibility are layout managers that do not
+ * react to user actions, do not draw any content, and do not have any special
+ * semantics in the context of the screen content. For example, a three by three
+ * grid can be implemented as three horizontal linear layouts and one vertical,
+ * or three vertical linear layouts and one horizontal, or one grid layout, etc.
+ * In this context the actual layout mangers used to achieve the grid configuration
+ * are not important, rather it is important that there are nine evenly distributed
+ * elements.
+ * </p>
+ */
+ public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x0000002;
+
+ /**
+ * This flag requests that the system gets into touch exploration mode.
+ * In this mode a single finger moving on the screen behaves as a mouse
+ * pointer hovering over the user interface. The system will also detect
+ * certain gestures performed on the touch screen and notify this service.
+ * The system will enable touch exploration mode if there is at least one
+ * accessibility service that has this flag set. Hence, clearing this
+ * flag does not guarantee that the device will not be in touch exploration
+ * mode since there may be another enabled service that requested it.
+ * <p>
+ * For accessibility services targeting API version higher than
+ * {@link Build.VERSION_CODES#JELLY_BEAN_MR1} that want to set
+ * this flag have to declare this capability in their meta-data by setting
+ * the attribute {@link android.R.attr#canRequestTouchExplorationMode
+ * canRequestTouchExplorationMode} to true, otherwise this flag will
+ * be ignored. For how to declare the meta-data of a service refer to
+ * {@value AccessibilityService#SERVICE_META_DATA}.
+ * </p>
+ * <p>
+ * Services targeting API version equal to or lower than
+ * {@link Build.VERSION_CODES#JELLY_BEAN_MR1} will work normally, i.e.
+ * the first time they are run, if this flag is specified, a dialog is
+ * shown to the user to confirm enabling explore by touch.
+ * </p>
+ * @see android.R.styleable#AccessibilityService_canRequestTouchExplorationMode
+ */
+ public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 0x0000004;
+
+ /**
+ * @deprecated No longer used
+ */
+ public static final int FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 0x00000008;
+
+ /**
+ * This flag requests that the {@link AccessibilityNodeInfo}s obtained
+ * by an {@link AccessibilityService} contain the id of the source view.
+ * The source view id will be a fully qualified resource name of the
+ * form "package:id/name", for example "foo.bar:id/my_list", and it is
+ * useful for UI test automation. This flag is not set by default.
+ */
+ public static final int FLAG_REPORT_VIEW_IDS = 0x00000010;
+
+ /**
+ * This flag requests from the system to filter key events. If this flag
+ * is set the accessibility service will receive the key events before
+ * applications allowing it implement global shortcuts.
+ * <p>
+ * Services that want to set this flag have to declare this capability
+ * in their meta-data by setting the attribute {@link android.R.attr
+ * #canRequestFilterKeyEvents canRequestFilterKeyEvents} to true,
+ * otherwise this flag will be ignored. For how to declare the meta-data
+ * of a service refer to {@value AccessibilityService#SERVICE_META_DATA}.
+ * </p>
+ * @see android.R.styleable#AccessibilityService_canRequestFilterKeyEvents
+ */
+ public static final int FLAG_REQUEST_FILTER_KEY_EVENTS = 0x00000020;
+
+ /**
+ * This flag indicates to the system that the accessibility service wants
+ * to access content of all interactive windows. An interactive window is a
+ * window that has input focus or can be touched by a sighted user when explore
+ * by touch is not enabled. If this flag is not set your service will not receive
+ * {@link android.view.accessibility.AccessibilityEvent#TYPE_WINDOWS_CHANGED}
+ * events, calling AccessibilityService{@link AccessibilityService#getWindows()
+ * AccessibilityService.getWindows()} will return an empty list, and {@link
+ * AccessibilityNodeInfo#getWindow() AccessibilityNodeInfo.getWindow()} will
+ * return null.
+ * <p>
+ * Services that want to set this flag have to declare the capability
+ * to retrieve window content in their meta-data by setting the attribute
+ * {@link android.R.attr#canRetrieveWindowContent canRetrieveWindowContent} to
+ * true, otherwise this flag will be ignored. For how to declare the meta-data
+ * of a service refer to {@value AccessibilityService#SERVICE_META_DATA}.
+ * </p>
+ * @see android.R.styleable#AccessibilityService_canRetrieveWindowContent
+ */
+ public static final int FLAG_RETRIEVE_INTERACTIVE_WINDOWS = 0x00000040;
+
+ /**
+ * This flag requests that all audio tracks system-wide with
+ * {@link android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY} be controlled by the
+ * {@link android.media.AudioManager#STREAM_ACCESSIBILITY} volume.
+ */
+ public static final int FLAG_ENABLE_ACCESSIBILITY_VOLUME = 0x00000080;
+
+ /**
+ * This flag indicates to the system that the accessibility service requests that an
+ * accessibility button be shown within the system's navigation area, if available.
+ */
+ public static final int FLAG_REQUEST_ACCESSIBILITY_BUTTON = 0x00000100;
+
+ /**
+ * This flag requests that all fingerprint gestures be sent to the accessibility service.
+ * It is handled in {@link FingerprintGestureController}
+ */
+ public static final int FLAG_REQUEST_FINGERPRINT_GESTURES = 0x00000200;
+
+ /** {@hide} */
+ public static final int FLAG_FORCE_DIRECT_BOOT_AWARE = 0x00010000;
+
+ /**
+ * The event types an {@link AccessibilityService} is interested in.
+ * <p>
+ * <strong>Can be dynamically set at runtime.</strong>
+ * </p>
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_CLICKED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_LONG_CLICKED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_FOCUSED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_SELECTED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_START
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_HOVER_ENTER
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_HOVER_EXIT
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_SCROLLED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_TEXT_SELECTION_CHANGED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_TOUCH_INTERACTION_START
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_TOUCH_INTERACTION_END
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_ANNOUNCEMENT
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_GESTURE_DETECTION_START
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_GESTURE_DETECTION_END
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUSED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY
+ * @see android.view.accessibility.AccessibilityEvent#TYPE_WINDOWS_CHANGED
+ */
+ public int eventTypes;
+
+ /**
+ * The package names an {@link AccessibilityService} is interested in. Setting
+ * to <code>null</code> is equivalent to all packages.
+ * <p>
+ * <strong>Can be dynamically set at runtime.</strong>
+ * </p>
+ */
+ public String[] packageNames;
+
+ /**
+ * The feedback type an {@link AccessibilityService} provides.
+ * <p>
+ * <strong>Can be dynamically set at runtime.</strong>
+ * </p>
+ * @see #FEEDBACK_AUDIBLE
+ * @see #FEEDBACK_GENERIC
+ * @see #FEEDBACK_HAPTIC
+ * @see #FEEDBACK_SPOKEN
+ * @see #FEEDBACK_VISUAL
+ * @see #FEEDBACK_BRAILLE
+ */
+ public int feedbackType;
+
+ /**
+ * The timeout after the most recent event of a given type before an
+ * {@link AccessibilityService} is notified.
+ * <p>
+ * <strong>Can be dynamically set at runtime.</strong>.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> The event notification timeout is useful to avoid propagating
+ * events to the client too frequently since this is accomplished via an expensive
+ * interprocess call. One can think of the timeout as a criteria to determine when
+ * event generation has settled down.
+ */
+ public long notificationTimeout;
+
+ /**
+ * This field represents a set of flags used for configuring an
+ * {@link AccessibilityService}.
+ * <p>
+ * <strong>Can be dynamically set at runtime.</strong>
+ * </p>
+ * @see #DEFAULT
+ * @see #FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ * @see #FLAG_REQUEST_TOUCH_EXPLORATION_MODE
+ * @see #FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY
+ * @see #FLAG_REQUEST_FILTER_KEY_EVENTS
+ * @see #FLAG_REPORT_VIEW_IDS
+ * @see #FLAG_RETRIEVE_INTERACTIVE_WINDOWS
+ * @see #FLAG_ENABLE_ACCESSIBILITY_VOLUME
+ * @see #FLAG_REQUEST_ACCESSIBILITY_BUTTON
+ */
+ public int flags;
+
+ /**
+ * The component name the accessibility service.
+ */
+ private ComponentName mComponentName;
+
+ /**
+ * The Service that implements this accessibility service component.
+ */
+ private ResolveInfo mResolveInfo;
+
+ /**
+ * The accessibility service setting activity's name, used by the system
+ * settings to launch the setting activity of this accessibility service.
+ */
+ private String mSettingsActivityName;
+
+ /**
+ * Bit mask with capabilities of this service.
+ */
+ private int mCapabilities;
+
+ /**
+ * Resource id of the summary of the accessibility service.
+ */
+ private int mSummaryResId;
+
+ /**
+ * Non-localized summary of the accessibility service.
+ */
+ private String mNonLocalizedSummary;
+
+ /**
+ * Resource id of the description of the accessibility service.
+ */
+ private int mDescriptionResId;
+
+ /**
+ * Non localized description of the accessibility service.
+ */
+ private String mNonLocalizedDescription;
+
+ /**
+ * Creates a new instance.
+ */
+ public AccessibilityServiceInfo() {
+ /* do nothing */
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param resolveInfo The service resolve info.
+ * @param context Context for accessing resources.
+ * @throws XmlPullParserException If a XML parsing error occurs.
+ * @throws IOException If a XML parsing error occurs.
+ *
+ * @hide
+ */
+ public AccessibilityServiceInfo(ResolveInfo resolveInfo, Context context)
+ throws XmlPullParserException, IOException {
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ mComponentName = new ComponentName(serviceInfo.packageName, serviceInfo.name);
+ mResolveInfo = resolveInfo;
+
+ XmlResourceParser parser = null;
+
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ parser = serviceInfo.loadXmlMetaData(packageManager,
+ AccessibilityService.SERVICE_META_DATA);
+ if (parser == null) {
+ return;
+ }
+
+ int type = 0;
+ while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+ type = parser.next();
+ }
+
+ String nodeName = parser.getName();
+ if (!TAG_ACCESSIBILITY_SERVICE.equals(nodeName)) {
+ throw new XmlPullParserException( "Meta-data does not start with"
+ + TAG_ACCESSIBILITY_SERVICE + " tag");
+ }
+
+ AttributeSet allAttributes = Xml.asAttributeSet(parser);
+ Resources resources = packageManager.getResourcesForApplication(
+ serviceInfo.applicationInfo);
+ TypedArray asAttributes = resources.obtainAttributes(allAttributes,
+ com.android.internal.R.styleable.AccessibilityService);
+ eventTypes = asAttributes.getInt(
+ com.android.internal.R.styleable.AccessibilityService_accessibilityEventTypes,
+ 0);
+ String packageNamez = asAttributes.getString(
+ com.android.internal.R.styleable.AccessibilityService_packageNames);
+ if (packageNamez != null) {
+ packageNames = packageNamez.split("(\\s)*,(\\s)*");
+ }
+ feedbackType = asAttributes.getInt(
+ com.android.internal.R.styleable.AccessibilityService_accessibilityFeedbackType,
+ 0);
+ notificationTimeout = asAttributes.getInt(
+ com.android.internal.R.styleable.AccessibilityService_notificationTimeout,
+ 0);
+ flags = asAttributes.getInt(
+ com.android.internal.R.styleable.AccessibilityService_accessibilityFlags, 0);
+ mSettingsActivityName = asAttributes.getString(
+ com.android.internal.R.styleable.AccessibilityService_settingsActivity);
+ if (asAttributes.getBoolean(com.android.internal.R.styleable
+ .AccessibilityService_canRetrieveWindowContent, false)) {
+ mCapabilities |= CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT;
+ }
+ if (asAttributes.getBoolean(com.android.internal.R.styleable
+ .AccessibilityService_canRequestTouchExplorationMode, false)) {
+ mCapabilities |= CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION;
+ }
+ if (asAttributes.getBoolean(com.android.internal.R.styleable
+ .AccessibilityService_canRequestFilterKeyEvents, false)) {
+ mCapabilities |= CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS;
+ }
+ if (asAttributes.getBoolean(com.android.internal.R.styleable
+ .AccessibilityService_canControlMagnification, false)) {
+ mCapabilities |= CAPABILITY_CAN_CONTROL_MAGNIFICATION;
+ }
+ if (asAttributes.getBoolean(com.android.internal.R.styleable
+ .AccessibilityService_canPerformGestures, false)) {
+ mCapabilities |= CAPABILITY_CAN_PERFORM_GESTURES;
+ }
+ if (asAttributes.getBoolean(com.android.internal.R.styleable
+ .AccessibilityService_canRequestFingerprintGestures, false)) {
+ mCapabilities |= CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES;
+ }
+ TypedValue peekedValue = asAttributes.peekValue(
+ com.android.internal.R.styleable.AccessibilityService_description);
+ if (peekedValue != null) {
+ mDescriptionResId = peekedValue.resourceId;
+ CharSequence nonLocalizedDescription = peekedValue.coerceToString();
+ if (nonLocalizedDescription != null) {
+ mNonLocalizedDescription = nonLocalizedDescription.toString().trim();
+ }
+ }
+ peekedValue = asAttributes.peekValue(
+ com.android.internal.R.styleable.AccessibilityService_summary);
+ if (peekedValue != null) {
+ mSummaryResId = peekedValue.resourceId;
+ CharSequence nonLocalizedSummary = peekedValue.coerceToString();
+ if (nonLocalizedSummary != null) {
+ mNonLocalizedSummary = nonLocalizedSummary.toString().trim();
+ }
+ }
+ asAttributes.recycle();
+ } catch (NameNotFoundException e) {
+ throw new XmlPullParserException( "Unable to create context for: "
+ + serviceInfo.packageName);
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+ }
+
+ /**
+ * Updates the properties that an AccessibilitySerivice can change dynamically.
+ *
+ * @param other The info from which to update the properties.
+ *
+ * @hide
+ */
+ public void updateDynamicallyConfigurableProperties(AccessibilityServiceInfo other) {
+ eventTypes = other.eventTypes;
+ packageNames = other.packageNames;
+ feedbackType = other.feedbackType;
+ notificationTimeout = other.notificationTimeout;
+ flags = other.flags;
+ }
+
+ /**
+ * @hide
+ */
+ public void setComponentName(ComponentName component) {
+ mComponentName = component;
+ }
+
+ /**
+ * @hide
+ */
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ /**
+ * The accessibility service id.
+ * <p>
+ * <strong>Generated by the system.</strong>
+ * </p>
+ * @return The id.
+ */
+ public String getId() {
+ return mComponentName.flattenToShortString();
+ }
+
+ /**
+ * The service {@link ResolveInfo}.
+ * <p>
+ * <strong>Generated by the system.</strong>
+ * </p>
+ * @return The info.
+ */
+ public ResolveInfo getResolveInfo() {
+ return mResolveInfo;
+ }
+
+ /**
+ * The settings activity name.
+ * <p>
+ * <strong>Statically set from
+ * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong>
+ * </p>
+ * @return The settings activity name.
+ */
+ public String getSettingsActivityName() {
+ return mSettingsActivityName;
+ }
+
+ /**
+ * Whether this service can retrieve the current window's content.
+ * <p>
+ * <strong>Statically set from
+ * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong>
+ * </p>
+ * @return True if window content can be retrieved.
+ *
+ * @deprecated Use {@link #getCapabilities()}.
+ */
+ public boolean getCanRetrieveWindowContent() {
+ return (mCapabilities & CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT) != 0;
+ }
+
+ /**
+ * Returns the bit mask of capabilities this accessibility service has such as
+ * being able to retrieve the active window content, etc.
+ *
+ * @return The capability bit mask.
+ *
+ * @see #CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT
+ * @see #CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION
+ * @see #CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS
+ * @see #CAPABILITY_CAN_CONTROL_MAGNIFICATION
+ * @see #CAPABILITY_CAN_PERFORM_GESTURES
+ */
+ public int getCapabilities() {
+ return mCapabilities;
+ }
+
+ /**
+ * Sets the bit mask of capabilities this accessibility service has such as
+ * being able to retrieve the active window content, etc.
+ *
+ * @param capabilities The capability bit mask.
+ *
+ * @see #CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT
+ * @see #CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION
+ * @see #CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS
+ * @see #CAPABILITY_CAN_CONTROL_MAGNIFICATION
+ * @see #CAPABILITY_CAN_PERFORM_GESTURES
+ *
+ * @hide
+ */
+ public void setCapabilities(int capabilities) {
+ mCapabilities = capabilities;
+ }
+
+ /**
+ * The localized summary of the accessibility service.
+ * <p>
+ * <strong>Statically set from
+ * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong>
+ * </p>
+ * @return The localized summary if available, and {@code null} if a summary
+ * has not been provided.
+ */
+ public CharSequence loadSummary(PackageManager packageManager) {
+ if (mSummaryResId == 0) {
+ return mNonLocalizedSummary;
+ }
+ ServiceInfo serviceInfo = mResolveInfo.serviceInfo;
+ CharSequence summary = packageManager.getText(serviceInfo.packageName,
+ mSummaryResId, serviceInfo.applicationInfo);
+ if (summary != null) {
+ return summary.toString().trim();
+ }
+ return null;
+ }
+
+ /**
+ * Gets the non-localized description of the accessibility service.
+ * <p>
+ * <strong>Statically set from
+ * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong>
+ * </p>
+ * @return The description.
+ *
+ * @deprecated Use {@link #loadDescription(PackageManager)}.
+ */
+ public String getDescription() {
+ return mNonLocalizedDescription;
+ }
+
+ /**
+ * The localized description of the accessibility service.
+ * <p>
+ * <strong>Statically set from
+ * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong>
+ * </p>
+ * @return The localized description.
+ */
+ public String loadDescription(PackageManager packageManager) {
+ if (mDescriptionResId == 0) {
+ return mNonLocalizedDescription;
+ }
+ ServiceInfo serviceInfo = mResolveInfo.serviceInfo;
+ CharSequence description = packageManager.getText(serviceInfo.packageName,
+ mDescriptionResId, serviceInfo.applicationInfo);
+ if (description != null) {
+ return description.toString().trim();
+ }
+ return null;
+ }
+
+ /** {@hide} */
+ public boolean isDirectBootAware() {
+ return ((flags & FLAG_FORCE_DIRECT_BOOT_AWARE) != 0)
+ || mResolveInfo.serviceInfo.directBootAware;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flagz) {
+ parcel.writeInt(eventTypes);
+ parcel.writeStringArray(packageNames);
+ parcel.writeInt(feedbackType);
+ parcel.writeLong(notificationTimeout);
+ parcel.writeInt(flags);
+ parcel.writeParcelable(mComponentName, flagz);
+ parcel.writeParcelable(mResolveInfo, 0);
+ parcel.writeString(mSettingsActivityName);
+ parcel.writeInt(mCapabilities);
+ parcel.writeInt(mSummaryResId);
+ parcel.writeString(mNonLocalizedSummary);
+ parcel.writeInt(mDescriptionResId);
+ parcel.writeString(mNonLocalizedDescription);
+ }
+
+ private void initFromParcel(Parcel parcel) {
+ eventTypes = parcel.readInt();
+ packageNames = parcel.readStringArray();
+ feedbackType = parcel.readInt();
+ notificationTimeout = parcel.readLong();
+ flags = parcel.readInt();
+ mComponentName = parcel.readParcelable(this.getClass().getClassLoader());
+ mResolveInfo = parcel.readParcelable(null);
+ mSettingsActivityName = parcel.readString();
+ mCapabilities = parcel.readInt();
+ mSummaryResId = parcel.readInt();
+ mNonLocalizedSummary = parcel.readString();
+ mDescriptionResId = parcel.readInt();
+ mNonLocalizedDescription = parcel.readString();
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * 1 + ((mComponentName == null) ? 0 : mComponentName.hashCode());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ AccessibilityServiceInfo other = (AccessibilityServiceInfo) obj;
+ if (mComponentName == null) {
+ if (other.mComponentName != null) {
+ return false;
+ }
+ } else if (!mComponentName.equals(other.mComponentName)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ appendEventTypes(stringBuilder, eventTypes);
+ stringBuilder.append(", ");
+ appendPackageNames(stringBuilder, packageNames);
+ stringBuilder.append(", ");
+ appendFeedbackTypes(stringBuilder, feedbackType);
+ stringBuilder.append(", ");
+ stringBuilder.append("notificationTimeout: ").append(notificationTimeout);
+ stringBuilder.append(", ");
+ appendFlags(stringBuilder, flags);
+ stringBuilder.append(", ");
+ stringBuilder.append("id: ").append(getId());
+ stringBuilder.append(", ");
+ stringBuilder.append("resolveInfo: ").append(mResolveInfo);
+ stringBuilder.append(", ");
+ stringBuilder.append("settingsActivityName: ").append(mSettingsActivityName);
+ stringBuilder.append(", ");
+ stringBuilder.append("summary: ").append(mNonLocalizedSummary);
+ stringBuilder.append(", ");
+ appendCapabilities(stringBuilder, mCapabilities);
+ return stringBuilder.toString();
+ }
+
+ private static void appendFeedbackTypes(StringBuilder stringBuilder, int feedbackTypes) {
+ stringBuilder.append("feedbackTypes:");
+ stringBuilder.append("[");
+ while (feedbackTypes != 0) {
+ final int feedbackTypeBit = (1 << Integer.numberOfTrailingZeros(feedbackTypes));
+ stringBuilder.append(feedbackTypeToString(feedbackTypeBit));
+ feedbackTypes &= ~feedbackTypeBit;
+ if (feedbackTypes != 0) {
+ stringBuilder.append(", ");
+ }
+ }
+ stringBuilder.append("]");
+ }
+
+ private static void appendPackageNames(StringBuilder stringBuilder, String[] packageNames) {
+ stringBuilder.append("packageNames:");
+ stringBuilder.append("[");
+ if (packageNames != null) {
+ final int packageNameCount = packageNames.length;
+ for (int i = 0; i < packageNameCount; i++) {
+ stringBuilder.append(packageNames[i]);
+ if (i < packageNameCount - 1) {
+ stringBuilder.append(", ");
+ }
+ }
+ }
+ stringBuilder.append("]");
+ }
+
+ private static void appendEventTypes(StringBuilder stringBuilder, int eventTypes) {
+ stringBuilder.append("eventTypes:");
+ stringBuilder.append("[");
+ while (eventTypes != 0) {
+ final int eventTypeBit = (1 << Integer.numberOfTrailingZeros(eventTypes));
+ stringBuilder.append(AccessibilityEvent.eventTypeToString(eventTypeBit));
+ eventTypes &= ~eventTypeBit;
+ if (eventTypes != 0) {
+ stringBuilder.append(", ");
+ }
+ }
+ stringBuilder.append("]");
+ }
+
+ private static void appendFlags(StringBuilder stringBuilder, int flags) {
+ stringBuilder.append("flags:");
+ stringBuilder.append("[");
+ while (flags != 0) {
+ final int flagBit = (1 << Integer.numberOfTrailingZeros(flags));
+ stringBuilder.append(flagToString(flagBit));
+ flags &= ~flagBit;
+ if (flags != 0) {
+ stringBuilder.append(", ");
+ }
+ }
+ stringBuilder.append("]");
+ }
+
+ private static void appendCapabilities(StringBuilder stringBuilder, int capabilities) {
+ stringBuilder.append("capabilities:");
+ stringBuilder.append("[");
+ while (capabilities != 0) {
+ final int capabilityBit = (1 << Integer.numberOfTrailingZeros(capabilities));
+ stringBuilder.append(capabilityToString(capabilityBit));
+ capabilities &= ~capabilityBit;
+ if (capabilities != 0) {
+ stringBuilder.append(", ");
+ }
+ }
+ stringBuilder.append("]");
+ }
+
+ /**
+ * Returns the string representation of a feedback type. For example,
+ * {@link #FEEDBACK_SPOKEN} is represented by the string FEEDBACK_SPOKEN.
+ *
+ * @param feedbackType The feedback type.
+ * @return The string representation.
+ */
+ public static String feedbackTypeToString(int feedbackType) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[");
+ while (feedbackType != 0) {
+ final int feedbackTypeFlag = 1 << Integer.numberOfTrailingZeros(feedbackType);
+ feedbackType &= ~feedbackTypeFlag;
+ switch (feedbackTypeFlag) {
+ case FEEDBACK_AUDIBLE:
+ if (builder.length() > 1) {
+ builder.append(", ");
+ }
+ builder.append("FEEDBACK_AUDIBLE");
+ break;
+ case FEEDBACK_HAPTIC:
+ if (builder.length() > 1) {
+ builder.append(", ");
+ }
+ builder.append("FEEDBACK_HAPTIC");
+ break;
+ case FEEDBACK_GENERIC:
+ if (builder.length() > 1) {
+ builder.append(", ");
+ }
+ builder.append("FEEDBACK_GENERIC");
+ break;
+ case FEEDBACK_SPOKEN:
+ if (builder.length() > 1) {
+ builder.append(", ");
+ }
+ builder.append("FEEDBACK_SPOKEN");
+ break;
+ case FEEDBACK_VISUAL:
+ if (builder.length() > 1) {
+ builder.append(", ");
+ }
+ builder.append("FEEDBACK_VISUAL");
+ break;
+ case FEEDBACK_BRAILLE:
+ if (builder.length() > 1) {
+ builder.append(", ");
+ }
+ builder.append("FEEDBACK_BRAILLE");
+ break;
+ }
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+
+ /**
+ * Returns the string representation of a flag. For example,
+ * {@link #DEFAULT} is represented by the string DEFAULT.
+ *
+ * @param flag The flag.
+ * @return The string representation.
+ */
+ public static String flagToString(int flag) {
+ switch (flag) {
+ case DEFAULT:
+ return "DEFAULT";
+ case FLAG_INCLUDE_NOT_IMPORTANT_VIEWS:
+ return "FLAG_INCLUDE_NOT_IMPORTANT_VIEWS";
+ case FLAG_REQUEST_TOUCH_EXPLORATION_MODE:
+ return "FLAG_REQUEST_TOUCH_EXPLORATION_MODE";
+ case FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY:
+ return "FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY";
+ case FLAG_REPORT_VIEW_IDS:
+ return "FLAG_REPORT_VIEW_IDS";
+ case FLAG_REQUEST_FILTER_KEY_EVENTS:
+ return "FLAG_REQUEST_FILTER_KEY_EVENTS";
+ case FLAG_RETRIEVE_INTERACTIVE_WINDOWS:
+ return "FLAG_RETRIEVE_INTERACTIVE_WINDOWS";
+ case FLAG_ENABLE_ACCESSIBILITY_VOLUME:
+ return "FLAG_ENABLE_ACCESSIBILITY_VOLUME";
+ case FLAG_REQUEST_ACCESSIBILITY_BUTTON:
+ return "FLAG_REQUEST_ACCESSIBILITY_BUTTON";
+ case FLAG_REQUEST_FINGERPRINT_GESTURES:
+ return "FLAG_REQUEST_FINGERPRINT_GESTURES";
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Returns the string representation of a capability. For example,
+ * {@link #CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT} is represented
+ * by the string CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT.
+ *
+ * @param capability The capability.
+ * @return The string representation.
+ */
+ public static String capabilityToString(int capability) {
+ switch (capability) {
+ case CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT:
+ return "CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT";
+ case CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION:
+ return "CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION";
+ case CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY:
+ return "CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY";
+ case CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS:
+ return "CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS";
+ case CAPABILITY_CAN_CONTROL_MAGNIFICATION:
+ return "CAPABILITY_CAN_CONTROL_MAGNIFICATION";
+ case CAPABILITY_CAN_PERFORM_GESTURES:
+ return "CAPABILITY_CAN_PERFORM_GESTURES";
+ case CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES:
+ return "CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ /**
+ * @hide
+ * @return The list of {@link CapabilityInfo} objects.
+ * @deprecated The version that takes a context works better.
+ */
+ public List<CapabilityInfo> getCapabilityInfos() {
+ return getCapabilityInfos(null);
+ }
+
+ /**
+ * @hide
+ * @param context A valid context
+ * @return The list of {@link CapabilityInfo} objects.
+ */
+ public List<CapabilityInfo> getCapabilityInfos(Context context) {
+ if (mCapabilities == 0) {
+ return Collections.emptyList();
+ }
+ int capabilities = mCapabilities;
+ List<CapabilityInfo> capabilityInfos = new ArrayList<CapabilityInfo>();
+ SparseArray<CapabilityInfo> capabilityInfoSparseArray =
+ getCapabilityInfoSparseArray(context);
+ while (capabilities != 0) {
+ final int capabilityBit = 1 << Integer.numberOfTrailingZeros(capabilities);
+ capabilities &= ~capabilityBit;
+ CapabilityInfo capabilityInfo = capabilityInfoSparseArray.get(capabilityBit);
+ if (capabilityInfo != null) {
+ capabilityInfos.add(capabilityInfo);
+ }
+ }
+ return capabilityInfos;
+ }
+
+ private static SparseArray<CapabilityInfo> getCapabilityInfoSparseArray(Context context) {
+ if (sAvailableCapabilityInfos == null) {
+ sAvailableCapabilityInfos = new SparseArray<CapabilityInfo>();
+ sAvailableCapabilityInfos.put(CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT,
+ new CapabilityInfo(CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT,
+ R.string.capability_title_canRetrieveWindowContent,
+ R.string.capability_desc_canRetrieveWindowContent));
+ sAvailableCapabilityInfos.put(CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION,
+ new CapabilityInfo(CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION,
+ R.string.capability_title_canRequestTouchExploration,
+ R.string.capability_desc_canRequestTouchExploration));
+ sAvailableCapabilityInfos.put(CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS,
+ new CapabilityInfo(CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS,
+ R.string.capability_title_canRequestFilterKeyEvents,
+ R.string.capability_desc_canRequestFilterKeyEvents));
+ sAvailableCapabilityInfos.put(CAPABILITY_CAN_CONTROL_MAGNIFICATION,
+ new CapabilityInfo(CAPABILITY_CAN_CONTROL_MAGNIFICATION,
+ R.string.capability_title_canControlMagnification,
+ R.string.capability_desc_canControlMagnification));
+ sAvailableCapabilityInfos.put(CAPABILITY_CAN_PERFORM_GESTURES,
+ new CapabilityInfo(CAPABILITY_CAN_PERFORM_GESTURES,
+ R.string.capability_title_canPerformGestures,
+ R.string.capability_desc_canPerformGestures));
+ if ((context == null) || fingerprintAvailable(context)) {
+ sAvailableCapabilityInfos.put(CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES,
+ new CapabilityInfo(CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES,
+ R.string.capability_title_canCaptureFingerprintGestures,
+ R.string.capability_desc_canCaptureFingerprintGestures));
+ }
+ }
+ return sAvailableCapabilityInfos;
+ }
+
+ private static boolean fingerprintAvailable(Context context) {
+ return context.getPackageManager().hasSystemFeature(FEATURE_FINGERPRINT)
+ && context.getSystemService(FingerprintManager.class).isHardwareDetected();
+ }
+ /**
+ * @hide
+ */
+ public static final class CapabilityInfo {
+ public final int capability;
+ public final int titleResId;
+ public final int descResId;
+
+ public CapabilityInfo(int capability, int titleResId, int descResId) {
+ this.capability = capability;
+ this.titleResId = titleResId;
+ this.descResId = descResId;
+ }
+ }
+
+ /**
+ * @see Parcelable.Creator
+ */
+ public static final Parcelable.Creator<AccessibilityServiceInfo> CREATOR =
+ new Parcelable.Creator<AccessibilityServiceInfo>() {
+ public AccessibilityServiceInfo createFromParcel(Parcel parcel) {
+ AccessibilityServiceInfo info = new AccessibilityServiceInfo();
+ info.initFromParcel(parcel);
+ return info;
+ }
+
+ public AccessibilityServiceInfo[] newArray(int size) {
+ return new AccessibilityServiceInfo[size];
+ }
+ };
+}
diff --git a/android/accessibilityservice/FingerprintGestureController.java b/android/accessibilityservice/FingerprintGestureController.java
new file mode 100644
index 00000000..c30030d6
--- /dev/null
+++ b/android/accessibilityservice/FingerprintGestureController.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2017 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * An {@link AccessibilityService} can capture gestures performed on a device's fingerprint
+ * sensor, as long as the device has a sensor capable of detecting gestures.
+ * <p>
+ * This capability must be declared by the service as
+ * {@link AccessibilityServiceInfo#CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES}. It also requires
+ * the permission {@link android.Manifest.permission#USE_FINGERPRINT}.
+ * <p>
+ * Because capturing fingerprint gestures may have side effects, services with the capability only
+ * capture gestures when {@link AccessibilityServiceInfo#FLAG_REQUEST_FINGERPRINT_GESTURES} is set.
+ * <p>
+ * <strong>Note: </strong>The fingerprint sensor is used for authentication in critical use cases,
+ * so services must carefully design their user's experience when performing gestures on the sensor.
+ * When the sensor is in use by an app, for example, when authenticating or enrolling a user,
+ * the sensor will not detect gestures. Services need to ensure that users understand when the
+ * sensor is in-use for authentication to prevent users from authenticating unintentionally when
+ * trying to interact with the service. They can use
+ * {@link FingerprintGestureCallback#onGestureDetectionAvailabilityChanged(boolean)} to learn when
+ * gesture detection becomes unavailable.
+ * <p>
+ * Multiple accessibility services may listen for fingerprint gestures simultaneously, so services
+ * should provide a way for the user to disable the use of this feature so multiple services don't
+ * conflict with each other.
+ * <p>
+ * {@see android.hardware.fingerprint.FingerprintManager#isHardwareDetected}
+ */
+public final class FingerprintGestureController {
+ /** Identifier for a swipe right on the fingerprint sensor */
+ public static final int FINGERPRINT_GESTURE_SWIPE_RIGHT = 0x00000001;
+
+ /** Identifier for a swipe left on the fingerprint sensor */
+ public static final int FINGERPRINT_GESTURE_SWIPE_LEFT = 0x00000002;
+
+ /** Identifier for a swipe up on the fingerprint sensor */
+ public static final int FINGERPRINT_GESTURE_SWIPE_UP = 0x00000004;
+
+ /** Identifier for a swipe down on the fingerprint sensor */
+ public static final int FINGERPRINT_GESTURE_SWIPE_DOWN = 0x00000008;
+
+ private static final String LOG_TAG = "FingerprintGestureController";
+ private final Object mLock = new Object();
+ private final IAccessibilityServiceConnection mAccessibilityServiceConnection;
+
+ private final ArrayMap<FingerprintGestureCallback, Handler> mCallbackHandlerMap =
+ new ArrayMap<>(1);
+
+ /**
+ * @param connection The connection to use for system interactions
+ * @hide
+ */
+ @VisibleForTesting
+ public FingerprintGestureController(IAccessibilityServiceConnection connection) {
+ mAccessibilityServiceConnection = connection;
+ }
+
+ /**
+ * Gets if the fingerprint sensor's gesture detection is available.
+ *
+ * @return {@code true} if the sensor's gesture detection is available. {@code false} if it is
+ * not currently detecting gestures (for example, if it is enrolling a finger).
+ */
+ public boolean isGestureDetectionAvailable() {
+ try {
+ return mAccessibilityServiceConnection.isFingerprintGestureDetectionAvailable();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to check if fingerprint gestures are active", re);
+ re.rethrowFromSystemServer();
+ return false;
+ }
+ }
+
+ /**
+ * Register a callback to be informed of fingerprint sensor gesture events.
+ *
+ * @param callback The listener to be added.
+ * @param handler The handler to use for the callback. If {@code null}, callbacks will happen
+ * on the service's main thread.
+ */
+ public void registerFingerprintGestureCallback(
+ @NonNull FingerprintGestureCallback callback, @Nullable Handler handler) {
+ synchronized (mLock) {
+ mCallbackHandlerMap.put(callback, handler);
+ }
+ }
+
+ /**
+ * Unregister a listener added with {@link #registerFingerprintGestureCallback}.
+ *
+ * @param callback The callback to remove. Removing a callback that was never added has no
+ * effect.
+ */
+ public void unregisterFingerprintGestureCallback(FingerprintGestureCallback callback) {
+ synchronized (mLock) {
+ mCallbackHandlerMap.remove(callback);
+ }
+ }
+
+ /**
+ * Called when gesture detection becomes active or inactive
+ * @hide
+ */
+ public void onGestureDetectionActiveChanged(boolean active) {
+ final ArrayMap<FingerprintGestureCallback, Handler> handlerMap;
+ synchronized (mLock) {
+ handlerMap = new ArrayMap<>(mCallbackHandlerMap);
+ }
+ int numListeners = handlerMap.size();
+ for (int i = 0; i < numListeners; i++) {
+ FingerprintGestureCallback callback = handlerMap.keyAt(i);
+ Handler handler = handlerMap.valueAt(i);
+ if (handler != null) {
+ handler.post(() -> callback.onGestureDetectionAvailabilityChanged(active));
+ } else {
+ callback.onGestureDetectionAvailabilityChanged(active);
+ }
+ }
+ }
+
+ /**
+ * Called when gesture is detected.
+ * @hide
+ */
+ public void onGesture(int gesture) {
+ final ArrayMap<FingerprintGestureCallback, Handler> handlerMap;
+ synchronized (mLock) {
+ handlerMap = new ArrayMap<>(mCallbackHandlerMap);
+ }
+ int numListeners = handlerMap.size();
+ for (int i = 0; i < numListeners; i++) {
+ FingerprintGestureCallback callback = handlerMap.keyAt(i);
+ Handler handler = handlerMap.valueAt(i);
+ if (handler != null) {
+ handler.post(() -> callback.onGestureDetected(gesture));
+ } else {
+ callback.onGestureDetected(gesture);
+ }
+ }
+ }
+
+ /**
+ * Class that is called back when fingerprint gestures are being used for accessibility.
+ */
+ public abstract static class FingerprintGestureCallback {
+ /**
+ * Called when the fingerprint sensor's gesture detection becomes available or unavailable.
+ *
+ * @param available Whether or not the sensor's gesture detection is now available.
+ */
+ public void onGestureDetectionAvailabilityChanged(boolean available) {}
+
+ /**
+ * Called when the fingerprint sensor detects gestures.
+ *
+ * @param gesture The id of the gesture that was detected. For example,
+ * {@link #FINGERPRINT_GESTURE_SWIPE_RIGHT}.
+ */
+ public void onGestureDetected(int gesture) {}
+ }
+}
diff --git a/android/accessibilityservice/GestureDescription.java b/android/accessibilityservice/GestureDescription.java
new file mode 100644
index 00000000..92567d75
--- /dev/null
+++ b/android/accessibilityservice/GestureDescription.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.accessibilityservice;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Accessibility services with the
+ * {@link android.R.styleable#AccessibilityService_canPerformGestures} property can dispatch
+ * gestures. This class describes those gestures. Gestures are made up of one or more strokes.
+ * Gestures are immutable once built.
+ * <p>
+ * Spatial dimensions throughout are in screen pixels. Time is measured in milliseconds.
+ */
+public final class GestureDescription {
+ /** Gestures may contain no more than this many strokes */
+ private static final int MAX_STROKE_COUNT = 10;
+
+ /**
+ * Upper bound on total gesture duration. Nearly all gestures will be much shorter.
+ */
+ private static final long MAX_GESTURE_DURATION_MS = 60 * 1000;
+
+ private final List<StrokeDescription> mStrokes = new ArrayList<>();
+ private final float[] mTempPos = new float[2];
+
+ /**
+ * Get the upper limit for the number of strokes a gesture may contain.
+ *
+ * @return The maximum number of strokes.
+ */
+ public static int getMaxStrokeCount() {
+ return MAX_STROKE_COUNT;
+ }
+
+ /**
+ * Get the upper limit on a gesture's duration.
+ *
+ * @return The maximum duration in milliseconds.
+ */
+ public static long getMaxGestureDuration() {
+ return MAX_GESTURE_DURATION_MS;
+ }
+
+ private GestureDescription() {}
+
+ private GestureDescription(List<StrokeDescription> strokes) {
+ mStrokes.addAll(strokes);
+ }
+
+ /**
+ * Get the number of stroke in the gesture.
+ *
+ * @return the number of strokes in this gesture
+ */
+ public int getStrokeCount() {
+ return mStrokes.size();
+ }
+
+ /**
+ * Read a stroke from the gesture
+ *
+ * @param index the index of the stroke
+ *
+ * @return A description of the stroke.
+ */
+ public StrokeDescription getStroke(@IntRange(from = 0) int index) {
+ return mStrokes.get(index);
+ }
+
+ /**
+ * Return the smallest key point (where a path starts or ends) that is at least a specified
+ * offset
+ * @param offset the minimum start time
+ * @return The next key time that is at least the offset or -1 if one can't be found
+ */
+ private long getNextKeyPointAtLeast(long offset) {
+ long nextKeyPoint = Long.MAX_VALUE;
+ for (int i = 0; i < mStrokes.size(); i++) {
+ long thisStartTime = mStrokes.get(i).mStartTime;
+ if ((thisStartTime < nextKeyPoint) && (thisStartTime >= offset)) {
+ nextKeyPoint = thisStartTime;
+ }
+ long thisEndTime = mStrokes.get(i).mEndTime;
+ if ((thisEndTime < nextKeyPoint) && (thisEndTime >= offset)) {
+ nextKeyPoint = thisEndTime;
+ }
+ }
+ return (nextKeyPoint == Long.MAX_VALUE) ? -1L : nextKeyPoint;
+ }
+
+ /**
+ * Get the points that correspond to a particular moment in time.
+ * @param time The time of interest
+ * @param touchPoints An array to hold the current touch points. Must be preallocated to at
+ * least the number of paths in the gesture to prevent going out of bounds
+ * @return The number of points found, and thus the number of elements set in each array
+ */
+ private int getPointsForTime(long time, TouchPoint[] touchPoints) {
+ int numPointsFound = 0;
+ for (int i = 0; i < mStrokes.size(); i++) {
+ StrokeDescription strokeDescription = mStrokes.get(i);
+ if (strokeDescription.hasPointForTime(time)) {
+ touchPoints[numPointsFound].mStrokeId = strokeDescription.getId();
+ touchPoints[numPointsFound].mContinuedStrokeId =
+ strokeDescription.getContinuedStrokeId();
+ touchPoints[numPointsFound].mIsStartOfPath =
+ (strokeDescription.getContinuedStrokeId() < 0)
+ && (time == strokeDescription.mStartTime);
+ touchPoints[numPointsFound].mIsEndOfPath = !strokeDescription.willContinue()
+ && (time == strokeDescription.mEndTime);
+ strokeDescription.getPosForTime(time, mTempPos);
+ touchPoints[numPointsFound].mX = Math.round(mTempPos[0]);
+ touchPoints[numPointsFound].mY = Math.round(mTempPos[1]);
+ numPointsFound++;
+ }
+ }
+ return numPointsFound;
+ }
+
+ // Total duration assumes that the gesture starts at 0; waiting around to start a gesture
+ // counts against total duration
+ private static long getTotalDuration(List<StrokeDescription> paths) {
+ long latestEnd = Long.MIN_VALUE;
+ for (int i = 0; i < paths.size(); i++) {
+ StrokeDescription path = paths.get(i);
+ latestEnd = Math.max(latestEnd, path.mEndTime);
+ }
+ return Math.max(latestEnd, 0);
+ }
+
+ /**
+ * Builder for a {@code GestureDescription}
+ */
+ public static class Builder {
+
+ private final List<StrokeDescription> mStrokes = new ArrayList<>();
+
+ /**
+ * Add a stroke to the gesture description. Up to
+ * {@link GestureDescription#getMaxStrokeCount()} paths may be
+ * added to a gesture, and the total gesture duration (earliest path start time to latest
+ * path end time) may not exceed {@link GestureDescription#getMaxGestureDuration()}.
+ *
+ * @param strokeDescription the stroke to add.
+ *
+ * @return this
+ */
+ public Builder addStroke(@NonNull StrokeDescription strokeDescription) {
+ if (mStrokes.size() >= MAX_STROKE_COUNT) {
+ throw new IllegalStateException(
+ "Attempting to add too many strokes to a gesture");
+ }
+
+ mStrokes.add(strokeDescription);
+
+ if (getTotalDuration(mStrokes) > MAX_GESTURE_DURATION_MS) {
+ mStrokes.remove(strokeDescription);
+ throw new IllegalStateException(
+ "Gesture would exceed maximum duration with new stroke");
+ }
+ return this;
+ }
+
+ public GestureDescription build() {
+ if (mStrokes.size() == 0) {
+ throw new IllegalStateException("Gestures must have at least one stroke");
+ }
+ return new GestureDescription(mStrokes);
+ }
+ }
+
+ /**
+ * Immutable description of stroke that can be part of a gesture.
+ */
+ public static class StrokeDescription {
+ private static final int INVALID_STROKE_ID = -1;
+
+ static int sIdCounter;
+
+ Path mPath;
+ long mStartTime;
+ long mEndTime;
+ private float mTimeToLengthConversion;
+ private PathMeasure mPathMeasure;
+ // The tap location is only set for zero-length paths
+ float[] mTapLocation;
+ int mId;
+ boolean mContinued;
+ int mContinuedStrokeId = INVALID_STROKE_ID;
+
+ /**
+ * @param path The path to follow. Must have exactly one contour. The bounds of the path
+ * must not be negative. The path must not be empty. If the path has zero length
+ * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move.
+ * @param startTime The time, in milliseconds, from the time the gesture starts to the
+ * time the stroke should start. Must not be negative.
+ * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
+ * Must be positive.
+ */
+ public StrokeDescription(@NonNull Path path,
+ @IntRange(from = 0) long startTime,
+ @IntRange(from = 0) long duration) {
+ this(path, startTime, duration, false);
+ }
+
+ /**
+ * @param path The path to follow. Must have exactly one contour. The bounds of the path
+ * must not be negative. The path must not be empty. If the path has zero length
+ * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move.
+ * @param startTime The time, in milliseconds, from the time the gesture starts to the
+ * time the stroke should start. Must not be negative.
+ * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
+ * Must be positive.
+ * @param willContinue {@code true} if this stroke will be continued by one in the
+ * next gesture {@code false} otherwise. Continued strokes keep their pointers down when
+ * the gesture completes.
+ */
+ public StrokeDescription(@NonNull Path path,
+ @IntRange(from = 0) long startTime,
+ @IntRange(from = 0) long duration,
+ boolean willContinue) {
+ mContinued = willContinue;
+ Preconditions.checkArgument(duration > 0, "Duration must be positive");
+ Preconditions.checkArgument(startTime >= 0, "Start time must not be negative");
+ Preconditions.checkArgument(!path.isEmpty(), "Path is empty");
+ RectF bounds = new RectF();
+ path.computeBounds(bounds, false /* unused */);
+ Preconditions.checkArgument((bounds.bottom >= 0) && (bounds.top >= 0)
+ && (bounds.right >= 0) && (bounds.left >= 0),
+ "Path bounds must not be negative");
+ mPath = new Path(path);
+ mPathMeasure = new PathMeasure(path, false);
+ if (mPathMeasure.getLength() == 0) {
+ // Treat zero-length paths as taps
+ Path tempPath = new Path(path);
+ tempPath.lineTo(-1, -1);
+ mTapLocation = new float[2];
+ PathMeasure pathMeasure = new PathMeasure(tempPath, false);
+ pathMeasure.getPosTan(0, mTapLocation, null);
+ }
+ if (mPathMeasure.nextContour()) {
+ throw new IllegalArgumentException("Path has more than one contour");
+ }
+ /*
+ * Calling nextContour has moved mPathMeasure off the first contour, which is the only
+ * one we care about. Set the path again to go back to the first contour.
+ */
+ mPathMeasure.setPath(mPath, false);
+ mStartTime = startTime;
+ mEndTime = startTime + duration;
+ mTimeToLengthConversion = getLength() / duration;
+ mId = sIdCounter++;
+ }
+
+ /**
+ * Retrieve a copy of the path for this stroke
+ *
+ * @return A copy of the path
+ */
+ public Path getPath() {
+ return new Path(mPath);
+ }
+
+ /**
+ * Get the stroke's start time
+ *
+ * @return the start time for this stroke.
+ */
+ public long getStartTime() {
+ return mStartTime;
+ }
+
+ /**
+ * Get the stroke's duration
+ *
+ * @return the duration for this stroke
+ */
+ public long getDuration() {
+ return mEndTime - mStartTime;
+ }
+
+ /**
+ * Get the stroke's ID. The ID is used when a stroke is to be continued by another
+ * stroke in a future gesture.
+ *
+ * @return the ID of this stroke
+ * @hide
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Create a new stroke that will continue this one. This is only possible if this stroke
+ * will continue.
+ *
+ * @param path The path for the stroke that continues this one. The starting point of
+ * this path must match the ending point of the stroke it continues.
+ * @param startTime The time, in milliseconds, from the time the gesture starts to the
+ * time this stroke should start. Must not be negative. This time is from
+ * the start of the new gesture, not the one being continued.
+ * @param duration The duration for the new stroke. Must not be negative.
+ * @param willContinue {@code true} if this stroke will be continued by one in the
+ * next gesture {@code false} otherwise.
+ * @return
+ */
+ public StrokeDescription continueStroke(Path path, long startTime, long duration,
+ boolean willContinue) {
+ if (!mContinued) {
+ throw new IllegalStateException(
+ "Only strokes marked willContinue can be continued");
+ }
+ StrokeDescription strokeDescription =
+ new StrokeDescription(path, startTime, duration, willContinue);
+ strokeDescription.mContinuedStrokeId = mId;
+ return strokeDescription;
+ }
+
+ /**
+ * Check if this stroke is marked to continue in the next gesture.
+ *
+ * @return {@code true} if the stroke is to be continued.
+ */
+ public boolean willContinue() {
+ return mContinued;
+ }
+
+ /**
+ * Get the ID of the stroke that this one will continue.
+ *
+ * @return The ID of the stroke that this stroke continues, or 0 if no such stroke exists.
+ * @hide
+ */
+ public int getContinuedStrokeId() {
+ return mContinuedStrokeId;
+ }
+
+ float getLength() {
+ return mPathMeasure.getLength();
+ }
+
+ /* Assumes hasPointForTime returns true */
+ boolean getPosForTime(long time, float[] pos) {
+ if (mTapLocation != null) {
+ pos[0] = mTapLocation[0];
+ pos[1] = mTapLocation[1];
+ return true;
+ }
+ if (time == mEndTime) {
+ // Close to the end time, roundoff can be a problem
+ return mPathMeasure.getPosTan(getLength(), pos, null);
+ }
+ float length = mTimeToLengthConversion * ((float) (time - mStartTime));
+ return mPathMeasure.getPosTan(length, pos, null);
+ }
+
+ boolean hasPointForTime(long time) {
+ return ((time >= mStartTime) && (time <= mEndTime));
+ }
+ }
+
+ /**
+ * The location of a finger for gesture dispatch
+ *
+ * @hide
+ */
+ public static class TouchPoint implements Parcelable {
+ private static final int FLAG_IS_START_OF_PATH = 0x01;
+ private static final int FLAG_IS_END_OF_PATH = 0x02;
+
+ public int mStrokeId;
+ public int mContinuedStrokeId;
+ public boolean mIsStartOfPath;
+ public boolean mIsEndOfPath;
+ public float mX;
+ public float mY;
+
+ public TouchPoint() {
+ }
+
+ public TouchPoint(TouchPoint pointToCopy) {
+ copyFrom(pointToCopy);
+ }
+
+ public TouchPoint(Parcel parcel) {
+ mStrokeId = parcel.readInt();
+ mContinuedStrokeId = parcel.readInt();
+ int startEnd = parcel.readInt();
+ mIsStartOfPath = (startEnd & FLAG_IS_START_OF_PATH) != 0;
+ mIsEndOfPath = (startEnd & FLAG_IS_END_OF_PATH) != 0;
+ mX = parcel.readFloat();
+ mY = parcel.readFloat();
+ }
+
+ public void copyFrom(TouchPoint other) {
+ mStrokeId = other.mStrokeId;
+ mContinuedStrokeId = other.mContinuedStrokeId;
+ mIsStartOfPath = other.mIsStartOfPath;
+ mIsEndOfPath = other.mIsEndOfPath;
+ mX = other.mX;
+ mY = other.mY;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStrokeId);
+ dest.writeInt(mContinuedStrokeId);
+ int startEnd = mIsStartOfPath ? FLAG_IS_START_OF_PATH : 0;
+ startEnd |= mIsEndOfPath ? FLAG_IS_END_OF_PATH : 0;
+ dest.writeInt(startEnd);
+ dest.writeFloat(mX);
+ dest.writeFloat(mY);
+ }
+
+ public static final Parcelable.Creator<TouchPoint> CREATOR
+ = new Parcelable.Creator<TouchPoint>() {
+ public TouchPoint createFromParcel(Parcel in) {
+ return new TouchPoint(in);
+ }
+
+ public TouchPoint[] newArray(int size) {
+ return new TouchPoint[size];
+ }
+ };
+ }
+
+ /**
+ * A step along a gesture. Contains all of the touch points at a particular time
+ *
+ * @hide
+ */
+ public static class GestureStep implements Parcelable {
+ public long timeSinceGestureStart;
+ public int numTouchPoints;
+ public TouchPoint[] touchPoints;
+
+ public GestureStep(long timeSinceGestureStart, int numTouchPoints,
+ TouchPoint[] touchPointsToCopy) {
+ this.timeSinceGestureStart = timeSinceGestureStart;
+ this.numTouchPoints = numTouchPoints;
+ this.touchPoints = new TouchPoint[numTouchPoints];
+ for (int i = 0; i < numTouchPoints; i++) {
+ this.touchPoints[i] = new TouchPoint(touchPointsToCopy[i]);
+ }
+ }
+
+ public GestureStep(Parcel parcel) {
+ timeSinceGestureStart = parcel.readLong();
+ Parcelable[] parcelables =
+ parcel.readParcelableArray(TouchPoint.class.getClassLoader());
+ numTouchPoints = (parcelables == null) ? 0 : parcelables.length;
+ touchPoints = new TouchPoint[numTouchPoints];
+ for (int i = 0; i < numTouchPoints; i++) {
+ touchPoints[i] = (TouchPoint) parcelables[i];
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(timeSinceGestureStart);
+ dest.writeParcelableArray(touchPoints, flags);
+ }
+
+ public static final Parcelable.Creator<GestureStep> CREATOR
+ = new Parcelable.Creator<GestureStep>() {
+ public GestureStep createFromParcel(Parcel in) {
+ return new GestureStep(in);
+ }
+
+ public GestureStep[] newArray(int size) {
+ return new GestureStep[size];
+ }
+ };
+ }
+
+ /**
+ * Class to convert a GestureDescription to a series of GestureSteps.
+ *
+ * @hide
+ */
+ public static class MotionEventGenerator {
+ /* Lazily-created scratch memory for processing touches */
+ private static TouchPoint[] sCurrentTouchPoints;
+
+ public static List<GestureStep> getGestureStepsFromGestureDescription(
+ GestureDescription description, int sampleTimeMs) {
+ final List<GestureStep> gestureSteps = new ArrayList<>();
+
+ // Point data at each time we generate an event for
+ final TouchPoint[] currentTouchPoints =
+ getCurrentTouchPoints(description.getStrokeCount());
+ int currentTouchPointSize = 0;
+ /* Loop through each time slice where there are touch points */
+ long timeSinceGestureStart = 0;
+ long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart);
+ while (nextKeyPointTime >= 0) {
+ timeSinceGestureStart = (currentTouchPointSize == 0) ? nextKeyPointTime
+ : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs);
+ currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart,
+ currentTouchPoints);
+ gestureSteps.add(new GestureStep(timeSinceGestureStart, currentTouchPointSize,
+ currentTouchPoints));
+
+ /* Move to next time slice */
+ nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1);
+ }
+ return gestureSteps;
+ }
+
+ private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) {
+ if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) {
+ sCurrentTouchPoints = new TouchPoint[requiredCapacity];
+ for (int i = 0; i < requiredCapacity; i++) {
+ sCurrentTouchPoints[i] = new TouchPoint();
+ }
+ }
+ return sCurrentTouchPoints;
+ }
+ }
+}