diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/accessibilityservice | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-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')
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> <service android:name=".MyAccessibilityService" + * android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> + * <intent-filter> + * <action android:name="android.accessibilityservice.AccessibilityService" /> + * </intent-filter> + * . . . + * </service></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> <service android:name=".MyAccessibilityService"> + * <intent-filter> + * <action android:name="android.accessibilityservice.AccessibilityService" /> + * </intent-filter> + * <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibilityservice" /> + * </service></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><{@link android.R.styleable#AccessibilityService accessibility-service}></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><{@link android.R.styleable#AccessibilityService accessibility-service}></code> + * tag. This is a a sample XML file configuring an accessibility service: + * <pre> <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" + * . . . + * /></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; + } + } +} |