Automatic sources dropoff on 2020-06-10 18:32:38.095721
The change is generated with prebuilt drop tool.
Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/.prebuilt_info/AndroidX/prebuilt_info_sdk-repo-linux-sources-*_zip.asciipb b/.prebuilt_info/AndroidX/prebuilt_info_sdk-repo-linux-sources-*_zip.asciipb
new file mode 100644
index 0000000..75aeb2a
--- /dev/null
+++ b/.prebuilt_info/AndroidX/prebuilt_info_sdk-repo-linux-sources-*_zip.asciipb
@@ -0,0 +1,12 @@
+drops {
+ android_build_drop {
+ build_id: "6520161"
+ target: "sdk_phone_x86_64-sdk"
+ source_file: "sdk-repo-linux-sources-6520161.zip"
+ }
+ dest_file: "sdk-repo-linux-sources-*.zip"
+ version: ""
+ version_group: "AndroidX"
+ git_project: "platform/prebuilts/fullsdk/sources/android-30"
+ git_branch: "master"
+}
diff --git a/android/accessibilityservice/AccessibilityButtonController.java b/android/accessibilityservice/AccessibilityButtonController.java
new file mode 100644
index 0000000..2ccad1d
--- /dev/null
+++ b/android/accessibilityservice/AccessibilityButtonController.java
@@ -0,0 +1,220 @@
+/*
+ * 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.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import java.util.Objects;
+
+/**
+ * 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() {
+ if (mServiceConnection != null) {
+ try {
+ return mServiceConnection.isAccessibilityButtonAvailable();
+ } catch (RemoteException re) {
+ Slog.w(LOG_TAG, "Failed to get accessibility button availability.", re);
+ re.rethrowFromSystemServer();
+ return false;
+ }
+ }
+ 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) {
+ Objects.requireNonNull(callback);
+ Objects.requireNonNull(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) {
+ Objects.requireNonNull(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/AccessibilityGestureEvent.java b/android/accessibilityservice/AccessibilityGestureEvent.java
new file mode 100644
index 0000000..25729ab
--- /dev/null
+++ b/android/accessibilityservice/AccessibilityGestureEvent.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2019 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 static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_DOUBLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_DOUBLE_TAP_AND_HOLD;
+import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SINGLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_RIGHT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_TRIPLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_DOUBLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_DOUBLE_TAP_AND_HOLD;
+import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SINGLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_RIGHT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_TRIPLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_4_FINGER_DOUBLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_4_FINGER_DOUBLE_TAP_AND_HOLD;
+import static android.accessibilityservice.AccessibilityService.GESTURE_4_FINGER_SINGLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_4_FINGER_SWIPE_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_4_FINGER_SWIPE_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_4_FINGER_SWIPE_RIGHT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_4_FINGER_SWIPE_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_4_FINGER_TRIPLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP_AND_HOLD;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class describes the gesture event including gesture id and which display it happens
+ * on.
+ * <p>
+ * <strong>Note:</strong> Accessibility services setting the
+ * {@link android.accessibilityservice.AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE}
+ * flag can receive gestures.
+ *
+ * @see AccessibilityService#onGesture(AccessibilityGestureEvent)
+ */
+
+public final class AccessibilityGestureEvent implements Parcelable {
+
+ /** @hide */
+ @IntDef(prefix = { "GESTURE_" }, value = {
+ GESTURE_2_FINGER_SINGLE_TAP,
+ GESTURE_2_FINGER_DOUBLE_TAP,
+ GESTURE_2_FINGER_DOUBLE_TAP_AND_HOLD,
+ GESTURE_2_FINGER_TRIPLE_TAP,
+ GESTURE_3_FINGER_SINGLE_TAP,
+ GESTURE_3_FINGER_DOUBLE_TAP,
+ GESTURE_3_FINGER_DOUBLE_TAP_AND_HOLD,
+ GESTURE_3_FINGER_TRIPLE_TAP,
+ GESTURE_DOUBLE_TAP,
+ GESTURE_DOUBLE_TAP_AND_HOLD,
+ GESTURE_SWIPE_UP,
+ GESTURE_SWIPE_UP_AND_LEFT,
+ GESTURE_SWIPE_UP_AND_DOWN,
+ GESTURE_SWIPE_UP_AND_RIGHT,
+ GESTURE_SWIPE_DOWN,
+ GESTURE_SWIPE_DOWN_AND_LEFT,
+ GESTURE_SWIPE_DOWN_AND_UP,
+ GESTURE_SWIPE_DOWN_AND_RIGHT,
+ GESTURE_SWIPE_LEFT,
+ GESTURE_SWIPE_LEFT_AND_UP,
+ GESTURE_SWIPE_LEFT_AND_RIGHT,
+ GESTURE_SWIPE_LEFT_AND_DOWN,
+ GESTURE_SWIPE_RIGHT,
+ GESTURE_SWIPE_RIGHT_AND_UP,
+ GESTURE_SWIPE_RIGHT_AND_LEFT,
+ GESTURE_SWIPE_RIGHT_AND_DOWN,
+ GESTURE_2_FINGER_SWIPE_DOWN,
+ GESTURE_2_FINGER_SWIPE_LEFT,
+ GESTURE_2_FINGER_SWIPE_RIGHT,
+ GESTURE_2_FINGER_SWIPE_UP,
+ GESTURE_3_FINGER_SWIPE_DOWN,
+ GESTURE_3_FINGER_SWIPE_LEFT,
+ GESTURE_3_FINGER_SWIPE_RIGHT,
+ GESTURE_3_FINGER_SWIPE_UP,
+ GESTURE_4_FINGER_DOUBLE_TAP,
+ GESTURE_4_FINGER_DOUBLE_TAP_AND_HOLD,
+ GESTURE_4_FINGER_SINGLE_TAP,
+ GESTURE_4_FINGER_SWIPE_DOWN,
+ GESTURE_4_FINGER_SWIPE_LEFT,
+ GESTURE_4_FINGER_SWIPE_RIGHT,
+ GESTURE_4_FINGER_SWIPE_UP,
+ GESTURE_4_FINGER_TRIPLE_TAP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface GestureId {}
+
+ @GestureId
+ private final int mGestureId;
+ private final int mDisplayId;
+
+ /** @hide */
+ @TestApi
+ public AccessibilityGestureEvent(int gestureId, int displayId) {
+ mGestureId = gestureId;
+ mDisplayId = displayId;
+ }
+
+ private AccessibilityGestureEvent(@NonNull Parcel parcel) {
+ mGestureId = parcel.readInt();
+ mDisplayId = parcel.readInt();
+ }
+
+ /**
+ * Returns the display id of the received-gesture display, for use with
+ * {@link android.hardware.display.DisplayManager#getDisplay(int)}.
+ *
+ * @return the display id.
+ */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /**
+ * Returns performed gesture id.
+ *
+ * @return the performed gesture id.
+ *
+ */
+ @GestureId public int getGestureId() {
+ return mGestureId;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder("AccessibilityGestureEvent[");
+ stringBuilder.append("gestureId: ").append(eventTypeToString(mGestureId));
+ stringBuilder.append(", ");
+ stringBuilder.append("displayId: ").append(mDisplayId);
+ stringBuilder.append(']');
+ return stringBuilder.toString();
+ }
+
+ private static String eventTypeToString(int eventType) {
+ switch (eventType) {
+ case GESTURE_2_FINGER_SINGLE_TAP: return "GESTURE_2_FINGER_SINGLE_TAP";
+ case GESTURE_2_FINGER_DOUBLE_TAP: return "GESTURE_2_FINGER_DOUBLE_TAP";
+ case GESTURE_2_FINGER_DOUBLE_TAP_AND_HOLD:
+ return "GESTURE_2_FINGER_DOUBLE_TAP_AND_HOLD";
+ case GESTURE_2_FINGER_TRIPLE_TAP: return "GESTURE_2_FINGER_TRIPLE_TAP";
+ case GESTURE_3_FINGER_SINGLE_TAP: return "GESTURE_3_FINGER_SINGLE_TAP";
+ case GESTURE_3_FINGER_DOUBLE_TAP: return "GESTURE_3_FINGER_DOUBLE_TAP";
+ case GESTURE_3_FINGER_DOUBLE_TAP_AND_HOLD:
+ return "GESTURE_3_FINGER_DOUBLE_TAP_AND_HOLD";
+ case GESTURE_3_FINGER_TRIPLE_TAP: return "GESTURE_3_FINGER_TRIPLE_TAP";
+ case GESTURE_4_FINGER_SINGLE_TAP: return "GESTURE_4_FINGER_SINGLE_TAP";
+ case GESTURE_4_FINGER_DOUBLE_TAP: return "GESTURE_4_FINGER_DOUBLE_TAP";
+ case GESTURE_4_FINGER_DOUBLE_TAP_AND_HOLD:
+ return "GESTURE_4_FINGER_DOUBLE_TAP_AND_HOLD";
+ case GESTURE_4_FINGER_TRIPLE_TAP: return "GESTURE_4_FINGER_TRIPLE_TAP";
+ case GESTURE_DOUBLE_TAP: return "GESTURE_DOUBLE_TAP";
+ case GESTURE_DOUBLE_TAP_AND_HOLD: return "GESTURE_DOUBLE_TAP_AND_HOLD";
+ case GESTURE_SWIPE_DOWN: return "GESTURE_SWIPE_DOWN";
+ case GESTURE_SWIPE_DOWN_AND_LEFT: return "GESTURE_SWIPE_DOWN_AND_LEFT";
+ case GESTURE_SWIPE_DOWN_AND_UP: return "GESTURE_SWIPE_DOWN_AND_UP";
+ case GESTURE_SWIPE_DOWN_AND_RIGHT: return "GESTURE_SWIPE_DOWN_AND_RIGHT";
+ case GESTURE_SWIPE_LEFT: return "GESTURE_SWIPE_LEFT";
+ case GESTURE_SWIPE_LEFT_AND_UP: return "GESTURE_SWIPE_LEFT_AND_UP";
+ case GESTURE_SWIPE_LEFT_AND_RIGHT: return "GESTURE_SWIPE_LEFT_AND_RIGHT";
+ case GESTURE_SWIPE_LEFT_AND_DOWN: return "GESTURE_SWIPE_LEFT_AND_DOWN";
+ case GESTURE_SWIPE_RIGHT: return "GESTURE_SWIPE_RIGHT";
+ case GESTURE_SWIPE_RIGHT_AND_UP: return "GESTURE_SWIPE_RIGHT_AND_UP";
+ case GESTURE_SWIPE_RIGHT_AND_LEFT: return "GESTURE_SWIPE_RIGHT_AND_LEFT";
+ case GESTURE_SWIPE_RIGHT_AND_DOWN: return "GESTURE_SWIPE_RIGHT_AND_DOWN";
+ case GESTURE_SWIPE_UP: return "GESTURE_SWIPE_UP";
+ case GESTURE_SWIPE_UP_AND_LEFT: return "GESTURE_SWIPE_UP_AND_LEFT";
+ case GESTURE_SWIPE_UP_AND_DOWN: return "GESTURE_SWIPE_UP_AND_DOWN";
+ case GESTURE_SWIPE_UP_AND_RIGHT: return "GESTURE_SWIPE_UP_AND_RIGHT";
+ case GESTURE_2_FINGER_SWIPE_DOWN: return "GESTURE_2_FINGER_SWIPE_DOWN";
+ case GESTURE_2_FINGER_SWIPE_LEFT: return "GESTURE_2_FINGER_SWIPE_LEFT";
+ case GESTURE_2_FINGER_SWIPE_RIGHT: return "GESTURE_2_FINGER_SWIPE_RIGHT";
+ case GESTURE_2_FINGER_SWIPE_UP: return "GESTURE_2_FINGER_SWIPE_UP";
+ case GESTURE_3_FINGER_SWIPE_DOWN: return "GESTURE_3_FINGER_SWIPE_DOWN";
+ case GESTURE_3_FINGER_SWIPE_LEFT: return "GESTURE_3_FINGER_SWIPE_LEFT";
+ case GESTURE_3_FINGER_SWIPE_RIGHT: return "GESTURE_3_FINGER_SWIPE_RIGHT";
+ case GESTURE_3_FINGER_SWIPE_UP: return "GESTURE_3_FINGER_SWIPE_UP";
+ case GESTURE_4_FINGER_SWIPE_DOWN: return "GESTURE_4_FINGER_SWIPE_DOWN";
+ case GESTURE_4_FINGER_SWIPE_LEFT: return "GESTURE_4_FINGER_SWIPE_LEFT";
+ case GESTURE_4_FINGER_SWIPE_RIGHT: return "GESTURE_4_FINGER_SWIPE_RIGHT";
+ case GESTURE_4_FINGER_SWIPE_UP: return "GESTURE_4_FINGER_SWIPE_UP";
+ default: return Integer.toHexString(eventType);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel parcel, int flags) {
+ parcel.writeInt(mGestureId);
+ parcel.writeInt(mDisplayId);
+ }
+
+ /**
+ * @see Parcelable.Creator
+ */
+ public static final @NonNull Parcelable.Creator<AccessibilityGestureEvent> CREATOR =
+ new Parcelable.Creator<AccessibilityGestureEvent>() {
+ public AccessibilityGestureEvent createFromParcel(Parcel parcel) {
+ return new AccessibilityGestureEvent(parcel);
+ }
+
+ public AccessibilityGestureEvent[] newArray(int size) {
+ return new AccessibilityGestureEvent[size];
+ }
+ };
+
+}
diff --git a/android/accessibilityservice/AccessibilityService.java b/android/accessibilityservice/AccessibilityService.java
new file mode 100644
index 0000000..77b3c81
--- /dev/null
+++ b/android/accessibilityservice/AccessibilityService.java
@@ -0,0 +1,2573 @@
+/*
+ * 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.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.ParcelableColorSpace;
+import android.graphics.Region;
+import android.hardware.HardwareBuffer;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.KeyEvent;
+import android.view.SurfaceView;
+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.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * 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 overridden 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 user has performed a double tap gesture on the touch screen.
+ */
+ public static final int GESTURE_DOUBLE_TAP = 17;
+
+ /**
+ * The user has performed a double tap and hold gesture on the touch screen.
+ */
+ public static final int GESTURE_DOUBLE_TAP_AND_HOLD = 18;
+
+ /**
+ * The user has performed a two-finger single tap gesture on the touch screen.
+ */
+ public static final int GESTURE_2_FINGER_SINGLE_TAP = 19;
+
+ /**
+ * The user has performed a two-finger double tap gesture on the touch screen.
+ */
+ public static final int GESTURE_2_FINGER_DOUBLE_TAP = 20;
+
+ /**
+ * The user has performed a two-finger triple tap gesture on the touch screen.
+ */
+ public static final int GESTURE_2_FINGER_TRIPLE_TAP = 21;
+
+ /**
+ * The user has performed a three-finger single tap gesture on the touch screen.
+ */
+ public static final int GESTURE_3_FINGER_SINGLE_TAP = 22;
+
+ /**
+ * The user has performed a three-finger double tap gesture on the touch screen.
+ */
+ public static final int GESTURE_3_FINGER_DOUBLE_TAP = 23;
+
+ /**
+ * The user has performed a three-finger triple tap gesture on the touch screen.
+ */
+ public static final int GESTURE_3_FINGER_TRIPLE_TAP = 24;
+
+ /**
+ * The user has performed a two-finger swipe up gesture on the touch screen.
+ */
+ public static final int GESTURE_2_FINGER_SWIPE_UP = 25;
+
+ /**
+ * The user has performed a two-finger swipe down gesture on the touch screen.
+ */
+ public static final int GESTURE_2_FINGER_SWIPE_DOWN = 26;
+
+ /**
+ * The user has performed a two-finger swipe left gesture on the touch screen.
+ */
+ public static final int GESTURE_2_FINGER_SWIPE_LEFT = 27;
+
+ /**
+ * The user has performed a two-finger swipe right gesture on the touch screen.
+ */
+ public static final int GESTURE_2_FINGER_SWIPE_RIGHT = 28;
+
+ /**
+ * The user has performed a three-finger swipe up gesture on the touch screen.
+ */
+ public static final int GESTURE_3_FINGER_SWIPE_UP = 29;
+
+ /**
+ * The user has performed a three-finger swipe down gesture on the touch screen.
+ */
+ public static final int GESTURE_3_FINGER_SWIPE_DOWN = 30;
+
+ /**
+ * The user has performed a three-finger swipe left gesture on the touch screen.
+ */
+ public static final int GESTURE_3_FINGER_SWIPE_LEFT = 31;
+
+ /**
+ * The user has performed a three-finger swipe right gesture on the touch screen.
+ */
+ public static final int GESTURE_3_FINGER_SWIPE_RIGHT = 32;
+
+ /** The user has performed a four-finger swipe up gesture on the touch screen. */
+ public static final int GESTURE_4_FINGER_SWIPE_UP = 33;
+
+ /** The user has performed a four-finger swipe down gesture on the touch screen. */
+ public static final int GESTURE_4_FINGER_SWIPE_DOWN = 34;
+
+ /** The user has performed a four-finger swipe left gesture on the touch screen. */
+ public static final int GESTURE_4_FINGER_SWIPE_LEFT = 35;
+
+ /** The user has performed a four-finger swipe right gesture on the touch screen. */
+ public static final int GESTURE_4_FINGER_SWIPE_RIGHT = 36;
+
+ /** The user has performed a four-finger single tap gesture on the touch screen. */
+ public static final int GESTURE_4_FINGER_SINGLE_TAP = 37;
+
+ /** The user has performed a four-finger double tap gesture on the touch screen. */
+ public static final int GESTURE_4_FINGER_DOUBLE_TAP = 38;
+
+ /** The user has performed a four-finger triple tap gesture on the touch screen. */
+ public static final int GESTURE_4_FINGER_TRIPLE_TAP = 39;
+
+ /** The user has performed a two-finger double tap and hold gesture on the touch screen. */
+ public static final int GESTURE_2_FINGER_DOUBLE_TAP_AND_HOLD = 40;
+
+ /** The user has performed a three-finger double tap and hold gesture on the touch screen. */
+ public static final int GESTURE_3_FINGER_DOUBLE_TAP_AND_HOLD = 41;
+
+ /** The user has performed a two-finger double tap and hold gesture on the touch screen. */
+ public static final int GESTURE_4_FINGER_DOUBLE_TAP_AND_HOLD = 42;
+
+ /**
+ * 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 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;
+
+ /**
+ * Action to lock the screen
+ */
+ public static final int GLOBAL_ACTION_LOCK_SCREEN = 8;
+
+ /**
+ * Action to take a screenshot
+ */
+ public static final int GLOBAL_ACTION_TAKE_SCREENSHOT = 9;
+
+ /**
+ * Action to send the KEYCODE_HEADSETHOOK KeyEvent, which is used to answer/hang up calls and
+ * play/stop media
+ * @hide
+ */
+ public static final int GLOBAL_ACTION_KEYCODE_HEADSETHOOK = 10;
+
+ /**
+ * Action to trigger the Accessibility Button
+ * @hide
+ */
+ public static final int GLOBAL_ACTION_ACCESSIBILITY_BUTTON = 11;
+
+ /**
+ * Action to bring up the Accessibility Button's chooser menu
+ * @hide
+ */
+ public static final int GLOBAL_ACTION_ACCESSIBILITY_BUTTON_CHOOSER = 12;
+
+ private static final String LOG_TAG = "AccessibilityService";
+
+ /**
+ * Interface used by IAccessibilityServiceClientWrapper 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);
+ /** The detected gesture information for different displays */
+ boolean onGesture(AccessibilityGestureEvent gestureInfo);
+ boolean onKeyEvent(KeyEvent event);
+ /** Magnification changed callbacks for different displays */
+ void onMagnificationChanged(int displayId, @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);
+ /** Accessbility button clicked callbacks for different displays */
+ void onAccessibilityButtonClicked(int displayId);
+ void onAccessibilityButtonAvailabilityChanged(boolean available);
+ /** This is called when the system action list is changed. */
+ void onSystemActionsChanged();
+ }
+
+ /**
+ * Annotations for Soft Keyboard show modes so tools can catch invalid show modes.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "SHOW_MODE_" }, value = {
+ SHOW_MODE_AUTO,
+ SHOW_MODE_HIDDEN,
+ SHOW_MODE_IGNORE_HARD_KEYBOARD
+ })
+ public @interface SoftKeyboardShowMode {}
+
+ /**
+ * Allow the system to control when the soft keyboard is shown.
+ * @see SoftKeyboardController
+ */
+ public static final int SHOW_MODE_AUTO = 0;
+
+ /**
+ * Never show the soft keyboard.
+ * @see SoftKeyboardController
+ */
+ public static final int SHOW_MODE_HIDDEN = 1;
+
+ /**
+ * Allow the soft keyboard to be shown, even if a hard keyboard is connected
+ * @see SoftKeyboardController
+ */
+ public static final int SHOW_MODE_IGNORE_HARD_KEYBOARD = 2;
+
+ /**
+ * Mask used to cover the show modes supported in public API
+ * @hide
+ */
+ public static final int SHOW_MODE_MASK = 0x03;
+
+ /**
+ * Bit used to hold the old value of the hard IME setting to restore when a service is shut
+ * down.
+ * @hide
+ */
+ public static final int SHOW_MODE_HARD_KEYBOARD_ORIGINAL_VALUE = 0x20000000;
+
+ /**
+ * Bit for show mode setting to indicate that the user has overridden the hard keyboard
+ * behavior.
+ * @hide
+ */
+ public static final int SHOW_MODE_HARD_KEYBOARD_OVERRIDDEN = 0x40000000;
+
+ /**
+ * Annotations for error codes of taking screenshot.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "TAKE_SCREENSHOT_" }, value = {
+ ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR,
+ ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS,
+ ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT,
+ ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY
+ })
+ public @interface ScreenshotErrorCode {}
+
+ /**
+ * The status of taking screenshot is success.
+ * @hide
+ */
+ public static final int TAKE_SCREENSHOT_SUCCESS = 0;
+
+ /**
+ * The status of taking screenshot is failure and the reason is internal error.
+ */
+ public static final int ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR = 1;
+
+ /**
+ * The status of taking screenshot is failure and the reason is no accessibility access.
+ */
+ public static final int ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS = 2;
+
+ /**
+ * The status of taking screenshot is failure and the reason is that too little time has
+ * elapsed since the last screenshot.
+ */
+ public static final int ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT = 3;
+
+ /**
+ * The status of taking screenshot is failure and the reason is invalid display Id.
+ */
+ public static final int ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY = 4;
+
+ /**
+ * The interval time of calling
+ * {@link AccessibilityService#takeScreenshot(int, Executor, Consumer)} API.
+ * @hide
+ */
+ @TestApi
+ public static final int ACCESSIBILITY_TAKE_SCREENSHOT_REQUEST_INTERVAL_TIMES_MS = 1000;
+
+ /** @hide */
+ public static final String KEY_ACCESSIBILITY_SCREENSHOT_STATUS =
+ "screenshot_status";
+
+ /** @hide */
+ public static final String KEY_ACCESSIBILITY_SCREENSHOT_HARDWAREBUFFER =
+ "screenshot_hardwareBuffer";
+
+ /** @hide */
+ public static final String KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE =
+ "screenshot_colorSpace";
+
+ /** @hide */
+ public static final String KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP =
+ "screenshot_timestamp";
+
+ private int mConnectionId = AccessibilityInteractionClient.NO_ID;
+
+ @UnsupportedAppUsage
+ private AccessibilityServiceInfo mInfo;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private IBinder mWindowToken;
+
+ private WindowManager mWindowManager;
+
+ /** List of magnification controllers, mapping from displayId -> MagnificationController. */
+ private final SparseArray<MagnificationController> mMagnificationControllers =
+ new SparseArray<>(0);
+ private SoftKeyboardController mSoftKeyboardController;
+ private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers =
+ new SparseArray<>(0);
+
+ 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() {
+ synchronized (mLock) {
+ for (int i = 0; i < mMagnificationControllers.size(); i++) {
+ mMagnificationControllers.valueAt(i).onServiceConnectedLocked();
+ }
+ }
+ 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 {@link #onGesture(AccessibilityGestureEvent)} when the user performs a specific
+ * gesture on the default display.
+ *
+ * <strong>Note:</strong> To receive gestures an accessibility service must
+ * request that the device is in touch exploration mode by setting the
+ * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE}
+ * flag.
+ *
+ * @param gestureId The unique id of the performed gesture.
+ *
+ * @return Whether the gesture was handled.
+ * @deprecated Override {@link #onGesture(AccessibilityGestureEvent)} instead.
+ *
+ * @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
+ */
+ @Deprecated
+ protected boolean onGesture(int gestureId) {
+ return false;
+ }
+
+ /**
+ * Called by the system when the user performs a specific gesture on the
+ * specific touch screen.
+ *<p>
+ * <strong>Note:</strong> To receive gestures an accessibility service must
+ * request that the device is in touch exploration mode by setting the
+ * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE}
+ * flag.
+ *<p>
+ * <strong>Note:</strong> The default implementation calls {@link #onGesture(int)} when the
+ * touch screen is default display.
+ *
+ * @param gestureEvent The information of gesture.
+ *
+ * @return Whether the gesture was handled.
+ *
+ */
+ public boolean onGesture(@NonNull AccessibilityGestureEvent gestureEvent) {
+ if (gestureEvent.getDisplayId() == Display.DEFAULT_DISPLAY) {
+ onGesture(gestureEvent.getGestureId());
+ }
+ 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 of the default display. 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 on top 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 windows on the screen of all displays. 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 on top 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 of all displays if there are windows and the service is can retrieve
+ * them, otherwise an empty list. The key of SparseArray is display ID.
+ */
+ @NonNull
+ public final SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays() {
+ return AccessibilityInteractionClient.getInstance().getWindowsOnAllDisplays(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. It could be from any logical display.
+ * <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);
+ }
+ }
+ }
+
+ @Override
+ public Context createDisplayContext(Display display) {
+ final Context context = super.createDisplayContext(display);
+ final int displayId = display.getDisplayId();
+ setDefaultTokenInternal(context, displayId);
+ return context;
+ }
+
+ private void setDefaultTokenInternal(Context context, int displayId) {
+ final WindowManagerImpl wm = (WindowManagerImpl) context.getSystemService(WINDOW_SERVICE);
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId);
+ IBinder token = null;
+ if (connection != null) {
+ synchronized (mLock) {
+ try {
+ token = connection.getOverlayWindowToken(displayId);
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to get window token", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ wm.setDefaultToken(token);
+ }
+ }
+
+ /**
+ * 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() {
+ return getMagnificationController(Display.DEFAULT_DISPLAY);
+ }
+
+ /**
+ * Returns the magnification controller of specified logical display, 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}.
+ *
+ * @param displayId The logic display id, use {@link Display#DEFAULT_DISPLAY} for
+ * default display.
+ * @return the magnification controller
+ *
+ * @hide
+ */
+ @NonNull
+ public final MagnificationController getMagnificationController(int displayId) {
+ synchronized (mLock) {
+ MagnificationController controller = mMagnificationControllers.get(displayId);
+ if (controller == null) {
+ controller = new MagnificationController(this, mLock, displayId);
+ mMagnificationControllers.put(displayId, controller);
+ }
+ return controller;
+ }
+ }
+
+ /**
+ * 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, 16);
+ 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.dispatchGesture(mGestureStatusCallbackSequence,
+ new ParceledListSlice<>(steps), gesture.getDisplayId());
+ }
+ } 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);
+ mGestureStatusCallbackInfos.remove(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(int displayId, @NonNull Region region, float scale,
+ float centerX, float centerY) {
+ MagnificationController controller;
+ synchronized (mLock) {
+ controller = mMagnificationControllers.get(displayId);
+ }
+ if (controller != null) {
+ controller.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;
+ private final int mDisplayId;
+
+ /**
+ * 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,
+ int displayId) {
+ mService = service;
+ mLock = lock;
+ mDisplayId = displayId;
+ }
+
+ /**
+ * Called when the service is connected.
+ */
+ void onServiceConnectedLocked() {
+ 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(mDisplayId, 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(mDisplayId);
+ } 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(mDisplayId);
+ } 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(mDisplayId);
+ } 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(mDisplayId);
+ } 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(mDisplayId, 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 <= 8
+ * @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(mDisplayId,
+ 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(mDisplayId,
+ 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, query, and listen for changes to the soft keyboard show mode.
+ * <p>
+ * Accessibility services may request to override the decisions normally made about whether or
+ * not the soft keyboard is shown.
+ * <p>
+ * If multiple services make conflicting requests, the last request is honored. A service may
+ * register a listener to find out if the mode has changed under it.
+ * <p>
+ * If the user takes action to override the behavior behavior requested by an accessibility
+ * service, the user's request takes precendence, the show mode will be reset to
+ * {@link AccessibilityService#SHOW_MODE_AUTO}, and services will no longer be able to control
+ * that aspect of the soft keyboard's behavior.
+ * <p>
+ * Note: Because soft keyboards are independent apps, the framework does not have total control
+ * over their behavior. They may choose to show themselves, or not, without regard to requests
+ * made here. So the framework will make a best effort to deliver the behavior requested, but
+ * cannot guarantee success.
+ *
+ * @see AccessibilityService#SHOW_MODE_AUTO
+ * @see AccessibilityService#SHOW_MODE_HIDDEN
+ * @see AccessibilityService#SHOW_MODE_IGNORE_HARD_KEYBOARD
+ */
+ 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.
+ *
+ * @return the current soft keyboard show mode
+ *
+ * @see AccessibilityService#SHOW_MODE_AUTO
+ * @see AccessibilityService#SHOW_MODE_HIDDEN
+ * @see AccessibilityService#SHOW_MODE_IGNORE_HARD_KEYBOARD
+ */
+ @SoftKeyboardShowMode
+ public int getShowMode() {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.getSoftKeyboardShowMode();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to set soft keyboard behavior", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return SHOW_MODE_AUTO;
+ }
+
+ /**
+ * Sets the soft keyboard show mode.
+ * <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
+ *
+ * @see AccessibilityService#SHOW_MODE_AUTO
+ * @see AccessibilityService#SHOW_MODE_HIDDEN
+ * @see AccessibilityService#SHOW_MODE_IGNORE_HARD_KEYBOARD
+ */
+ 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);
+ }
+
+ /**
+ * Switches the current IME for the user for whom the service is enabled. The change will
+ * persist until the current IME is explicitly changed again, and may persist beyond the
+ * life cycle of the requesting service.
+ *
+ * @param imeId The ID of the input method to make current. This IME must be installed and
+ * enabled.
+ * @return {@code true} if the current input method was successfully switched to the input
+ * method by {@code imeId},
+ * {@code false} if the input method specified is not installed, not enabled, or
+ * otherwise not available to become the current IME
+ *
+ * @see android.view.inputmethod.InputMethodInfo#getId()
+ */
+ public boolean switchToInputMethod(@NonNull String imeId) {
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mService.mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.switchToInputMethod(imeId);
+ } catch (RemoteException re) {
+ throw new RuntimeException(re);
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * 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() {
+ return getAccessibilityButtonController(Display.DEFAULT_DISPLAY);
+ }
+
+ /**
+ * Returns the controller of specified logical display 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>
+ *
+ * @param displayId The logic display id, use {@link Display#DEFAULT_DISPLAY} for default
+ * display.
+ * @return the accessibility button controller for this {@link AccessibilityService}
+ */
+ @NonNull
+ public final AccessibilityButtonController getAccessibilityButtonController(int displayId) {
+ synchronized (mLock) {
+ AccessibilityButtonController controller = mAccessibilityButtonControllers.get(
+ displayId);
+ if (controller == null) {
+ controller = new AccessibilityButtonController(
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId));
+ mAccessibilityButtonControllers.put(displayId, controller);
+ }
+ return controller;
+ }
+ }
+
+ private void onAccessibilityButtonClicked(int displayId) {
+ getAccessibilityButtonController(displayId).dispatchAccessibilityButtonClicked();
+ }
+
+ private void onAccessibilityButtonAvailabilityChanged(boolean available) {
+ getAccessibilityButtonController().dispatchAccessibilityButtonAvailabilityChanged(
+ available);
+ }
+
+ /** This is called when the system action list is changed. */
+ public void onSystemActionsChanged() {
+ }
+
+ /**
+ * Returns a list of system actions available in the system right now.
+ * <p>
+ * System actions that correspond to the global action constants will have matching action IDs.
+ * For example, an with id {@link #GLOBAL_ACTION_BACK} will perform the back action.
+ * </p>
+ * <p>
+ * These actions should be called by {@link #performGlobalAction}.
+ * </p>
+ *
+ * @return A list of available system actions.
+ */
+ public final @NonNull List<AccessibilityAction> getSystemActions() {
+ IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId);
+ if (connection != null) {
+ try {
+ return connection.getSystemActions();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while calling getSystemActions", re);
+ re.rethrowFromSystemServer();
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * 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>
+ * <p>
+ * <strong>Note:</strong> If the view with {@link AccessibilityNodeInfo#FOCUS_INPUT}
+ * is on an embedded view hierarchy which is embedded in a {@link SurfaceView} via
+ * {@link SurfaceView#setChildSurfacePackage}, there is a limitation that this API
+ * won't be able to find the node for the view. It's because views don't know about
+ * the embedded hierarchies. Instead, you could traverse all the nodes to find the
+ * focus.
+ * </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);
+ }
+
+ /**
+ * Takes a screenshot of the specified display and returns it via an
+ * {@link AccessibilityService.ScreenshotResult}. You can use {@link Bitmap#wrapHardwareBuffer}
+ * to construct the bitmap from the ScreenshotResult's payload.
+ * <p>
+ * <strong>Note:</strong> In order to take screenshot your service has
+ * to declare the capability to take screenshot by setting the
+ * {@link android.R.styleable#AccessibilityService_canTakeScreenshot}
+ * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}.
+ * </p>
+ *
+ * @param displayId The logic display id, must be {@link Display#DEFAULT_DISPLAY} for
+ * default display.
+ * @param executor Executor on which to run the callback.
+ * @param callback The callback invoked when taking screenshot has succeeded or failed.
+ * See {@link TakeScreenshotCallback} for details.
+ */
+ public void takeScreenshot(int displayId, @NonNull @CallbackExecutor Executor executor,
+ @NonNull TakeScreenshotCallback callback) {
+ Preconditions.checkNotNull(executor, "executor cannot be null");
+ Preconditions.checkNotNull(callback, "callback cannot be null");
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(
+ mConnectionId);
+ if (connection == null) {
+ sendScreenshotFailure(ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, executor, callback);
+ return;
+ }
+ try {
+ connection.takeScreenshot(displayId, new RemoteCallback((result) -> {
+ final int status = result.getInt(KEY_ACCESSIBILITY_SCREENSHOT_STATUS);
+ if (status != TAKE_SCREENSHOT_SUCCESS) {
+ sendScreenshotFailure(status, executor, callback);
+ return;
+ }
+ final HardwareBuffer hardwareBuffer =
+ result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_HARDWAREBUFFER);
+ final ParcelableColorSpace colorSpace =
+ result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE);
+ final ScreenshotResult screenshot = new ScreenshotResult(hardwareBuffer,
+ colorSpace.getColorSpace(),
+ result.getLong(KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP));
+ sendScreenshotSuccess(screenshot, executor, callback);
+ }));
+ } catch (RemoteException re) {
+ throw new RuntimeException(re);
+ }
+ }
+
+ /**
+ * 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(AccessibilityGestureEvent gestureEvent) {
+ return AccessibilityService.this.onGesture(gestureEvent);
+ }
+
+ @Override
+ public boolean onKeyEvent(KeyEvent event) {
+ return AccessibilityService.this.onKeyEvent(event);
+ }
+
+ @Override
+ public void onMagnificationChanged(int displayId, @NonNull Region region,
+ float scale, float centerX, float centerY) {
+ AccessibilityService.this.onMagnificationChanged(displayId, 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(int displayId) {
+ AccessibilityService.this.onAccessibilityButtonClicked(displayId);
+ }
+
+ @Override
+ public void onAccessibilityButtonAvailabilityChanged(boolean available) {
+ AccessibilityService.this.onAccessibilityButtonAvailabilityChanged(available);
+ }
+
+ @Override
+ public void onSystemActionsChanged() {
+ AccessibilityService.this.onSystemActionsChanged();
+ }
+ });
+ }
+
+ /**
+ * 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 static final int DO_ON_SYSTEM_ACTIONS_CHANGED = 14;
+
+ 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);
+ }
+
+ @Override
+ public void onGesture(AccessibilityGestureEvent gestureInfo) {
+ Message message = mCaller.obtainMessageO(DO_ON_GESTURE, gestureInfo);
+ 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);
+ }
+
+ /** Magnification changed callbacks for different displays */
+ public void onMagnificationChanged(int displayId, @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;
+ args.argi1 = displayId;
+
+ 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));
+ }
+
+ /** Accessibility button clicked callbacks for different displays */
+ public void onAccessibilityButtonClicked(int displayId) {
+ final Message message = mCaller.obtainMessageI(DO_ACCESSIBILITY_BUTTON_CLICKED,
+ displayId);
+ mCaller.sendMessage(message);
+ }
+
+ public void onAccessibilityButtonAvailabilityChanged(boolean available) {
+ final Message message = mCaller.obtainMessageI(
+ DO_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED, (available ? 1 : 0));
+ mCaller.sendMessage(message);
+ }
+
+ /** This is called when the system action list is changed. */
+ public void onSystemActionsChanged() {
+ mCaller.sendMessage(mCaller.obtainMessage(DO_ON_SYSTEM_ACTIONS_CHANGED));
+ }
+
+ @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) {
+ mCallback.onGesture((AccessibilityGestureEvent) message.obj);
+ }
+ 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;
+ final int displayId = args.argi1;
+ args.recycle();
+ mCallback.onMagnificationChanged(displayId, 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(message.arg1);
+ }
+ return;
+ }
+ case DO_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED: {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ final boolean available = (message.arg1 != 0);
+ mCallback.onAccessibilityButtonAvailabilityChanged(available);
+ }
+ return;
+ }
+ case DO_ON_SYSTEM_ACTIONS_CHANGED: {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ mCallback.onSystemActionsChanged();
+ }
+ 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;
+ }
+ }
+
+ private void sendScreenshotSuccess(ScreenshotResult screenshot, Executor executor,
+ TakeScreenshotCallback callback) {
+ executor.execute(() -> callback.onSuccess(screenshot));
+ }
+
+ private void sendScreenshotFailure(@ScreenshotErrorCode int errorCode, Executor executor,
+ TakeScreenshotCallback callback) {
+ executor.execute(() -> callback.onFailure(errorCode));
+ }
+
+ /**
+ * Interface used to report status of taking screenshot.
+ */
+ public interface TakeScreenshotCallback {
+ /** Called when taking screenshot has completed successfully.
+ *
+ * @param screenshot The content of screenshot.
+ */
+ void onSuccess(@NonNull ScreenshotResult screenshot);
+
+ /** Called when taking screenshot has failed. {@code errorCode} will identify the
+ * reason of failure.
+ *
+ * @param errorCode The error code of this operation.
+ */
+ void onFailure(@ScreenshotErrorCode int errorCode);
+ }
+
+ /**
+ * Can be used to construct a bitmap of the screenshot or any other operations for
+ * {@link AccessibilityService#takeScreenshot} API.
+ */
+ public static final class ScreenshotResult {
+ private final @NonNull HardwareBuffer mHardwareBuffer;
+ private final @NonNull ColorSpace mColorSpace;
+ private final long mTimestamp;
+
+ private ScreenshotResult(@NonNull HardwareBuffer hardwareBuffer,
+ @NonNull ColorSpace colorSpace, long timestamp) {
+ Preconditions.checkNotNull(hardwareBuffer, "hardwareBuffer cannot be null");
+ Preconditions.checkNotNull(colorSpace, "colorSpace cannot be null");
+ mHardwareBuffer = hardwareBuffer;
+ mColorSpace = colorSpace;
+ mTimestamp = timestamp;
+ }
+
+ /**
+ * Gets the {@link ColorSpace} identifying a specific organization of colors of the
+ * screenshot.
+ *
+ * @return the color space
+ */
+ @NonNull
+ public ColorSpace getColorSpace() {
+ return mColorSpace;
+ }
+
+ /**
+ * Gets the {@link HardwareBuffer} representing a memory buffer of the screenshot.
+ * <p>
+ * <strong>Note:</strong> The application should call {@link HardwareBuffer#close()} when
+ * the buffer is no longer needed to free the underlying resources.
+ * </p>
+ *
+ * @return the hardware buffer
+ */
+ @NonNull
+ public HardwareBuffer getHardwareBuffer() {
+ return mHardwareBuffer;
+ }
+
+ /**
+ * Gets the timestamp of taking the screenshot.
+ *
+ * @return milliseconds of non-sleep uptime before screenshot since boot and it's from
+ * {@link SystemClock#uptimeMillis()}
+ */
+ public long getTimestamp() {
+ return mTimestamp;
+ };
+ }
+
+ /**
+ * When {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled, this
+ * function requests that touch interactions starting in the specified region of the screen
+ * bypass the gesture detector. There can only be one gesture detection passthrough region per
+ * display. Requesting a new gesture detection passthrough region clears the existing one. To
+ * disable this passthrough and return to the original behavior, pass in an empty region. When
+ * {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is disabled this
+ * function has no effect.
+ *
+ * @param displayId The display on which to set this region.
+ * @param region the region of the screen.
+ */
+ public void setGestureDetectionPassthroughRegion(int displayId, @NonNull Region region) {
+ Preconditions.checkNotNull(region, "region cannot be null");
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId);
+ if (connection != null) {
+ try {
+ connection.setGestureDetectionPassthroughRegion(displayId, region);
+ } catch (RemoteException re) {
+ throw new RuntimeException(re);
+ }
+ }
+ }
+
+ /**
+ * When {@link AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled, this
+ * function requests that touch interactions starting in the specified region of the screen
+ * bypass the touch explorer and go straight to the view hierarchy. There can only be one touch
+ * exploration passthrough region per display. Requesting a new touch explorationpassthrough
+ * region clears the existing one. To disable this passthrough and return to the original
+ * behavior, pass in an empty region. When {@link
+ * AccessibilityServiceInfo#FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is disabled this function has
+ * no effect.
+ *
+ * @param displayId The display on which to set this region.
+ * @param region the region of the screen .
+ */
+ public void setTouchExplorationPassthroughRegion(int displayId, @NonNull Region region) {
+ Preconditions.checkNotNull(region, "region cannot be null");
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance().getConnection(mConnectionId);
+ if (connection != null) {
+ try {
+ connection.setTouchExplorationPassthroughRegion(displayId, region);
+ } catch (RemoteException re) {
+ throw new RuntimeException(re);
+ }
+ }
+ }
+}
diff --git a/android/accessibilityservice/AccessibilityServiceInfo.java b/android/accessibilityservice/AccessibilityServiceInfo.java
new file mode 100644
index 0000000..ca22bf4
--- /dev/null
+++ b/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -0,0 +1,1420 @@
+/*
+ * 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 static android.accessibilityservice.util.AccessibilityUtils.getFilteredHtmlText;
+import static android.accessibilityservice.util.AccessibilityUtils.loadSafeAnimatedImage;
+import static android.content.pm.PackageManager.FEATURE_FINGERPRINT;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
+import android.compat.annotation.UnsupportedAppUsage;
+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.graphics.drawable.Drawable;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+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 com.android.internal.compat.IPlatformCompat;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 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
+ * @attr ref android.R.styleable#AccessibilityService_nonInteractiveUiTimeout
+ * @attr ref android.R.styleable#AccessibilityService_interactiveUiTimeout
+ * @attr ref android.R.styleable#AccessibilityService_canTakeScreenshot
+ * @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;
+
+ /**
+ * Capability: This accessibility service can take screenshot.
+ * @see android.R.styleable#AccessibilityService_canTakeScreenshot
+ */
+ public static final int CAPABILITY_CAN_TAKE_SCREENSHOT = 0x00000080;
+
+ 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 Android 4.1 (API level 16) 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 Android 4.0.4 (API level
+ * 15) or lower, 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 managers 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 Android 4.3 (API level 18) or higher
+ * 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 Android 4.2.2 (API level 17) or lower will work
+ * normally. In other words, 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.
+ * <p>
+ * <strong>Note:</strong> For accessibility services targeting APIs greater than
+ * {@link Build.VERSION_CODES#Q API 29}, this flag must be specified in the
+ * accessibility service metadata file. Otherwise, it will be ignored.
+ * </p>
+ */
+ public static final int FLAG_REQUEST_ACCESSIBILITY_BUTTON = 0x00000100;
+
+ /**
+ * This flag requests that all fingerprint gestures be sent to the accessibility service.
+ * <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#canRequestFingerprintGestures} 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_canRequestFingerprintGestures
+ * @see AccessibilityService#getFingerprintGestureController()
+ */
+ public static final int FLAG_REQUEST_FINGERPRINT_GESTURES = 0x00000200;
+
+ /**
+ * This flag requests that accessibility shortcut warning dialog has spoken feedback when
+ * dialog is shown.
+ */
+ public static final int FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK = 0x00000400;
+
+ /**
+ * This flag requests that when {@link #FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled,
+ * double tap and double tap and hold gestures are dispatched to the service rather than being
+ * handled by the framework. If {@link #FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is disabled this
+ * flag has no effect.
+ *
+ * @see #FLAG_REQUEST_TOUCH_EXPLORATION_MODE
+ */
+ public static final int FLAG_SERVICE_HANDLES_DOUBLE_TAP = 0x0000800;
+
+ /**
+ * This flag requests that when when {@link #FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is enabled,
+ * multi-finger gestures are also enabled. As a consequence, two-finger bypass gestures will be
+ * disabled. If {@link #FLAG_REQUEST_TOUCH_EXPLORATION_MODE} is disabled this flag has no
+ * effect.
+ *
+ * @see #FLAG_REQUEST_TOUCH_EXPLORATION_MODE
+ */
+ public static final int FLAG_REQUEST_MULTI_FINGER_GESTURES = 0x0001000;
+
+ /** {@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;
+
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FEEDBACK_" }, value = {
+ FEEDBACK_AUDIBLE,
+ FEEDBACK_GENERIC,
+ FEEDBACK_HAPTIC,
+ FEEDBACK_SPOKEN,
+ FEEDBACK_VISUAL,
+ FEEDBACK_BRAILLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FeedbackType {}
+
+ /**
+ * 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
+ */
+ @FeedbackType
+ public int feedbackType;
+
+ /**
+ * The timeout, in milliseconds, 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>
+ * <p>
+ * <strong>Note:</strong> Accessibility services with targetSdkVersion greater than
+ * {@link Build.VERSION_CODES#Q API 29} cannot dynamically set the
+ * {@link #FLAG_REQUEST_ACCESSIBILITY_BUTTON} at runtime. It must be specified in the
+ * accessibility service metadata file.
+ * </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
+ * @see #FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK
+ */
+ public int flags;
+
+ /**
+ * Whether or not the service has crashed and is awaiting restart. Only valid from {@link
+ * android.view.accessibility.AccessibilityManager#getInstalledAccessibilityServiceList()},
+ * because that is populated from the internal list of running services.
+ *
+ * @hide
+ */
+ public boolean crashed;
+
+ /**
+ * A recommended timeout in milliseconds for non-interactive controls.
+ */
+ private int mNonInteractiveUiTimeout;
+
+ /**
+ * A recommended timeout in milliseconds for interactive controls.
+ */
+ private int mInteractiveUiTimeout;
+
+ /**
+ * 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;
+
+ /**
+ * For accessibility services targeting APIs greater than {@link Build.VERSION_CODES#Q API 29},
+ * {@link #FLAG_REQUEST_ACCESSIBILITY_BUTTON} must be specified in the accessibility service
+ * metadata file. Otherwise, it will be ignored.
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.Q)
+ private static final long REQUEST_ACCESSIBILITY_BUTTON_CHANGE = 136293963L;
+
+ /**
+ * Resource id of the animated image of the accessibility service.
+ */
+ private int mAnimatedImageRes;
+
+ /**
+ * Resource id of the html description of the accessibility service.
+ */
+ private int mHtmlDescriptionRes;
+
+ /**
+ * 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);
+ mNonInteractiveUiTimeout = asAttributes.getInt(
+ com.android.internal.R.styleable.AccessibilityService_nonInteractiveUiTimeout,
+ 0);
+ mInteractiveUiTimeout = asAttributes.getInt(
+ com.android.internal.R.styleable.AccessibilityService_interactiveUiTimeout,
+ 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;
+ }
+ if (asAttributes.getBoolean(com.android.internal.R.styleable
+ .AccessibilityService_canTakeScreenshot, false)) {
+ mCapabilities |= CAPABILITY_CAN_TAKE_SCREENSHOT;
+ }
+ 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();
+ }
+ }
+ peekedValue = asAttributes.peekValue(
+ com.android.internal.R.styleable.AccessibilityService_animatedImageDrawable);
+ if (peekedValue != null) {
+ mAnimatedImageRes = peekedValue.resourceId;
+ }
+ peekedValue = asAttributes.peekValue(
+ com.android.internal.R.styleable.AccessibilityService_htmlDescription);
+ if (peekedValue != null) {
+ mHtmlDescriptionRes = peekedValue.resourceId;
+ }
+ 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 AccessibilityService can change dynamically.
+ * <p>
+ * Note: A11y services targeting APIs > Q, it cannot update flagRequestAccessibilityButton
+ * dynamically.
+ * </p>
+ *
+ * @param platformCompat The platform compat service to check the compatibility change.
+ * @param other The info from which to update the properties.
+ *
+ * @hide
+ */
+ public void updateDynamicallyConfigurableProperties(IPlatformCompat platformCompat,
+ AccessibilityServiceInfo other) {
+ if (isRequestAccessibilityButtonChangeEnabled(platformCompat)) {
+ other.flags &= ~FLAG_REQUEST_ACCESSIBILITY_BUTTON;
+ other.flags |= (flags & FLAG_REQUEST_ACCESSIBILITY_BUTTON);
+ }
+ eventTypes = other.eventTypes;
+ packageNames = other.packageNames;
+ feedbackType = other.feedbackType;
+ notificationTimeout = other.notificationTimeout;
+ mNonInteractiveUiTimeout = other.mNonInteractiveUiTimeout;
+ mInteractiveUiTimeout = other.mInteractiveUiTimeout;
+ flags = other.flags;
+ }
+
+ private boolean isRequestAccessibilityButtonChangeEnabled(IPlatformCompat platformCompat) {
+ if (mResolveInfo == null) {
+ return true;
+ }
+ try {
+ if (platformCompat != null) {
+ return platformCompat.isChangeEnabled(REQUEST_ACCESSIBILITY_BUTTON_CHANGE,
+ mResolveInfo.serviceInfo.applicationInfo);
+ }
+ } catch (RemoteException ignore) {
+ }
+ return mResolveInfo.serviceInfo.applicationInfo.targetSdkVersion > Build.VERSION_CODES.Q;
+ }
+
+ /**
+ * @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;
+ }
+
+ /**
+ * Gets the animated image resource id.
+ *
+ * @return The animated image resource id.
+ *
+ * @hide
+ */
+ public int getAnimatedImageRes() {
+ return mAnimatedImageRes;
+ }
+
+ /**
+ * The animated image drawable.
+ * <p>
+ * Image can not exceed the screen size.
+ * <strong>Statically set from
+ * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong>
+ * </p>
+ * @return The animated image drawable, or null if the resource is invalid or the image
+ * exceed the screen size.
+ *
+ * @hide
+ */
+ @Nullable
+ public Drawable loadAnimatedImage(@NonNull Context context) {
+ if (mAnimatedImageRes == /* invalid */ 0) {
+ return null;
+ }
+
+ return loadSafeAnimatedImage(context, mResolveInfo.serviceInfo.applicationInfo,
+ mAnimatedImageRes);
+ }
+
+ /**
+ * 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
+ * @see #CAPABILITY_CAN_TAKE_SCREENSHOT
+ */
+ 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
+ * @see #CAPABILITY_CAN_TAKE_SCREENSHOT
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ 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;
+ }
+
+ /**
+ * The localized and restricted html description of the accessibility service.
+ * <p>
+ * Filters the <img> tag which do not meet the custom specification and the <a> tag.
+ * <strong>Statically set from
+ * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong>
+ * </p>
+ * @return The localized and restricted html description.
+ *
+ * @hide
+ */
+ @Nullable
+ public String loadHtmlDescription(@NonNull PackageManager packageManager) {
+ if (mHtmlDescriptionRes == /* invalid */ 0) {
+ return null;
+ }
+
+ final ServiceInfo serviceInfo = mResolveInfo.serviceInfo;
+ final CharSequence htmlDescription = packageManager.getText(serviceInfo.packageName,
+ mHtmlDescriptionRes, serviceInfo.applicationInfo);
+ if (htmlDescription != null) {
+ return getFilteredHtmlText(htmlDescription.toString().trim());
+ }
+ return null;
+ }
+
+ /**
+ * Set the recommended time that non-interactive controls need to remain on the screen to
+ * support the user.
+ * <p>
+ * <strong>This value can be dynamically set at runtime by
+ * {@link AccessibilityService#setServiceInfo(AccessibilityServiceInfo)}.</strong>
+ * </p>
+ *
+ * @param timeout The timeout in milliseconds.
+ *
+ * @see android.R.styleable#AccessibilityService_nonInteractiveUiTimeout
+ */
+ public void setNonInteractiveUiTimeoutMillis(@IntRange(from = 0) int timeout) {
+ mNonInteractiveUiTimeout = timeout;
+ }
+
+ /**
+ * Get the recommended timeout for non-interactive controls.
+ *
+ * @return The timeout in milliseconds.
+ *
+ * @see #setNonInteractiveUiTimeoutMillis(int)
+ */
+ public int getNonInteractiveUiTimeoutMillis() {
+ return mNonInteractiveUiTimeout;
+ }
+
+ /**
+ * Set the recommended time that interactive controls need to remain on the screen to
+ * support the user.
+ * <p>
+ * <strong>This value can be dynamically set at runtime by
+ * {@link AccessibilityService#setServiceInfo(AccessibilityServiceInfo)}.</strong>
+ * </p>
+ *
+ * @param timeout The timeout in milliseconds.
+ *
+ * @see android.R.styleable#AccessibilityService_interactiveUiTimeout
+ */
+ public void setInteractiveUiTimeoutMillis(@IntRange(from = 0) int timeout) {
+ mInteractiveUiTimeout = timeout;
+ }
+
+ /**
+ * Get the recommended timeout for interactive controls.
+ *
+ * @return The timeout in milliseconds.
+ *
+ * @see #setInteractiveUiTimeoutMillis(int)
+ */
+ public int getInteractiveUiTimeoutMillis() {
+ return mInteractiveUiTimeout;
+ }
+
+ /** {@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(mNonInteractiveUiTimeout);
+ parcel.writeInt(mInteractiveUiTimeout);
+ parcel.writeInt(flags);
+ parcel.writeInt(crashed ? 1 : 0);
+ parcel.writeParcelable(mComponentName, flagz);
+ parcel.writeParcelable(mResolveInfo, 0);
+ parcel.writeString(mSettingsActivityName);
+ parcel.writeInt(mCapabilities);
+ parcel.writeInt(mSummaryResId);
+ parcel.writeString(mNonLocalizedSummary);
+ parcel.writeInt(mDescriptionResId);
+ parcel.writeInt(mAnimatedImageRes);
+ parcel.writeInt(mHtmlDescriptionRes);
+ parcel.writeString(mNonLocalizedDescription);
+ }
+
+ private void initFromParcel(Parcel parcel) {
+ eventTypes = parcel.readInt();
+ packageNames = parcel.readStringArray();
+ feedbackType = parcel.readInt();
+ notificationTimeout = parcel.readLong();
+ mNonInteractiveUiTimeout = parcel.readInt();
+ mInteractiveUiTimeout = parcel.readInt();
+ flags = parcel.readInt();
+ crashed = parcel.readInt() != 0;
+ 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();
+ mAnimatedImageRes = parcel.readInt();
+ mHtmlDescriptionRes = 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(", ");
+ stringBuilder.append("nonInteractiveUiTimeout: ").append(mNonInteractiveUiTimeout);
+ stringBuilder.append(", ");
+ stringBuilder.append("interactiveUiTimeout: ").append(mInteractiveUiTimeout);
+ 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,
+ @FeedbackType 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_SERVICE_HANDLES_DOUBLE_TAP:
+ return "FLAG_SERVICE_HANDLES_DOUBLE_TAP";
+ case FLAG_REQUEST_MULTI_FINGER_GESTURES:
+ return "FLAG_REQUEST_MULTI_FINGER_GESTURES";
+ 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";
+ case FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK:
+ return "FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK";
+ 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";
+ case CAPABILITY_CAN_TAKE_SCREENSHOT:
+ return "CAPABILITY_CAN_TAKE_SCREENSHOT";
+ 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));
+ sAvailableCapabilityInfos.put(CAPABILITY_CAN_TAKE_SCREENSHOT,
+ new CapabilityInfo(CAPABILITY_CAN_TAKE_SCREENSHOT,
+ R.string.capability_title_canTakeScreenshot,
+ R.string.capability_desc_canTakeScreenshot));
+ 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 @android.annotation.NonNull 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/AccessibilityShortcutInfo.java b/android/accessibilityservice/AccessibilityShortcutInfo.java
new file mode 100644
index 0000000..623734e
--- /dev/null
+++ b/android/accessibilityservice/AccessibilityShortcutInfo.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2019 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 static android.accessibilityservice.util.AccessibilityUtils.getFilteredHtmlText;
+import static android.accessibilityservice.util.AccessibilityUtils.loadSafeAnimatedImage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Activities of interest to users with accessibility needs may request to be targets of the
+ * accessibility shortcut. These activities must handle the
+ * {@link Intent#ACTION_MAIN} intent with category
+ * {@link Intent#CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET}, which will be dispatched by the system
+ * when the user activates the shortcut when it is configured to point at this target.
+ *
+ * @see Intent#CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET
+ *
+ * @hide
+ */
+public final class AccessibilityShortcutInfo {
+ private static final String TAG_ACCESSIBILITY_SHORTCUT = "accessibility-shortcut-target";
+
+ /**
+ * Name under which an activity component of the accessibility shortcut publishes information
+ * about itself. This meta-data must reference an XML resource containing an
+ * <code><accessibility-shortcut-target></code> tag.
+ */
+ public static final String META_DATA = "android.accessibilityshortcut.target";
+
+ /**
+ * The component name of the accessibility shortcut target.
+ */
+ private final ComponentName mComponentName;
+
+ /**
+ * The activity info of the accessibility shortcut target.
+ */
+ private final ActivityInfo mActivityInfo;
+
+ /**
+ * Resource id of the summary of the accessibility shortcut target.
+ */
+ private final int mSummaryResId;
+
+ /**
+ * Resource id of the description of the accessibility shortcut target.
+ */
+ private final int mDescriptionResId;
+
+ /**
+ * Resource id of the animated image of the accessibility shortcut target.
+ */
+ private final int mAnimatedImageRes;
+
+ /**
+ * Resource id of the html description of the accessibility shortcut target.
+ */
+ private final int mHtmlDescriptionRes;
+
+ /**
+ * The accessibility shortcut target setting activity's name, used by the system
+ * settings to launch the setting activity of this accessibility shortcut target.
+ */
+ private String mSettingsActivityName;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param context Context for accessing resources.
+ * @param activityInfo The activity info.
+ * @throws XmlPullParserException If a XML parsing error occurs.
+ * @throws IOException If a XML parsing error occurs.
+ */
+ public AccessibilityShortcutInfo(@NonNull Context context, @NonNull ActivityInfo activityInfo)
+ throws XmlPullParserException, IOException {
+ final PackageManager packageManager = context.getPackageManager();
+ mComponentName = activityInfo.getComponentName();
+ mActivityInfo = activityInfo;
+
+ try (XmlResourceParser parser = mActivityInfo.loadXmlMetaData(
+ packageManager, META_DATA)) {
+ if (parser == null) {
+ throw new XmlPullParserException("Meta-data "
+ + TAG_ACCESSIBILITY_SHORTCUT + " does not exist");
+ }
+
+ int type = 0;
+ while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+ type = parser.next();
+ }
+
+ final String nodeName = parser.getName();
+ if (!TAG_ACCESSIBILITY_SHORTCUT.equals(nodeName)) {
+ throw new XmlPullParserException("Meta-data does not start with"
+ + TAG_ACCESSIBILITY_SHORTCUT + " tag");
+ }
+
+ final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+ final Resources resources = packageManager.getResourcesForApplication(
+ mActivityInfo.applicationInfo);
+ final TypedArray asAttributes = resources.obtainAttributes(allAttributes,
+ com.android.internal.R.styleable.AccessibilityShortcutTarget);
+
+ // Gets description
+ mDescriptionResId = asAttributes.getResourceId(
+ com.android.internal.R.styleable.AccessibilityShortcutTarget_description, 0);
+ // Gets summary
+ mSummaryResId = asAttributes.getResourceId(
+ com.android.internal.R.styleable.AccessibilityShortcutTarget_summary, 0);
+ // Gets animated image
+ mAnimatedImageRes = asAttributes.getResourceId(
+ com.android.internal.R.styleable
+ .AccessibilityShortcutTarget_animatedImageDrawable, /* defValue= */ 0);
+ // Gets html description
+ mHtmlDescriptionRes = asAttributes.getResourceId(
+ com.android.internal.R.styleable.AccessibilityShortcutTarget_htmlDescription,
+ 0);
+ // Get settings activity name
+ mSettingsActivityName = asAttributes.getString(
+ com.android.internal.R.styleable.AccessibilityShortcutTarget_settingsActivity);
+ asAttributes.recycle();
+
+ if ((mDescriptionResId == 0 && mHtmlDescriptionRes == 0) || mSummaryResId == 0) {
+ throw new XmlPullParserException("No description or summary in meta-data");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new XmlPullParserException("Unable to create context for: "
+ + mActivityInfo.packageName);
+ }
+ }
+
+ /**
+ * The {@link ActivityInfo} of accessibility shortcut target.
+ *
+ * @return The activity info.
+ */
+ @NonNull
+ public ActivityInfo getActivityInfo() {
+ return mActivityInfo;
+ }
+
+ /**
+ * The {@link ComponentName} of the accessibility shortcut target.
+ *
+ * @return The component name
+ */
+ @NonNull
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ /**
+ * The localized summary of the accessibility shortcut target.
+ *
+ * @return The localized summary if available, and {@code null} if a summary
+ * has not been provided.
+ */
+ @Nullable
+ public String loadSummary(@NonNull PackageManager packageManager) {
+ return loadResourceString(packageManager, mActivityInfo, mSummaryResId);
+ }
+
+ /**
+ * The localized description of the accessibility shortcut target.
+ *
+ * @return The localized description.
+ */
+ @Nullable
+ public String loadDescription(@NonNull PackageManager packageManager) {
+ return loadResourceString(packageManager, mActivityInfo, mDescriptionResId);
+ }
+
+ /**
+ * Gets the animated image resource id.
+ *
+ * @return The animated image resource id.
+ *
+ * @hide
+ */
+ public int getAnimatedImageRes() {
+ return mAnimatedImageRes;
+ }
+
+ /**
+ * The animated image drawable of the accessibility shortcut target.
+ *
+ * @return The animated image drawable, or null if the resource is invalid or the image
+ * exceed the screen size.
+ *
+ * @hide
+ */
+ @Nullable
+ public Drawable loadAnimatedImage(@NonNull Context context) {
+ if (mAnimatedImageRes == /* invalid */ 0) {
+ return null;
+ }
+
+ return loadSafeAnimatedImage(context, mActivityInfo.applicationInfo, mAnimatedImageRes);
+ }
+
+ /**
+ * The localized and restricted html description of the accessibility shortcut target.
+ * It filters the <img> tag which do not meet the custom specification and the <a> tag.
+ *
+ * @return The localized and restricted html description.
+ *
+ * @hide
+ */
+ @Nullable
+ public String loadHtmlDescription(@NonNull PackageManager packageManager) {
+ final String htmlDescription = loadResourceString(packageManager, mActivityInfo,
+ mHtmlDescriptionRes);
+ if (htmlDescription != null) {
+ return getFilteredHtmlText(htmlDescription);
+ }
+ return null;
+ }
+
+ /**
+ * The settings activity name.
+ *
+ * @return The settings activity name.
+ */
+ @Nullable
+ public String getSettingsActivityName() {
+ return mSettingsActivityName;
+ }
+
+ /**
+ * Gets string resource by the given activity and resource id.
+ */
+ @Nullable
+ private String loadResourceString(@NonNull PackageManager packageManager,
+ @NonNull ActivityInfo activityInfo, int resId) {
+ if (resId == 0) {
+ return null;
+ }
+ final CharSequence text = packageManager.getText(activityInfo.packageName,
+ resId, activityInfo.applicationInfo);
+ if (text != null) {
+ return text.toString().trim();
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return 31 * 1 + ((mComponentName == null) ? 0 : mComponentName.hashCode());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final AccessibilityShortcutInfo other = (AccessibilityShortcutInfo) obj;
+ if (mComponentName == null) {
+ if (other.mComponentName != null) {
+ return false;
+ }
+ } else if (!mComponentName.equals(other.mComponentName)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("AccessibilityShortcutInfo[");
+ stringBuilder.append("activityInfo: ").append(mActivityInfo);
+ stringBuilder.append("]");
+ return stringBuilder.toString();
+ }
+}
diff --git a/android/accessibilityservice/FingerprintGestureController.java b/android/accessibilityservice/FingerprintGestureController.java
new file mode 100644
index 0000000..c30030d
--- /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 0000000..a821dad
--- /dev/null
+++ b/android/accessibilityservice/GestureDescription.java
@@ -0,0 +1,603 @@
+/*
+ * 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 android.view.Display;
+
+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 and will be dispatched to the specified display.
+ * <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 = 20;
+
+ /**
+ * 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];
+ private final int mDisplayId;
+
+ /**
+ * 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() {
+ this(new ArrayList<>());
+ }
+
+ private GestureDescription(List<StrokeDescription> strokes) {
+ this(strokes, Display.DEFAULT_DISPLAY);
+ }
+
+ private GestureDescription(List<StrokeDescription> strokes, int displayId) {
+ mStrokes.addAll(strokes);
+ mDisplayId = displayId;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Returns the ID of the display this gesture is sent on, for use with
+ * {@link android.hardware.display.DisplayManager#getDisplay(int)}.
+ *
+ * @return The logical display id.
+ */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /**
+ * 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<>();
+ private int mDisplayId = Display.DEFAULT_DISPLAY;
+
+ /**
+ * Adds 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. Maximum is "
+ + MAX_STROKE_COUNT
+ + ", got "
+ + mStrokes.size());
+ }
+
+ 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;
+ }
+
+ /**
+ * Sets the id of the display to dispatch gestures.
+ *
+ * @param displayId The logical display id
+ *
+ * @return this
+ */
+ public @NonNull Builder setDisplayId(int displayId) {
+ mDisplayId = displayId;
+ return this;
+ }
+
+ public GestureDescription build() {
+ if (mStrokes.size() == 0) {
+ throw new IllegalStateException("Gestures must have at least one stroke");
+ }
+ return new GestureDescription(mStrokes, mDisplayId);
+ }
+ }
+
+ /**
+ * 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 String toString() {
+ return "TouchPoint{"
+ + "mStrokeId=" + mStrokeId
+ + ", mContinuedStrokeId=" + mContinuedStrokeId
+ + ", mIsStartOfPath=" + mIsStartOfPath
+ + ", mIsEndOfPath=" + mIsEndOfPath
+ + ", mX=" + mX
+ + ", mY=" + 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 @android.annotation.NonNull 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 @android.annotation.NonNull 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;
+ }
+ }
+}
diff --git a/android/accessibilityservice/util/AccessibilityUtils.java b/android/accessibilityservice/util/AccessibilityUtils.java
new file mode 100644
index 0000000..fa32bb2
--- /dev/null
+++ b/android/accessibilityservice/util/AccessibilityUtils.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 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.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Collection of utilities for accessibility service.
+ *
+ * @hide
+ */
+public final class AccessibilityUtils {
+ private AccessibilityUtils() {}
+
+ // Used for html description of accessibility service. The <img> src tag must follow the
+ // prefix rule. e.g. <img src="R.drawable.fileName"/>
+ private static final String IMG_PREFIX = "R.drawable.";
+ private static final String ANCHOR_TAG = "a";
+ private static final List<String> UNSUPPORTED_TAG_LIST = new ArrayList<>(
+ Collections.singletonList(ANCHOR_TAG));
+
+ /**
+ * Gets the filtered html string for
+ * {@link android.accessibilityservice.AccessibilityServiceInfo} and
+ * {@link android.accessibilityservice.AccessibilityShortcutInfo}. It filters
+ * the <img> tag which do not meet the custom specification and the <a> tag.
+ *
+ * @param text the target text is html format.
+ * @return the filtered html string.
+ */
+ public static @NonNull String getFilteredHtmlText(@NonNull String text) {
+ final String replacementStart = "<invalidtag ";
+ final String replacementEnd = "</invalidtag>";
+
+ for (String tag : UNSUPPORTED_TAG_LIST) {
+ final String regexStart = "(?i)<" + tag + "(\\s+|>)";
+ final String regexEnd = "(?i)</" + tag + "\\s*>";
+ text = Pattern.compile(regexStart).matcher(text).replaceAll(replacementStart);
+ text = Pattern.compile(regexEnd).matcher(text).replaceAll(replacementEnd);
+ }
+
+ final String regexInvalidImgTag = "(?i)<img\\s+(?!src\\s*=\\s*\"(?-i)" + IMG_PREFIX + ")";
+ text = Pattern.compile(regexInvalidImgTag).matcher(text).replaceAll(
+ replacementStart);
+
+ return text;
+ }
+
+ /**
+ * Loads the animated image for
+ * {@link android.accessibilityservice.AccessibilityServiceInfo} and
+ * {@link android.accessibilityservice.AccessibilityShortcutInfo}. It checks the resource
+ * whether to exceed the screen size.
+ *
+ * @param context the current context.
+ * @param applicationInfo the current application.
+ * @param resId the animated image resource id.
+ * @return the animated image which is safe.
+ */
+ @Nullable
+ public static Drawable loadSafeAnimatedImage(@NonNull Context context,
+ @NonNull ApplicationInfo applicationInfo, @StringRes int resId) {
+ if (resId == /* invalid */ 0) {
+ return null;
+ }
+
+ final PackageManager packageManager = context.getPackageManager();
+ final String packageName = applicationInfo.packageName;
+ final Drawable bannerDrawable = packageManager.getDrawable(packageName, resId,
+ applicationInfo);
+ if (bannerDrawable == null) {
+ return null;
+ }
+
+ final boolean isImageWidthOverScreenLength =
+ bannerDrawable.getIntrinsicWidth() > getScreenWidthPixels(context);
+ final boolean isImageHeightOverScreenLength =
+ bannerDrawable.getIntrinsicHeight() > getScreenHeightPixels(context);
+
+ return (isImageWidthOverScreenLength || isImageHeightOverScreenLength)
+ ? null
+ : bannerDrawable;
+ }
+
+ /**
+ * Gets the width of the screen.
+ *
+ * @param context the current context.
+ * @return the width of the screen in term of pixels.
+ */
+ private static int getScreenWidthPixels(@NonNull Context context) {
+ final Resources resources = context.getResources();
+ final int screenWidthDp = resources.getConfiguration().screenWidthDp;
+
+ return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenWidthDp,
+ resources.getDisplayMetrics()));
+ }
+
+ /**
+ * Gets the height of the screen.
+ *
+ * @param context the current context.
+ * @return the height of the screen in term of pixels.
+ */
+ private static int getScreenHeightPixels(@NonNull Context context) {
+ final Resources resources = context.getResources();
+ final int screenHeightDp = resources.getConfiguration().screenHeightDp;
+
+ return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenHeightDp,
+ resources.getDisplayMetrics()));
+ }
+}
diff --git a/android/accounts/AbstractAccountAuthenticator.java b/android/accounts/AbstractAccountAuthenticator.java
new file mode 100644
index 0000000..6470a04
--- /dev/null
+++ b/android/accounts/AbstractAccountAuthenticator.java
@@ -0,0 +1,995 @@
+/*
+ * 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.accounts;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Arrays;
+
+/**
+ * Abstract base class for creating AccountAuthenticators.
+ * In order to be an authenticator one must extend this class, provide implementations for the
+ * abstract methods, and write a service that returns the result of {@link #getIBinder()}
+ * in the service's {@link android.app.Service#onBind(android.content.Intent)} when invoked
+ * with an intent with action {@link AccountManager#ACTION_AUTHENTICATOR_INTENT}. This service
+ * must specify the following intent filter and metadata tags in its AndroidManifest.xml file
+ * <pre>
+ * <intent-filter>
+ * <action android:name="android.accounts.AccountAuthenticator" />
+ * </intent-filter>
+ * <meta-data android:name="android.accounts.AccountAuthenticator"
+ * android:resource="@xml/authenticator" />
+ * </pre>
+ * The <code>android:resource</code> attribute must point to a resource that looks like:
+ * <pre>
+ * <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:accountType="typeOfAuthenticator"
+ * android:icon="@drawable/icon"
+ * android:smallIcon="@drawable/miniIcon"
+ * android:label="@string/label"
+ * android:accountPreferences="@xml/account_preferences"
+ * />
+ * </pre>
+ * Replace the icons and labels with your own resources. The <code>android:accountType</code>
+ * attribute must be a string that uniquely identifies your authenticator and will be the same
+ * string that user will use when making calls on the {@link AccountManager} and it also
+ * corresponds to {@link Account#type} for your accounts. One user of the android:icon is the
+ * "Account & Sync" settings page and one user of the android:smallIcon is the Contact Application's
+ * tab panels.
+ * <p>
+ * The preferences attribute points to a PreferenceScreen xml hierarchy that contains
+ * a list of PreferenceScreens that can be invoked to manage the authenticator. An example is:
+ * <pre>
+ * <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+ * <PreferenceCategory android:title="@string/title_fmt" />
+ * <PreferenceScreen
+ * android:key="key1"
+ * android:title="@string/key1_action"
+ * android:summary="@string/key1_summary">
+ * <intent
+ * android:action="key1.ACTION"
+ * android:targetPackage="key1.package"
+ * android:targetClass="key1.class" />
+ * </PreferenceScreen>
+ * </PreferenceScreen>
+ * </pre>
+ *
+ * <p>
+ * The standard pattern for implementing any of the abstract methods is the following:
+ * <ul>
+ * <li> If the supplied arguments are enough for the authenticator to fully satisfy the request
+ * then it will do so and return a {@link Bundle} that contains the results.
+ * <li> If the authenticator needs information from the user to satisfy the request then it
+ * will create an {@link Intent} to an activity that will prompt the user for the information
+ * and then carry out the request. This intent must be returned in a Bundle as key
+ * {@link AccountManager#KEY_INTENT}.
+ * <p>
+ * The activity needs to return the final result when it is complete so the Intent should contain
+ * the {@link AccountAuthenticatorResponse} as {@link AccountManager#KEY_ACCOUNT_MANAGER_RESPONSE}.
+ * The activity must then call {@link AccountAuthenticatorResponse#onResult} or
+ * {@link AccountAuthenticatorResponse#onError} when it is complete.
+ * <li> If the authenticator cannot synchronously process the request and return a result then it
+ * may choose to return null and then use the AccountManagerResponse to send the result
+ * when it has completed the request.
+ * </ul>
+ * <p>
+ * The following descriptions of each of the abstract authenticator methods will not describe the
+ * possible asynchronous nature of the request handling and will instead just describe the input
+ * parameters and the expected result.
+ * <p>
+ * When writing an activity to satisfy these requests one must pass in the AccountManagerResponse
+ * and return the result via that response when the activity finishes (or whenever else the
+ * activity author deems it is the correct time to respond).
+ */
+public abstract class AbstractAccountAuthenticator {
+ private static final String TAG = "AccountAuthenticator";
+
+ /**
+ * Bundle key used for the {@code long} expiration time (in millis from the unix epoch) of the
+ * associated auth token.
+ *
+ * @see #getAuthToken
+ */
+ public static final String KEY_CUSTOM_TOKEN_EXPIRY = "android.accounts.expiry";
+
+ /**
+ * Bundle key used for the {@link String} account type in session bundle.
+ * This is used in the default implementation of
+ * {@link #startAddAccountSession} and {@link startUpdateCredentialsSession}.
+ */
+ private static final String KEY_AUTH_TOKEN_TYPE =
+ "android.accounts.AbstractAccountAuthenticato.KEY_AUTH_TOKEN_TYPE";
+ /**
+ * Bundle key used for the {@link String} array of required features in
+ * session bundle. This is used in the default implementation of
+ * {@link #startAddAccountSession} and {@link startUpdateCredentialsSession}.
+ */
+ private static final String KEY_REQUIRED_FEATURES =
+ "android.accounts.AbstractAccountAuthenticator.KEY_REQUIRED_FEATURES";
+ /**
+ * Bundle key used for the {@link Bundle} options in session bundle. This is
+ * used in default implementation of {@link #startAddAccountSession} and
+ * {@link startUpdateCredentialsSession}.
+ */
+ private static final String KEY_OPTIONS =
+ "android.accounts.AbstractAccountAuthenticator.KEY_OPTIONS";
+ /**
+ * Bundle key used for the {@link Account} account in session bundle. This is used
+ * used in default implementation of {@link startUpdateCredentialsSession}.
+ */
+ private static final String KEY_ACCOUNT =
+ "android.accounts.AbstractAccountAuthenticator.KEY_ACCOUNT";
+
+ private final Context mContext;
+
+ public AbstractAccountAuthenticator(Context context) {
+ mContext = context;
+ }
+
+ private class Transport extends IAccountAuthenticator.Stub {
+ @Override
+ public void addAccount(IAccountAuthenticatorResponse response, String accountType,
+ String authTokenType, String[] features, Bundle options)
+ throws RemoteException {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "addAccount: accountType " + accountType
+ + ", authTokenType " + authTokenType
+ + ", features " + (features == null ? "[]" : Arrays.toString(features)));
+ }
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this.addAccount(
+ new AccountAuthenticatorResponse(response),
+ accountType, authTokenType, features, options);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ if (result != null) {
+ result.keySet(); // force it to be unparcelled
+ }
+ Log.v(TAG, "addAccount: result " + AccountManager.sanitizeResult(result));
+ }
+ if (result != null) {
+ response.onResult(result);
+ } else {
+ response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
+ "null bundle returned");
+ }
+ } catch (Exception e) {
+ handleException(response, "addAccount", accountType, e);
+ }
+ }
+
+ @Override
+ public void confirmCredentials(IAccountAuthenticatorResponse response,
+ Account account, Bundle options) throws RemoteException {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "confirmCredentials: " + account);
+ }
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this.confirmCredentials(
+ new AccountAuthenticatorResponse(response), account, options);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ if (result != null) {
+ result.keySet(); // force it to be unparcelled
+ }
+ Log.v(TAG, "confirmCredentials: result "
+ + AccountManager.sanitizeResult(result));
+ }
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "confirmCredentials", account.toString(), e);
+ }
+ }
+
+ @Override
+ public void getAuthTokenLabel(IAccountAuthenticatorResponse response,
+ String authTokenType)
+ throws RemoteException {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "getAuthTokenLabel: authTokenType " + authTokenType);
+ }
+ checkBinderPermission();
+ try {
+ Bundle result = new Bundle();
+ result.putString(AccountManager.KEY_AUTH_TOKEN_LABEL,
+ AbstractAccountAuthenticator.this.getAuthTokenLabel(authTokenType));
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ if (result != null) {
+ result.keySet(); // force it to be unparcelled
+ }
+ Log.v(TAG, "getAuthTokenLabel: result "
+ + AccountManager.sanitizeResult(result));
+ }
+ response.onResult(result);
+ } catch (Exception e) {
+ handleException(response, "getAuthTokenLabel", authTokenType, e);
+ }
+ }
+
+ @Override
+ public void getAuthToken(IAccountAuthenticatorResponse response,
+ Account account, String authTokenType, Bundle loginOptions)
+ throws RemoteException {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "getAuthToken: " + account
+ + ", authTokenType " + authTokenType);
+ }
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this.getAuthToken(
+ new AccountAuthenticatorResponse(response), account,
+ authTokenType, loginOptions);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ if (result != null) {
+ result.keySet(); // force it to be unparcelled
+ }
+ Log.v(TAG, "getAuthToken: result " + AccountManager.sanitizeResult(result));
+ }
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "getAuthToken",
+ account.toString() + "," + authTokenType, e);
+ }
+ }
+
+ @Override
+ public void updateCredentials(IAccountAuthenticatorResponse response, Account account,
+ String authTokenType, Bundle loginOptions) throws RemoteException {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "updateCredentials: " + account
+ + ", authTokenType " + authTokenType);
+ }
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this.updateCredentials(
+ new AccountAuthenticatorResponse(response), account,
+ authTokenType, loginOptions);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ // Result may be null.
+ if (result != null) {
+ result.keySet(); // force it to be unparcelled
+ }
+ Log.v(TAG, "updateCredentials: result "
+ + AccountManager.sanitizeResult(result));
+ }
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "updateCredentials",
+ account.toString() + "," + authTokenType, e);
+ }
+ }
+
+ @Override
+ public void editProperties(IAccountAuthenticatorResponse response,
+ String accountType) throws RemoteException {
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this.editProperties(
+ new AccountAuthenticatorResponse(response), accountType);
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "editProperties", accountType, e);
+ }
+ }
+
+ @Override
+ public void hasFeatures(IAccountAuthenticatorResponse response,
+ Account account, String[] features) throws RemoteException {
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this.hasFeatures(
+ new AccountAuthenticatorResponse(response), account, features);
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "hasFeatures", account.toString(), e);
+ }
+ }
+
+ @Override
+ public void getAccountRemovalAllowed(IAccountAuthenticatorResponse response,
+ Account account) throws RemoteException {
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this.getAccountRemovalAllowed(
+ new AccountAuthenticatorResponse(response), account);
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "getAccountRemovalAllowed", account.toString(), e);
+ }
+ }
+
+ @Override
+ public void getAccountCredentialsForCloning(IAccountAuthenticatorResponse response,
+ Account account) throws RemoteException {
+ checkBinderPermission();
+ try {
+ final Bundle result =
+ AbstractAccountAuthenticator.this.getAccountCredentialsForCloning(
+ new AccountAuthenticatorResponse(response), account);
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "getAccountCredentialsForCloning", account.toString(), e);
+ }
+ }
+
+ @Override
+ public void addAccountFromCredentials(IAccountAuthenticatorResponse response,
+ Account account,
+ Bundle accountCredentials) throws RemoteException {
+ checkBinderPermission();
+ try {
+ final Bundle result =
+ AbstractAccountAuthenticator.this.addAccountFromCredentials(
+ new AccountAuthenticatorResponse(response), account,
+ accountCredentials);
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "addAccountFromCredentials", account.toString(), e);
+ }
+ }
+
+ @Override
+ public void startAddAccountSession(IAccountAuthenticatorResponse response,
+ String accountType, String authTokenType, String[] features, Bundle options)
+ throws RemoteException {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG,
+ "startAddAccountSession: accountType " + accountType
+ + ", authTokenType " + authTokenType
+ + ", features " + (features == null ? "[]" : Arrays.toString(features)));
+ }
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this.startAddAccountSession(
+ new AccountAuthenticatorResponse(response), accountType, authTokenType,
+ features, options);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ if (result != null) {
+ result.keySet(); // force it to be unparcelled
+ }
+ Log.v(TAG, "startAddAccountSession: result "
+ + AccountManager.sanitizeResult(result));
+ }
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "startAddAccountSession", accountType, e);
+ }
+ }
+
+ @Override
+ public void startUpdateCredentialsSession(
+ IAccountAuthenticatorResponse response,
+ Account account,
+ String authTokenType,
+ Bundle loginOptions) throws RemoteException {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "startUpdateCredentialsSession: "
+ + account
+ + ", authTokenType "
+ + authTokenType);
+ }
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this
+ .startUpdateCredentialsSession(
+ new AccountAuthenticatorResponse(response),
+ account,
+ authTokenType,
+ loginOptions);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ // Result may be null.
+ if (result != null) {
+ result.keySet(); // force it to be unparcelled
+ }
+ Log.v(TAG, "startUpdateCredentialsSession: result "
+ + AccountManager.sanitizeResult(result));
+
+ }
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "startUpdateCredentialsSession",
+ account.toString() + "," + authTokenType, e);
+
+ }
+ }
+
+ @Override
+ public void finishSession(
+ IAccountAuthenticatorResponse response,
+ String accountType,
+ Bundle sessionBundle) throws RemoteException {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "finishSession: accountType " + accountType);
+ }
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this.finishSession(
+ new AccountAuthenticatorResponse(response), accountType, sessionBundle);
+ if (result != null) {
+ result.keySet(); // force it to be unparcelled
+ }
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "finishSession: result " + AccountManager.sanitizeResult(result));
+ }
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "finishSession", accountType, e);
+
+ }
+ }
+
+ @Override
+ public void isCredentialsUpdateSuggested(
+ IAccountAuthenticatorResponse response,
+ Account account,
+ String statusToken) throws RemoteException {
+ checkBinderPermission();
+ try {
+ final Bundle result = AbstractAccountAuthenticator.this
+ .isCredentialsUpdateSuggested(
+ new AccountAuthenticatorResponse(response), account, statusToken);
+ if (result != null) {
+ response.onResult(result);
+ }
+ } catch (Exception e) {
+ handleException(response, "isCredentialsUpdateSuggested", account.toString(), e);
+ }
+ }
+ }
+
+ private void handleException(IAccountAuthenticatorResponse response, String method,
+ String data, Exception e) throws RemoteException {
+ if (e instanceof NetworkErrorException) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, method + "(" + data + ")", e);
+ }
+ response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage());
+ } else if (e instanceof UnsupportedOperationException) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, method + "(" + data + ")", e);
+ }
+ response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION,
+ method + " not supported");
+ } else if (e instanceof IllegalArgumentException) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, method + "(" + data + ")", e);
+ }
+ response.onError(AccountManager.ERROR_CODE_BAD_ARGUMENTS,
+ method + " not supported");
+ } else {
+ Log.w(TAG, method + "(" + data + ")", e);
+ response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
+ method + " failed");
+ }
+ }
+
+ private void checkBinderPermission() {
+ final int uid = Binder.getCallingUid();
+ final String perm = Manifest.permission.ACCOUNT_MANAGER;
+ if (mContext.checkCallingOrSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("caller uid " + uid + " lacks " + perm);
+ }
+ }
+
+ private Transport mTransport = new Transport();
+
+ /**
+ * @return the IBinder for the AccountAuthenticator
+ */
+ public final IBinder getIBinder() {
+ return mTransport.asBinder();
+ }
+
+ /**
+ * Returns a Bundle that contains the Intent of the activity that can be used to edit the
+ * properties. In order to indicate success the activity should call response.setResult()
+ * with a non-null Bundle.
+ * @param response used to set the result for the request. If the Constants.INTENT_KEY
+ * is set in the bundle then this response field is to be used for sending future
+ * results if and when the Intent is started.
+ * @param accountType the AccountType whose properties are to be edited.
+ * @return a Bundle containing the result or the Intent to start to continue the request.
+ * If this is null then the request is considered to still be active and the result should
+ * sent later using response.
+ */
+ public abstract Bundle editProperties(AccountAuthenticatorResponse response,
+ String accountType);
+
+ /**
+ * Adds an account of the specified accountType.
+ * @param response to send the result back to the AccountManager, will never be null
+ * @param accountType the type of account to add, will never be null
+ * @param authTokenType the type of auth token to retrieve after adding the account, may be null
+ * @param requiredFeatures a String array of authenticator-specific features that the added
+ * account must support, may be null
+ * @param options a Bundle of authenticator-specific options. It always contains
+ * {@link AccountManager#KEY_CALLER_PID} and {@link AccountManager#KEY_CALLER_UID}
+ * fields which will let authenticator know the identity of the caller.
+ * @return a Bundle result or null if the result is to be returned via the response. The result
+ * will contain either:
+ * <ul>
+ * <li> {@link AccountManager#KEY_INTENT}, or
+ * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of
+ * the account that was added, or
+ * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
+ * indicate an error
+ * </ul>
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a
+ * network error
+ */
+ public abstract Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
+ String authTokenType, String[] requiredFeatures, Bundle options)
+ throws NetworkErrorException;
+
+ /**
+ * Checks that the user knows the credentials of an account.
+ * @param response to send the result back to the AccountManager, will never be null
+ * @param account the account whose credentials are to be checked, will never be null
+ * @param options a Bundle of authenticator-specific options, may be null
+ * @return a Bundle result or null if the result is to be returned via the response. The result
+ * will contain either:
+ * <ul>
+ * <li> {@link AccountManager#KEY_INTENT}, or
+ * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the check succeeded, false otherwise
+ * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
+ * indicate an error
+ * </ul>
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a
+ * network error
+ */
+ public abstract Bundle confirmCredentials(AccountAuthenticatorResponse response,
+ Account account, Bundle options)
+ throws NetworkErrorException;
+
+ /**
+ * Gets an authtoken for an account.
+ *
+ * If not {@code null}, the resultant {@link Bundle} will contain different sets of keys
+ * depending on whether a token was successfully issued and, if not, whether one
+ * could be issued via some {@link android.app.Activity}.
+ * <p>
+ * If a token cannot be provided without some additional activity, the Bundle should contain
+ * {@link AccountManager#KEY_INTENT} with an associated {@link Intent}. On the other hand, if
+ * there is no such activity, then a Bundle containing
+ * {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} should be
+ * returned.
+ * <p>
+ * If a token can be successfully issued, the implementation should return the
+ * {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of the
+ * account associated with the token as well as the {@link AccountManager#KEY_AUTHTOKEN}. In
+ * addition {@link AbstractAccountAuthenticator} implementations that declare themselves
+ * {@code android:customTokens=true} may also provide a non-negative {@link
+ * #KEY_CUSTOM_TOKEN_EXPIRY} long value containing the expiration timestamp of the expiration
+ * time (in millis since the unix epoch), tokens will be cached in memory based on
+ * application's packageName/signature for however long that was specified.
+ * <p>
+ * Implementers should assume that tokens will be cached on the basis of account and
+ * authTokenType. The system may ignore the contents of the supplied options Bundle when
+ * determining to re-use a cached token. Furthermore, implementers should assume a supplied
+ * expiration time will be treated as non-binding advice.
+ * <p>
+ * Finally, note that for {@code android:customTokens=false} authenticators, tokens are cached
+ * indefinitely until some client calls {@link
+ * AccountManager#invalidateAuthToken(String,String)}.
+ *
+ * @param response to send the result back to the AccountManager, will never be null
+ * @param account the account whose credentials are to be retrieved, will never be null
+ * @param authTokenType the type of auth token to retrieve, will never be null
+ * @param options a Bundle of authenticator-specific options. It always contains
+ * {@link AccountManager#KEY_CALLER_PID} and {@link AccountManager#KEY_CALLER_UID}
+ * fields which will let authenticator know the identity of the caller.
+ * @return a Bundle result or null if the result is to be returned via the response.
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a
+ * network error
+ */
+ public abstract Bundle getAuthToken(AccountAuthenticatorResponse response,
+ Account account, String authTokenType, Bundle options)
+ throws NetworkErrorException;
+
+ /**
+ * Ask the authenticator for a localized label for the given authTokenType.
+ * @param authTokenType the authTokenType whose label is to be returned, will never be null
+ * @return the localized label of the auth token type, may be null if the type isn't known
+ */
+ public abstract String getAuthTokenLabel(String authTokenType);
+
+ /**
+ * Update the locally stored credentials for an account.
+ * @param response to send the result back to the AccountManager, will never be null
+ * @param account the account whose credentials are to be updated, will never be null
+ * @param authTokenType the type of auth token to retrieve after updating the credentials,
+ * may be null
+ * @param options a Bundle of authenticator-specific options, may be null
+ * @return a Bundle result or null if the result is to be returned via the response. The result
+ * will contain either:
+ * <ul>
+ * <li> {@link AccountManager#KEY_INTENT}, or
+ * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of
+ * the account whose credentials were updated, or
+ * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
+ * indicate an error
+ * </ul>
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a
+ * network error
+ */
+ public abstract Bundle updateCredentials(AccountAuthenticatorResponse response,
+ Account account, String authTokenType, Bundle options) throws NetworkErrorException;
+
+ /**
+ * Checks if the account supports all the specified authenticator specific features.
+ * @param response to send the result back to the AccountManager, will never be null
+ * @param account the account to check, will never be null
+ * @param features an array of features to check, will never be null
+ * @return a Bundle result or null if the result is to be returned via the response. The result
+ * will contain either:
+ * <ul>
+ * <li> {@link AccountManager#KEY_INTENT}, or
+ * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the account has all the features,
+ * false otherwise
+ * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
+ * indicate an error
+ * </ul>
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a
+ * network error
+ */
+ public abstract Bundle hasFeatures(AccountAuthenticatorResponse response,
+ Account account, String[] features) throws NetworkErrorException;
+
+ /**
+ * Checks if the removal of this account is allowed.
+ * @param response to send the result back to the AccountManager, will never be null
+ * @param account the account to check, will never be null
+ * @return a Bundle result or null if the result is to be returned via the response. The result
+ * will contain either:
+ * <ul>
+ * <li> {@link AccountManager#KEY_INTENT}, or
+ * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the removal of the account is
+ * allowed, false otherwise
+ * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
+ * indicate an error
+ * </ul>
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a
+ * network error
+ */
+ public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
+ Account account) throws NetworkErrorException {
+ final Bundle result = new Bundle();
+ result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
+ return result;
+ }
+
+ /**
+ * Returns a Bundle that contains whatever is required to clone the account on a different
+ * user. The Bundle is passed to the authenticator instance in the target user via
+ * {@link #addAccountFromCredentials(AccountAuthenticatorResponse, Account, Bundle)}.
+ * The default implementation returns null, indicating that cloning is not supported.
+ * @param response to send the result back to the AccountManager, will never be null
+ * @param account the account to clone, will never be null
+ * @return a Bundle result or null if the result is to be returned via the response.
+ * @throws NetworkErrorException
+ * @see #addAccountFromCredentials(AccountAuthenticatorResponse, Account, Bundle)
+ */
+ public Bundle getAccountCredentialsForCloning(final AccountAuthenticatorResponse response,
+ final Account account) throws NetworkErrorException {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Bundle result = new Bundle();
+ result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
+ response.onResult(result);
+ }
+ }).start();
+ return null;
+ }
+
+ /**
+ * Creates an account based on credentials provided by the authenticator instance of another
+ * user on the device, who has chosen to share the account with this user.
+ * @param response to send the result back to the AccountManager, will never be null
+ * @param account the account to clone, will never be null
+ * @param accountCredentials the Bundle containing the required credentials to create the
+ * account. Contents of the Bundle are only meaningful to the authenticator. This Bundle is
+ * provided by {@link #getAccountCredentialsForCloning(AccountAuthenticatorResponse, Account)}.
+ * @return a Bundle result or null if the result is to be returned via the response.
+ * @throws NetworkErrorException
+ * @see #getAccountCredentialsForCloning(AccountAuthenticatorResponse, Account)
+ */
+ public Bundle addAccountFromCredentials(final AccountAuthenticatorResponse response,
+ Account account,
+ Bundle accountCredentials) throws NetworkErrorException {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Bundle result = new Bundle();
+ result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
+ response.onResult(result);
+ }
+ }).start();
+ return null;
+ }
+
+ /**
+ * Starts the add account session to authenticate user to an account of the
+ * specified accountType. No file I/O should be performed in this call.
+ * Account should be added to device only when {@link #finishSession} is
+ * called after this.
+ * <p>
+ * Note: when overriding this method, {@link #finishSession} should be
+ * overridden too.
+ * </p>
+ *
+ * @param response to send the result back to the AccountManager, will never
+ * be null
+ * @param accountType the type of account to authenticate with, will never
+ * be null
+ * @param authTokenType the type of auth token to retrieve after
+ * authenticating with the account, may be null
+ * @param requiredFeatures a String array of authenticator-specific features
+ * that the account authenticated with must support, may be null
+ * @param options a Bundle of authenticator-specific options, may be null
+ * @return a Bundle result or null if the result is to be returned via the
+ * response. The result will contain either:
+ * <ul>
+ * <li>{@link AccountManager#KEY_INTENT}, or
+ * <li>{@link AccountManager#KEY_ACCOUNT_SESSION_BUNDLE} for adding
+ * the account to device later, and if account is authenticated,
+ * optional {@link AccountManager#KEY_PASSWORD} and
+ * {@link AccountManager#KEY_ACCOUNT_STATUS_TOKEN} for checking the
+ * status of the account, or
+ * <li>{@link AccountManager#KEY_ERROR_CODE} and
+ * {@link AccountManager#KEY_ERROR_MESSAGE} to indicate an error
+ * </ul>
+ * @throws NetworkErrorException if the authenticator could not honor the
+ * request due to a network error
+ * @see #finishSession(AccountAuthenticatorResponse, String, Bundle)
+ */
+ public Bundle startAddAccountSession(
+ final AccountAuthenticatorResponse response,
+ final String accountType,
+ final String authTokenType,
+ final String[] requiredFeatures,
+ final Bundle options)
+ throws NetworkErrorException {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Bundle sessionBundle = new Bundle();
+ sessionBundle.putString(KEY_AUTH_TOKEN_TYPE, authTokenType);
+ sessionBundle.putStringArray(KEY_REQUIRED_FEATURES, requiredFeatures);
+ sessionBundle.putBundle(KEY_OPTIONS, options);
+ Bundle result = new Bundle();
+ result.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle);
+ response.onResult(result);
+ }
+
+ }).start();
+ return null;
+ }
+
+ /**
+ * Asks user to re-authenticate for an account but defers updating the
+ * locally stored credentials. No file I/O should be performed in this call.
+ * Local credentials should be updated only when {@link #finishSession} is
+ * called after this.
+ * <p>
+ * Note: when overriding this method, {@link #finishSession} should be
+ * overridden too.
+ * </p>
+ *
+ * @param response to send the result back to the AccountManager, will never
+ * be null
+ * @param account the account whose credentials are to be updated, will
+ * never be null
+ * @param authTokenType the type of auth token to retrieve after updating
+ * the credentials, may be null
+ * @param options a Bundle of authenticator-specific options, may be null
+ * @return a Bundle result or null if the result is to be returned via the
+ * response. The result will contain either:
+ * <ul>
+ * <li>{@link AccountManager#KEY_INTENT}, or
+ * <li>{@link AccountManager#KEY_ACCOUNT_SESSION_BUNDLE} for
+ * updating the locally stored credentials later, and if account is
+ * re-authenticated, optional {@link AccountManager#KEY_PASSWORD}
+ * and {@link AccountManager#KEY_ACCOUNT_STATUS_TOKEN} for checking
+ * the status of the account later, or
+ * <li>{@link AccountManager#KEY_ERROR_CODE} and
+ * {@link AccountManager#KEY_ERROR_MESSAGE} to indicate an error
+ * </ul>
+ * @throws NetworkErrorException if the authenticator could not honor the
+ * request due to a network error
+ * @see #finishSession(AccountAuthenticatorResponse, String, Bundle)
+ */
+ public Bundle startUpdateCredentialsSession(
+ final AccountAuthenticatorResponse response,
+ final Account account,
+ final String authTokenType,
+ final Bundle options) throws NetworkErrorException {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Bundle sessionBundle = new Bundle();
+ sessionBundle.putString(KEY_AUTH_TOKEN_TYPE, authTokenType);
+ sessionBundle.putParcelable(KEY_ACCOUNT, account);
+ sessionBundle.putBundle(KEY_OPTIONS, options);
+ Bundle result = new Bundle();
+ result.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle);
+ response.onResult(result);
+ }
+
+ }).start();
+ return null;
+ }
+
+ /**
+ * Finishes the session started by #startAddAccountSession or
+ * #startUpdateCredentials by installing the account to device with
+ * AccountManager, or updating the local credentials. File I/O may be
+ * performed in this call.
+ * <p>
+ * Note: when overriding this method, {@link #startAddAccountSession} and
+ * {@link #startUpdateCredentialsSession} should be overridden too.
+ * </p>
+ *
+ * @param response to send the result back to the AccountManager, will never
+ * be null
+ * @param accountType the type of account to authenticate with, will never
+ * be null
+ * @param sessionBundle a bundle of session data created by
+ * {@link #startAddAccountSession} used for adding account to
+ * device, or by {@link #startUpdateCredentialsSession} used for
+ * updating local credentials.
+ * @return a Bundle result or null if the result is to be returned via the
+ * response. The result will contain either:
+ * <ul>
+ * <li>{@link AccountManager#KEY_INTENT}, or
+ * <li>{@link AccountManager#KEY_ACCOUNT_NAME} and
+ * {@link AccountManager#KEY_ACCOUNT_TYPE} of the account that was
+ * added or local credentials were updated, and optional
+ * {@link AccountManager#KEY_ACCOUNT_STATUS_TOKEN} for checking
+ * the status of the account later, or
+ * <li>{@link AccountManager#KEY_ERROR_CODE} and
+ * {@link AccountManager#KEY_ERROR_MESSAGE} to indicate an error
+ * </ul>
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a
+ * network error
+ * @see #startAddAccountSession and #startUpdateCredentialsSession
+ */
+ public Bundle finishSession(
+ final AccountAuthenticatorResponse response,
+ final String accountType,
+ final Bundle sessionBundle) throws NetworkErrorException {
+ if (TextUtils.isEmpty(accountType)) {
+ Log.e(TAG, "Account type cannot be empty.");
+ Bundle result = new Bundle();
+ result.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_BAD_ARGUMENTS);
+ result.putString(AccountManager.KEY_ERROR_MESSAGE,
+ "accountType cannot be empty.");
+ return result;
+ }
+
+ if (sessionBundle == null) {
+ Log.e(TAG, "Session bundle cannot be null.");
+ Bundle result = new Bundle();
+ result.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_BAD_ARGUMENTS);
+ result.putString(AccountManager.KEY_ERROR_MESSAGE,
+ "sessionBundle cannot be null.");
+ return result;
+ }
+
+ if (!sessionBundle.containsKey(KEY_AUTH_TOKEN_TYPE)) {
+ // We cannot handle Session bundle not created by default startAddAccountSession(...)
+ // nor startUpdateCredentialsSession(...) implementation. Return error.
+ Bundle result = new Bundle();
+ result.putInt(AccountManager.KEY_ERROR_CODE,
+ AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
+ result.putString(AccountManager.KEY_ERROR_MESSAGE,
+ "Authenticator must override finishSession if startAddAccountSession"
+ + " or startUpdateCredentialsSession is overridden.");
+ response.onResult(result);
+ return result;
+ }
+ String authTokenType = sessionBundle.getString(KEY_AUTH_TOKEN_TYPE);
+ Bundle options = sessionBundle.getBundle(KEY_OPTIONS);
+ String[] requiredFeatures = sessionBundle.getStringArray(KEY_REQUIRED_FEATURES);
+ Account account = sessionBundle.getParcelable(KEY_ACCOUNT);
+ boolean containsKeyAccount = sessionBundle.containsKey(KEY_ACCOUNT);
+
+ // Actual options passed to add account or update credentials flow.
+ Bundle sessionOptions = new Bundle(sessionBundle);
+ // Remove redundant extras in session bundle before passing it to addAccount(...) or
+ // updateCredentials(...).
+ sessionOptions.remove(KEY_AUTH_TOKEN_TYPE);
+ sessionOptions.remove(KEY_REQUIRED_FEATURES);
+ sessionOptions.remove(KEY_OPTIONS);
+ sessionOptions.remove(KEY_ACCOUNT);
+
+ if (options != null) {
+ // options may contains old system info such as
+ // AccountManager.KEY_ANDROID_PACKAGE_NAME required by the add account flow or update
+ // credentials flow, we should replace with the new values of the current call added
+ // to sessionBundle by AccountManager or AccountManagerService.
+ options.putAll(sessionOptions);
+ sessionOptions = options;
+ }
+
+ // Session bundle created by startUpdateCredentialsSession default implementation should
+ // contain KEY_ACCOUNT.
+ if (containsKeyAccount) {
+ return updateCredentials(response, account, authTokenType, options);
+ }
+ // Otherwise, session bundle was created by startAddAccountSession default implementation.
+ return addAccount(response, accountType, authTokenType, requiredFeatures, sessionOptions);
+ }
+
+ /**
+ * Checks if update of the account credentials is suggested.
+ *
+ * @param response to send the result back to the AccountManager, will never be null.
+ * @param account the account to check, will never be null
+ * @param statusToken a String of token which can be used to check the status of locally
+ * stored credentials and if update of credentials is suggested
+ * @return a Bundle result or null if the result is to be returned via the response. The result
+ * will contain either:
+ * <ul>
+ * <li>{@link AccountManager#KEY_BOOLEAN_RESULT}, true if update of account's
+ * credentials is suggested, false otherwise
+ * <li>{@link AccountManager#KEY_ERROR_CODE} and
+ * {@link AccountManager#KEY_ERROR_MESSAGE} to indicate an error
+ * </ul>
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a
+ * network error
+ */
+ public Bundle isCredentialsUpdateSuggested(
+ final AccountAuthenticatorResponse response,
+ Account account,
+ String statusToken) throws NetworkErrorException {
+ Bundle result = new Bundle();
+ result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
+ return result;
+ }
+}
diff --git a/android/accounts/Account.java b/android/accounts/Account.java
new file mode 100644
index 0000000..9a18880
--- /dev/null
+++ b/android/accounts/Account.java
@@ -0,0 +1,179 @@
+/*
+ * 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.accounts;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Set;
+
+/**
+ * Value type that represents an Account in the {@link AccountManager}. This object is
+ * {@link Parcelable} and also overrides {@link #equals} and {@link #hashCode}, making it
+ * suitable for use as the key of a {@link java.util.Map}
+ */
+public class Account implements Parcelable {
+ @UnsupportedAppUsage
+ private static final String TAG = "Account";
+
+ @GuardedBy("sAccessedAccounts")
+ private static final Set<Account> sAccessedAccounts = new ArraySet<>();
+
+ public final String name;
+ public final String type;
+ private String mSafeName;
+ @UnsupportedAppUsage
+ private final @Nullable String accessId;
+
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof Account)) return false;
+ final Account other = (Account)o;
+ return name.equals(other.name) && type.equals(other.type);
+ }
+
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + name.hashCode();
+ result = 31 * result + type.hashCode();
+ return result;
+ }
+
+ public Account(String name, String type) {
+ this(name, type, null);
+ }
+
+ /**
+ * @hide
+ */
+ public Account(@NonNull Account other, @NonNull String accessId) {
+ this(other.name, other.type, accessId);
+ }
+
+ /**
+ * @hide
+ */
+ public Account(String name, String type, String accessId) {
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("the name must not be empty: " + name);
+ }
+ if (TextUtils.isEmpty(type)) {
+ throw new IllegalArgumentException("the type must not be empty: " + type);
+ }
+ this.name = name;
+ this.type = type;
+ this.accessId = accessId;
+ }
+
+ public Account(Parcel in) {
+ this.name = in.readString();
+ this.type = in.readString();
+ if (TextUtils.isEmpty(name)) {
+ throw new android.os.BadParcelableException("the name must not be empty: " + name);
+ }
+ if (TextUtils.isEmpty(type)) {
+ throw new android.os.BadParcelableException("the type must not be empty: " + type);
+ }
+ this.accessId = in.readString();
+ if (accessId != null) {
+ synchronized (sAccessedAccounts) {
+ if (sAccessedAccounts.add(this)) {
+ try {
+ IAccountManager accountManager = IAccountManager.Stub.asInterface(
+ ServiceManager.getService(Context.ACCOUNT_SERVICE));
+ accountManager.onAccountAccessed(accessId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error noting account access", e);
+ }
+ }
+ }
+ }
+ }
+
+ /** @hide */
+ public String getAccessId() {
+ return accessId;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(name);
+ dest.writeString(type);
+ dest.writeString(accessId);
+ }
+
+ public static final @android.annotation.NonNull Creator<Account> CREATOR = new Creator<Account>() {
+ public Account createFromParcel(Parcel source) {
+ return new Account(source);
+ }
+
+ public Account[] newArray(int size) {
+ return new Account[size];
+ }
+ };
+
+ public String toString() {
+ return "Account {name=" + name + ", type=" + type + "}";
+ }
+
+ /**
+ * Return a string representation of the account that is safe to print
+ * to logs and other places where PII should be avoided.
+ * @hide
+ */
+ public String toSafeString() {
+ if (mSafeName == null) {
+ mSafeName = toSafeName(name, 'x');
+ }
+ return "Account {name=" + mSafeName + ", type=" + type + "}";
+ }
+
+ /**
+ * Given a name, replace all letter or digits with the replacement char.
+ * @param name The input name string.
+ * @param replacement the replacement character.
+ * @return the string after replacement.
+ * @hide
+ */
+ public static String toSafeName(String name, char replacement) {
+ final StringBuilder builder = new StringBuilder(64);
+ final int len = name.length();
+ for (int i = 0; i < len; i++) {
+ final char c = name.charAt(i);
+ if (Character.isLetterOrDigit(c)) {
+ builder.append(replacement);
+ } else {
+ builder.append(c);
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/android/accounts/AccountAndUser.java b/android/accounts/AccountAndUser.java
new file mode 100644
index 0000000..fd67394
--- /dev/null
+++ b/android/accounts/AccountAndUser.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.accounts;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+/**
+ * Used to store the Account and the UserId this account is associated with.
+ *
+ * @hide
+ */
+public class AccountAndUser {
+ @UnsupportedAppUsage
+ public Account account;
+ @UnsupportedAppUsage
+ public int userId;
+
+ @UnsupportedAppUsage
+ public AccountAndUser(Account account, int userId) {
+ this.account = account;
+ this.userId = userId;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AccountAndUser)) return false;
+ final AccountAndUser other = (AccountAndUser) o;
+ return this.account.equals(other.account)
+ && this.userId == other.userId;
+ }
+
+ @Override
+ public int hashCode() {
+ return account.hashCode() + userId;
+ }
+
+ public String toString() {
+ return account.toString() + " u" + userId;
+ }
+}
diff --git a/android/accounts/AccountAuthenticatorActivity.java b/android/accounts/AccountAuthenticatorActivity.java
new file mode 100644
index 0000000..65ba35f
--- /dev/null
+++ b/android/accounts/AccountAuthenticatorActivity.java
@@ -0,0 +1,86 @@
+/*
+ * 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.accounts;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * Base class for implementing an Activity that is used to help implement an
+ * AbstractAccountAuthenticator. If the AbstractAccountAuthenticator needs to use an activity
+ * to handle the request then it can have the activity extend AccountAuthenticatorActivity.
+ * The AbstractAccountAuthenticator passes in the response to the intent using the following:
+ * <pre>
+ * intent.putExtra({@link AccountManager#KEY_ACCOUNT_AUTHENTICATOR_RESPONSE}, response);
+ * </pre>
+ * The activity then sets the result that is to be handed to the response via
+ * {@link #setAccountAuthenticatorResult(android.os.Bundle)}.
+ * This result will be sent as the result of the request when the activity finishes. If this
+ * is never set or if it is set to null then error {@link AccountManager#ERROR_CODE_CANCELED}
+ * will be called on the response.
+ *
+ * @deprecated Applications should extend Activity themselves. This class is not compatible with
+ * AppCompat, and the functionality it provides is not complex.
+ */
+@Deprecated
+public class AccountAuthenticatorActivity extends Activity {
+ private AccountAuthenticatorResponse mAccountAuthenticatorResponse = null;
+ private Bundle mResultBundle = null;
+
+ /**
+ * Set the result that is to be sent as the result of the request that caused this
+ * Activity to be launched. If result is null or this method is never called then
+ * the request will be canceled.
+ * @param result this is returned as the result of the AbstractAccountAuthenticator request
+ */
+ public final void setAccountAuthenticatorResult(Bundle result) {
+ mResultBundle = result;
+ }
+
+ /**
+ * Retrieves the AccountAuthenticatorResponse from either the intent of the icicle, if the
+ * icicle is non-zero.
+ * @param icicle the save instance data of this Activity, may be null
+ */
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mAccountAuthenticatorResponse =
+ getIntent().getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
+
+ if (mAccountAuthenticatorResponse != null) {
+ mAccountAuthenticatorResponse.onRequestContinued();
+ }
+ }
+
+ /**
+ * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present.
+ */
+ public void finish() {
+ if (mAccountAuthenticatorResponse != null) {
+ // send the result bundle back if set, otherwise send an error.
+ if (mResultBundle != null) {
+ mAccountAuthenticatorResponse.onResult(mResultBundle);
+ } else {
+ mAccountAuthenticatorResponse.onError(AccountManager.ERROR_CODE_CANCELED,
+ "canceled");
+ }
+ mAccountAuthenticatorResponse = null;
+ }
+ super.finish();
+ }
+}
diff --git a/android/accounts/AccountAuthenticatorResponse.java b/android/accounts/AccountAuthenticatorResponse.java
new file mode 100644
index 0000000..a2a5799
--- /dev/null
+++ b/android/accounts/AccountAuthenticatorResponse.java
@@ -0,0 +1,100 @@
+/*
+ * 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.accounts;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Object used to communicate responses back to the AccountManager
+ */
+public class AccountAuthenticatorResponse implements Parcelable {
+ private static final String TAG = "AccountAuthenticator";
+
+ private IAccountAuthenticatorResponse mAccountAuthenticatorResponse;
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public AccountAuthenticatorResponse(IAccountAuthenticatorResponse response) {
+ mAccountAuthenticatorResponse = response;
+ }
+
+ public AccountAuthenticatorResponse(Parcel parcel) {
+ mAccountAuthenticatorResponse =
+ IAccountAuthenticatorResponse.Stub.asInterface(parcel.readStrongBinder());
+ }
+
+ public void onResult(Bundle result) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ result.keySet(); // force it to be unparcelled
+ Log.v(TAG, "AccountAuthenticatorResponse.onResult: "
+ + AccountManager.sanitizeResult(result));
+ }
+ try {
+ mAccountAuthenticatorResponse.onResult(result);
+ } catch (RemoteException e) {
+ // this should never happen
+ }
+ }
+
+ public void onRequestContinued() {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "AccountAuthenticatorResponse.onRequestContinued");
+ }
+ try {
+ mAccountAuthenticatorResponse.onRequestContinued();
+ } catch (RemoteException e) {
+ // this should never happen
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "AccountAuthenticatorResponse.onError: " + errorCode + ", " + errorMessage);
+ }
+ try {
+ mAccountAuthenticatorResponse.onError(errorCode, errorMessage);
+ } catch (RemoteException e) {
+ // this should never happen
+ }
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mAccountAuthenticatorResponse.asBinder());
+ }
+
+ public static final @android.annotation.NonNull Creator<AccountAuthenticatorResponse> CREATOR =
+ new Creator<AccountAuthenticatorResponse>() {
+ public AccountAuthenticatorResponse createFromParcel(Parcel source) {
+ return new AccountAuthenticatorResponse(source);
+ }
+
+ public AccountAuthenticatorResponse[] newArray(int size) {
+ return new AccountAuthenticatorResponse[size];
+ }
+ };
+}
diff --git a/android/accounts/AccountManager.java b/android/accounts/AccountManager.java
new file mode 100644
index 0000000..f2702a8
--- /dev/null
+++ b/android/accounts/AccountManager.java
@@ -0,0 +1,3336 @@
+/*
+ * 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.accounts;
+
+import android.annotation.BroadcastBehavior;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.Size;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.UserHandleAware;
+import android.app.Activity;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.res.Resources;
+import android.database.SQLException;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.R;
+
+import com.google.android.collect.Maps;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides access to a centralized registry of the user's
+ * online accounts. The user enters credentials (username and password) once
+ * per account, granting applications access to online resources with
+ * "one-click" approval.
+ *
+ * <p>Different online services have different ways of handling accounts and
+ * authentication, so the account manager uses pluggable <em>authenticator</em>
+ * modules for different <em>account types</em>. Authenticators (which may be
+ * written by third parties) handle the actual details of validating account
+ * credentials and storing account information. For example, Google, Facebook,
+ * and Microsoft Exchange each have their own authenticator.
+ *
+ * <p>Many servers support some notion of an <em>authentication token</em>,
+ * which can be used to authenticate a request to the server without sending
+ * the user's actual password. (Auth tokens are normally created with a
+ * separate request which does include the user's credentials.) AccountManager
+ * can generate auth tokens for applications, so the application doesn't need to
+ * handle passwords directly. Auth tokens are normally reusable and cached by
+ * AccountManager, but must be refreshed periodically. It's the responsibility
+ * of applications to <em>invalidate</em> auth tokens when they stop working so
+ * the AccountManager knows it needs to regenerate them.
+ *
+ * <p>Applications accessing a server normally go through these steps:
+ *
+ * <ul>
+ * <li>Get an instance of AccountManager using {@link #get(Context)}.
+ *
+ * <li>List the available accounts using {@link #getAccountsByType} or
+ * {@link #getAccountsByTypeAndFeatures}. Normally applications will only
+ * be interested in accounts with one particular <em>type</em>, which
+ * identifies the authenticator. Account <em>features</em> are used to
+ * identify particular account subtypes and capabilities. Both the account
+ * type and features are authenticator-specific strings, and must be known by
+ * the application in coordination with its preferred authenticators.
+ *
+ * <li>Select one or more of the available accounts, possibly by asking the
+ * user for their preference. If no suitable accounts are available,
+ * {@link #addAccount} may be called to prompt the user to create an
+ * account of the appropriate type.
+ *
+ * <li><b>Important:</b> If the application is using a previously remembered
+ * account selection, it must make sure the account is still in the list
+ * of accounts returned by {@link #getAccountsByType}. Requesting an auth token
+ * for an account no longer on the device results in an undefined failure.
+ *
+ * <li>Request an auth token for the selected account(s) using one of the
+ * {@link #getAuthToken} methods or related helpers. Refer to the description
+ * of each method for exact usage and error handling details.
+ *
+ * <li>Make the request using the auth token. The form of the auth token,
+ * the format of the request, and the protocol used are all specific to the
+ * service you are accessing. The application may use whatever network and
+ * protocol libraries are useful.
+ *
+ * <li><b>Important:</b> If the request fails with an authentication error,
+ * it could be that a cached auth token is stale and no longer honored by
+ * the server. The application must call {@link #invalidateAuthToken} to remove
+ * the token from the cache, otherwise requests will continue failing! After
+ * invalidating the auth token, immediately go back to the "Request an auth
+ * token" step above. If the process fails the second time, then it can be
+ * treated as a "genuine" authentication failure and the user notified or other
+ * appropriate actions taken.
+ * </ul>
+ *
+ * <p>Some AccountManager methods may need to interact with the user to
+ * prompt for credentials, present options, or ask the user to add an account.
+ * The caller may choose whether to allow AccountManager to directly launch the
+ * necessary user interface and wait for the user, or to return an Intent which
+ * the caller may use to launch the interface, or (in some cases) to install a
+ * notification which the user can select at any time to launch the interface.
+ * To have AccountManager launch the interface directly, the caller must supply
+ * the current foreground {@link Activity} context.
+ *
+ * <p>Many AccountManager methods take {@link AccountManagerCallback} and
+ * {@link Handler} as parameters. These methods return immediately and
+ * run asynchronously. If a callback is provided then
+ * {@link AccountManagerCallback#run} will be invoked on the Handler's
+ * thread when the request completes, successfully or not.
+ * The result is retrieved by calling {@link AccountManagerFuture#getResult()}
+ * on the {@link AccountManagerFuture} returned by the method (and also passed
+ * to the callback). This method waits for the operation to complete (if
+ * necessary) and either returns the result or throws an exception if an error
+ * occurred during the operation. To make the request synchronously, call
+ * {@link AccountManagerFuture#getResult()} immediately on receiving the
+ * future from the method; no callback need be supplied.
+ *
+ * <p>Requests which may block, including
+ * {@link AccountManagerFuture#getResult()}, must never be called on
+ * the application's main event thread. These operations throw
+ * {@link IllegalStateException} if they are used on the main thread.
+ */
+@SystemService(Context.ACCOUNT_SERVICE)
+public class AccountManager {
+
+ private static final String TAG = "AccountManager";
+
+ public static final int ERROR_CODE_REMOTE_EXCEPTION = 1;
+ public static final int ERROR_CODE_NETWORK_ERROR = 3;
+ public static final int ERROR_CODE_CANCELED = 4;
+ public static final int ERROR_CODE_INVALID_RESPONSE = 5;
+ public static final int ERROR_CODE_UNSUPPORTED_OPERATION = 6;
+ public static final int ERROR_CODE_BAD_ARGUMENTS = 7;
+ public static final int ERROR_CODE_BAD_REQUEST = 8;
+ public static final int ERROR_CODE_BAD_AUTHENTICATION = 9;
+
+ /** @hide */
+ public static final int ERROR_CODE_USER_RESTRICTED = 100;
+ /** @hide */
+ public static final int ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE = 101;
+
+ /**
+ * Bundle key used for the {@link String} account name in results
+ * from methods which return information about a particular account.
+ */
+ public static final String KEY_ACCOUNT_NAME = "authAccount";
+
+ /**
+ * Bundle key used for the {@link String} account type in results
+ * from methods which return information about a particular account.
+ */
+ public static final String KEY_ACCOUNT_TYPE = "accountType";
+
+ /**
+ * Bundle key used for the account access id used for noting the
+ * account was accessed when unmarshaled from a parcel.
+ *
+ * @hide
+ */
+ public static final String KEY_ACCOUNT_ACCESS_ID = "accountAccessId";
+
+ /**
+ * Bundle key used for the auth token value in results
+ * from {@link #getAuthToken} and friends.
+ */
+ public static final String KEY_AUTHTOKEN = "authtoken";
+
+ /**
+ * Bundle key used for an {@link Intent} in results from methods that
+ * may require the caller to interact with the user. The Intent can
+ * be used to start the corresponding user interface activity.
+ */
+ public static final String KEY_INTENT = "intent";
+
+ /**
+ * Bundle key used to supply the password directly in options to
+ * {@link #confirmCredentials}, rather than prompting the user with
+ * the standard password prompt.
+ */
+ public static final String KEY_PASSWORD = "password";
+
+ public static final String KEY_ACCOUNTS = "accounts";
+
+ public static final String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = "accountAuthenticatorResponse";
+ public static final String KEY_ACCOUNT_MANAGER_RESPONSE = "accountManagerResponse";
+ public static final String KEY_AUTHENTICATOR_TYPES = "authenticator_types";
+ public static final String KEY_AUTH_FAILED_MESSAGE = "authFailedMessage";
+ public static final String KEY_AUTH_TOKEN_LABEL = "authTokenLabelKey";
+ public static final String KEY_BOOLEAN_RESULT = "booleanResult";
+ public static final String KEY_ERROR_CODE = "errorCode";
+ public static final String KEY_ERROR_MESSAGE = "errorMessage";
+ public static final String KEY_USERDATA = "userdata";
+
+ /**
+ * Bundle key used to supply the last time the credentials of the account
+ * were authenticated successfully. Time is specified in milliseconds since
+ * epoch. Associated time is updated on successful authentication of account
+ * on adding account, confirming credentials, or updating credentials.
+ */
+ public static final String KEY_LAST_AUTHENTICATED_TIME = "lastAuthenticatedTime";
+
+ /**
+ * The UID of caller app.
+ */
+ public static final String KEY_CALLER_UID = "callerUid";
+
+ /**
+ * The process id of caller app.
+ */
+ public static final String KEY_CALLER_PID = "callerPid";
+
+ /**
+ * The Android package of the caller will be set in the options bundle by the
+ * {@link AccountManager} and will be passed to the AccountManagerService and
+ * to the AccountAuthenticators. The uid of the caller will be known by the
+ * AccountManagerService as well as the AccountAuthenticators so they will be able to
+ * verify that the package is consistent with the uid (a uid might be shared by many
+ * packages).
+ */
+ public static final String KEY_ANDROID_PACKAGE_NAME = "androidPackageName";
+
+ /**
+ * Boolean, if set and 'customTokens' the authenticator is responsible for
+ * notifications.
+ * @hide
+ */
+ public static final String KEY_NOTIFY_ON_FAILURE = "notifyOnAuthFailure";
+
+ /**
+ * Bundle key used for a {@link Bundle} in result from
+ * {@link #startAddAccountSession} and friends which returns session data
+ * for installing an account later.
+ */
+ public static final String KEY_ACCOUNT_SESSION_BUNDLE = "accountSessionBundle";
+
+ /**
+ * Bundle key used for the {@link String} account status token in result
+ * from {@link #startAddAccountSession} and friends which returns
+ * information about a particular account.
+ */
+ public static final String KEY_ACCOUNT_STATUS_TOKEN = "accountStatusToken";
+
+ public static final String ACTION_AUTHENTICATOR_INTENT =
+ "android.accounts.AccountAuthenticator";
+ public static final String AUTHENTICATOR_META_DATA_NAME =
+ "android.accounts.AccountAuthenticator";
+ public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "VISIBILITY_" }, value = {
+ VISIBILITY_UNDEFINED,
+ VISIBILITY_VISIBLE,
+ VISIBILITY_USER_MANAGED_VISIBLE,
+ VISIBILITY_NOT_VISIBLE,
+ VISIBILITY_USER_MANAGED_NOT_VISIBLE
+ })
+ public @interface AccountVisibility {
+ }
+
+ /**
+ * Account visibility was not set. Default visibility value will be used.
+ * See {@link #PACKAGE_NAME_KEY_LEGACY_VISIBLE}, {@link #PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE}
+ */
+ public static final int VISIBILITY_UNDEFINED = 0;
+
+ /**
+ * Account is always visible to given application and only authenticator can revoke visibility.
+ */
+ public static final int VISIBILITY_VISIBLE = 1;
+
+ /**
+ * Account is visible to given application, but user can revoke visibility.
+ */
+ public static final int VISIBILITY_USER_MANAGED_VISIBLE = 2;
+
+ /**
+ * Account is not visible to given application and only authenticator can grant visibility.
+ */
+ public static final int VISIBILITY_NOT_VISIBLE = 3;
+
+ /**
+ * Account is not visible to given application, but user can reveal it, for example, using
+ * {@link #newChooseAccountIntent(Account, List, String[], String, String, String[], Bundle)}
+ */
+ public static final int VISIBILITY_USER_MANAGED_NOT_VISIBLE = 4;
+
+ /**
+ * Token type for the special case where a UID has access only to an account
+ * but no authenticator specific auth token types.
+ *
+ * @hide
+ */
+ public static final String ACCOUNT_ACCESS_TOKEN_TYPE =
+ "com.android.AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE";
+
+ @UnsupportedAppUsage
+ private final Context mContext;
+ private final IAccountManager mService;
+ private final Handler mMainHandler;
+
+ /**
+ * Action sent as a broadcast Intent by the AccountsService when accounts are added, accounts
+ * are removed, or an account's credentials (saved password, etc) are changed.
+ *
+ * @see #addOnAccountsUpdatedListener
+ * @see #ACTION_ACCOUNT_REMOVED
+ *
+ * @deprecated use {@link #addOnAccountsUpdatedListener} to get account updates in runtime.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(includeBackground = true)
+ public static final String LOGIN_ACCOUNTS_CHANGED_ACTION =
+ "android.accounts.LOGIN_ACCOUNTS_CHANGED";
+
+ /**
+ * Action sent as a broadcast Intent by the AccountsService when any account is removed
+ * or renamed. Only applications which were able to see the account will receive the intent.
+ * Intent extra will include the following fields:
+ * <ul>
+ * <li> {@link #KEY_ACCOUNT_NAME} - the name of the removed account
+ * <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * </ul>
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(includeBackground = true)
+ public static final String ACTION_ACCOUNT_REMOVED =
+ "android.accounts.action.ACCOUNT_REMOVED";
+
+ /**
+ * Action sent as a broadcast Intent to specific package by the AccountsService
+ * when account visibility or account's credentials (saved password, etc) are changed.
+ *
+ * @see #addOnAccountsUpdatedListener
+ *
+ * @hide
+ */
+ public static final String ACTION_VISIBLE_ACCOUNTS_CHANGED =
+ "android.accounts.action.VISIBLE_ACCOUNTS_CHANGED";
+
+ /**
+ * Key to set visibility for applications which satisfy one of the following conditions:
+ * <ul>
+ * <li>Target API level below {@link android.os.Build.VERSION_CODES#O} and have
+ * deprecated {@link android.Manifest.permission#GET_ACCOUNTS} permission.
+ * </li>
+ * <li> Have {@link android.Manifest.permission#GET_ACCOUNTS_PRIVILEGED} permission. </li>
+ * <li> Have the same signature as authenticator. </li>
+ * <li> Have {@link android.Manifest.permission#READ_CONTACTS} permission and
+ * account type may be associated with contacts data - (verified by
+ * {@link android.Manifest.permission#WRITE_CONTACTS} permission check for the authenticator).
+ * </li>
+ * </ul>
+ * See {@link #getAccountVisibility}. If the value was not set by authenticator
+ * {@link #VISIBILITY_USER_MANAGED_VISIBLE} is used.
+ */
+ public static final String PACKAGE_NAME_KEY_LEGACY_VISIBLE =
+ "android:accounts:key_legacy_visible";
+
+ /**
+ * Key to set default visibility for applications which don't satisfy conditions in
+ * {@link #PACKAGE_NAME_KEY_LEGACY_VISIBLE}. If the value was not set by authenticator
+ * {@link #VISIBILITY_USER_MANAGED_NOT_VISIBLE} is used.
+ */
+ public static final String PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE =
+ "android:accounts:key_legacy_not_visible";
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public AccountManager(Context context, IAccountManager service) {
+ mContext = context;
+ mService = service;
+ mMainHandler = new Handler(mContext.getMainLooper());
+ }
+
+ /**
+ * @hide used for testing only
+ */
+ @UnsupportedAppUsage
+ public AccountManager(Context context, IAccountManager service, Handler handler) {
+ mContext = context;
+ mService = service;
+ mMainHandler = handler;
+ }
+
+ /**
+ * @hide for internal use only
+ */
+ public static Bundle sanitizeResult(Bundle result) {
+ if (result != null) {
+ if (result.containsKey(KEY_AUTHTOKEN)
+ && !TextUtils.isEmpty(result.getString(KEY_AUTHTOKEN))) {
+ final Bundle newResult = new Bundle(result);
+ newResult.putString(KEY_AUTHTOKEN, "<omitted for logging purposes>");
+ return newResult;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Gets an AccountManager instance associated with a Context.
+ * The {@link Context} will be used as long as the AccountManager is
+ * active, so make sure to use a {@link Context} whose lifetime is
+ * commensurate with any listeners registered to
+ * {@link #addOnAccountsUpdatedListener} or similar methods.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p>No permission is required to call this method.
+ *
+ * @param context The {@link Context} to use when necessary
+ * @return An {@link AccountManager} instance
+ */
+ public static AccountManager get(Context context) {
+ if (context == null) throw new IllegalArgumentException("context is null");
+ return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
+ }
+
+ /**
+ * Gets the saved password associated with the account. This is intended for authenticators and
+ * related code; applications should get an auth token instead.
+ *
+ * <p>
+ * It is safe to call this method from the main thread.
+ *
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
+ *
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and before, AUTHENTICATE_ACCOUNTS
+ * permission is needed for those platforms. See docs for this function in API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
+ *
+ * @param account The account to query for a password. Must not be {@code null}.
+ * @return The account's password, null if none or if the account doesn't exist
+ */
+ public String getPassword(final Account account) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ try {
+ return mService.getPassword(account);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the user data named by "key" associated with the account. This is intended for
+ * authenticators and related code to store arbitrary metadata along with accounts. The meaning
+ * of the keys and values is up to the authenticator for the account.
+ *
+ * <p>
+ * It is safe to call this method from the main thread.
+ *
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
+ *
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and before, AUTHENTICATE_ACCOUNTS
+ * permission is needed for those platforms. See docs for this function in API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
+ *
+ * @param account The account to query for user data
+ * @return The user data, null if the account, key doesn't exist, or the user is locked
+ */
+ public String getUserData(final Account account, final String key) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (key == null) throw new IllegalArgumentException("key is null");
+ try {
+ return mService.getUserData(account, key);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Lists the currently registered authenticators.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p>No permission is required to call this method.
+ *
+ * @return An array of {@link AuthenticatorDescription} for every
+ * authenticator known to the AccountManager service. Empty (never
+ * null) if no authenticators are known.
+ */
+ @UserHandleAware
+ public AuthenticatorDescription[] getAuthenticatorTypes() {
+ return getAuthenticatorTypesAsUser(mContext.getUserId());
+ }
+
+ /**
+ * @hide
+ * Lists the currently registered authenticators for a given user id.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p>The caller has to be in the same user or have the permission
+ * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
+ *
+ * @return An array of {@link AuthenticatorDescription} for every
+ * authenticator known to the AccountManager service. Empty (never
+ * null) if no authenticators are known.
+ */
+ public AuthenticatorDescription[] getAuthenticatorTypesAsUser(int userId) {
+ try {
+ return mService.getAuthenticatorTypes(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Lists all accounts visible to the caller regardless of type. Equivalent to
+ * getAccountsByType(null). These accounts may be visible because the user granted access to the
+ * account, or the AbstractAcccountAuthenticator managing the account did so or because the
+ * client shares a signature with the managing AbstractAccountAuthenticator.
+ *
+ * <div class="caution"><p><b>Caution: </b>This method returns personal and sensitive user data.
+ * If your app accesses, collects, uses, or shares personal and sensitive data, you must clearly
+ * disclose that fact to users. For apps published on Google Play, policies protecting user data
+ * require that you do the following:</p>
+ * <ul>
+ * <li>Disclose to the user how your app accesses, collects, uses, or shares personal and
+ * sensitive data. Learn more about
+ * <a href="https://play.google.com/about/privacy-security-deception/user-data/#!#personal-sensitive">acceptable
+ * disclosure and consent</a>.</li>
+ * <li>Provide a privacy policy that describes your use of this data on- and off-device.</li>
+ * </ul>
+ * <p>To learn more, visit the
+ * <a href="https://play.google.com/about/privacy-security-deception/user-data">Google Play
+ * Policy regarding user data</a>.</p></div>
+ *
+ * <p>
+ * It is safe to call this method from the main thread.
+ *
+ * @return An array of {@link Account}, one for each account. Empty (never null) if no accounts
+ * have been added.
+ */
+ @UserHandleAware
+ @NonNull
+ public Account[] getAccounts() {
+ return getAccountsAsUser(mContext.getUserId());
+ }
+
+ /**
+ * @hide
+ * Lists all accounts visible to caller regardless of type for a given user id. Equivalent to
+ * getAccountsByType(null).
+ *
+ * <p>
+ * It is safe to call this method from the main thread.
+ *
+ * @return An array of {@link Account}, one for each account. Empty (never null) if no accounts
+ * have been added.
+ */
+ @NonNull
+ public Account[] getAccountsAsUser(int userId) {
+ try {
+ return mService.getAccountsAsUser(null, userId, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * For use by internal activities. Returns the list of accounts that the calling package
+ * is authorized to use, particularly for shared accounts.
+ * @param packageName package name of the calling app.
+ * @param uid the uid of the calling app.
+ * @return the accounts that are available to this package and user.
+ */
+ @NonNull
+ public Account[] getAccountsForPackage(String packageName, int uid) {
+ try {
+ return mService.getAccountsForPackage(packageName, uid, mContext.getOpPackageName());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the accounts visible to the specified package in an environment where some apps are
+ * not authorized to view all accounts. This method can only be called by system apps and
+ * authenticators managing the type.
+ * Beginning API level {@link android.os.Build.VERSION_CODES#O} it also return accounts
+ * which user can make visible to the application (see {@link #VISIBILITY_USER_MANAGED_VISIBLE}).
+ *
+ * @param type The type of accounts to return, null to retrieve all accounts
+ * @param packageName The package name of the app for which the accounts are to be returned
+ * @return An array of {@link Account}, one per matching account. Empty (never null) if no
+ * accounts of the specified type can be accessed by the package.
+ *
+ */
+ @NonNull
+ public Account[] getAccountsByTypeForPackage(String type, String packageName) {
+ try {
+ return mService.getAccountsByTypeForPackage(type, packageName,
+ mContext.getOpPackageName());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Lists all accounts of particular type visible to the caller. These accounts may be visible
+ * because the user granted access to the account, or the AbstractAcccountAuthenticator managing
+ * the account did so or because the client shares a signature with the managing
+ * AbstractAccountAuthenticator.
+ *
+ * <p>
+ * The account type is a string token corresponding to the authenticator and useful domain of
+ * the account. For example, there are types corresponding to Google and Facebook. The exact
+ * string token to use will be published somewhere associated with the authenticator in
+ * question.
+ * </p>
+ *
+ * <div class="caution"><p><b>Caution: </b>This method returns personal and sensitive user data.
+ * If your app accesses, collects, uses, or shares personal and sensitive data, you must clearly
+ * disclose that fact to users. For apps published on Google Play, policies protecting user data
+ * require that you do the following:</p>
+ * <ul>
+ * <li>Disclose to the user how your app accesses, collects, uses, or shares personal and
+ * sensitive data. Learn more about
+ * <a href="https://play.google.com/about/privacy-security-deception/user-data/#!#personal-sensitive">acceptable
+ * disclosure and consent</a>.</li>
+ * <li>Provide a privacy policy that describes your use of this data on- and off-device.</li>
+ * </ul>
+ * <p>To learn more, visit the
+ * <a href="https://play.google.com/about/privacy-security-deception/user-data">Google Play
+ * Policy regarding user data</a>.</p></div>
+ *
+ * <p>
+ * It is safe to call this method from the main thread.
+ *
+ * <p>
+ * Caller targeting API level {@link android.os.Build.VERSION_CODES#O} and above, will get list
+ * of accounts made visible to it by user
+ * (see {@link #newChooseAccountIntent(Account, List, String[], String,
+ * String, String[], Bundle)}) or AbstractAcccountAuthenticator
+ * using {@link #setAccountVisibility}.
+ * {@link android.Manifest.permission#GET_ACCOUNTS} permission is not used.
+ *
+ * <p>
+ * Caller targeting API level below {@link android.os.Build.VERSION_CODES#O} that have not been
+ * granted the {@link android.Manifest.permission#GET_ACCOUNTS} permission, will only see those
+ * accounts managed by AbstractAccountAuthenticators whose signature matches the client.
+ *
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and before,
+ * {@link android.Manifest.permission#GET_ACCOUNTS} permission is
+ * needed for those platforms, irrespective of uid or signature match. See docs for this
+ * function in API level {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
+ *
+ * @param type The type of accounts to return, null to retrieve all accounts
+ * @return An array of {@link Account}, one per matching account. Empty (never null) if no
+ * accounts of the specified type have been added.
+ */
+ @UserHandleAware
+ @NonNull
+ public Account[] getAccountsByType(String type) {
+ return getAccountsByTypeAsUser(type, mContext.getUser());
+ }
+
+ /** @hide Same as {@link #getAccountsByType(String)} but for a specific user. */
+ @NonNull
+ @UnsupportedAppUsage
+ public Account[] getAccountsByTypeAsUser(String type, UserHandle userHandle) {
+ try {
+ return mService.getAccountsAsUser(type, userHandle.getIdentifier(),
+ mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Change whether or not an app (identified by its uid) is allowed to retrieve an authToken
+ * for an account.
+ * <p>
+ * This is only meant to be used by system activities and is not in the SDK.
+ * @param account The account whose permissions are being modified
+ * @param authTokenType The type of token whose permissions are being modified
+ * @param uid The uid that identifies the app which is being granted or revoked permission.
+ * @param value true is permission is being granted, false for revoked
+ * @hide
+ */
+ public void updateAppPermission(Account account, String authTokenType, int uid, boolean value) {
+ try {
+ mService.updateAppPermission(account, authTokenType, uid, value);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the user-friendly label associated with an authenticator's auth token.
+ * @param accountType the type of the authenticator. must not be null.
+ * @param authTokenType the token type. must not be null.
+ * @param callback callback to invoke when the result is available. may be null.
+ * @param handler the handler on which to invoke the callback, or null for the main thread
+ * @return a future containing the label string
+ * @hide
+ */
+ public AccountManagerFuture<String> getAuthTokenLabel(
+ final String accountType, final String authTokenType,
+ AccountManagerCallback<String> callback, Handler handler) {
+ if (accountType == null) throw new IllegalArgumentException("accountType is null");
+ if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+ return new Future2Task<String>(handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.getAuthTokenLabel(mResponse, accountType, authTokenType);
+ }
+
+ @Override
+ public String bundleToResult(Bundle bundle) throws AuthenticatorException {
+ if (!bundle.containsKey(KEY_AUTH_TOKEN_LABEL)) {
+ throw new AuthenticatorException("no result in response");
+ }
+ return bundle.getString(KEY_AUTH_TOKEN_LABEL);
+ }
+ }.start();
+ }
+
+ /**
+ * Finds out whether a particular account has all the specified features. Account features are
+ * authenticator-specific string tokens identifying boolean account properties. For example,
+ * features are used to tell whether Google accounts have a particular service (such as Google
+ * Calendar or Google Talk) enabled. The feature names and their meanings are published
+ * somewhere associated with the authenticator in question.
+ *
+ * <p>
+ * This method may be called from any thread, but the returned {@link AccountManagerFuture} must
+ * not be used on the main thread.
+ *
+ * <p>
+ * If caller target API level is below {@link android.os.Build.VERSION_CODES#O}, it is
+ * required to hold the permission {@link android.Manifest.permission#GET_ACCOUNTS} or have a
+ * signature match with the AbstractAccountAuthenticator that manages the account.
+ *
+ * @param account The {@link Account} to test
+ * @param features An array of the account features to check
+ * @param callback Callback to invoke when the request completes, null for no callback
+ * @param handler {@link Handler} identifying the callback thread, null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Boolean, true if the account
+ * exists and has all of the specified features.
+ */
+ public AccountManagerFuture<Boolean> hasFeatures(final Account account,
+ final String[] features,
+ AccountManagerCallback<Boolean> callback, Handler handler) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (features == null) throw new IllegalArgumentException("features is null");
+ return new Future2Task<Boolean>(handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.hasFeatures(mResponse, account, features, mContext.getOpPackageName());
+ }
+ @Override
+ public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException {
+ if (!bundle.containsKey(KEY_BOOLEAN_RESULT)) {
+ throw new AuthenticatorException("no result in response");
+ }
+ return bundle.getBoolean(KEY_BOOLEAN_RESULT);
+ }
+ }.start();
+ }
+
+ /**
+ * Lists all accounts of a type which have certain features. The account type identifies the
+ * authenticator (see {@link #getAccountsByType}). Account features are authenticator-specific
+ * string tokens identifying boolean account properties (see {@link #hasFeatures}).
+ *
+ * <p>
+ * Unlike {@link #getAccountsByType}, this method calls the authenticator, which may contact the
+ * server or do other work to check account features, so the method returns an
+ * {@link AccountManagerFuture}.
+ *
+ * <p>
+ * This method may be called from any thread, but the returned {@link AccountManagerFuture} must
+ * not be used on the main thread.
+ *
+ * <p>
+ * Caller targeting API level {@link android.os.Build.VERSION_CODES#O} and above, will get list
+ * of accounts made visible to it by user
+ * (see {@link #newChooseAccountIntent(Account, List, String[], String,
+ * String, String[], Bundle)}) or AbstractAcccountAuthenticator
+ * using {@link #setAccountVisibility}.
+ * {@link android.Manifest.permission#GET_ACCOUNTS} permission is not used.
+ *
+ * <p>
+ * Caller targeting API level below {@link android.os.Build.VERSION_CODES#O} that have not been
+ * granted the {@link android.Manifest.permission#GET_ACCOUNTS} permission, will only see those
+ * accounts managed by AbstractAccountAuthenticators whose signature matches the client.
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and before,
+ * {@link android.Manifest.permission#GET_ACCOUNTS} permission is
+ * needed for those platforms, irrespective of uid or signature match. See docs for this
+ * function in API level {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
+ *
+ *
+ * @param type The type of accounts to return, must not be null
+ * @param features An array of the account features to require, may be null or empty *
+ * @param callback Callback to invoke when the request completes, null for no callback
+ * @param handler {@link Handler} identifying the callback thread, null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to an array of {@link Account}, one
+ * per account of the specified type which matches the requested features.
+ */
+ public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
+ final String type, final String[] features,
+ AccountManagerCallback<Account[]> callback, Handler handler) {
+ if (type == null) throw new IllegalArgumentException("type is null");
+ return new Future2Task<Account[]>(handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.getAccountsByFeatures(mResponse, type, features,
+ mContext.getOpPackageName());
+ }
+ @Override
+ public Account[] bundleToResult(Bundle bundle) throws AuthenticatorException {
+ if (!bundle.containsKey(KEY_ACCOUNTS)) {
+ throw new AuthenticatorException("no result in response");
+ }
+ final Parcelable[] parcelables = bundle.getParcelableArray(KEY_ACCOUNTS);
+ Account[] descs = new Account[parcelables.length];
+ for (int i = 0; i < parcelables.length; i++) {
+ descs[i] = (Account) parcelables[i];
+ }
+ return descs;
+ }
+ }.start();
+ }
+
+ /**
+ * Adds an account directly to the AccountManager. Normally used by sign-up
+ * wizards associated with authenticators, not directly by applications.
+ * <p>Calling this method does not update the last authenticated timestamp,
+ * referred by {@link #KEY_LAST_AUTHENTICATED_TIME}. To update it, call
+ * {@link #notifyAccountAuthenticated(Account)} after getting success.
+ * However, if this method is called when it is triggered by addAccount() or
+ * addAccountAsUser() or similar functions, then there is no need to update
+ * timestamp manually as it is updated automatically by framework on
+ * successful completion of the mentioned functions.
+ * <p>It is safe to call this method from the main thread.
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that owns the specified account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * AUTHENTICATE_ACCOUNTS permission is needed for those platforms. See docs
+ * for this function in API level 22.
+ *
+ * @param account The {@link Account} to add
+ * @param password The password to associate with the account, null for none
+ * @param userdata String values to use for the account's userdata, null for
+ * none
+ * @return True if the account was successfully added, false if the account
+ * already exists, the account is null, the user is locked, or another error occurs.
+ */
+ public boolean addAccountExplicitly(Account account, String password, Bundle userdata) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ try {
+ return mService.addAccountExplicitly(account, password, userdata);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Adds an account directly to the AccountManager. Additionally it specifies Account visibility
+ * for given list of packages.
+ * <p>
+ * Normally used by sign-up wizards associated with authenticators, not directly by
+ * applications.
+ * <p>
+ * Calling this method does not update the last authenticated timestamp, referred by
+ * {@link #KEY_LAST_AUTHENTICATED_TIME}. To update it, call
+ * {@link #notifyAccountAuthenticated(Account)} after getting success.
+ * <p>
+ * It is safe to call this method from the main thread.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
+ *
+ * @param account The {@link Account} to add
+ * @param password The password to associate with the account, null for none
+ * @param extras String values to use for the account's userdata, null for none
+ * @param visibility Map from packageName to visibility values which will be set before account
+ * is added. See {@link #getAccountVisibility} for possible values.
+ *
+ * @return True if the account was successfully added, false if the account already exists, the
+ * account is null, or another error occurs.
+ */
+ public boolean addAccountExplicitly(Account account, String password, Bundle extras,
+ Map<String, Integer> visibility) {
+ if (account == null)
+ throw new IllegalArgumentException("account is null");
+ try {
+ return mService.addAccountExplicitlyWithVisibility(account, password, extras,
+ visibility);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns package names and visibility which were explicitly set for given account.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
+ *
+ * @param account The account for which visibility data should be returned
+ *
+ * @return Map from package names to visibility for given account
+ */
+ public Map<String, Integer> getPackagesAndVisibilityForAccount(Account account) {
+ try {
+ if (account == null)
+ throw new IllegalArgumentException("account is null");
+ @SuppressWarnings("unchecked")
+ Map<String, Integer> result = (Map<String, Integer>) mService
+ .getPackagesAndVisibilityForAccount(account);
+ return result;
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets all accounts of given type and their visibility for specific package. This method
+ * requires the caller to have a signature match with the authenticator that manages
+ * accountType. It is a helper method which combines calls to {@link #getAccountsByType} by
+ * authenticator and {@link #getAccountVisibility} for every returned account.
+ *
+ * <p>
+ *
+ * @param packageName Package name
+ * @param accountType {@link Account} type
+ *
+ * @return Map with visibility for all accounts of given type
+ * See {@link #getAccountVisibility} for possible values
+ */
+ public Map<Account, Integer> getAccountsAndVisibilityForPackage(String packageName,
+ String accountType) {
+ try {
+ @SuppressWarnings("unchecked")
+ Map<Account, Integer> result = (Map<Account, Integer>) mService
+ .getAccountsAndVisibilityForPackage(packageName, accountType);
+ return result;
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set visibility value of given account to certain package.
+ * Package name must match installed application, or be equal to
+ * {@link #PACKAGE_NAME_KEY_LEGACY_VISIBLE} or {@link #PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE}.
+ * <p>
+ * Possible visibility values:
+ * <ul>
+ * <li>{@link #VISIBILITY_UNDEFINED}</li>
+ * <li>{@link #VISIBILITY_VISIBLE}</li>
+ * <li>{@link #VISIBILITY_USER_MANAGED_VISIBLE}</li>
+ * <li>{@link #VISIBILITY_NOT_VISIBLE}
+ * <li>{@link #VISIBILITY_USER_MANAGED_NOT_VISIBLE}</li>
+ * </ul>
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
+ *
+ * @param account {@link Account} to update visibility
+ * @param packageName Package name of the application to modify account visibility
+ * @param visibility New visibility value
+ *
+ * @return True, if visibility value was successfully updated.
+ */
+ public boolean setAccountVisibility(Account account, String packageName,
+ @AccountVisibility int visibility) {
+ if (account == null)
+ throw new IllegalArgumentException("account is null");
+ try {
+ return mService.setAccountVisibility(account, packageName, visibility);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get visibility of certain account for given application. Possible returned values are:
+ * <ul>
+ * <li>{@link #VISIBILITY_VISIBLE}</li>
+ * <li>{@link #VISIBILITY_USER_MANAGED_VISIBLE}</li>
+ * <li>{@link #VISIBILITY_NOT_VISIBLE}
+ * <li>{@link #VISIBILITY_USER_MANAGED_NOT_VISIBLE}</li>
+ * </ul>
+ *
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
+ *
+ * @param account {@link Account} to get visibility
+ * @param packageName Package name of the application to get account visibility
+ *
+ * @return int Visibility of given account.
+ */
+ public @AccountVisibility int getAccountVisibility(Account account, String packageName) {
+ if (account == null)
+ throw new IllegalArgumentException("account is null");
+ try {
+ return mService.getAccountVisibility(account, packageName);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Notifies the system that the account has just been authenticated. This
+ * information may be used by other applications to verify the account. This
+ * should be called only when the user has entered correct credentials for
+ * the account.
+ * <p>
+ * It is not safe to call this method from the main thread. As such, call it
+ * from another thread.
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that owns the specified account.
+ *
+ * @param account The {@link Account} to be updated.
+ * @return boolean {@code true} if the authentication of the account has been successfully
+ * acknowledged. Otherwise {@code false}.
+ */
+ public boolean notifyAccountAuthenticated(Account account) {
+ if (account == null)
+ throw new IllegalArgumentException("account is null");
+ try {
+ return mService.accountAuthenticated(account);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Rename the specified {@link Account}. This is equivalent to removing
+ * the existing account and adding a new renamed account with the old
+ * account's user data.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that manages the specified account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * AUTHENTICATE_ACCOUNTS permission and same UID as account's authenticator
+ * is needed for those platforms. See docs for this function in API level 22.
+ *
+ * @param account The {@link Account} to rename
+ * @param newName String name to be associated with the account.
+ * @param callback Callback to invoke when the request completes, null for
+ * no callback
+ * @param handler {@link Handler} identifying the callback thread, null for
+ * the main thread
+ * @return An {@link AccountManagerFuture} which resolves to the Account
+ * after the name change. If successful the account's name will be the
+ * specified new name.
+ */
+ public AccountManagerFuture<Account> renameAccount(
+ final Account account,
+ @Size(min = 1) final String newName,
+ AccountManagerCallback<Account> callback,
+ Handler handler) {
+ if (account == null) throw new IllegalArgumentException("account is null.");
+ if (TextUtils.isEmpty(newName)) {
+ throw new IllegalArgumentException("newName is empty or null.");
+ }
+ return new Future2Task<Account>(handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.renameAccount(mResponse, account, newName);
+ }
+ @Override
+ public Account bundleToResult(Bundle bundle) throws AuthenticatorException {
+ String name = bundle.getString(KEY_ACCOUNT_NAME);
+ String type = bundle.getString(KEY_ACCOUNT_TYPE);
+ String accessId = bundle.getString(KEY_ACCOUNT_ACCESS_ID);
+ return new Account(name, type, accessId);
+ }
+ }.start();
+ }
+
+ /**
+ * Gets the previous name associated with the account or {@code null}, if
+ * none. This is intended so that clients of
+ * {@link OnAccountsUpdateListener} can determine if an
+ * authenticator has renamed an account.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * @param account The account to query for a previous name.
+ * @return The account's previous name, null if the account has never been
+ * renamed.
+ */
+ public String getPreviousName(final Account account) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ try {
+ return mService.getPreviousName(account);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes an account from the AccountManager. Does nothing if the account
+ * does not exist. Does not delete the account from the server.
+ * The authenticator may have its own policies preventing account
+ * deletion, in which case the account will not be deleted.
+ *
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that manages the specified account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * MANAGE_ACCOUNTS permission is needed for those platforms. See docs for
+ * this function in API level 22.
+ *
+ * @param account The {@link Account} to remove
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Boolean,
+ * true if the account has been successfully removed
+ * @deprecated use
+ * {@link #removeAccount(Account, Activity, AccountManagerCallback, Handler)}
+ * instead
+ */
+ @UserHandleAware
+ @Deprecated
+ public AccountManagerFuture<Boolean> removeAccount(final Account account,
+ AccountManagerCallback<Boolean> callback, Handler handler) {
+ return removeAccountAsUser(account, callback, handler, mContext.getUser());
+ }
+
+ /**
+ * Removes an account from the AccountManager. Does nothing if the account
+ * does not exist. Does not delete the account from the server.
+ * The authenticator may have its own policies preventing account
+ * deletion, in which case the account will not be deleted.
+ *
+ * <p>This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ *
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that manages the specified account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * MANAGE_ACCOUNTS permission is needed for those platforms. See docs for
+ * this function in API level 22.
+ *
+ * @param account The {@link Account} to remove
+ * @param activity The {@link Activity} context to use for launching a new
+ * authenticator-defined sub-Activity to prompt the user to delete an
+ * account; used only to call startActivity(); if null, the prompt
+ * will not be launched directly, but the {@link Intent} may be
+ * returned to the caller instead
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with
+ * {@link #KEY_BOOLEAN_RESULT} if activity was specified and an account
+ * was removed or if active. If no activity was specified, the returned
+ * Bundle contains only {@link #KEY_INTENT} with the {@link Intent}
+ * needed to launch the actual account removal process, if authenticator
+ * needs the activity launch. If an error occurred,
+ * {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li> {@link AuthenticatorException} if no authenticator was registered for
+ * this account type or the authenticator failed to respond
+ * <li> {@link OperationCanceledException} if the operation was canceled for
+ * any reason, including the user canceling the creation process or
+ * adding accounts (of this type) has been disabled by policy
+ * </ul>
+ */
+ @UserHandleAware
+ public AccountManagerFuture<Bundle> removeAccount(final Account account,
+ final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
+ return removeAccountAsUser(account, activity, callback, handler, mContext.getUser());
+ }
+
+ /**
+ * @see #removeAccount(Account, AccountManagerCallback, Handler)
+ * @hide
+ * @deprecated use
+ * {@link #removeAccountAsUser(Account, Activity, AccountManagerCallback, Handler)}
+ * instead
+ */
+ @Deprecated
+ public AccountManagerFuture<Boolean> removeAccountAsUser(final Account account,
+ AccountManagerCallback<Boolean> callback, Handler handler,
+ final UserHandle userHandle) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (userHandle == null) throw new IllegalArgumentException("userHandle is null");
+ return new Future2Task<Boolean>(handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.removeAccountAsUser(mResponse, account, false, userHandle.getIdentifier());
+ }
+ @Override
+ public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException {
+ if (!bundle.containsKey(KEY_BOOLEAN_RESULT)) {
+ throw new AuthenticatorException("no result in response");
+ }
+ return bundle.getBoolean(KEY_BOOLEAN_RESULT);
+ }
+ }.start();
+ }
+
+ /**
+ * @see #removeAccount(Account, Activity, AccountManagerCallback, Handler)
+ * @hide
+ */
+ public AccountManagerFuture<Bundle> removeAccountAsUser(final Account account,
+ final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler,
+ final UserHandle userHandle) {
+ if (account == null)
+ throw new IllegalArgumentException("account is null");
+ if (userHandle == null)
+ throw new IllegalArgumentException("userHandle is null");
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.removeAccountAsUser(mResponse, account, activity != null,
+ userHandle.getIdentifier());
+ }
+ }.start();
+ }
+
+ /**
+ * Removes an account directly. Normally used by authenticators, not
+ * directly by applications. Does not delete the account from the server.
+ * The authenticator may have its own policies preventing account deletion,
+ * in which case the account will not be deleted.
+ * <p>
+ * It is safe to call this method from the main thread.
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that manages the specified account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * AUTHENTICATE_ACCOUNTS permission and same UID as account's authenticator
+ * is needed for those platforms. See docs for this function in API level 22.
+ *
+ * @param account The {@link Account} to delete.
+ * @return True if the account was successfully deleted, false if the
+ * account did not exist, the account is null, or another error
+ * occurs.
+ */
+ public boolean removeAccountExplicitly(Account account) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ try {
+ return mService.removeAccountExplicitly(account);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes an auth token from the AccountManager's cache. Does nothing if
+ * the auth token is not currently in the cache. Applications must call this
+ * method when the auth token is found to have expired or otherwise become
+ * invalid for authenticating requests. The AccountManager does not validate
+ * or expire cached auth tokens otherwise.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * MANAGE_ACCOUNTS or USE_CREDENTIALS permission is needed for those
+ * platforms. See docs for this function in API level 22.
+ *
+ * @param accountType The account type of the auth token to invalidate, must not be null
+ * @param authToken The auth token to invalidate, may be null
+ */
+ public void invalidateAuthToken(final String accountType, final String authToken) {
+ if (accountType == null) throw new IllegalArgumentException("accountType is null");
+ try {
+ if (authToken != null) {
+ mService.invalidateAuthToken(accountType, authToken);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets an auth token from the AccountManager's cache. If no auth
+ * token is cached for this account, null will be returned -- a new
+ * auth token will not be generated, and the server will not be contacted.
+ * Intended for use by the authenticator, not directly by applications.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that manages the specified account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * AUTHENTICATE_ACCOUNTS permission and same UID as account's authenticator
+ * is needed for those platforms. See docs for this function in API level 22.
+ *
+ * @param account The account for which an auth token is to be fetched. Cannot be {@code null}.
+ * @param authTokenType The type of auth token to fetch. Cannot be {@code null}.
+ * @return The cached auth token for this account and type, or null if
+ * no auth token is cached, the account does not exist, or the user is locked
+ * @see #getAuthToken
+ */
+ public String peekAuthToken(final Account account, final String authTokenType) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+ try {
+ return mService.peekAuthToken(account, authTokenType);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets or forgets a saved password. This modifies the local copy of the
+ * password used to automatically authenticate the user; it does not change
+ * the user's account password on the server. Intended for use by the
+ * authenticator, not directly by applications.
+ * <p>Calling this method does not update the last authenticated timestamp,
+ * referred by {@link #KEY_LAST_AUTHENTICATED_TIME}. To update it, call
+ * {@link #notifyAccountAuthenticated(Account)} after getting success.
+ * <p>It is safe to call this method from the main thread.
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that manages the specified account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * AUTHENTICATE_ACCOUNTS permission and same UID as account's authenticator
+ * is needed for those platforms. See docs for this function in API level 22.
+ *
+ * @param account The account whose password is to be set. Cannot be
+ * {@code null}.
+ * @param password The password to set, null to clear the password
+ */
+ public void setPassword(final Account account, final String password) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ try {
+ mService.setPassword(account, password);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Forgets a saved password. This erases the local copy of the password;
+ * it does not change the user's account password on the server.
+ * Has the same effect as setPassword(account, null) but requires fewer
+ * permissions, and may be used by applications or management interfaces
+ * to "sign out" from an account.
+ *
+ * <p>This method only successfully clear the account's password when the
+ * caller has the same signature as the authenticator that owns the
+ * specified account. Otherwise, this method will silently fail.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * MANAGE_ACCOUNTS permission is needed for those platforms. See docs for
+ * this function in API level 22.
+ *
+ * @param account The account whose password to clear
+ */
+ public void clearPassword(final Account account) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ try {
+ mService.clearPassword(account);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets one userdata key for an account. Intended by use for the
+ * authenticator to stash state for itself, not directly by applications.
+ * The meaning of the keys and values is up to the authenticator.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that manages the specified account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * AUTHENTICATE_ACCOUNTS permission and same UID as account's authenticator
+ * is needed for those platforms. See docs for this function in API level 22.
+ *
+ * @param account Account whose user data is to be set. Must not be {@code null}.
+ * @param key String user data key to set. Must not be null
+ * @param value String value to set, {@code null} to clear this user data key
+ */
+ public void setUserData(final Account account, final String key, final String value) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (key == null) throw new IllegalArgumentException("key is null");
+ try {
+ mService.setUserData(account, key, value);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Adds an auth token to the AccountManager cache for an account.
+ * If the account does not exist then this call has no effect.
+ * Replaces any previous auth token for this account and auth token type.
+ * Intended for use by the authenticator, not directly by applications.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p>This method requires the caller to have a signature match with the
+ * authenticator that manages the specified account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * AUTHENTICATE_ACCOUNTS permission and same UID as account's authenticator
+ * is needed for those platforms. See docs for this function in API level 22.
+ *
+ * @param account The account to set an auth token for
+ * @param authTokenType The type of the auth token, see {#getAuthToken}
+ * @param authToken The auth token to add to the cache
+ */
+ public void setAuthToken(Account account, final String authTokenType, final String authToken) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+ try {
+ mService.setAuthToken(account, authTokenType, authToken);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * This convenience helper synchronously gets an auth token with
+ * {@link #getAuthToken(Account, String, boolean, AccountManagerCallback, Handler)}.
+ *
+ * <p>This method may block while a network request completes, and must
+ * never be made from the main thread.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * USE_CREDENTIALS permission is needed for those platforms. See docs for
+ * this function in API level 22.
+ *
+ * @param account The account to fetch an auth token for
+ * @param authTokenType The auth token type, see {@link #getAuthToken getAuthToken()}
+ * @param notifyAuthFailure If true, display a notification and return null
+ * if authentication fails; if false, prompt and wait for the user to
+ * re-enter correct credentials before returning
+ * @return An auth token of the specified type for this account, or null
+ * if authentication fails or none can be fetched.
+ * @throws AuthenticatorException if the authenticator failed to respond
+ * @throws OperationCanceledException if the request was canceled for any
+ * reason, including the user canceling a credential request
+ * @throws java.io.IOException if the authenticator experienced an I/O problem
+ * creating a new auth token, usually because of network trouble
+ */
+ public String blockingGetAuthToken(Account account, String authTokenType,
+ boolean notifyAuthFailure)
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+ Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */,
+ null /* handler */).getResult();
+ if (bundle == null) {
+ // This should never happen, but it does, occasionally. If it does return null to
+ // signify that we were not able to get the authtoken.
+ // TODO: remove this when the bug is found that sometimes causes a null bundle to be
+ // returned
+ Log.e(TAG, "blockingGetAuthToken: null was returned from getResult() for "
+ + account + ", authTokenType " + authTokenType);
+ return null;
+ }
+ return bundle.getString(KEY_AUTHTOKEN);
+ }
+
+ /**
+ * Gets an auth token of the specified type for a particular account,
+ * prompting the user for credentials if necessary. This method is
+ * intended for applications running in the foreground where it makes
+ * sense to ask the user directly for a password.
+ *
+ * <p>If a previously generated auth token is cached for this account and
+ * type, then it is returned. Otherwise, if a saved password is
+ * available, it is sent to the server to generate a new auth token.
+ * Otherwise, the user is prompted to enter a password.
+ *
+ * <p>Some authenticators have auth token <em>types</em>, whose value
+ * is authenticator-dependent. Some services use different token types to
+ * access different functionality -- for example, Google uses different auth
+ * tokens to access Gmail and Google Calendar for the same account.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * USE_CREDENTIALS permission is needed for those platforms. See docs for
+ * this function in API level 22.
+ *
+ * <p>This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ *
+ * @param account The account to fetch an auth token for
+ * @param authTokenType The auth token type, an authenticator-dependent
+ * string token, must not be null
+ * @param options Authenticator-specific options for the request,
+ * may be null or empty
+ * @param activity The {@link Activity} context to use for launching a new
+ * authenticator-defined sub-Activity to prompt the user for a password
+ * if necessary; used only to call startActivity(); must not be null.
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with
+ * at least the following fields:
+ * <ul>
+ * <li> {@link #KEY_ACCOUNT_NAME} - the name of the account you supplied
+ * <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * <li> {@link #KEY_AUTHTOKEN} - the auth token you wanted
+ * </ul>
+ *
+ * (Other authenticator-specific values may be returned.) If an auth token
+ * could not be fetched, {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li> {@link AuthenticatorException} if the authenticator failed to respond
+ * <li> {@link OperationCanceledException} if the operation is canceled for
+ * any reason, incluidng the user canceling a credential request
+ * <li> {@link IOException} if the authenticator experienced an I/O problem
+ * creating a new auth token, usually because of network trouble
+ * </ul>
+ * If the account is no longer present on the device, the return value is
+ * authenticator-dependent. The caller should verify the validity of the
+ * account before requesting an auth token.
+ */
+ public AccountManagerFuture<Bundle> getAuthToken(
+ final Account account, final String authTokenType, final Bundle options,
+ final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+ final Bundle optionsIn = new Bundle();
+ if (options != null) {
+ optionsIn.putAll(options);
+ }
+ optionsIn.putString(KEY_ANDROID_PACKAGE_NAME, mContext.getPackageName());
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.getAuthToken(mResponse, account, authTokenType,
+ false /* notifyOnAuthFailure */, true /* expectActivityLaunch */,
+ optionsIn);
+ }
+ }.start();
+ }
+
+ /**
+ * Gets an auth token of the specified type for a particular account,
+ * optionally raising a notification if the user must enter credentials.
+ * This method is intended for background tasks and services where the
+ * user should not be immediately interrupted with a password prompt.
+ *
+ * <p>If a previously generated auth token is cached for this account and
+ * type, then it is returned. Otherwise, if a saved password is
+ * available, it is sent to the server to generate a new auth token.
+ * Otherwise, an {@link Intent} is returned which, when started, will
+ * prompt the user for a password. If the notifyAuthFailure parameter is
+ * set, a status bar notification is also created with the same Intent,
+ * alerting the user that they need to enter a password at some point.
+ *
+ * <p>In that case, you may need to wait until the user responds, which
+ * could take hours or days or forever. When the user does respond and
+ * supply a new password, the account manager will broadcast the
+ * {@link #LOGIN_ACCOUNTS_CHANGED_ACTION} Intent and
+ * notify {@link OnAccountsUpdateListener} which applications can
+ * use to try again.
+ *
+ * <p>If notifyAuthFailure is not set, it is the application's
+ * responsibility to launch the returned Intent at some point.
+ * Either way, the result from this call will not wait for user action.
+ *
+ * <p>Some authenticators have auth token <em>types</em>, whose value
+ * is authenticator-dependent. Some services use different token types to
+ * access different functionality -- for example, Google uses different auth
+ * tokens to access Gmail and Google Calendar for the same account.
+ *
+ * <p>This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ *
+ * @param account The account to fetch an auth token for
+ * @param authTokenType The auth token type, an authenticator-dependent
+ * string token, must not be null
+ * @param notifyAuthFailure True to add a notification to prompt the
+ * user for a password if necessary, false to leave that to the caller
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with
+ * at least the following fields on success:
+ * <ul>
+ * <li> {@link #KEY_ACCOUNT_NAME} - the name of the account you supplied
+ * <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * <li> {@link #KEY_AUTHTOKEN} - the auth token you wanted
+ * </ul>
+ *
+ * (Other authenticator-specific values may be returned.) If the user
+ * must enter credentials, the returned Bundle contains only
+ * {@link #KEY_INTENT} with the {@link Intent} needed to launch a prompt.
+ *
+ * If an error occurred, {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li> {@link AuthenticatorException} if the authenticator failed to respond
+ * <li> {@link OperationCanceledException} if the operation is canceled for
+ * any reason, incluidng the user canceling a credential request
+ * <li> {@link IOException} if the authenticator experienced an I/O problem
+ * creating a new auth token, usually because of network trouble
+ * </ul>
+ * If the account is no longer present on the device, the return value is
+ * authenticator-dependent. The caller should verify the validity of the
+ * account before requesting an auth token.
+ * @deprecated use {@link #getAuthToken(Account, String, android.os.Bundle,
+ * boolean, AccountManagerCallback, android.os.Handler)} instead
+ */
+ @Deprecated
+ public AccountManagerFuture<Bundle> getAuthToken(
+ final Account account, final String authTokenType,
+ final boolean notifyAuthFailure,
+ AccountManagerCallback<Bundle> callback, Handler handler) {
+ return getAuthToken(account, authTokenType, null, notifyAuthFailure, callback,
+ handler);
+ }
+
+ /**
+ * Gets an auth token of the specified type for a particular account,
+ * optionally raising a notification if the user must enter credentials.
+ * This method is intended for background tasks and services where the
+ * user should not be immediately interrupted with a password prompt.
+ *
+ * <p>If a previously generated auth token is cached for this account and
+ * type, then it is returned. Otherwise, if a saved password is
+ * available, it is sent to the server to generate a new auth token.
+ * Otherwise, an {@link Intent} is returned which, when started, will
+ * prompt the user for a password. If the notifyAuthFailure parameter is
+ * set, a status bar notification is also created with the same Intent,
+ * alerting the user that they need to enter a password at some point.
+ *
+ * <p>In that case, you may need to wait until the user responds, which
+ * could take hours or days or forever. When the user does respond and
+ * supply a new password, the account manager will broadcast the
+ * {@link #LOGIN_ACCOUNTS_CHANGED_ACTION} Intent and
+ * notify {@link OnAccountsUpdateListener} which applications can
+ * use to try again.
+ *
+ * <p>If notifyAuthFailure is not set, it is the application's
+ * responsibility to launch the returned Intent at some point.
+ * Either way, the result from this call will not wait for user action.
+ *
+ * <p>Some authenticators have auth token <em>types</em>, whose value
+ * is authenticator-dependent. Some services use different token types to
+ * access different functionality -- for example, Google uses different auth
+ * tokens to access Gmail and Google Calendar for the same account.
+ *
+ * <p>This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * USE_CREDENTIALS permission is needed for those platforms. See docs for
+ * this function in API level 22.
+ *
+ * @param account The account to fetch an auth token for
+ * @param authTokenType The auth token type, an authenticator-dependent
+ * string token, must not be null
+ * @param options Authenticator-specific options for the request,
+ * may be null or empty
+ * @param notifyAuthFailure True to add a notification to prompt the
+ * user for a password if necessary, false to leave that to the caller
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with
+ * at least the following fields on success:
+ * <ul>
+ * <li> {@link #KEY_ACCOUNT_NAME} - the name of the account you supplied
+ * <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * <li> {@link #KEY_AUTHTOKEN} - the auth token you wanted
+ * </ul>
+ *
+ * (Other authenticator-specific values may be returned.) If the user
+ * must enter credentials, the returned Bundle contains only
+ * {@link #KEY_INTENT} with the {@link Intent} needed to launch a prompt.
+ *
+ * If an error occurred, {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li> {@link AuthenticatorException} if the authenticator failed to respond
+ * <li> {@link OperationCanceledException} if the operation is canceled for
+ * any reason, incluidng the user canceling a credential request
+ * <li> {@link IOException} if the authenticator experienced an I/O problem
+ * creating a new auth token, usually because of network trouble
+ * </ul>
+ * If the account is no longer present on the device, the return value is
+ * authenticator-dependent. The caller should verify the validity of the
+ * account before requesting an auth token.
+ */
+ public AccountManagerFuture<Bundle> getAuthToken(
+ final Account account, final String authTokenType, final Bundle options,
+ final boolean notifyAuthFailure,
+ AccountManagerCallback<Bundle> callback, Handler handler) {
+
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+ final Bundle optionsIn = new Bundle();
+ if (options != null) {
+ optionsIn.putAll(options);
+ }
+ optionsIn.putString(KEY_ANDROID_PACKAGE_NAME, mContext.getPackageName());
+ return new AmsTask(null, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.getAuthToken(mResponse, account, authTokenType,
+ notifyAuthFailure, false /* expectActivityLaunch */, optionsIn);
+ }
+ }.start();
+ }
+
+ /**
+ * Asks the user to add an account of a specified type. The authenticator
+ * for this account type processes this request with the appropriate user
+ * interface. If the user does elect to create a new account, the account
+ * name is returned.
+ *
+ * <p>This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * MANAGE_ACCOUNTS permission is needed for those platforms. See docs for
+ * this function in API level 22.
+ *
+ * @param accountType The type of account to add; must not be null
+ * @param authTokenType The type of auth token (see {@link #getAuthToken})
+ * this account will need to be able to generate, null for none
+ * @param requiredFeatures The features (see {@link #hasFeatures}) this
+ * account must have, null for none
+ * @param addAccountOptions Authenticator-specific options for the request,
+ * may be null or empty
+ * @param activity The {@link Activity} context to use for launching a new
+ * authenticator-defined sub-Activity to prompt the user to create an
+ * account; used only to call startActivity(); if null, the prompt
+ * will not be launched directly, but the necessary {@link Intent}
+ * will be returned to the caller instead
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with
+ * these fields if activity was specified and an account was created:
+ * <ul>
+ * <li> {@link #KEY_ACCOUNT_NAME} - the name of the account created
+ * <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * </ul>
+ *
+ * If no activity was specified, the returned Bundle contains only
+ * {@link #KEY_INTENT} with the {@link Intent} needed to launch the
+ * actual account creation process. If an error occurred,
+ * {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li> {@link AuthenticatorException} if no authenticator was registered for
+ * this account type or the authenticator failed to respond
+ * <li> {@link OperationCanceledException} if the operation was canceled for
+ * any reason, including the user canceling the creation process or adding accounts
+ * (of this type) has been disabled by policy
+ * <li> {@link IOException} if the authenticator experienced an I/O problem
+ * creating a new account, usually because of network trouble
+ * </ul>
+ */
+ @UserHandleAware
+ public AccountManagerFuture<Bundle> addAccount(final String accountType,
+ final String authTokenType, final String[] requiredFeatures,
+ final Bundle addAccountOptions,
+ final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
+ if (Process.myUserHandle().equals(mContext.getUser())) {
+ if (accountType == null) throw new IllegalArgumentException("accountType is null");
+ final Bundle optionsIn = new Bundle();
+ if (addAccountOptions != null) {
+ optionsIn.putAll(addAccountOptions);
+ }
+ optionsIn.putString(KEY_ANDROID_PACKAGE_NAME, mContext.getPackageName());
+
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.addAccount(mResponse, accountType, authTokenType,
+ requiredFeatures, activity != null, optionsIn);
+ }
+ }.start();
+ } else {
+ return addAccountAsUser(accountType, authTokenType, requiredFeatures, addAccountOptions,
+ activity, callback, handler, mContext.getUser());
+ }
+ }
+
+ /**
+ * @see #addAccount(String, String, String[], Bundle, Activity, AccountManagerCallback, Handler)
+ * @hide
+ */
+ public AccountManagerFuture<Bundle> addAccountAsUser(final String accountType,
+ final String authTokenType, final String[] requiredFeatures,
+ final Bundle addAccountOptions, final Activity activity,
+ AccountManagerCallback<Bundle> callback, Handler handler, final UserHandle userHandle) {
+ if (accountType == null) throw new IllegalArgumentException("accountType is null");
+ if (userHandle == null) throw new IllegalArgumentException("userHandle is null");
+ final Bundle optionsIn = new Bundle();
+ if (addAccountOptions != null) {
+ optionsIn.putAll(addAccountOptions);
+ }
+ optionsIn.putString(KEY_ANDROID_PACKAGE_NAME, mContext.getPackageName());
+
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.addAccountAsUser(mResponse, accountType, authTokenType,
+ requiredFeatures, activity != null, optionsIn, userHandle.getIdentifier());
+ }
+ }.start();
+ }
+
+
+ /**
+ * Adds shared accounts from a parent user to a secondary user. Adding the shared account
+ * doesn't take effect immediately. When the target user starts up, any pending shared accounts
+ * are attempted to be copied to the target user from the primary via calls to the
+ * authenticator.
+ * @param parentUser parent user
+ * @param user target user
+ * @hide
+ */
+ public void addSharedAccountsFromParentUser(UserHandle parentUser, UserHandle user) {
+ try {
+ mService.addSharedAccountsFromParentUser(parentUser.getIdentifier(),
+ user.getIdentifier(), mContext.getOpPackageName());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Copies an account from one user to another user.
+ * @param account the account to copy
+ * @param fromUser the user to copy the account from
+ * @param toUser the target user
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Boolean indicated wether it
+ * succeeded.
+ * @hide
+ */
+ public AccountManagerFuture<Boolean> copyAccountToUser(
+ final Account account, final UserHandle fromUser, final UserHandle toUser,
+ AccountManagerCallback<Boolean> callback, Handler handler) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ if (toUser == null || fromUser == null) {
+ throw new IllegalArgumentException("fromUser and toUser cannot be null");
+ }
+
+ return new Future2Task<Boolean>(handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.copyAccountToUser(
+ mResponse, account, fromUser.getIdentifier(), toUser.getIdentifier());
+ }
+ @Override
+ public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException {
+ if (!bundle.containsKey(KEY_BOOLEAN_RESULT)) {
+ throw new AuthenticatorException("no result in response");
+ }
+ return bundle.getBoolean(KEY_BOOLEAN_RESULT);
+ }
+ }.start();
+ }
+
+ /**
+ * Confirms that the user knows the password for an account to make extra
+ * sure they are the owner of the account. The user-entered password can
+ * be supplied directly, otherwise the authenticator for this account type
+ * prompts the user with the appropriate interface. This method is
+ * intended for applications which want extra assurance; for example, the
+ * phone lock screen uses this to let the user unlock the phone with an
+ * account password if they forget the lock pattern.
+ *
+ * <p>If the user-entered password matches a saved password for this
+ * account, the request is considered valid; otherwise the authenticator
+ * verifies the password (usually by contacting the server).
+ *
+ * <p>This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * MANAGE_ACCOUNTS permission is needed for those platforms. See docs
+ * for this function in API level 22.
+ *
+ * @param account The account to confirm password knowledge for
+ * @param options Authenticator-specific options for the request;
+ * if the {@link #KEY_PASSWORD} string field is present, the
+ * authenticator may use it directly rather than prompting the user;
+ * may be null or empty
+ * @param activity The {@link Activity} context to use for launching a new
+ * authenticator-defined sub-Activity to prompt the user to enter a
+ * password; used only to call startActivity(); if null, the prompt
+ * will not be launched directly, but the necessary {@link Intent}
+ * will be returned to the caller instead
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle
+ * with these fields if activity or password was supplied and
+ * the account was successfully verified:
+ * <ul>
+ * <li> {@link #KEY_ACCOUNT_NAME} - the name of the account verified
+ * <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * <li> {@link #KEY_BOOLEAN_RESULT} - true to indicate success
+ * </ul>
+ *
+ * If no activity or password was specified, the returned Bundle contains
+ * {@link #KEY_INTENT} with the {@link Intent} needed to launch the
+ * password prompt.
+ *
+ * <p>Also the returning Bundle may contain {@link
+ * #KEY_LAST_AUTHENTICATED_TIME} indicating the last time the
+ * credential was validated/created.
+ *
+ * If an error occurred,{@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li> {@link AuthenticatorException} if the authenticator failed to respond
+ * <li> {@link OperationCanceledException} if the operation was canceled for
+ * any reason, including the user canceling the password prompt
+ * <li> {@link IOException} if the authenticator experienced an I/O problem
+ * verifying the password, usually because of network trouble
+ * </ul>
+ */
+ @UserHandleAware
+ public AccountManagerFuture<Bundle> confirmCredentials(final Account account,
+ final Bundle options,
+ final Activity activity,
+ final AccountManagerCallback<Bundle> callback,
+ final Handler handler) {
+ return confirmCredentialsAsUser(account, options, activity, callback, handler,
+ mContext.getUser());
+ }
+
+ /**
+ * @hide
+ * Same as {@link #confirmCredentials(Account, Bundle, Activity, AccountManagerCallback, Handler)}
+ * but for the specified user.
+ */
+ @UnsupportedAppUsage
+ public AccountManagerFuture<Bundle> confirmCredentialsAsUser(final Account account,
+ final Bundle options,
+ final Activity activity,
+ final AccountManagerCallback<Bundle> callback,
+ final Handler handler, UserHandle userHandle) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ final int userId = userHandle.getIdentifier();
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.confirmCredentialsAsUser(mResponse, account, options, activity != null,
+ userId);
+ }
+ }.start();
+ }
+
+ /**
+ * Asks the user to enter a new password for an account, updating the
+ * saved credentials for the account. Normally this happens automatically
+ * when the server rejects credentials during an auth token fetch, but this
+ * can be invoked directly to ensure we have the correct credentials stored.
+ *
+ * <p>This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * MANAGE_ACCOUNTS permission is needed for those platforms. See docs for
+ * this function in API level 22.
+ *
+ * @param account The account to update credentials for
+ * @param authTokenType The credentials entered must allow an auth token
+ * of this type to be created (but no actual auth token is returned);
+ * may be null
+ * @param options Authenticator-specific options for the request;
+ * may be null or empty
+ * @param activity The {@link Activity} context to use for launching a new
+ * authenticator-defined sub-Activity to prompt the user to enter a
+ * password; used only to call startActivity(); if null, the prompt
+ * will not be launched directly, but the necessary {@link Intent}
+ * will be returned to the caller instead
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle
+ * with these fields if an activity was supplied and the account
+ * credentials were successfully updated:
+ * <ul>
+ * <li> {@link #KEY_ACCOUNT_NAME} - the name of the account
+ * <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * </ul>
+ *
+ * If no activity was specified, the returned Bundle contains
+ * {@link #KEY_INTENT} with the {@link Intent} needed to launch the
+ * password prompt. If an error occurred,
+ * {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li> {@link AuthenticatorException} if the authenticator failed to respond
+ * <li> {@link OperationCanceledException} if the operation was canceled for
+ * any reason, including the user canceling the password prompt
+ * <li> {@link IOException} if the authenticator experienced an I/O problem
+ * verifying the password, usually because of network trouble
+ * </ul>
+ */
+ public AccountManagerFuture<Bundle> updateCredentials(final Account account,
+ final String authTokenType,
+ final Bundle options, final Activity activity,
+ final AccountManagerCallback<Bundle> callback,
+ final Handler handler) {
+ if (account == null) throw new IllegalArgumentException("account is null");
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.updateCredentials(mResponse, account, authTokenType, activity != null,
+ options);
+ }
+ }.start();
+ }
+
+ /**
+ * Offers the user an opportunity to change an authenticator's settings.
+ * These properties are for the authenticator in general, not a particular
+ * account. Not all authenticators support this method.
+ *
+ * <p>This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ *
+ * <p>This method requires the caller to have the same signature as the
+ * authenticator associated with the specified account type.
+ *
+ * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
+ * MANAGE_ACCOUNTS permission is needed for those platforms. See docs
+ * for this function in API level 22.
+ *
+ * @param accountType The account type associated with the authenticator
+ * to adjust
+ * @param activity The {@link Activity} context to use for launching a new
+ * authenticator-defined sub-Activity to adjust authenticator settings;
+ * used only to call startActivity(); if null, the settings dialog will
+ * not be launched directly, but the necessary {@link Intent} will be
+ * returned to the caller instead
+ * @param callback Callback to invoke when the request completes,
+ * null for no callback
+ * @param handler {@link Handler} identifying the callback thread,
+ * null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle
+ * which is empty if properties were edited successfully, or
+ * if no activity was specified, contains only {@link #KEY_INTENT}
+ * needed to launch the authenticator's settings dialog.
+ * If an error occurred, {@link AccountManagerFuture#getResult()}
+ * throws:
+ * <ul>
+ * <li> {@link AuthenticatorException} if no authenticator was registered for
+ * this account type or the authenticator failed to respond
+ * <li> {@link OperationCanceledException} if the operation was canceled for
+ * any reason, including the user canceling the settings dialog
+ * <li> {@link IOException} if the authenticator experienced an I/O problem
+ * updating settings, usually because of network trouble
+ * </ul>
+ */
+ public AccountManagerFuture<Bundle> editProperties(final String accountType,
+ final Activity activity, final AccountManagerCallback<Bundle> callback,
+ final Handler handler) {
+ if (accountType == null) throw new IllegalArgumentException("accountType is null");
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.editProperties(mResponse, accountType, activity != null);
+ }
+ }.start();
+ }
+
+ /**
+ * @hide
+ * Checks if the given account exists on any of the users on the device.
+ * Only the system process can call this method.
+ *
+ * @param account The account to check for existence.
+ * @return whether any user has this account
+ */
+ public boolean someUserHasAccount(@NonNull final Account account) {
+ try {
+ return mService.someUserHasAccount(account);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ private void ensureNotOnMainThread() {
+ final Looper looper = Looper.myLooper();
+ if (looper != null && looper == mContext.getMainLooper()) {
+ final IllegalStateException exception = new IllegalStateException(
+ "calling this from your main thread can lead to deadlock");
+ Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs",
+ exception);
+ if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.FROYO) {
+ throw exception;
+ }
+ }
+ }
+
+ private void postToHandler(Handler handler, final AccountManagerCallback<Bundle> callback,
+ final AccountManagerFuture<Bundle> future) {
+ handler = handler == null ? mMainHandler : handler;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.run(future);
+ }
+ });
+ }
+
+ private void postToHandler(Handler handler, final OnAccountsUpdateListener listener,
+ final Account[] accounts) {
+ final Account[] accountsCopy = new Account[accounts.length];
+ // send a copy to make sure that one doesn't
+ // change what another sees
+ System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length);
+ handler = (handler == null) ? mMainHandler : handler;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mAccountsUpdatedListeners) {
+ try {
+ if (mAccountsUpdatedListeners.containsKey(listener)) {
+ Set<String> types = mAccountsUpdatedListenersTypes.get(listener);
+ if (types != null) {
+ // filter by account type;
+ ArrayList<Account> filtered = new ArrayList<>();
+ for (Account account : accountsCopy) {
+ if (types.contains(account.type)) {
+ filtered.add(account);
+ }
+ }
+ listener.onAccountsUpdated(
+ filtered.toArray(new Account[filtered.size()]));
+ } else {
+ listener.onAccountsUpdated(accountsCopy);
+ }
+ }
+ } catch (SQLException e) {
+ // Better luck next time. If the problem was disk-full,
+ // the STORAGE_OK intent will re-trigger the update.
+ Log.e(TAG, "Can't update accounts", e);
+ }
+ }
+ }
+ });
+ }
+
+ private abstract class AmsTask extends FutureTask<Bundle> implements AccountManagerFuture<Bundle> {
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ final IAccountManagerResponse mResponse;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ final Handler mHandler;
+ final AccountManagerCallback<Bundle> mCallback;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ final Activity mActivity;
+ public AmsTask(Activity activity, Handler handler, AccountManagerCallback<Bundle> callback) {
+ super(new Callable<Bundle>() {
+ @Override
+ public Bundle call() throws Exception {
+ throw new IllegalStateException("this should never be called");
+ }
+ });
+
+ mHandler = handler;
+ mCallback = callback;
+ mActivity = activity;
+ mResponse = new Response();
+ }
+
+ public final AccountManagerFuture<Bundle> start() {
+ try {
+ doWork();
+ } catch (RemoteException e) {
+ setException(e);
+ }
+ return this;
+ }
+
+ @Override
+ protected void set(Bundle bundle) {
+ // TODO: somehow a null is being set as the result of the Future. Log this
+ // case to help debug where this is occurring. When this bug is fixed this
+ // condition statement should be removed.
+ if (bundle == null) {
+ Log.e(TAG, "the bundle must not be null", new Exception());
+ }
+ super.set(bundle);
+ }
+
+ public abstract void doWork() throws RemoteException;
+
+ private Bundle internalGetResult(Long timeout, TimeUnit unit)
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ if (!isDone()) {
+ ensureNotOnMainThread();
+ }
+ try {
+ if (timeout == null) {
+ return get();
+ } else {
+ return get(timeout, unit);
+ }
+ } catch (CancellationException e) {
+ throw new OperationCanceledException();
+ } catch (TimeoutException e) {
+ // fall through and cancel
+ } catch (InterruptedException e) {
+ // fall through and cancel
+ } catch (ExecutionException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof IOException) {
+ throw (IOException) cause;
+ } else if (cause instanceof UnsupportedOperationException) {
+ throw new AuthenticatorException(cause);
+ } else if (cause instanceof AuthenticatorException) {
+ throw (AuthenticatorException) cause;
+ } else if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ } else if (cause instanceof Error) {
+ throw (Error) cause;
+ } else {
+ throw new IllegalStateException(cause);
+ }
+ } finally {
+ cancel(true /* interrupt if running */);
+ }
+ throw new OperationCanceledException();
+ }
+
+ @Override
+ public Bundle getResult()
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return internalGetResult(null, null);
+ }
+
+ @Override
+ public Bundle getResult(long timeout, TimeUnit unit)
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return internalGetResult(timeout, unit);
+ }
+
+ @Override
+ protected void done() {
+ if (mCallback != null) {
+ postToHandler(mHandler, mCallback, this);
+ }
+ }
+
+ /** Handles the responses from the AccountManager */
+ private class Response extends IAccountManagerResponse.Stub {
+ @Override
+ public void onResult(Bundle bundle) {
+ if (bundle == null) {
+ onError(ERROR_CODE_INVALID_RESPONSE, "null bundle returned");
+ return;
+ }
+ Intent intent = bundle.getParcelable(KEY_INTENT);
+ if (intent != null && mActivity != null) {
+ // since the user provided an Activity we will silently start intents
+ // that we see
+ mActivity.startActivity(intent);
+ // leave the Future running to wait for the real response to this request
+ } else if (bundle.getBoolean("retry")) {
+ try {
+ doWork();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } else {
+ set(bundle);
+ }
+ }
+
+ @Override
+ public void onError(int code, String message) {
+ if (code == ERROR_CODE_CANCELED || code == ERROR_CODE_USER_RESTRICTED
+ || code == ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE) {
+ // the authenticator indicated that this request was canceled or we were
+ // forbidden to fulfill; cancel now
+ cancel(true /* mayInterruptIfRunning */);
+ return;
+ }
+ setException(convertErrorToException(code, message));
+ }
+ }
+
+ }
+
+ private abstract class BaseFutureTask<T> extends FutureTask<T> {
+ final public IAccountManagerResponse mResponse;
+ final Handler mHandler;
+
+ public BaseFutureTask(Handler handler) {
+ super(new Callable<T>() {
+ @Override
+ public T call() throws Exception {
+ throw new IllegalStateException("this should never be called");
+ }
+ });
+ mHandler = handler;
+ mResponse = new Response();
+ }
+
+ public abstract void doWork() throws RemoteException;
+
+ public abstract T bundleToResult(Bundle bundle) throws AuthenticatorException;
+
+ protected void postRunnableToHandler(Runnable runnable) {
+ Handler handler = (mHandler == null) ? mMainHandler : mHandler;
+ handler.post(runnable);
+ }
+
+ protected void startTask() {
+ try {
+ doWork();
+ } catch (RemoteException e) {
+ setException(e);
+ }
+ }
+
+ protected class Response extends IAccountManagerResponse.Stub {
+ @Override
+ public void onResult(Bundle bundle) {
+ try {
+ T result = bundleToResult(bundle);
+ if (result == null) {
+ return;
+ }
+ set(result);
+ return;
+ } catch (ClassCastException e) {
+ // we will set the exception below
+ } catch (AuthenticatorException e) {
+ // we will set the exception below
+ }
+ onError(ERROR_CODE_INVALID_RESPONSE, "no result in response");
+ }
+
+ @Override
+ public void onError(int code, String message) {
+ if (code == ERROR_CODE_CANCELED || code == ERROR_CODE_USER_RESTRICTED
+ || code == ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE) {
+ // the authenticator indicated that this request was canceled or we were
+ // forbidden to fulfill; cancel now
+ cancel(true /* mayInterruptIfRunning */);
+ return;
+ }
+ setException(convertErrorToException(code, message));
+ }
+ }
+ }
+
+ private abstract class Future2Task<T>
+ extends BaseFutureTask<T> implements AccountManagerFuture<T> {
+ final AccountManagerCallback<T> mCallback;
+ public Future2Task(Handler handler, AccountManagerCallback<T> callback) {
+ super(handler);
+ mCallback = callback;
+ }
+
+ @Override
+ protected void done() {
+ if (mCallback != null) {
+ postRunnableToHandler(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.run(Future2Task.this);
+ }
+ });
+ }
+ }
+
+ public Future2Task<T> start() {
+ startTask();
+ return this;
+ }
+
+ private T internalGetResult(Long timeout, TimeUnit unit)
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ if (!isDone()) {
+ ensureNotOnMainThread();
+ }
+ try {
+ if (timeout == null) {
+ return get();
+ } else {
+ return get(timeout, unit);
+ }
+ } catch (InterruptedException e) {
+ // fall through and cancel
+ } catch (TimeoutException e) {
+ // fall through and cancel
+ } catch (CancellationException e) {
+ // fall through and cancel
+ } catch (ExecutionException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof IOException) {
+ throw (IOException) cause;
+ } else if (cause instanceof UnsupportedOperationException) {
+ throw new AuthenticatorException(cause);
+ } else if (cause instanceof AuthenticatorException) {
+ throw (AuthenticatorException) cause;
+ } else if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ } else if (cause instanceof Error) {
+ throw (Error) cause;
+ } else {
+ throw new IllegalStateException(cause);
+ }
+ } finally {
+ cancel(true /* interrupt if running */);
+ }
+ throw new OperationCanceledException();
+ }
+
+ @Override
+ public T getResult()
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return internalGetResult(null, null);
+ }
+
+ @Override
+ public T getResult(long timeout, TimeUnit unit)
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return internalGetResult(timeout, unit);
+ }
+
+ }
+
+ private Exception convertErrorToException(int code, String message) {
+ if (code == ERROR_CODE_NETWORK_ERROR) {
+ return new IOException(message);
+ }
+
+ if (code == ERROR_CODE_UNSUPPORTED_OPERATION) {
+ return new UnsupportedOperationException(message);
+ }
+
+ if (code == ERROR_CODE_INVALID_RESPONSE) {
+ return new AuthenticatorException(message);
+ }
+
+ if (code == ERROR_CODE_BAD_ARGUMENTS) {
+ return new IllegalArgumentException(message);
+ }
+
+ return new AuthenticatorException(message);
+ }
+
+ private void getAccountByTypeAndFeatures(String accountType, String[] features,
+ AccountManagerCallback<Bundle> callback, Handler handler) {
+ (new AmsTask(null, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.getAccountByTypeAndFeatures(mResponse, accountType, features,
+ mContext.getOpPackageName());
+ }
+
+ }).start();
+ }
+
+ private class GetAuthTokenByTypeAndFeaturesTask
+ extends AmsTask implements AccountManagerCallback<Bundle> {
+ GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType,
+ final String[] features, Activity activityForPrompting,
+ final Bundle addAccountOptions, final Bundle loginOptions,
+ AccountManagerCallback<Bundle> callback, Handler handler) {
+ super(activityForPrompting, handler, callback);
+ if (accountType == null) throw new IllegalArgumentException("account type is null");
+ mAccountType = accountType;
+ mAuthTokenType = authTokenType;
+ mFeatures = features;
+ mAddAccountOptions = addAccountOptions;
+ mLoginOptions = loginOptions;
+ mMyCallback = this;
+ }
+ volatile AccountManagerFuture<Bundle> mFuture = null;
+ final String mAccountType;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ final String mAuthTokenType;
+ final String[] mFeatures;
+ final Bundle mAddAccountOptions;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ final Bundle mLoginOptions;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ final AccountManagerCallback<Bundle> mMyCallback;
+ private volatile int mNumAccounts = 0;
+
+ @Override
+ public void doWork() throws RemoteException {
+ getAccountByTypeAndFeatures(mAccountType, mFeatures,
+ new AccountManagerCallback<Bundle>() {
+ @Override
+ public void run(AccountManagerFuture<Bundle> future) {
+ String accountName = null;
+ String accountType = null;
+ try {
+ Bundle result = future.getResult();
+ accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME);
+ accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
+ } catch (OperationCanceledException e) {
+ setException(e);
+ return;
+ } catch (IOException e) {
+ setException(e);
+ return;
+ } catch (AuthenticatorException e) {
+ setException(e);
+ return;
+ }
+
+ if (accountName == null) {
+ if (mActivity != null) {
+ // no accounts, add one now. pretend that the user directly
+ // made this request
+ mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures,
+ mAddAccountOptions, mActivity, mMyCallback, mHandler);
+ } else {
+ // send result since we can't prompt to add an account
+ Bundle result = new Bundle();
+ result.putString(KEY_ACCOUNT_NAME, null);
+ result.putString(KEY_ACCOUNT_TYPE, null);
+ result.putString(KEY_AUTHTOKEN, null);
+ result.putBinder(KEY_ACCOUNT_ACCESS_ID, null);
+ try {
+ mResponse.onResult(result);
+ } catch (RemoteException e) {
+ // this will never happen
+ }
+ // we are done
+ }
+ } else {
+ mNumAccounts = 1;
+ Account account = new Account(accountName, accountType);
+ // have a single account, return an authtoken for it
+ if (mActivity == null) {
+ mFuture = getAuthToken(account, mAuthTokenType,
+ false /* notifyAuthFailure */, mMyCallback, mHandler);
+ } else {
+ mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions,
+ mActivity, mMyCallback, mHandler);
+ }
+ }
+ }}, mHandler);
+ }
+
+ @Override
+ public void run(AccountManagerFuture<Bundle> future) {
+ try {
+ final Bundle result = future.getResult();
+ if (mNumAccounts == 0) {
+ final String accountName = result.getString(KEY_ACCOUNT_NAME);
+ final String accountType = result.getString(KEY_ACCOUNT_TYPE);
+ if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
+ setException(new AuthenticatorException("account not in result"));
+ return;
+ }
+ final String accessId = result.getString(KEY_ACCOUNT_ACCESS_ID);
+ final Account account = new Account(accountName, accountType, accessId);
+ mNumAccounts = 1;
+ getAuthToken(account, mAuthTokenType, null /* options */, mActivity,
+ mMyCallback, mHandler);
+ return;
+ }
+ set(result);
+ } catch (OperationCanceledException e) {
+ cancel(true /* mayInterruptIfRUnning */);
+ } catch (IOException e) {
+ setException(e);
+ } catch (AuthenticatorException e) {
+ setException(e);
+ }
+ }
+ }
+
+ /**
+ * This convenience helper combines the functionality of {@link #getAccountsByTypeAndFeatures},
+ * {@link #getAuthToken}, and {@link #addAccount}.
+ *
+ * <p>
+ * This method gets a list of the accounts matching specific type and feature set which are
+ * visible to the caller (see {@link #getAccountsByType} for details);
+ * if there is exactly one already visible account, it is used; if there are some
+ * accounts for which user grant visibility, the user is prompted to pick one; if there are
+ * none, the user is prompted to add one. Finally, an auth token is acquired for the chosen
+ * account.
+ *
+ * <p>
+ * This method may be called from any thread, but the returned {@link AccountManagerFuture} must
+ * not be used on the main thread.
+ *
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level 22 and before, MANAGE_ACCOUNTS
+ * permission is needed for those platforms. See docs for this function in API level 22.
+ *
+ * @param accountType The account type required (see {@link #getAccountsByType}), must not be
+ * null
+ * @param authTokenType The desired auth token type (see {@link #getAuthToken}), must not be
+ * null
+ * @param features Required features for the account (see
+ * {@link #getAccountsByTypeAndFeatures}), may be null or empty
+ * @param activity The {@link Activity} context to use for launching new sub-Activities to
+ * prompt to add an account, select an account, and/or enter a password, as necessary;
+ * used only to call startActivity(); should not be null
+ * @param addAccountOptions Authenticator-specific options to use for adding new accounts; may
+ * be null or empty
+ * @param getAuthTokenOptions Authenticator-specific options to use for getting auth tokens; may
+ * be null or empty
+ * @param callback Callback to invoke when the request completes, null for no callback
+ * @param handler {@link Handler} identifying the callback thread, null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with at least the
+ * following fields:
+ * <ul>
+ * <li>{@link #KEY_ACCOUNT_NAME} - the name of the account
+ * <li>{@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * <li>{@link #KEY_AUTHTOKEN} - the auth token you wanted
+ * </ul>
+ *
+ * If an error occurred, {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li>{@link AuthenticatorException} if no authenticator was registered for this
+ * account type or the authenticator failed to respond
+ * <li>{@link OperationCanceledException} if the operation was canceled for any reason,
+ * including the user canceling any operation
+ * <li>{@link IOException} if the authenticator experienced an I/O problem updating
+ * settings, usually because of network trouble
+ * </ul>
+ */
+ public AccountManagerFuture<Bundle> getAuthTokenByFeatures(
+ final String accountType, final String authTokenType, final String[] features,
+ final Activity activity, final Bundle addAccountOptions,
+ final Bundle getAuthTokenOptions, final AccountManagerCallback<Bundle> callback,
+ final Handler handler) {
+ if (accountType == null) throw new IllegalArgumentException("account type is null");
+ if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+ final GetAuthTokenByTypeAndFeaturesTask task =
+ new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType, features,
+ activity, addAccountOptions, getAuthTokenOptions, callback, handler);
+ task.start();
+ return task;
+ }
+
+ /**
+ * Deprecated in favor of {@link #newChooseAccountIntent(Account, List, String[], String,
+ * String, String[], Bundle)}.
+ *
+ * Returns an intent to an {@link Activity} that prompts the user to choose from a list of
+ * accounts.
+ * The caller will then typically start the activity by calling
+ * <code>startActivityForResult(intent, ...);</code>.
+ * <p>
+ * On success the activity returns a Bundle with the account name and type specified using
+ * keys {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE}.
+ * Chosen account is marked as {@link #VISIBILITY_USER_MANAGED_VISIBLE} to the caller
+ * (see {@link #setAccountVisibility}) and will be returned to it in consequent
+ * {@link #getAccountsByType}) calls.
+ * <p>
+ * The most common case is to call this with one account type, e.g.:
+ * <p>
+ * <pre> newChooseAccountIntent(null, null, new String[]{"com.google"}, false, null,
+ * null, null, null);</pre>
+ * @param selectedAccount if specified, indicates that the {@link Account} is the currently
+ * selected one, according to the caller's definition of selected.
+ * @param allowableAccounts an optional {@link List} of accounts that are allowed to be
+ * shown. If not specified then this field will not limit the displayed accounts.
+ * @param allowableAccountTypes an optional string array of account types. These are used
+ * both to filter the shown accounts and to filter the list of account types that are shown
+ * when adding an account. If not specified then this field will not limit the displayed
+ * account types when adding an account.
+ * @param alwaysPromptForAccount boolean that is ignored.
+ * @param descriptionOverrideText if non-null this string is used as the description in the
+ * accounts chooser screen rather than the default
+ * @param addAccountAuthTokenType this string is passed as the {@link #addAccount}
+ * authTokenType parameter
+ * @param addAccountRequiredFeatures this string array is passed as the {@link #addAccount}
+ * requiredFeatures parameter
+ * @param addAccountOptions This {@link Bundle} is passed as the {@link #addAccount} options
+ * parameter
+ * @return an {@link Intent} that can be used to launch the ChooseAccount activity flow.
+ */
+ @Deprecated
+ static public Intent newChooseAccountIntent(
+ Account selectedAccount,
+ ArrayList<Account> allowableAccounts,
+ String[] allowableAccountTypes,
+ boolean alwaysPromptForAccount,
+ String descriptionOverrideText,
+ String addAccountAuthTokenType,
+ String[] addAccountRequiredFeatures,
+ Bundle addAccountOptions) {
+ return newChooseAccountIntent(
+ selectedAccount,
+ allowableAccounts,
+ allowableAccountTypes,
+ descriptionOverrideText,
+ addAccountAuthTokenType,
+ addAccountRequiredFeatures,
+ addAccountOptions);
+ }
+
+ /**
+ * Returns an intent to an {@link Activity} that prompts the user to choose from a list of
+ * accounts.
+ * The caller will then typically start the activity by calling
+ * <code>startActivityForResult(intent, ...);</code>.
+ * <p>
+ * On success the activity returns a Bundle with the account name and type specified using
+ * keys {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE}.
+ * Chosen account is marked as {@link #VISIBILITY_USER_MANAGED_VISIBLE} to the caller
+ * (see {@link #setAccountVisibility}) and will be returned to it in consequent
+ * {@link #getAccountsByType}) calls.
+ * <p>
+ * The most common case is to call this with one account type, e.g.:
+ * <p>
+ * <pre> newChooseAccountIntent(null, null, new String[]{"com.google"}, null, null, null,
+ * null);</pre>
+ * @param selectedAccount if specified, indicates that the {@link Account} is the currently
+ * selected one, according to the caller's definition of selected.
+ * @param allowableAccounts an optional {@link List} of accounts that are allowed to be
+ * shown. If not specified then this field will not limit the displayed accounts.
+ * @param allowableAccountTypes an optional string array of account types. These are used
+ * both to filter the shown accounts and to filter the list of account types that are shown
+ * when adding an account. If not specified then this field will not limit the displayed
+ * account types when adding an account.
+ * @param descriptionOverrideText if non-null this string is used as the description in the
+ * accounts chooser screen rather than the default
+ * @param addAccountAuthTokenType this string is passed as the {@link #addAccount}
+ * authTokenType parameter
+ * @param addAccountRequiredFeatures this string array is passed as the {@link #addAccount}
+ * requiredFeatures parameter
+ * @param addAccountOptions This {@link Bundle} is passed as the {@link #addAccount} options
+ * parameter
+ * @return an {@link Intent} that can be used to launch the ChooseAccount activity flow.
+ */
+ static public Intent newChooseAccountIntent(
+ Account selectedAccount,
+ List<Account> allowableAccounts,
+ String[] allowableAccountTypes,
+ String descriptionOverrideText,
+ String addAccountAuthTokenType,
+ String[] addAccountRequiredFeatures,
+ Bundle addAccountOptions) {
+ Intent intent = new Intent();
+ ComponentName componentName = ComponentName.unflattenFromString(
+ Resources.getSystem().getString(R.string.config_chooseTypeAndAccountActivity));
+ intent.setClassName(componentName.getPackageName(),
+ componentName.getClassName());
+ intent.putExtra(ChooseTypeAndAccountActivity.EXTRA_ALLOWABLE_ACCOUNTS_ARRAYLIST,
+ allowableAccounts == null ? null : new ArrayList<Account>(allowableAccounts));
+ intent.putExtra(ChooseTypeAndAccountActivity.EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY,
+ allowableAccountTypes);
+ intent.putExtra(ChooseTypeAndAccountActivity.EXTRA_ADD_ACCOUNT_OPTIONS_BUNDLE,
+ addAccountOptions);
+ intent.putExtra(ChooseTypeAndAccountActivity.EXTRA_SELECTED_ACCOUNT, selectedAccount);
+ intent.putExtra(ChooseTypeAndAccountActivity.EXTRA_DESCRIPTION_TEXT_OVERRIDE,
+ descriptionOverrideText);
+ intent.putExtra(ChooseTypeAndAccountActivity.EXTRA_ADD_ACCOUNT_AUTH_TOKEN_TYPE_STRING,
+ addAccountAuthTokenType);
+ intent.putExtra(
+ ChooseTypeAndAccountActivity.EXTRA_ADD_ACCOUNT_REQUIRED_FEATURES_STRING_ARRAY,
+ addAccountRequiredFeatures);
+ return intent;
+ }
+
+ private final HashMap<OnAccountsUpdateListener, Handler> mAccountsUpdatedListeners =
+ Maps.newHashMap();
+
+ private final HashMap<OnAccountsUpdateListener, Set<String> > mAccountsUpdatedListenersTypes =
+ Maps.newHashMap();
+
+ /**
+ * BroadcastReceiver that listens for the ACTION_VISIBLE_ACCOUNTS_CHANGED intent
+ * so that it can read the updated list of accounts and send them to the listener
+ * in mAccountsUpdatedListeners.
+ */
+ private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final Account[] accounts = getAccounts();
+ // send the result to the listeners
+ synchronized (mAccountsUpdatedListeners) {
+ for (Map.Entry<OnAccountsUpdateListener, Handler> entry :
+ mAccountsUpdatedListeners.entrySet()) {
+ postToHandler(entry.getValue(), entry.getKey(), accounts);
+ }
+ }
+ }
+ };
+
+ /**
+ * Adds an {@link OnAccountsUpdateListener} to this instance of the {@link AccountManager}. This
+ * listener will be notified whenever user or AbstractAcccountAuthenticator made changes to
+ * accounts of any type related to the caller. This method is equivalent to
+ * addOnAccountsUpdatedListener(listener, handler, updateImmediately, null)
+ *
+ * @see #addOnAccountsUpdatedListener(OnAccountsUpdateListener, Handler, boolean,
+ * String[])
+ */
+ public void addOnAccountsUpdatedListener(final OnAccountsUpdateListener listener,
+ Handler handler, boolean updateImmediately) {
+ addOnAccountsUpdatedListener(listener, handler,updateImmediately, null);
+ }
+
+ /**
+ * Adds an {@link OnAccountsUpdateListener} to this instance of the {@link AccountManager}. This
+ * listener will be notified whenever user or AbstractAcccountAuthenticator made changes to
+ * accounts of given types related to the caller -
+ * either list of accounts returned by {@link #getAccounts()}
+ * was changed, or new account was added for which user can grant access to the caller.
+ * <p>
+ * As long as this listener is present, the AccountManager instance will not be
+ * garbage-collected, and neither will the {@link Context} used to retrieve it, which may be a
+ * large Activity instance. To avoid memory leaks, you must remove this listener before then.
+ * Normally listeners are added in an Activity or Service's {@link Activity#onCreate} and
+ * removed in {@link Activity#onDestroy}.
+ * <p>
+ * It is safe to call this method from the main thread.
+ *
+ * @param listener The listener to send notifications to
+ * @param handler {@link Handler} identifying the thread to use for notifications, null for the
+ * main thread
+ * @param updateImmediately If true, the listener will be invoked (on the handler thread) right
+ * away with the current account list
+ * @param accountTypes If set, only changes to accounts of given types will be reported.
+ * @throws IllegalArgumentException if listener is null
+ * @throws IllegalStateException if listener was already added
+ */
+ public void addOnAccountsUpdatedListener(final OnAccountsUpdateListener listener,
+ Handler handler, boolean updateImmediately, String[] accountTypes) {
+ if (listener == null) {
+ throw new IllegalArgumentException("the listener is null");
+ }
+ synchronized (mAccountsUpdatedListeners) {
+ if (mAccountsUpdatedListeners.containsKey(listener)) {
+ throw new IllegalStateException("this listener is already added");
+ }
+ final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty();
+
+ mAccountsUpdatedListeners.put(listener, handler);
+ if (accountTypes != null) {
+ mAccountsUpdatedListenersTypes.put(listener,
+ new HashSet<String>(Arrays.asList(accountTypes)));
+ } else {
+ mAccountsUpdatedListenersTypes.put(listener, null);
+ }
+
+ if (wasEmpty) {
+ // Register a broadcast receiver to monitor account changes
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_VISIBLE_ACCOUNTS_CHANGED);
+ // To recover from disk-full.
+ intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+ mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter);
+ }
+
+ try {
+ // Notify AccountManagedService about new receiver.
+ // The receiver must be unregistered later exactly one time
+ mService.registerAccountListener(accountTypes, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ if (updateImmediately) {
+ postToHandler(handler, listener, getAccounts());
+ }
+ }
+
+ /**
+ * Removes an {@link OnAccountsUpdateListener} previously registered with
+ * {@link #addOnAccountsUpdatedListener}. The listener will no longer
+ * receive notifications of account changes.
+ *
+ * <p>It is safe to call this method from the main thread.
+ *
+ * <p>No permission is required to call this method.
+ *
+ * @param listener The previously added listener to remove
+ * @throws IllegalArgumentException if listener is null
+ * @throws IllegalStateException if listener was not already added
+ */
+ public void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) {
+ if (listener == null) throw new IllegalArgumentException("listener is null");
+ synchronized (mAccountsUpdatedListeners) {
+ if (!mAccountsUpdatedListeners.containsKey(listener)) {
+ Log.e(TAG, "Listener was not previously added");
+ return;
+ }
+ Set<String> accountTypes = mAccountsUpdatedListenersTypes.get(listener);
+ String[] accountsArray;
+ if (accountTypes != null) {
+ accountsArray = accountTypes.toArray(new String[accountTypes.size()]);
+ } else {
+ accountsArray = null;
+ }
+ mAccountsUpdatedListeners.remove(listener);
+ mAccountsUpdatedListenersTypes.remove(listener);
+ if (mAccountsUpdatedListeners.isEmpty()) {
+ mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver);
+ }
+ try {
+ mService.unregisterAccountListener(accountsArray, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Asks the user to authenticate with an account of a specified type. The
+ * authenticator for this account type processes this request with the
+ * appropriate user interface. If the user does elect to authenticate with a
+ * new account, a bundle of session data for installing the account later is
+ * returned with optional account password and account status token.
+ * <p>
+ * This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ * <p>
+ * <p>
+ * <b>NOTE:</b> The account will not be installed to the device by calling
+ * this api alone. #finishSession should be called after this to install the
+ * account on device.
+ *
+ * @param accountType The type of account to add; must not be null
+ * @param authTokenType The type of auth token (see {@link #getAuthToken})
+ * this account will need to be able to generate, null for none
+ * @param requiredFeatures The features (see {@link #hasFeatures}) this
+ * account must have, null for none
+ * @param options Authenticator-specific options for the request, may be
+ * null or empty
+ * @param activity The {@link Activity} context to use for launching a new
+ * authenticator-defined sub-Activity to prompt the user to
+ * create an account; used only to call startActivity(); if null,
+ * the prompt will not be launched directly, but the necessary
+ * {@link Intent} will be returned to the caller instead
+ * @param callback Callback to invoke when the request completes, null for
+ * no callback
+ * @param handler {@link Handler} identifying the callback thread, null for
+ * the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with
+ * these fields if activity was specified and user was authenticated
+ * with an account:
+ * <ul>
+ * <li>{@link #KEY_ACCOUNT_SESSION_BUNDLE} - encrypted Bundle for
+ * adding the the to the device later.
+ * <li>{@link #KEY_ACCOUNT_STATUS_TOKEN} - optional, token to check
+ * status of the account
+ * </ul>
+ * If no activity was specified, the returned Bundle contains only
+ * {@link #KEY_INTENT} with the {@link Intent} needed to launch the
+ * actual account creation process. If authenticator doesn't support
+ * this method, the returned Bundle contains only
+ * {@link #KEY_ACCOUNT_SESSION_BUNDLE} with encrypted
+ * {@code options} needed to add account later. If an error
+ * occurred, {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li>{@link AuthenticatorException} if no authenticator was
+ * registered for this account type or the authenticator failed to
+ * respond
+ * <li>{@link OperationCanceledException} if the operation was
+ * canceled for any reason, including the user canceling the
+ * creation process or adding accounts (of this type) has been
+ * disabled by policy
+ * <li>{@link IOException} if the authenticator experienced an I/O
+ * problem creating a new account, usually because of network
+ * trouble
+ * </ul>
+ * @see #finishSession
+ */
+ public AccountManagerFuture<Bundle> startAddAccountSession(
+ final String accountType,
+ final String authTokenType,
+ final String[] requiredFeatures,
+ final Bundle options,
+ final Activity activity,
+ AccountManagerCallback<Bundle> callback,
+ Handler handler) {
+ if (accountType == null) throw new IllegalArgumentException("accountType is null");
+ final Bundle optionsIn = new Bundle();
+ if (options != null) {
+ optionsIn.putAll(options);
+ }
+ optionsIn.putString(KEY_ANDROID_PACKAGE_NAME, mContext.getPackageName());
+
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.startAddAccountSession(
+ mResponse,
+ accountType,
+ authTokenType,
+ requiredFeatures,
+ activity != null,
+ optionsIn);
+ }
+ }.start();
+ }
+
+ /**
+ * Asks the user to enter a new password for the account but not updating the
+ * saved credentials for the account until {@link #finishSession} is called.
+ * <p>
+ * This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ * <p>
+ * <b>NOTE:</b> The saved credentials for the account alone will not be
+ * updated by calling this API alone. #finishSession should be called after
+ * this to update local credentials
+ *
+ * @param account The account to update credentials for
+ * @param authTokenType The credentials entered must allow an auth token of
+ * this type to be created (but no actual auth token is
+ * returned); may be null
+ * @param options Authenticator-specific options for the request; may be
+ * null or empty
+ * @param activity The {@link Activity} context to use for launching a new
+ * authenticator-defined sub-Activity to prompt the user to enter
+ * a password; used only to call startActivity(); if null, the
+ * prompt will not be launched directly, but the necessary
+ * {@link Intent} will be returned to the caller instead
+ * @param callback Callback to invoke when the request completes, null for
+ * no callback
+ * @param handler {@link Handler} identifying the callback thread, null for
+ * the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with
+ * these fields if an activity was supplied and user was
+ * successfully re-authenticated to the account:
+ * <ul>
+ * <li>{@link #KEY_ACCOUNT_SESSION_BUNDLE} - encrypted Bundle for
+ * updating the local credentials on device later.
+ * <li>{@link #KEY_ACCOUNT_STATUS_TOKEN} - optional, token to check
+ * status of the account
+ * </ul>
+ * If no activity was specified, the returned Bundle contains
+ * {@link #KEY_INTENT} with the {@link Intent} needed to launch the
+ * password prompt. If an error occurred,
+ * {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li>{@link AuthenticatorException} if the authenticator failed to
+ * respond
+ * <li>{@link OperationCanceledException} if the operation was
+ * canceled for any reason, including the user canceling the
+ * password prompt
+ * <li>{@link IOException} if the authenticator experienced an I/O
+ * problem verifying the password, usually because of network
+ * trouble
+ * </ul>
+ * @see #finishSession
+ */
+ public AccountManagerFuture<Bundle> startUpdateCredentialsSession(
+ final Account account,
+ final String authTokenType,
+ final Bundle options,
+ final Activity activity,
+ final AccountManagerCallback<Bundle> callback,
+ final Handler handler) {
+ if (account == null) {
+ throw new IllegalArgumentException("account is null");
+ }
+
+ // Always include the calling package name. This just makes life easier
+ // down stream.
+ final Bundle optionsIn = new Bundle();
+ if (options != null) {
+ optionsIn.putAll(options);
+ }
+ optionsIn.putString(KEY_ANDROID_PACKAGE_NAME, mContext.getPackageName());
+
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.startUpdateCredentialsSession(
+ mResponse,
+ account,
+ authTokenType,
+ activity != null,
+ optionsIn);
+ }
+ }.start();
+ }
+
+ /**
+ * Finishes the session started by {@link #startAddAccountSession} or
+ * {@link #startUpdateCredentialsSession}. This will either add the account
+ * to AccountManager or update the local credentials stored.
+ * <p>
+ * This method may be called from any thread, but the returned
+ * {@link AccountManagerFuture} must not be used on the main thread.
+ *
+ * @param sessionBundle a {@link Bundle} created by {@link #startAddAccountSession} or
+ * {@link #startUpdateCredentialsSession}
+ * @param activity The {@link Activity} context to use for launching a new
+ * authenticator-defined sub-Activity to prompt the user to
+ * create an account or reauthenticate existing account; used
+ * only to call startActivity(); if null, the prompt will not
+ * be launched directly, but the necessary {@link Intent} will
+ * be returned to the caller instead
+ * @param callback Callback to invoke when the request completes, null for
+ * no callback
+ * @param handler {@link Handler} identifying the callback thread, null for
+ * the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with
+ * these fields if an activity was supplied and an account was added
+ * to device or local credentials were updated::
+ * <ul>
+ * <li>{@link #KEY_ACCOUNT_NAME} - the name of the account created
+ * <li>{@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * <li>{@link #KEY_ACCOUNT_STATUS_TOKEN} - optional, token to check
+ * status of the account
+ * </ul>
+ * If no activity was specified and additional information is needed
+ * from user, the returned Bundle may contains only
+ * {@link #KEY_INTENT} with the {@link Intent} needed to launch the
+ * actual account creation process. If an error occurred,
+ * {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li>{@link AuthenticatorException} if no authenticator was
+ * registered for this account type or the authenticator failed to
+ * respond
+ * <li>{@link OperationCanceledException} if the operation was
+ * canceled for any reason, including the user canceling the
+ * creation process or adding accounts (of this type) has been
+ * disabled by policy
+ * <li>{@link IOException} if the authenticator experienced an I/O
+ * problem creating a new account, usually because of network
+ * trouble
+ * </ul>
+ * @see #startAddAccountSession and #startUpdateCredentialsSession
+ */
+ @UserHandleAware
+ public AccountManagerFuture<Bundle> finishSession(
+ final Bundle sessionBundle,
+ final Activity activity,
+ AccountManagerCallback<Bundle> callback,
+ Handler handler) {
+ return finishSessionAsUser(
+ sessionBundle,
+ activity,
+ mContext.getUser(),
+ callback,
+ handler);
+ }
+
+ /**
+ * @see #finishSession
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ public AccountManagerFuture<Bundle> finishSessionAsUser(
+ final Bundle sessionBundle,
+ final Activity activity,
+ final UserHandle userHandle,
+ AccountManagerCallback<Bundle> callback,
+ Handler handler) {
+ if (sessionBundle == null) {
+ throw new IllegalArgumentException("sessionBundle is null");
+ }
+
+ /* Add information required by add account flow */
+ final Bundle appInfo = new Bundle();
+ appInfo.putString(KEY_ANDROID_PACKAGE_NAME, mContext.getPackageName());
+
+ return new AmsTask(activity, handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.finishSessionAsUser(
+ mResponse,
+ sessionBundle,
+ activity != null,
+ appInfo,
+ userHandle.getIdentifier());
+ }
+ }.start();
+ }
+
+ /**
+ * Checks whether {@link #updateCredentials} or {@link #startUpdateCredentialsSession} should be
+ * called with respect to the specified account.
+ * <p>
+ * This method may be called from any thread, but the returned {@link AccountManagerFuture} must
+ * not be used on the main thread.
+ *
+ * @param account The {@link Account} to be checked whether {@link #updateCredentials} or
+ * {@link #startUpdateCredentialsSession} should be called
+ * @param statusToken a String of token to check account staus
+ * @param callback Callback to invoke when the request completes, null for no callback
+ * @param handler {@link Handler} identifying the callback thread, null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Boolean, true if the credentials
+ * of the account should be updated.
+ */
+ public AccountManagerFuture<Boolean> isCredentialsUpdateSuggested(
+ final Account account,
+ final String statusToken,
+ AccountManagerCallback<Boolean> callback,
+ Handler handler) {
+ if (account == null) {
+ throw new IllegalArgumentException("account is null");
+ }
+
+ if (TextUtils.isEmpty(statusToken)) {
+ throw new IllegalArgumentException("status token is empty");
+ }
+
+ return new Future2Task<Boolean>(handler, callback) {
+ @Override
+ public void doWork() throws RemoteException {
+ mService.isCredentialsUpdateSuggested(
+ mResponse,
+ account,
+ statusToken);
+ }
+ @Override
+ public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException {
+ if (!bundle.containsKey(KEY_BOOLEAN_RESULT)) {
+ throw new AuthenticatorException("no result in response");
+ }
+ return bundle.getBoolean(KEY_BOOLEAN_RESULT);
+ }
+ }.start();
+ }
+
+ /**
+ * Gets whether a given package under a user has access to an account.
+ * Can be called only from the system UID.
+ *
+ * @param account The account for which to check.
+ * @param packageName The package for which to check.
+ * @param userHandle The user for which to check.
+ * @return True if the package can access the account.
+ *
+ * @hide
+ */
+ public boolean hasAccountAccess(@NonNull Account account, @NonNull String packageName,
+ @NonNull UserHandle userHandle) {
+ try {
+ return mService.hasAccountAccess(account, packageName, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Creates an intent to request access to a given account for a UID.
+ * The returned intent should be stated for a result where {@link
+ * Activity#RESULT_OK} result means access was granted whereas {@link
+ * Activity#RESULT_CANCELED} result means access wasn't granted. Can
+ * be called only from the system UID.
+ *
+ * @param account The account for which to request.
+ * @param packageName The package name which to request.
+ * @param userHandle The user for which to request.
+ * @return The intent to request account access or null if the package
+ * doesn't exist.
+ *
+ * @hide
+ */
+ public IntentSender createRequestAccountAccessIntentSenderAsUser(@NonNull Account account,
+ @NonNull String packageName, @NonNull UserHandle userHandle) {
+ try {
+ return mService.createRequestAccountAccessIntentSenderAsUser(account, packageName,
+ userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/accounts/AccountManagerCallback.java b/android/accounts/AccountManagerCallback.java
new file mode 100644
index 0000000..4aa7169
--- /dev/null
+++ b/android/accounts/AccountManagerCallback.java
@@ -0,0 +1,20 @@
+/*
+ * 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.accounts;
+
+public interface AccountManagerCallback<V> {
+ void run(AccountManagerFuture<V> future);
+}
\ No newline at end of file
diff --git a/android/accounts/AccountManagerFuture.java b/android/accounts/AccountManagerFuture.java
new file mode 100644
index 0000000..77670d9
--- /dev/null
+++ b/android/accounts/AccountManagerFuture.java
@@ -0,0 +1,115 @@
+/*
+ * 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.accounts;
+
+import java.util.concurrent.TimeUnit;
+import java.io.IOException;
+
+/**
+ * A <tt>AccountManagerFuture</tt> represents the result of an asynchronous
+ * {@link AccountManager} call. Methods are provided to check if the computation is
+ * complete, to wait for its completion, and to retrieve the result of
+ * the computation. The result can only be retrieved using method
+ * <tt>get</tt> when the computation has completed, blocking if
+ * necessary until it is ready. Cancellation is performed by the
+ * <tt>cancel</tt> method. Additional methods are provided to
+ * determine if the task completed normally or was cancelled. Once a
+ * computation has completed, the computation cannot be cancelled.
+ * If you would like to use a <tt>Future</tt> for the sake
+ * of cancellability but not provide a usable result, you can
+ * declare types of the form <tt>Future<?></tt> and
+ * return <tt>null</tt> as a result of the underlying task.
+ */
+public interface AccountManagerFuture<V> {
+ /**
+ * Attempts to cancel execution of this task. This attempt will
+ * fail if the task has already completed, has already been cancelled,
+ * or could not be cancelled for some other reason. If successful,
+ * and this task has not started when <tt>cancel</tt> is called,
+ * this task should never run. If the task has already started,
+ * then the <tt>mayInterruptIfRunning</tt> parameter determines
+ * whether the thread executing this task should be interrupted in
+ * an attempt to stop the task.
+ *
+ * <p>After this method returns, subsequent calls to {@link #isDone} will
+ * always return <tt>true</tt>. Subsequent calls to {@link #isCancelled}
+ * will always return <tt>true</tt> if this method returned <tt>true</tt>.
+ *
+ * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
+ * task should be interrupted; otherwise, in-progress tasks are allowed
+ * to complete
+ * @return <tt>false</tt> if the task could not be cancelled,
+ * typically because it has already completed normally;
+ * <tt>true</tt> otherwise
+ */
+ boolean cancel(boolean mayInterruptIfRunning);
+
+ /**
+ * Returns <tt>true</tt> if this task was cancelled before it completed
+ * normally.
+ *
+ * @return <tt>true</tt> if this task was cancelled before it completed
+ */
+ boolean isCancelled();
+
+ /**
+ * Returns <tt>true</tt> if this task completed.
+ *
+ * Completion may be due to normal termination, an exception, or
+ * cancellation -- in all of these cases, this method will return
+ * <tt>true</tt>.
+ *
+ * @return <tt>true</tt> if this task completed
+ */
+ boolean isDone();
+
+ /**
+ * Accessor for the future result the {@link AccountManagerFuture} represents. This
+ * call will block until the result is available. In order to check if the result is
+ * available without blocking, one may call {@link #isDone()} and {@link #isCancelled()}.
+ * If the request that generated this result fails or is canceled then an exception
+ * will be thrown rather than the call returning normally.
+ * @return the actual result
+ * @throws android.accounts.OperationCanceledException if the request was canceled for any
+ * reason (including if it is forbidden
+ * by policy to modify an account (of that type))
+ * @throws android.accounts.AuthenticatorException if there was an error communicating with
+ * the authenticator or if the authenticator returned an invalid response
+ * @throws java.io.IOException if the authenticator returned an error response that indicates
+ * that it encountered an IOException while communicating with the authentication server
+ */
+ V getResult() throws OperationCanceledException, IOException, AuthenticatorException;
+
+ /**
+ * Accessor for the future result the {@link AccountManagerFuture} represents. This
+ * call will block until the result is available. In order to check if the result is
+ * available without blocking, one may call {@link #isDone()} and {@link #isCancelled()}.
+ * If the request that generated this result fails or is canceled then an exception
+ * will be thrown rather than the call returning normally. If a timeout is specified then
+ * the request will automatically be canceled if it does not complete in that amount of time.
+ * @param timeout the maximum time to wait
+ * @param unit the time unit of the timeout argument. This must not be null.
+ * @return the actual result
+ * @throws android.accounts.OperationCanceledException if the request was canceled for any
+ * reason
+ * @throws android.accounts.AuthenticatorException if there was an error communicating with
+ * the authenticator or if the authenticator returned an invalid response
+ * @throws java.io.IOException if the authenticator returned an error response that indicates
+ * that it encountered an IOException while communicating with the authentication server
+ */
+ V getResult(long timeout, TimeUnit unit)
+ throws OperationCanceledException, IOException, AuthenticatorException;
+}
\ No newline at end of file
diff --git a/android/accounts/AccountManagerInternal.java b/android/accounts/AccountManagerInternal.java
new file mode 100644
index 0000000..68c17c3
--- /dev/null
+++ b/android/accounts/AccountManagerInternal.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2016 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.accounts;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.os.RemoteCallback;
+
+/**
+ * Account manager local system service interface.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class AccountManagerInternal {
+
+ /**
+ * Listener for explicit UID account access grant changes.
+ */
+ public interface OnAppPermissionChangeListener {
+
+ /**
+ * Called when the explicit grant state for a given UID to
+ * access an account changes.
+ *
+ * @param account The account
+ * @param uid The UID for which the grant changed
+ */
+ public void onAppPermissionChanged(Account account, int uid);
+ }
+
+ /**
+ * Requests that a given package is given access to an account.
+ * The provided callback will be invoked with a {@link android.os.Bundle}
+ * containing the result which will be a boolean value mapped to the
+ * {@link AccountManager#KEY_BOOLEAN_RESULT} key.
+ *
+ * @param account The account for which to request.
+ * @param packageName The package name for which to request.
+ * @param userId Concrete user id for which to request.
+ * @param callback A callback for receiving the result.
+ */
+ public abstract void requestAccountAccess(@NonNull Account account,
+ @NonNull String packageName, @IntRange(from = 0) int userId,
+ @NonNull RemoteCallback callback);
+
+ /**
+ * Check whether the given UID has access to the account.
+ *
+ * @param account The account
+ * @param uid The UID
+ * @return Whether the UID can access the account
+ */
+ public abstract boolean hasAccountAccess(@NonNull Account account, @IntRange(from = 0) int uid);
+
+ /**
+ * Adds a listener for explicit UID account access grant changes.
+ *
+ * @param listener The listener
+ */
+ public abstract void addOnAppPermissionChangeListener(
+ @NonNull OnAppPermissionChangeListener listener);
+
+ /**
+ * Backups the account access permissions.
+ * @param userId The user for which to backup.
+ * @return The backup data.
+ */
+ public abstract byte[] backupAccountAccessPermissions(int userId);
+
+ /**
+ * Restores the account access permissions.
+ * @param data The restore data.
+ * @param userId The user for which to restore.
+ */
+ public abstract void restoreAccountAccessPermissions(byte[] data, int userId);
+}
diff --git a/android/accounts/AccountManagerPerfTest.java b/android/accounts/AccountManagerPerfTest.java
new file mode 100644
index 0000000..e455e6a
--- /dev/null
+++ b/android/accounts/AccountManagerPerfTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 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.accounts;
+
+import static junit.framework.Assert.fail;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class AccountManagerPerfTest {
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testGetAccounts() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final Context context = InstrumentationRegistry.getTargetContext();
+ if (context.checkSelfPermission(Manifest.permission.GET_ACCOUNTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ fail("Missing required GET_ACCOUNTS permission");
+ }
+ AccountManager accountManager = AccountManager.get(context);
+ while (state.keepRunning()) {
+ accountManager.getAccounts();
+ }
+ }
+}
diff --git a/android/accounts/AccountManagerResponse.java b/android/accounts/AccountManagerResponse.java
new file mode 100644
index 0000000..369a7c3
--- /dev/null
+++ b/android/accounts/AccountManagerResponse.java
@@ -0,0 +1,79 @@
+/*
+ * 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.accounts;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+/**
+ * Used to return a response to the AccountManager.
+ * @hide
+ */
+public class AccountManagerResponse implements Parcelable {
+ private IAccountManagerResponse mResponse;
+
+ /** @hide */
+ public AccountManagerResponse(IAccountManagerResponse response) {
+ mResponse = response;
+ }
+
+ /** @hide */
+ public AccountManagerResponse(Parcel parcel) {
+ mResponse =
+ IAccountManagerResponse.Stub.asInterface(parcel.readStrongBinder());
+ }
+
+ public void onResult(Bundle result) {
+ try {
+ mResponse.onResult(result);
+ } catch (RemoteException e) {
+ // this should never happen
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage) {
+ try {
+ mResponse.onError(errorCode, errorMessage);
+ } catch (RemoteException e) {
+ // this should never happen
+ }
+ }
+
+ /** @hide */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mResponse.asBinder());
+ }
+
+ /** @hide */
+ public static final @android.annotation.NonNull Creator<AccountManagerResponse> CREATOR =
+ new Creator<AccountManagerResponse>() {
+ public AccountManagerResponse createFromParcel(Parcel source) {
+ return new AccountManagerResponse(source);
+ }
+
+ public AccountManagerResponse[] newArray(int size) {
+ return new AccountManagerResponse[size];
+ }
+ };
+}
diff --git a/android/accounts/AccountsException.java b/android/accounts/AccountsException.java
new file mode 100644
index 0000000..b997390
--- /dev/null
+++ b/android/accounts/AccountsException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.accounts;
+
+public class AccountsException extends Exception {
+ public AccountsException() {
+ super();
+ }
+ public AccountsException(String message) {
+ super(message);
+ }
+ public AccountsException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ public AccountsException(Throwable cause) {
+ super(cause);
+ }
+}
\ No newline at end of file
diff --git a/android/accounts/AuthenticatorDescription.java b/android/accounts/AuthenticatorDescription.java
new file mode 100644
index 0000000..b7bf11d
--- /dev/null
+++ b/android/accounts/AuthenticatorDescription.java
@@ -0,0 +1,149 @@
+/*
+ * 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.accounts;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A {@link Parcelable} value type that contains information about an account authenticator.
+ */
+public class AuthenticatorDescription implements Parcelable {
+ /** The string that uniquely identifies an authenticator */
+ final public String type;
+
+ /** A resource id of a label for the authenticator that is suitable for displaying */
+ final public int labelId;
+
+ /** A resource id of a icon for the authenticator */
+ final public int iconId;
+
+ /** A resource id of a smaller icon for the authenticator */
+ final public int smallIconId;
+
+ /**
+ * A resource id for a hierarchy of PreferenceScreen to be added to the settings page for the
+ * account. See {@link AbstractAccountAuthenticator} for an example.
+ */
+ final public int accountPreferencesId;
+
+ /** The package name that can be used to lookup the resources from above. */
+ final public String packageName;
+
+ /** Authenticator handles its own token caching and permission screen */
+ final public boolean customTokens;
+
+ /** A constructor for a full AuthenticatorDescription */
+ public AuthenticatorDescription(String type, String packageName, int labelId, int iconId,
+ int smallIconId, int prefId, boolean customTokens) {
+ if (type == null) throw new IllegalArgumentException("type cannot be null");
+ if (packageName == null) throw new IllegalArgumentException("packageName cannot be null");
+ this.type = type;
+ this.packageName = packageName;
+ this.labelId = labelId;
+ this.iconId = iconId;
+ this.smallIconId = smallIconId;
+ this.accountPreferencesId = prefId;
+ this.customTokens = customTokens;
+ }
+
+ public AuthenticatorDescription(String type, String packageName, int labelId, int iconId,
+ int smallIconId, int prefId) {
+ this(type, packageName, labelId, iconId, smallIconId, prefId, false);
+ }
+
+ /**
+ * A factory method for creating an AuthenticatorDescription that can be used as a key
+ * to identify the authenticator by its type.
+ */
+
+ public static AuthenticatorDescription newKey(String type) {
+ if (type == null) throw new IllegalArgumentException("type cannot be null");
+ return new AuthenticatorDescription(type);
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private AuthenticatorDescription(String type) {
+ this.type = type;
+ this.packageName = null;
+ this.labelId = 0;
+ this.iconId = 0;
+ this.smallIconId = 0;
+ this.accountPreferencesId = 0;
+ this.customTokens = false;
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private AuthenticatorDescription(Parcel source) {
+ this.type = source.readString();
+ this.packageName = source.readString();
+ this.labelId = source.readInt();
+ this.iconId = source.readInt();
+ this.smallIconId = source.readInt();
+ this.accountPreferencesId = source.readInt();
+ this.customTokens = source.readByte() == 1;
+ }
+
+ /** @inheritDoc */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Returns the hashcode of the type only. */
+ public int hashCode() {
+ return type.hashCode();
+ }
+
+ /** Compares the type only, suitable for key comparisons. */
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof AuthenticatorDescription)) return false;
+ final AuthenticatorDescription other = (AuthenticatorDescription) o;
+ return type.equals(other.type);
+ }
+
+ public String toString() {
+ return "AuthenticatorDescription {type=" + type + "}";
+ }
+
+ /** @inheritDoc */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(type);
+ dest.writeString(packageName);
+ dest.writeInt(labelId);
+ dest.writeInt(iconId);
+ dest.writeInt(smallIconId);
+ dest.writeInt(accountPreferencesId);
+ dest.writeByte((byte) (customTokens ? 1 : 0));
+ }
+
+ /** Used to create the object from a parcel. */
+ public static final @android.annotation.NonNull Creator<AuthenticatorDescription> CREATOR =
+ new Creator<AuthenticatorDescription>() {
+ /** @inheritDoc */
+ public AuthenticatorDescription createFromParcel(Parcel source) {
+ return new AuthenticatorDescription(source);
+ }
+
+ /** @inheritDoc */
+ public AuthenticatorDescription[] newArray(int size) {
+ return new AuthenticatorDescription[size];
+ }
+ };
+}
diff --git a/android/accounts/AuthenticatorException.java b/android/accounts/AuthenticatorException.java
new file mode 100644
index 0000000..f778d7d
--- /dev/null
+++ b/android/accounts/AuthenticatorException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.accounts;
+
+public class AuthenticatorException extends AccountsException {
+ public AuthenticatorException() {
+ super();
+ }
+ public AuthenticatorException(String message) {
+ super(message);
+ }
+ public AuthenticatorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ public AuthenticatorException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/android/accounts/CantAddAccountActivity.java b/android/accounts/CantAddAccountActivity.java
new file mode 100644
index 0000000..f7f232e
--- /dev/null
+++ b/android/accounts/CantAddAccountActivity.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 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.accounts;
+
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+
+import com.android.internal.R;
+
+/**
+ * @hide
+ * Just shows an error message about the account restrictions for the limited user.
+ */
+public class CantAddAccountActivity extends Activity {
+ public static final String EXTRA_ERROR_CODE = "android.accounts.extra.ERROR_CODE";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.app_not_authorized);
+ }
+
+ public void onCancelButtonClicked(View view) {
+ onBackPressed();
+ }
+}
diff --git a/android/accounts/ChooseAccountActivity.java b/android/accounts/ChooseAccountActivity.java
new file mode 100644
index 0000000..4af22bf
--- /dev/null
+++ b/android/accounts/ChooseAccountActivity.java
@@ -0,0 +1,223 @@
+/*
+ * 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.accounts;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import com.android.internal.R;
+
+import java.util.HashMap;
+
+/**
+ * @hide
+ */
+public class ChooseAccountActivity extends Activity {
+
+ private static final String TAG = "AccountManager";
+
+ private Parcelable[] mAccounts = null;
+ private AccountManagerResponse mAccountManagerResponse = null;
+ private Bundle mResult;
+ private int mCallingUid;
+ private String mCallingPackage;
+
+ private HashMap<String, AuthenticatorDescription> mTypeToAuthDescription
+ = new HashMap<String, AuthenticatorDescription>();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAccounts = getIntent().getParcelableArrayExtra(AccountManager.KEY_ACCOUNTS);
+ mAccountManagerResponse =
+ getIntent().getParcelableExtra(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE);
+
+ // KEY_ACCOUNTS is a required parameter
+ if (mAccounts == null) {
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ try {
+ IBinder activityToken = getActivityToken();
+ mCallingUid = ActivityTaskManager.getService().getLaunchedFromUid(activityToken);
+ mCallingPackage = ActivityTaskManager.getService().getLaunchedFromPackage(
+ activityToken);
+ } catch (RemoteException re) {
+ // Couldn't figure out caller details
+ Log.w(getClass().getSimpleName(), "Unable to get caller identity \n" + re);
+ }
+
+ if (UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID) &&
+ getIntent().getStringExtra(AccountManager.KEY_ANDROID_PACKAGE_NAME) != null) {
+ mCallingPackage = getIntent().getStringExtra(
+ AccountManager.KEY_ANDROID_PACKAGE_NAME);
+ }
+
+ if (!UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID) &&
+ getIntent().getStringExtra(AccountManager.KEY_ANDROID_PACKAGE_NAME) != null) {
+ Log.w(getClass().getSimpleName(),
+ "Non-system Uid: " + mCallingUid + " tried to override packageName \n");
+ }
+
+ getAuthDescriptions();
+
+ AccountInfo[] mAccountInfos = new AccountInfo[mAccounts.length];
+ for (int i = 0; i < mAccounts.length; i++) {
+ mAccountInfos[i] = new AccountInfo(((Account) mAccounts[i]).name,
+ getDrawableForType(((Account) mAccounts[i]).type));
+ }
+
+ setContentView(R.layout.choose_account);
+
+ // Setup the list
+ ListView list = findViewById(android.R.id.list);
+ // Use an existing ListAdapter that will map an array of strings to TextViews
+ list.setAdapter(new AccountArrayAdapter(this,
+ android.R.layout.simple_list_item_1, mAccountInfos));
+ list.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ list.setTextFilterEnabled(true);
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ onListItemClick((ListView)parent, v, position, id);
+ }
+ });
+ }
+
+ private void getAuthDescriptions() {
+ for(AuthenticatorDescription desc : AccountManager.get(this).getAuthenticatorTypes()) {
+ mTypeToAuthDescription.put(desc.type, desc);
+ }
+ }
+
+ private Drawable getDrawableForType(String accountType) {
+ Drawable icon = null;
+ if(mTypeToAuthDescription.containsKey(accountType)) {
+ try {
+ AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
+ Context authContext = createPackageContext(desc.packageName, 0);
+ icon = authContext.getDrawable(desc.iconId);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Nothing we can do much here, just log
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "No icon name for account type " + accountType);
+ }
+ } catch (Resources.NotFoundException e) {
+ // Nothing we can do much here, just log
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "No icon resource for account type " + accountType);
+ }
+ }
+ }
+ return icon;
+ }
+
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ Account account = (Account) mAccounts[position];
+ // Mark account as visible since user chose it.
+ AccountManager am = AccountManager.get(this);
+ Integer oldVisibility = am.getAccountVisibility(account, mCallingPackage);
+ if (oldVisibility != null
+ && oldVisibility == AccountManager.VISIBILITY_USER_MANAGED_NOT_VISIBLE) {
+ am.setAccountVisibility(account, mCallingPackage,
+ AccountManager.VISIBILITY_USER_MANAGED_VISIBLE);
+ }
+ Log.d(TAG, "selected account " + account);
+ Bundle bundle = new Bundle();
+ bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+ bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+ mResult = bundle;
+ finish();
+ }
+
+ public void finish() {
+ if (mAccountManagerResponse != null) {
+ if (mResult != null) {
+ mAccountManagerResponse.onResult(mResult);
+ } else {
+ mAccountManagerResponse.onError(AccountManager.ERROR_CODE_CANCELED, "canceled");
+ }
+ }
+ super.finish();
+ }
+
+ private static class AccountInfo {
+ final String name;
+ final Drawable drawable;
+
+ AccountInfo(String name, Drawable drawable) {
+ this.name = name;
+ this.drawable = drawable;
+ }
+ }
+
+ private static class ViewHolder {
+ ImageView icon;
+ TextView text;
+ }
+
+ private static class AccountArrayAdapter extends ArrayAdapter<AccountInfo> {
+ private LayoutInflater mLayoutInflater;
+ private AccountInfo[] mInfos;
+
+ public AccountArrayAdapter(Context context, int textViewResourceId, AccountInfo[] infos) {
+ super(context, textViewResourceId, infos);
+ mInfos = infos;
+ mLayoutInflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+
+ if (convertView == null) {
+ convertView = mLayoutInflater.inflate(R.layout.choose_account_row, null);
+ holder = new ViewHolder();
+ holder.text = (TextView) convertView.findViewById(R.id.account_row_text);
+ holder.icon = (ImageView) convertView.findViewById(R.id.account_row_icon);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ holder.text.setText(mInfos[position].name);
+ holder.icon.setImageDrawable(mInfos[position].drawable);
+
+ return convertView;
+ }
+ }
+}
diff --git a/android/accounts/ChooseAccountTypeActivity.java b/android/accounts/ChooseAccountTypeActivity.java
new file mode 100644
index 0000000..e3352bc
--- /dev/null
+++ b/android/accounts/ChooseAccountTypeActivity.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.accounts;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @hide
+ */
+public class ChooseAccountTypeActivity extends Activity {
+ private static final String TAG = "AccountChooser";
+
+ private HashMap<String, AuthInfo> mTypeToAuthenticatorInfo = new HashMap<String, AuthInfo>();
+ private ArrayList<AuthInfo> mAuthenticatorInfosToDisplay;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "ChooseAccountTypeActivity.onCreate(savedInstanceState="
+ + savedInstanceState + ")");
+ }
+
+ // Read the validAccountTypes, if present, and add them to the setOfAllowableAccountTypes
+ Set<String> setOfAllowableAccountTypes = null;
+ String[] validAccountTypes = getIntent().getStringArrayExtra(
+ ChooseTypeAndAccountActivity.EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY);
+ if (validAccountTypes != null) {
+ setOfAllowableAccountTypes = new HashSet<String>(validAccountTypes.length);
+ for (String type : validAccountTypes) {
+ setOfAllowableAccountTypes.add(type);
+ }
+ }
+
+ // create a map of account authenticators
+ buildTypeToAuthDescriptionMap();
+
+ // Create a list of authenticators that are allowable. Filter out those that
+ // don't match the allowable account types, if provided.
+ mAuthenticatorInfosToDisplay = new ArrayList<AuthInfo>(mTypeToAuthenticatorInfo.size());
+ for (Map.Entry<String, AuthInfo> entry: mTypeToAuthenticatorInfo.entrySet()) {
+ final String type = entry.getKey();
+ final AuthInfo info = entry.getValue();
+ if (setOfAllowableAccountTypes != null
+ && !setOfAllowableAccountTypes.contains(type)) {
+ continue;
+ }
+ mAuthenticatorInfosToDisplay.add(info);
+ }
+
+ if (mAuthenticatorInfosToDisplay.isEmpty()) {
+ Bundle bundle = new Bundle();
+ bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "no allowable account types");
+ setResult(Activity.RESULT_OK, new Intent().putExtras(bundle));
+ finish();
+ return;
+ }
+
+ if (mAuthenticatorInfosToDisplay.size() == 1) {
+ setResultAndFinish(mAuthenticatorInfosToDisplay.get(0).desc.type);
+ return;
+ }
+
+ setContentView(R.layout.choose_account_type);
+ // Setup the list
+ ListView list = findViewById(android.R.id.list);
+ // Use an existing ListAdapter that will map an array of strings to TextViews
+ list.setAdapter(new AccountArrayAdapter(this,
+ android.R.layout.simple_list_item_1, mAuthenticatorInfosToDisplay));
+ list.setChoiceMode(ListView.CHOICE_MODE_NONE);
+ list.setTextFilterEnabled(false);
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ setResultAndFinish(mAuthenticatorInfosToDisplay.get(position).desc.type);
+ }
+ });
+ }
+
+ private void setResultAndFinish(final String type) {
+ Bundle bundle = new Bundle();
+ bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, type);
+ setResult(Activity.RESULT_OK, new Intent().putExtras(bundle));
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "ChooseAccountTypeActivity.setResultAndFinish: "
+ + "selected account type " + type);
+ }
+ finish();
+ }
+
+ private void buildTypeToAuthDescriptionMap() {
+ for(AuthenticatorDescription desc : AccountManager.get(this).getAuthenticatorTypes()) {
+ String name = null;
+ Drawable icon = null;
+ try {
+ Context authContext = createPackageContext(desc.packageName, 0);
+ icon = authContext.getDrawable(desc.iconId);
+ final CharSequence sequence = authContext.getResources().getText(desc.labelId);
+ if (sequence != null) {
+ name = sequence.toString();
+ }
+ name = sequence.toString();
+ } catch (PackageManager.NameNotFoundException e) {
+ // Nothing we can do much here, just log
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "No icon name for account type " + desc.type);
+ }
+ } catch (Resources.NotFoundException e) {
+ // Nothing we can do much here, just log
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "No icon resource for account type " + desc.type);
+ }
+ }
+ AuthInfo authInfo = new AuthInfo(desc, name, icon);
+ mTypeToAuthenticatorInfo.put(desc.type, authInfo);
+ }
+ }
+
+ private static class AuthInfo {
+ final AuthenticatorDescription desc;
+ final String name;
+ final Drawable drawable;
+
+ AuthInfo(AuthenticatorDescription desc, String name, Drawable drawable) {
+ this.desc = desc;
+ this.name = name;
+ this.drawable = drawable;
+ }
+ }
+
+ private static class ViewHolder {
+ ImageView icon;
+ TextView text;
+ }
+
+ private static class AccountArrayAdapter extends ArrayAdapter<AuthInfo> {
+ private LayoutInflater mLayoutInflater;
+ private ArrayList<AuthInfo> mInfos;
+
+ public AccountArrayAdapter(Context context, int textViewResourceId,
+ ArrayList<AuthInfo> infos) {
+ super(context, textViewResourceId, infos);
+ mInfos = infos;
+ mLayoutInflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+
+ if (convertView == null) {
+ convertView = mLayoutInflater.inflate(R.layout.choose_account_row, null);
+ holder = new ViewHolder();
+ holder.text = (TextView) convertView.findViewById(R.id.account_row_text);
+ holder.icon = (ImageView) convertView.findViewById(R.id.account_row_icon);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ holder.text.setText(mInfos.get(position).name);
+ holder.icon.setImageDrawable(mInfos.get(position).drawable);
+
+ return convertView;
+ }
+ }
+}
diff --git a/android/accounts/ChooseTypeAndAccountActivity.java b/android/accounts/ChooseTypeAndAccountActivity.java
new file mode 100644
index 0000000..57c1083
--- /dev/null
+++ b/android/accounts/ChooseTypeAndAccountActivity.java
@@ -0,0 +1,629 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.accounts;
+
+import android.app.ActivityTaskManager;
+import com.google.android.collect.Sets;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.internal.R;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @hide
+ */
+public class ChooseTypeAndAccountActivity extends Activity
+ implements AccountManagerCallback<Bundle> {
+ private static final String TAG = "AccountChooser";
+
+ /**
+ * A Parcelable ArrayList of Account objects that limits the choosable accounts to those
+ * in this list, if this parameter is supplied.
+ */
+ public static final String EXTRA_ALLOWABLE_ACCOUNTS_ARRAYLIST = "allowableAccounts";
+
+ /**
+ * A Parcelable ArrayList of String objects that limits the accounts to choose to those
+ * that match the types in this list, if this parameter is supplied. This list is also
+ * used to filter the allowable account types if add account is selected.
+ */
+ public static final String EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY = "allowableAccountTypes";
+
+ /**
+ * This is passed as the addAccountOptions parameter in AccountManager.addAccount()
+ * if it is called.
+ */
+ public static final String EXTRA_ADD_ACCOUNT_OPTIONS_BUNDLE = "addAccountOptions";
+
+ /**
+ * This is passed as the requiredFeatures parameter in AccountManager.addAccount()
+ * if it is called.
+ */
+ public static final String EXTRA_ADD_ACCOUNT_REQUIRED_FEATURES_STRING_ARRAY =
+ "addAccountRequiredFeatures";
+
+ /**
+ * This is passed as the authTokenType string in AccountManager.addAccount()
+ * if it is called.
+ */
+ public static final String EXTRA_ADD_ACCOUNT_AUTH_TOKEN_TYPE_STRING = "authTokenType";
+
+ /**
+ * If set then the specified account is already "selected".
+ */
+ public static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
+
+ /**
+ * Deprecated. Providing this extra to {@link ChooseTypeAndAccountActivity}
+ * will have no effect.
+ */
+ @Deprecated
+ public static final String EXTRA_ALWAYS_PROMPT_FOR_ACCOUNT =
+ "alwaysPromptForAccount";
+
+ /**
+ * If set then this string will be used as the description rather than
+ * the default.
+ */
+ public static final String EXTRA_DESCRIPTION_TEXT_OVERRIDE = "descriptionTextOverride";
+
+ public static final int REQUEST_NULL = 0;
+ public static final int REQUEST_CHOOSE_TYPE = 1;
+ public static final int REQUEST_ADD_ACCOUNT = 2;
+
+ private static final String KEY_INSTANCE_STATE_PENDING_REQUEST = "pendingRequest";
+ private static final String KEY_INSTANCE_STATE_EXISTING_ACCOUNTS = "existingAccounts";
+ private static final String KEY_INSTANCE_STATE_SELECTED_ACCOUNT_NAME = "selectedAccountName";
+ private static final String KEY_INSTANCE_STATE_SELECTED_ADD_ACCOUNT = "selectedAddAccount";
+ private static final String KEY_INSTANCE_STATE_ACCOUNTS_LIST = "accountsList";
+ private static final String KEY_INSTANCE_STATE_VISIBILITY_LIST = "visibilityList";
+
+ private static final int SELECTED_ITEM_NONE = -1;
+
+ private Set<Account> mSetOfAllowableAccounts;
+ private Set<String> mSetOfRelevantAccountTypes;
+ private String mSelectedAccountName = null;
+ private boolean mSelectedAddNewAccount = false;
+ private String mDescriptionOverride;
+
+ private LinkedHashMap<Account, Integer> mAccounts;
+ // TODO Redesign flow to show NOT_VISIBLE accounts
+ // and display a warning if they are selected.
+ // Currently NOT_VISBILE accounts are not shown at all.
+ private ArrayList<Account> mPossiblyVisibleAccounts;
+ private int mPendingRequest = REQUEST_NULL;
+ private Parcelable[] mExistingAccounts = null;
+ private int mSelectedItemIndex;
+ private Button mOkButton;
+ private int mCallingUid;
+ private String mCallingPackage;
+ private boolean mDisallowAddAccounts;
+ private boolean mDontShowPicker;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "ChooseTypeAndAccountActivity.onCreate(savedInstanceState="
+ + savedInstanceState + ")");
+ }
+
+ String message = null;
+
+ try {
+ IBinder activityToken = getActivityToken();
+ mCallingUid = ActivityTaskManager.getService().getLaunchedFromUid(activityToken);
+ mCallingPackage = ActivityTaskManager.getService().getLaunchedFromPackage(
+ activityToken);
+ if (mCallingUid != 0 && mCallingPackage != null) {
+ Bundle restrictions = UserManager.get(this)
+ .getUserRestrictions(new UserHandle(UserHandle.getUserId(mCallingUid)));
+ mDisallowAddAccounts =
+ restrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false);
+ }
+ } catch (RemoteException re) {
+ // Couldn't figure out caller details
+ Log.w(getClass().getSimpleName(), "Unable to get caller identity \n" + re);
+ }
+
+ // save some items we use frequently
+ final Intent intent = getIntent();
+
+ mSetOfAllowableAccounts = getAllowableAccountSet(intent);
+ mSetOfRelevantAccountTypes = getReleventAccountTypes(intent);
+ mDescriptionOverride = intent.getStringExtra(EXTRA_DESCRIPTION_TEXT_OVERRIDE);
+
+ if (savedInstanceState != null) {
+ mPendingRequest = savedInstanceState.getInt(KEY_INSTANCE_STATE_PENDING_REQUEST);
+ mExistingAccounts =
+ savedInstanceState.getParcelableArray(KEY_INSTANCE_STATE_EXISTING_ACCOUNTS);
+
+ // Makes sure that any user selection is preserved across orientation changes.
+ mSelectedAccountName =
+ savedInstanceState.getString(KEY_INSTANCE_STATE_SELECTED_ACCOUNT_NAME);
+ mSelectedAddNewAccount =
+ savedInstanceState.getBoolean(KEY_INSTANCE_STATE_SELECTED_ADD_ACCOUNT, false);
+ // restore mAccounts
+ Parcelable[] accounts =
+ savedInstanceState.getParcelableArray(KEY_INSTANCE_STATE_ACCOUNTS_LIST);
+ ArrayList<Integer> visibility =
+ savedInstanceState.getIntegerArrayList(KEY_INSTANCE_STATE_VISIBILITY_LIST);
+ mAccounts = new LinkedHashMap<>();
+ for (int i = 0; i < accounts.length; i++) {
+ mAccounts.put((Account) accounts[i], visibility.get(i));
+ }
+ } else {
+ mPendingRequest = REQUEST_NULL;
+ mExistingAccounts = null;
+ // If the selected account as specified in the intent matches one in the list we will
+ // show is as pre-selected.
+ Account selectedAccount = (Account) intent.getParcelableExtra(EXTRA_SELECTED_ACCOUNT);
+ if (selectedAccount != null) {
+ mSelectedAccountName = selectedAccount.name;
+ }
+ mAccounts = getAcceptableAccountChoices(AccountManager.get(this));
+ }
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "selected account name is " + mSelectedAccountName);
+ }
+
+ mPossiblyVisibleAccounts = new ArrayList<>(mAccounts.size());
+ for (Map.Entry<Account, Integer> entry : mAccounts.entrySet()) {
+ if (AccountManager.VISIBILITY_NOT_VISIBLE != entry.getValue()) {
+ mPossiblyVisibleAccounts.add(entry.getKey());
+ }
+ }
+
+ if (mPossiblyVisibleAccounts.isEmpty() && mDisallowAddAccounts) {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.app_not_authorized);
+ mDontShowPicker = true;
+ }
+
+ if (mDontShowPicker) {
+ super.onCreate(savedInstanceState);
+ return;
+ }
+
+ // In cases where the activity does not need to show an account picker, cut the chase
+ // and return the result directly. Eg:
+ // Single account -> select it directly
+ // No account -> launch add account activity directly
+ if (mPendingRequest == REQUEST_NULL) {
+ // If there are no relevant accounts and only one relevant account type go directly to
+ // add account. Otherwise let the user choose.
+ if (mPossiblyVisibleAccounts.isEmpty()) {
+ setNonLabelThemeAndCallSuperCreate(savedInstanceState);
+ if (mSetOfRelevantAccountTypes.size() == 1) {
+ runAddAccountForAuthenticator(mSetOfRelevantAccountTypes.iterator().next());
+ } else {
+ startChooseAccountTypeActivity();
+ }
+ }
+ }
+
+ String[] listItems = getListOfDisplayableOptions(mPossiblyVisibleAccounts);
+ mSelectedItemIndex = getItemIndexToSelect(mPossiblyVisibleAccounts, mSelectedAccountName,
+ mSelectedAddNewAccount);
+
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.choose_type_and_account);
+ overrideDescriptionIfSupplied(mDescriptionOverride);
+ populateUIAccountList(listItems);
+
+ // Only enable "OK" button if something has been selected.
+ mOkButton = findViewById(android.R.id.button2);
+ mOkButton.setEnabled(mSelectedItemIndex != SELECTED_ITEM_NONE);
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "ChooseTypeAndAccountActivity.onDestroy()");
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(KEY_INSTANCE_STATE_PENDING_REQUEST, mPendingRequest);
+ if (mPendingRequest == REQUEST_ADD_ACCOUNT) {
+ outState.putParcelableArray(KEY_INSTANCE_STATE_EXISTING_ACCOUNTS, mExistingAccounts);
+ }
+ if (mSelectedItemIndex != SELECTED_ITEM_NONE) {
+ if (mSelectedItemIndex == mPossiblyVisibleAccounts.size()) {
+ outState.putBoolean(KEY_INSTANCE_STATE_SELECTED_ADD_ACCOUNT, true);
+ } else {
+ outState.putBoolean(KEY_INSTANCE_STATE_SELECTED_ADD_ACCOUNT, false);
+ outState.putString(KEY_INSTANCE_STATE_SELECTED_ACCOUNT_NAME,
+ mPossiblyVisibleAccounts.get(mSelectedItemIndex).name);
+ }
+ }
+ // save mAccounts
+ Parcelable[] accounts = new Parcelable[mAccounts.size()];
+ ArrayList<Integer> visibility = new ArrayList<>(mAccounts.size());
+ int i = 0;
+ for (Map.Entry<Account, Integer> e : mAccounts.entrySet()) {
+ accounts[i++] = e.getKey();
+ visibility.add(e.getValue());
+ }
+ outState.putParcelableArray(KEY_INSTANCE_STATE_ACCOUNTS_LIST, accounts);
+ outState.putIntegerArrayList(KEY_INSTANCE_STATE_VISIBILITY_LIST, visibility);
+ }
+
+ public void onCancelButtonClicked(View view) {
+ onBackPressed();
+ }
+
+ public void onOkButtonClicked(View view) {
+ if (mSelectedItemIndex == mPossiblyVisibleAccounts.size()) {
+ // Selected "Add New Account" option
+ startChooseAccountTypeActivity();
+ } else if (mSelectedItemIndex != SELECTED_ITEM_NONE) {
+ onAccountSelected(mPossiblyVisibleAccounts.get(mSelectedItemIndex));
+ }
+ }
+
+ // Called when the choose account type activity (for adding an account) returns.
+ // If it was a success read the account and set it in the result. In all cases
+ // return the result and finish this activity.
+ @Override
+ protected void onActivityResult(final int requestCode, final int resultCode,
+ final Intent data) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ if (data != null && data.getExtras() != null) data.getExtras().keySet();
+ Bundle extras = data != null ? data.getExtras() : null;
+ Log.v(TAG, "ChooseTypeAndAccountActivity.onActivityResult(reqCode=" + requestCode
+ + ", resCode=" + resultCode + ", extras=" + extras + ")");
+ }
+
+ // we got our result, so clear the fact that we had a pending request
+ mPendingRequest = REQUEST_NULL;
+
+ if (resultCode == RESULT_CANCELED) {
+ // if canceling out of addAccount and the original state caused us to skip this,
+ // finish this activity
+ if (mPossiblyVisibleAccounts.isEmpty()) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
+ return;
+ }
+
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_CHOOSE_TYPE) {
+ if (data != null) {
+ String accountType = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE);
+ if (accountType != null) {
+ runAddAccountForAuthenticator(accountType);
+ return;
+ }
+ }
+ Log.d(TAG, "ChooseTypeAndAccountActivity.onActivityResult: unable to find account "
+ + "type, pretending the request was canceled");
+ } else if (requestCode == REQUEST_ADD_ACCOUNT) {
+ String accountName = null;
+ String accountType = null;
+
+ if (data != null) {
+ accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
+ accountType = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE);
+ }
+
+ if (accountName == null || accountType == null) {
+ // new account was added.
+ Account[] currentAccounts = AccountManager.get(this).getAccountsForPackage(
+ mCallingPackage, mCallingUid);
+ Set<Account> preExistingAccounts = new HashSet<Account>();
+ for (Parcelable accountParcel : mExistingAccounts) {
+ preExistingAccounts.add((Account) accountParcel);
+ }
+ for (Account account : currentAccounts) {
+ // New account is visible to the app - return it.
+ if (!preExistingAccounts.contains(account)) {
+ accountName = account.name;
+ accountType = account.type;
+ break;
+ }
+ }
+ }
+
+ if (accountName != null || accountType != null) {
+ setResultAndFinish(accountName, accountType);
+ return;
+ }
+ }
+ Log.d(TAG, "ChooseTypeAndAccountActivity.onActivityResult: unable to find added "
+ + "account, pretending the request was canceled");
+ }
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "ChooseTypeAndAccountActivity.onActivityResult: canceled");
+ }
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
+
+ protected void runAddAccountForAuthenticator(String type) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runAddAccountForAuthenticator: " + type);
+ }
+ final Bundle options = getIntent().getBundleExtra(
+ ChooseTypeAndAccountActivity.EXTRA_ADD_ACCOUNT_OPTIONS_BUNDLE);
+ final String[] requiredFeatures = getIntent().getStringArrayExtra(
+ ChooseTypeAndAccountActivity.EXTRA_ADD_ACCOUNT_REQUIRED_FEATURES_STRING_ARRAY);
+ final String authTokenType = getIntent().getStringExtra(
+ ChooseTypeAndAccountActivity.EXTRA_ADD_ACCOUNT_AUTH_TOKEN_TYPE_STRING);
+ AccountManager.get(this).addAccount(type, authTokenType, requiredFeatures,
+ options, null /* activity */, this /* callback */, null /* Handler */);
+ }
+
+ @Override
+ public void run(final AccountManagerFuture<Bundle> accountManagerFuture) {
+ try {
+ final Bundle accountManagerResult = accountManagerFuture.getResult();
+ final Intent intent = (Intent)accountManagerResult.getParcelable(
+ AccountManager.KEY_INTENT);
+ if (intent != null) {
+ mPendingRequest = REQUEST_ADD_ACCOUNT;
+ mExistingAccounts = AccountManager.get(this).getAccountsForPackage(mCallingPackage,
+ mCallingUid);
+ intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivityForResult(intent, REQUEST_ADD_ACCOUNT);
+ return;
+ }
+ } catch (OperationCanceledException e) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return;
+ } catch (IOException e) {
+ } catch (AuthenticatorException e) {
+ }
+ Bundle bundle = new Bundle();
+ bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "error communicating with server");
+ setResult(Activity.RESULT_OK, new Intent().putExtras(bundle));
+ finish();
+ }
+
+ /**
+ * The default activity theme shows label at the top. Set a theme which does
+ * not show label, which effectively makes the activity invisible. Note that
+ * no content is being set. If something gets set, using this theme may be
+ * useless.
+ */
+ private void setNonLabelThemeAndCallSuperCreate(Bundle savedInstanceState) {
+ setTheme(R.style.Theme_DeviceDefault_Light_Dialog_NoActionBar);
+ super.onCreate(savedInstanceState);
+ }
+
+ private void onAccountSelected(Account account) {
+ Log.d(TAG, "selected account " + account);
+ setResultAndFinish(account.name, account.type);
+ }
+
+ private void setResultAndFinish(final String accountName, final String accountType) {
+ // Mark account as visible since user chose it.
+ Account account = new Account(accountName, accountType);
+ Integer oldVisibility =
+ AccountManager.get(this).getAccountVisibility(account, mCallingPackage);
+ if (oldVisibility != null
+ && oldVisibility == AccountManager.VISIBILITY_USER_MANAGED_NOT_VISIBLE) {
+ AccountManager.get(this).setAccountVisibility(account, mCallingPackage,
+ AccountManager.VISIBILITY_USER_MANAGED_VISIBLE);
+ }
+
+ if (oldVisibility != null && oldVisibility == AccountManager.VISIBILITY_NOT_VISIBLE) {
+ // Added account is not visible to caller.
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putString(AccountManager.KEY_ACCOUNT_NAME, accountName);
+ bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType);
+ setResult(Activity.RESULT_OK, new Intent().putExtras(bundle));
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "ChooseTypeAndAccountActivity.setResultAndFinish: selected account "
+ + accountName + ", " + accountType);
+ }
+
+ finish();
+ }
+
+ private void startChooseAccountTypeActivity() {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "ChooseAccountTypeActivity.startChooseAccountTypeActivity()");
+ }
+ final Intent intent = new Intent(this, ChooseAccountTypeActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ intent.putExtra(EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY,
+ getIntent().getStringArrayExtra(EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY));
+ intent.putExtra(EXTRA_ADD_ACCOUNT_OPTIONS_BUNDLE,
+ getIntent().getBundleExtra(EXTRA_ADD_ACCOUNT_OPTIONS_BUNDLE));
+ intent.putExtra(EXTRA_ADD_ACCOUNT_REQUIRED_FEATURES_STRING_ARRAY,
+ getIntent().getStringArrayExtra(EXTRA_ADD_ACCOUNT_REQUIRED_FEATURES_STRING_ARRAY));
+ intent.putExtra(EXTRA_ADD_ACCOUNT_AUTH_TOKEN_TYPE_STRING,
+ getIntent().getStringExtra(EXTRA_ADD_ACCOUNT_AUTH_TOKEN_TYPE_STRING));
+ startActivityForResult(intent, REQUEST_CHOOSE_TYPE);
+ mPendingRequest = REQUEST_CHOOSE_TYPE;
+ }
+
+ /**
+ * @return a value between 0 (inclusive) and accounts.size() (inclusive) or SELECTED_ITEM_NONE.
+ * An index value of accounts.size() indicates 'Add account' option.
+ */
+ private int getItemIndexToSelect(ArrayList<Account> accounts, String selectedAccountName,
+ boolean selectedAddNewAccount) {
+ // If "Add account" option was previously selected by user, preserve it across
+ // orientation changes.
+ if (selectedAddNewAccount) {
+ return accounts.size();
+ }
+ // search for the selected account name if present
+ for (int i = 0; i < accounts.size(); i++) {
+ if (accounts.get(i).name.equals(selectedAccountName)) {
+ return i;
+ }
+ }
+ // no account selected.
+ return SELECTED_ITEM_NONE;
+ }
+
+ private String[] getListOfDisplayableOptions(ArrayList<Account> accounts) {
+ // List of options includes all accounts found together with "Add new account" as the
+ // last item in the list.
+ String[] listItems = new String[accounts.size() + (mDisallowAddAccounts ? 0 : 1)];
+ for (int i = 0; i < accounts.size(); i++) {
+ listItems[i] = accounts.get(i).name;
+ }
+ if (!mDisallowAddAccounts) {
+ listItems[accounts.size()] = getResources().getString(
+ R.string.add_account_button_label);
+ }
+ return listItems;
+ }
+
+ /**
+ * Create a list of Account objects for each account that is acceptable. Filter out accounts
+ * that don't match the allowable types, if provided, or that don't match the allowable
+ * accounts, if provided.
+ */
+ private LinkedHashMap<Account, Integer> getAcceptableAccountChoices(AccountManager accountManager) {
+ Map<Account, Integer> accountsAndVisibilityForCaller =
+ accountManager.getAccountsAndVisibilityForPackage(mCallingPackage, null);
+ Account[] allAccounts = accountManager.getAccounts();
+ LinkedHashMap<Account, Integer> accountsToPopulate =
+ new LinkedHashMap<>(accountsAndVisibilityForCaller.size());
+ for (Account account : allAccounts) {
+ if (mSetOfAllowableAccounts != null
+ && !mSetOfAllowableAccounts.contains(account)) {
+ continue;
+ }
+ if (mSetOfRelevantAccountTypes != null
+ && !mSetOfRelevantAccountTypes.contains(account.type)) {
+ continue;
+ }
+ if (accountsAndVisibilityForCaller.get(account) != null) {
+ accountsToPopulate.put(account, accountsAndVisibilityForCaller.get(account));
+ }
+ }
+ return accountsToPopulate;
+ }
+
+ /**
+ * Return a set of account types specified by the intent as well as supported by the
+ * AccountManager.
+ */
+ private Set<String> getReleventAccountTypes(final Intent intent) {
+ // An account type is relevant iff it is allowed by the caller and supported by the account
+ // manager.
+ Set<String> setOfRelevantAccountTypes = null;
+ final String[] allowedAccountTypes =
+ intent.getStringArrayExtra(EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY);
+ AuthenticatorDescription[] descs = AccountManager.get(this).getAuthenticatorTypes();
+ Set<String> supportedAccountTypes = new HashSet<String>(descs.length);
+ for (AuthenticatorDescription desc : descs) {
+ supportedAccountTypes.add(desc.type);
+ }
+ if (allowedAccountTypes != null) {
+ setOfRelevantAccountTypes = Sets.newHashSet(allowedAccountTypes);
+ setOfRelevantAccountTypes.retainAll(supportedAccountTypes);
+ } else {
+ setOfRelevantAccountTypes = supportedAccountTypes;
+ }
+ return setOfRelevantAccountTypes;
+ }
+
+ /**
+ * Returns a set of whitelisted accounts given by the intent or null if none specified by the
+ * intent.
+ */
+ private Set<Account> getAllowableAccountSet(final Intent intent) {
+ Set<Account> setOfAllowableAccounts = null;
+ final ArrayList<Parcelable> validAccounts =
+ intent.getParcelableArrayListExtra(EXTRA_ALLOWABLE_ACCOUNTS_ARRAYLIST);
+ if (validAccounts != null) {
+ setOfAllowableAccounts = new HashSet<Account>(validAccounts.size());
+ for (Parcelable parcelable : validAccounts) {
+ setOfAllowableAccounts.add((Account)parcelable);
+ }
+ }
+ return setOfAllowableAccounts;
+ }
+
+ /**
+ * Overrides the description text view for the picker activity if specified by the intent.
+ * If not specified then makes the description invisible.
+ */
+ private void overrideDescriptionIfSupplied(String descriptionOverride) {
+ TextView descriptionView = findViewById(R.id.description);
+ if (!TextUtils.isEmpty(descriptionOverride)) {
+ descriptionView.setText(descriptionOverride);
+ } else {
+ descriptionView.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Populates the UI ListView with the given list of items and selects an item
+ * based on {@code mSelectedItemIndex} member variable.
+ */
+ private final void populateUIAccountList(String[] listItems) {
+ ListView list = findViewById(android.R.id.list);
+ list.setAdapter(new ArrayAdapter<String>(this,
+ android.R.layout.simple_list_item_single_choice, listItems));
+ list.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ list.setItemsCanFocus(false);
+ list.setOnItemClickListener(
+ new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ mSelectedItemIndex = position;
+ mOkButton.setEnabled(true);
+ }
+ });
+ if (mSelectedItemIndex != SELECTED_ITEM_NONE) {
+ list.setItemChecked(mSelectedItemIndex, true);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "List item " + mSelectedItemIndex + " should be selected");
+ }
+ }
+ }
+}
diff --git a/android/accounts/GrantCredentialsPermissionActivity.java b/android/accounts/GrantCredentialsPermissionActivity.java
new file mode 100644
index 0000000..af74b03
--- /dev/null
+++ b/android/accounts/GrantCredentialsPermissionActivity.java
@@ -0,0 +1,198 @@
+/*
+ * 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.accounts;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.widget.TextView;
+import android.widget.LinearLayout;
+import android.view.View;
+import android.view.LayoutInflater;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+import com.android.internal.R;
+
+import java.io.IOException;
+
+/**
+ * @hide
+ */
+public class GrantCredentialsPermissionActivity extends Activity implements View.OnClickListener {
+ public static final String EXTRAS_ACCOUNT = "account";
+ public static final String EXTRAS_AUTH_TOKEN_TYPE = "authTokenType";
+ public static final String EXTRAS_RESPONSE = "response";
+ public static final String EXTRAS_REQUESTING_UID = "uid";
+
+ private Account mAccount;
+ private String mAuthTokenType;
+ private int mUid;
+ private Bundle mResultBundle = null;
+ protected LayoutInflater mInflater;
+
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.grant_credentials_permission);
+ setTitle(R.string.grant_permissions_header_text);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ final Bundle extras = getIntent().getExtras();
+ if (extras == null) {
+ // we were somehow started with bad parameters. abort the activity.
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ // Grant 'account'/'type' to mUID
+ mAccount = extras.getParcelable(EXTRAS_ACCOUNT);
+ mAuthTokenType = extras.getString(EXTRAS_AUTH_TOKEN_TYPE);
+ mUid = extras.getInt(EXTRAS_REQUESTING_UID);
+ final PackageManager pm = getPackageManager();
+ final String[] packages = pm.getPackagesForUid(mUid);
+
+ if (mAccount == null || mAuthTokenType == null || packages == null) {
+ // we were somehow started with bad parameters. abort the activity.
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ String accountTypeLabel;
+ try {
+ accountTypeLabel = getAccountLabel(mAccount);
+ } catch (IllegalArgumentException e) {
+ // label or resource was missing. abort the activity.
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ final TextView authTokenTypeView = findViewById(R.id.authtoken_type);
+ authTokenTypeView.setVisibility(View.GONE);
+
+ final AccountManagerCallback<String> callback = new AccountManagerCallback<String>() {
+ public void run(AccountManagerFuture<String> future) {
+ try {
+ final String authTokenLabel = future.getResult();
+ if (!TextUtils.isEmpty(authTokenLabel)) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ if (!isFinishing()) {
+ authTokenTypeView.setText(authTokenLabel);
+ authTokenTypeView.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+ }
+ } catch (OperationCanceledException e) {
+ } catch (IOException e) {
+ } catch (AuthenticatorException e) {
+ }
+ }
+ };
+
+ if (!AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE.equals(mAuthTokenType)) {
+ AccountManager.get(this).getAuthTokenLabel(mAccount.type,
+ mAuthTokenType, callback, null);
+ }
+
+ findViewById(R.id.allow_button).setOnClickListener(this);
+ findViewById(R.id.deny_button).setOnClickListener(this);
+
+ LinearLayout packagesListView = findViewById(R.id.packages_list);
+
+ for (String pkg : packages) {
+ String packageLabel;
+ try {
+ packageLabel = pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString();
+ } catch (PackageManager.NameNotFoundException e) {
+ packageLabel = pkg;
+ }
+ packagesListView.addView(newPackageView(packageLabel));
+ }
+
+ ((TextView) findViewById(R.id.account_name)).setText(mAccount.name);
+ ((TextView) findViewById(R.id.account_type)).setText(accountTypeLabel);
+ }
+
+ private String getAccountLabel(Account account) {
+ final AuthenticatorDescription[] authenticatorTypes =
+ AccountManager.get(this).getAuthenticatorTypes();
+ for (int i = 0, N = authenticatorTypes.length; i < N; i++) {
+ final AuthenticatorDescription desc = authenticatorTypes[i];
+ if (desc.type.equals(account.type)) {
+ try {
+ return createPackageContext(desc.packageName, 0).getString(desc.labelId);
+ } catch (PackageManager.NameNotFoundException e) {
+ return account.type;
+ } catch (Resources.NotFoundException e) {
+ return account.type;
+ }
+ }
+ }
+ return account.type;
+ }
+
+ private View newPackageView(String packageLabel) {
+ View view = mInflater.inflate(R.layout.permissions_package_list_item, null);
+ ((TextView) view.findViewById(R.id.package_label)).setText(packageLabel);
+ return view;
+ }
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.allow_button:
+ AccountManager.get(this).updateAppPermission(mAccount, mAuthTokenType, mUid, true);
+ Intent result = new Intent();
+ result.putExtra("retry", true);
+ setResult(RESULT_OK, result);
+ setAccountAuthenticatorResult(result.getExtras());
+ break;
+
+ case R.id.deny_button:
+ AccountManager.get(this).updateAppPermission(mAccount, mAuthTokenType, mUid, false);
+ setResult(RESULT_CANCELED);
+ break;
+ }
+ finish();
+ }
+
+ public final void setAccountAuthenticatorResult(Bundle result) {
+ mResultBundle = result;
+ }
+
+ /**
+ * Sends the result or a {@link AccountManager#ERROR_CODE_CANCELED} error if a
+ * result isn't present.
+ */
+ public void finish() {
+ Intent intent = getIntent();
+ AccountAuthenticatorResponse response = intent.getParcelableExtra(EXTRAS_RESPONSE);
+ if (response != null) {
+ // send the result bundle back if set, otherwise send an error.
+ if (mResultBundle != null) {
+ response.onResult(mResultBundle);
+ } else {
+ response.onError(AccountManager.ERROR_CODE_CANCELED, "canceled");
+ }
+ }
+ super.finish();
+ }
+}
diff --git a/android/accounts/NetworkErrorException.java b/android/accounts/NetworkErrorException.java
new file mode 100644
index 0000000..07f4ce9
--- /dev/null
+++ b/android/accounts/NetworkErrorException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.accounts;
+
+public class NetworkErrorException extends AccountsException {
+ public NetworkErrorException() {
+ super();
+ }
+ public NetworkErrorException(String message) {
+ super(message);
+ }
+ public NetworkErrorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ public NetworkErrorException(Throwable cause) {
+ super(cause);
+ }
+}
\ No newline at end of file
diff --git a/android/accounts/OnAccountsUpdateListener.java b/android/accounts/OnAccountsUpdateListener.java
new file mode 100644
index 0000000..2b4ee50
--- /dev/null
+++ b/android/accounts/OnAccountsUpdateListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2007 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.accounts;
+
+/**
+ * An interface that contains the callback used by the AccountManager
+ */
+public interface OnAccountsUpdateListener {
+ /**
+ * This invoked when the AccountManager starts up and whenever the account
+ * set changes.
+ * @param accounts the current accounts
+ */
+ void onAccountsUpdated(Account[] accounts);
+}
diff --git a/android/accounts/OperationCanceledException.java b/android/accounts/OperationCanceledException.java
new file mode 100644
index 0000000..896d194
--- /dev/null
+++ b/android/accounts/OperationCanceledException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.accounts;
+
+public class OperationCanceledException extends AccountsException {
+ public OperationCanceledException() {
+ super();
+ }
+ public OperationCanceledException(String message) {
+ super(message);
+ }
+ public OperationCanceledException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ public OperationCanceledException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/android/animation/AnimationHandler.java b/android/animation/AnimationHandler.java
new file mode 100644
index 0000000..260323f
--- /dev/null
+++ b/android/animation/AnimationHandler.java
@@ -0,0 +1,317 @@
+/*
+ * 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.animation;
+
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.view.Choreographer;
+
+import java.util.ArrayList;
+
+/**
+ * This custom, static handler handles the timing pulse that is shared by all active
+ * ValueAnimators. This approach ensures that the setting of animation values will happen on the
+ * same thread that animations start on, and that all animations will share the same times for
+ * calculating their values, which makes synchronizing animations possible.
+ *
+ * The handler uses the Choreographer by default for doing periodic callbacks. A custom
+ * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
+ * may be independent of UI frame update. This could be useful in testing.
+ *
+ * @hide
+ */
+public class AnimationHandler {
+ /**
+ * Internal per-thread collections used to avoid set collisions as animations start and end
+ * while being processed.
+ * @hide
+ */
+ private final ArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
+ new ArrayMap<>();
+ private final ArrayList<AnimationFrameCallback> mAnimationCallbacks =
+ new ArrayList<>();
+ private final ArrayList<AnimationFrameCallback> mCommitCallbacks =
+ new ArrayList<>();
+ private AnimationFrameCallbackProvider mProvider;
+
+ private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ doAnimationFrame(getProvider().getFrameTime());
+ if (mAnimationCallbacks.size() > 0) {
+ getProvider().postFrameCallback(this);
+ }
+ }
+ };
+
+ public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
+ private boolean mListDirty = false;
+
+ public static AnimationHandler getInstance() {
+ if (sAnimatorHandler.get() == null) {
+ sAnimatorHandler.set(new AnimationHandler());
+ }
+ return sAnimatorHandler.get();
+ }
+
+ /**
+ * By default, the Choreographer is used to provide timing for frame callbacks. A custom
+ * provider can be used here to provide different timing pulse.
+ */
+ public void setProvider(AnimationFrameCallbackProvider provider) {
+ if (provider == null) {
+ mProvider = new MyFrameCallbackProvider();
+ } else {
+ mProvider = provider;
+ }
+ }
+
+ private AnimationFrameCallbackProvider getProvider() {
+ if (mProvider == null) {
+ mProvider = new MyFrameCallbackProvider();
+ }
+ return mProvider;
+ }
+
+ /**
+ * Register to get a callback on the next frame after the delay.
+ */
+ public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
+ if (mAnimationCallbacks.size() == 0) {
+ getProvider().postFrameCallback(mFrameCallback);
+ }
+ if (!mAnimationCallbacks.contains(callback)) {
+ mAnimationCallbacks.add(callback);
+ }
+
+ if (delay > 0) {
+ mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
+ }
+ }
+
+ /**
+ * Register to get a one shot callback for frame commit timing. Frame commit timing is the
+ * time *after* traversals are done, as opposed to the animation frame timing, which is
+ * before any traversals. This timing can be used to adjust the start time of an animation
+ * when expensive traversals create big delta between the animation frame timing and the time
+ * that animation is first shown on screen.
+ *
+ * Note this should only be called when the animation has already registered to receive
+ * animation frame callbacks. This callback will be guaranteed to happen *after* the next
+ * animation frame callback.
+ */
+ public void addOneShotCommitCallback(final AnimationFrameCallback callback) {
+ if (!mCommitCallbacks.contains(callback)) {
+ mCommitCallbacks.add(callback);
+ }
+ }
+
+ /**
+ * Removes the given callback from the list, so it will no longer be called for frame related
+ * timing.
+ */
+ public void removeCallback(AnimationFrameCallback callback) {
+ mCommitCallbacks.remove(callback);
+ mDelayedCallbackStartTime.remove(callback);
+ int id = mAnimationCallbacks.indexOf(callback);
+ if (id >= 0) {
+ mAnimationCallbacks.set(id, null);
+ mListDirty = true;
+ }
+ }
+
+ private void doAnimationFrame(long frameTime) {
+ long currentTime = SystemClock.uptimeMillis();
+ final int size = mAnimationCallbacks.size();
+ for (int i = 0; i < size; i++) {
+ final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
+ if (callback == null) {
+ continue;
+ }
+ if (isCallbackDue(callback, currentTime)) {
+ callback.doAnimationFrame(frameTime);
+ if (mCommitCallbacks.contains(callback)) {
+ getProvider().postCommitCallback(new Runnable() {
+ @Override
+ public void run() {
+ commitAnimationFrame(callback, getProvider().getFrameTime());
+ }
+ });
+ }
+ }
+ }
+ cleanUpList();
+ }
+
+ private void commitAnimationFrame(AnimationFrameCallback callback, long frameTime) {
+ if (!mDelayedCallbackStartTime.containsKey(callback) &&
+ mCommitCallbacks.contains(callback)) {
+ callback.commitAnimationFrame(frameTime);
+ mCommitCallbacks.remove(callback);
+ }
+ }
+
+ /**
+ * Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay
+ * so that they can start getting frame callbacks.
+ *
+ * @return true if they have passed the initial delay or have no delay, false otherwise.
+ */
+ private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
+ Long startTime = mDelayedCallbackStartTime.get(callback);
+ if (startTime == null) {
+ return true;
+ }
+ if (startTime < currentTime) {
+ mDelayedCallbackStartTime.remove(callback);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Return the number of callbacks that have registered for frame callbacks.
+ */
+ public static int getAnimationCount() {
+ AnimationHandler handler = sAnimatorHandler.get();
+ if (handler == null) {
+ return 0;
+ }
+ return handler.getCallbackSize();
+ }
+
+ public static void setFrameDelay(long delay) {
+ getInstance().getProvider().setFrameDelay(delay);
+ }
+
+ public static long getFrameDelay() {
+ return getInstance().getProvider().getFrameDelay();
+ }
+
+ void autoCancelBasedOn(ObjectAnimator objectAnimator) {
+ for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
+ AnimationFrameCallback cb = mAnimationCallbacks.get(i);
+ if (cb == null) {
+ continue;
+ }
+ if (objectAnimator.shouldAutoCancel(cb)) {
+ ((Animator) mAnimationCallbacks.get(i)).cancel();
+ }
+ }
+ }
+
+ private void cleanUpList() {
+ if (mListDirty) {
+ for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
+ if (mAnimationCallbacks.get(i) == null) {
+ mAnimationCallbacks.remove(i);
+ }
+ }
+ mListDirty = false;
+ }
+ }
+
+ private int getCallbackSize() {
+ int count = 0;
+ int size = mAnimationCallbacks.size();
+ for (int i = size - 1; i >= 0; i--) {
+ if (mAnimationCallbacks.get(i) != null) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Default provider of timing pulse that uses Choreographer for frame callbacks.
+ */
+ private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
+
+ final Choreographer mChoreographer = Choreographer.getInstance();
+
+ @Override
+ public void postFrameCallback(Choreographer.FrameCallback callback) {
+ mChoreographer.postFrameCallback(callback);
+ }
+
+ @Override
+ public void postCommitCallback(Runnable runnable) {
+ mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
+ }
+
+ @Override
+ public long getFrameTime() {
+ return mChoreographer.getFrameTime();
+ }
+
+ @Override
+ public long getFrameDelay() {
+ return Choreographer.getFrameDelay();
+ }
+
+ @Override
+ public void setFrameDelay(long delay) {
+ Choreographer.setFrameDelay(delay);
+ }
+ }
+
+ /**
+ * Callbacks that receives notifications for animation timing and frame commit timing.
+ */
+ interface AnimationFrameCallback {
+ /**
+ * Run animation based on the frame time.
+ * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
+ * base.
+ * @return if the animation has finished.
+ */
+ boolean doAnimationFrame(long frameTime);
+
+ /**
+ * This notifies the callback of frame commit time. Frame commit time is the time after
+ * traversals happen, as opposed to the normal animation frame time that is before
+ * traversals. This is used to compensate expensive traversals that happen as the
+ * animation starts. When traversals take a long time to complete, the rendering of the
+ * initial frame will be delayed (by a long time). But since the startTime of the
+ * animation is set before the traversal, by the time of next frame, a lot of time would
+ * have passed since startTime was set, the animation will consequently skip a few frames
+ * to respect the new frameTime. By having the commit time, we can adjust the start time to
+ * when the first frame was drawn (after any expensive traversals) so that no frames
+ * will be skipped.
+ *
+ * @param frameTime The frame time after traversals happen, if any, in the
+ * {@link SystemClock#uptimeMillis()} time base.
+ */
+ void commitAnimationFrame(long frameTime);
+ }
+
+ /**
+ * The intention for having this interface is to increase the testability of ValueAnimator.
+ * Specifically, we can have a custom implementation of the interface below and provide
+ * timing pulse without using Choreographer. That way we could use any arbitrary interval for
+ * our timing pulse in the tests.
+ *
+ * @hide
+ */
+ public interface AnimationFrameCallbackProvider {
+ void postFrameCallback(Choreographer.FrameCallback callback);
+ void postCommitCallback(Runnable runnable);
+ long getFrameTime();
+ long getFrameDelay();
+ void setFrameDelay(long delay);
+ }
+}
diff --git a/android/animation/AnimationThread.java b/android/animation/AnimationThread.java
new file mode 100644
index 0000000..ce2aec7
--- /dev/null
+++ b/android/animation/AnimationThread.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2010 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.animation;
+
+import com.android.ide.common.rendering.api.IAnimationListener;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.Result.Status;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.RenderSessionImpl;
+
+import android.os.Handler;
+import android.os.Handler_Delegate;
+import android.os.Message;
+
+import java.util.PriorityQueue;
+import java.util.Queue;
+
+/**
+ * Abstract animation thread.
+ * <p/>
+ * This does not actually start an animation, instead it fakes a looper that will play whatever
+ * animation is sending messages to its own {@link Handler}.
+ * <p/>
+ * Classes should implement {@link #preAnimation()} and {@link #postAnimation()}.
+ * <p/>
+ * If {@link #preAnimation()} does not start an animation somehow then the thread doesn't do
+ * anything.
+ *
+ */
+public abstract class AnimationThread extends Thread {
+
+ private static class MessageBundle implements Comparable<MessageBundle> {
+ final Handler mTarget;
+ final Message mMessage;
+ final long mUptimeMillis;
+
+ MessageBundle(Handler target, Message message, long uptimeMillis) {
+ mTarget = target;
+ mMessage = message;
+ mUptimeMillis = uptimeMillis;
+ }
+
+ @Override
+ public int compareTo(MessageBundle bundle) {
+ if (mUptimeMillis < bundle.mUptimeMillis) {
+ return -1;
+ }
+ return 1;
+ }
+ }
+
+ private final RenderSessionImpl mSession;
+
+ private Queue<MessageBundle> mQueue = new PriorityQueue<MessageBundle>();
+ private final IAnimationListener mListener;
+
+ public AnimationThread(RenderSessionImpl scene, String threadName,
+ IAnimationListener listener) {
+ super(threadName);
+ mSession = scene;
+ mListener = listener;
+ }
+
+ public abstract Result preAnimation();
+ public abstract void postAnimation();
+
+ @Override
+ public void run() {
+ Bridge.prepareThread();
+ try {
+ /* FIXME: The ANIMATION_FRAME message no longer exists. Instead, the
+ * animation timing loop is completely based on a Choreographer objects
+ * that schedules animation and drawing frames. The animation handler is
+ * no longer even a handler; it is just a Runnable enqueued on the Choreographer.
+ Handler_Delegate.setCallback(new IHandlerCallback() {
+ @Override
+ public void sendMessageAtTime(Handler handler, Message msg, long uptimeMillis) {
+ if (msg.what == ValueAnimator.ANIMATION_START ||
+ msg.what == ValueAnimator.ANIMATION_FRAME) {
+ mQueue.add(new MessageBundle(handler, msg, uptimeMillis));
+ } else {
+ // just ignore.
+ }
+ }
+ });
+ */
+
+ // call out to the pre-animation work, which should start an animation or more.
+ Result result = preAnimation();
+ if (result.isSuccess() == false) {
+ mListener.done(result);
+ }
+
+ // loop the animation
+ RenderSession session = mSession.getSession();
+ do {
+ // check early.
+ if (mListener.isCanceled()) {
+ break;
+ }
+
+ // get the next message.
+ MessageBundle bundle = mQueue.poll();
+ if (bundle == null) {
+ break;
+ }
+
+ // sleep enough for this bundle to be on time
+ long currentTime = System.currentTimeMillis();
+ if (currentTime < bundle.mUptimeMillis) {
+ try {
+ sleep(bundle.mUptimeMillis - currentTime);
+ } catch (InterruptedException e) {
+ // FIXME log/do something/sleep again?
+ e.printStackTrace();
+ }
+ }
+
+ // check after sleeping.
+ if (mListener.isCanceled()) {
+ break;
+ }
+
+ // ready to do the work, acquire the scene.
+ result = mSession.acquire(250);
+ if (result.isSuccess() == false) {
+ mListener.done(result);
+ return;
+ }
+
+ // process the bundle. If the animation is not finished, this will enqueue
+ // the next message, so mQueue will have another one.
+ try {
+ // check after acquiring in case it took a while.
+ if (mListener.isCanceled()) {
+ break;
+ }
+
+ bundle.mTarget.handleMessage(bundle.mMessage);
+ if (mSession.render(false /*freshRender*/).isSuccess()) {
+ mListener.onNewFrame(session);
+ }
+ } finally {
+ mSession.release();
+ }
+ } while (mListener.isCanceled() == false && mQueue.size() > 0);
+
+ mListener.done(Status.SUCCESS.createResult());
+
+ } catch (Throwable throwable) {
+ // can't use Bridge.getLog() as the exception might be thrown outside
+ // of an acquire/release block.
+ mListener.done(Status.ERROR_UNKNOWN.createResult("Error playing animation", throwable));
+
+ } finally {
+ postAnimation();
+ Handler_Delegate.setCallback(null);
+ Bridge.cleanupThread();
+ }
+ }
+}
diff --git a/android/animation/Animator.java b/android/animation/Animator.java
new file mode 100644
index 0000000..3cdd691
--- /dev/null
+++ b/android/animation/Animator.java
@@ -0,0 +1,678 @@
+/*
+ * Copyright (C) 2010 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.animation;
+
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ConstantState;
+
+import java.util.ArrayList;
+
+/**
+ * This is the superclass for classes which provide basic support for animations which can be
+ * started, ended, and have <code>AnimatorListeners</code> added to them.
+ */
+public abstract class Animator implements Cloneable {
+
+ /**
+ * The value used to indicate infinite duration (e.g. when Animators repeat infinitely).
+ */
+ public static final long DURATION_INFINITE = -1;
+ /**
+ * The set of listeners to be sent events through the life of an animation.
+ */
+ ArrayList<AnimatorListener> mListeners = null;
+
+ /**
+ * The set of listeners to be sent pause/resume events through the life
+ * of an animation.
+ */
+ ArrayList<AnimatorPauseListener> mPauseListeners = null;
+
+ /**
+ * Whether this animator is currently in a paused state.
+ */
+ boolean mPaused = false;
+
+ /**
+ * A set of flags which identify the type of configuration changes that can affect this
+ * Animator. Used by the Animator cache.
+ */
+ @Config int mChangingConfigurations = 0;
+
+ /**
+ * If this animator is inflated from a constant state, keep a reference to it so that
+ * ConstantState will not be garbage collected until this animator is collected
+ */
+ private AnimatorConstantState mConstantState;
+
+ /**
+ * Starts this animation. If the animation has a nonzero startDelay, the animation will start
+ * running after that delay elapses. A non-delayed animation will have its initial
+ * value(s) set immediately, followed by calls to
+ * {@link AnimatorListener#onAnimationStart(Animator)} for any listeners of this animator.
+ *
+ * <p>The animation started by calling this method will be run on the thread that called
+ * this method. This thread should have a Looper on it (a runtime exception will be thrown if
+ * this is not the case). Also, if the animation will animate
+ * properties of objects in the view hierarchy, then the calling thread should be the UI
+ * thread for that view hierarchy.</p>
+ *
+ */
+ public void start() {
+ }
+
+ /**
+ * Cancels the animation. Unlike {@link #end()}, <code>cancel()</code> causes the animation to
+ * stop in its tracks, sending an
+ * {@link android.animation.Animator.AnimatorListener#onAnimationCancel(Animator)} to
+ * its listeners, followed by an
+ * {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator)} message.
+ *
+ * <p>This method must be called on the thread that is running the animation.</p>
+ */
+ public void cancel() {
+ }
+
+ /**
+ * Ends the animation. This causes the animation to assign the end value of the property being
+ * animated, then calling the
+ * {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator)} method on
+ * its listeners.
+ *
+ * <p>This method must be called on the thread that is running the animation.</p>
+ */
+ public void end() {
+ }
+
+ /**
+ * Pauses a running animation. This method should only be called on the same thread on
+ * which the animation was started. If the animation has not yet been {@link
+ * #isStarted() started} or has since ended, then the call is ignored. Paused
+ * animations can be resumed by calling {@link #resume()}.
+ *
+ * @see #resume()
+ * @see #isPaused()
+ * @see AnimatorPauseListener
+ */
+ public void pause() {
+ if (isStarted() && !mPaused) {
+ mPaused = true;
+ if (mPauseListeners != null) {
+ ArrayList<AnimatorPauseListener> tmpListeners =
+ (ArrayList<AnimatorPauseListener>) mPauseListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationPause(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * Resumes a paused animation, causing the animator to pick up where it left off
+ * when it was paused. This method should only be called on the same thread on
+ * which the animation was started. Calls to resume() on an animator that is
+ * not currently paused will be ignored.
+ *
+ * @see #pause()
+ * @see #isPaused()
+ * @see AnimatorPauseListener
+ */
+ public void resume() {
+ if (mPaused) {
+ mPaused = false;
+ if (mPauseListeners != null) {
+ ArrayList<AnimatorPauseListener> tmpListeners =
+ (ArrayList<AnimatorPauseListener>) mPauseListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationResume(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether this animator is currently in a paused state.
+ *
+ * @return True if the animator is currently paused, false otherwise.
+ *
+ * @see #pause()
+ * @see #resume()
+ */
+ public boolean isPaused() {
+ return mPaused;
+ }
+
+ /**
+ * The amount of time, in milliseconds, to delay processing the animation
+ * after {@link #start()} is called.
+ *
+ * @return the number of milliseconds to delay running the animation
+ */
+ public abstract long getStartDelay();
+
+ /**
+ * The amount of time, in milliseconds, to delay processing the animation
+ * after {@link #start()} is called.
+
+ * @param startDelay The amount of the delay, in milliseconds
+ */
+ public abstract void setStartDelay(long startDelay);
+
+ /**
+ * Sets the duration of the animation.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ */
+ public abstract Animator setDuration(long duration);
+
+ /**
+ * Gets the duration of the animation.
+ *
+ * @return The length of the animation, in milliseconds.
+ */
+ public abstract long getDuration();
+
+ /**
+ * Gets the total duration of the animation, accounting for animation sequences, start delay,
+ * and repeating. Return {@link #DURATION_INFINITE} if the duration is infinite.
+ *
+ * @return Total time an animation takes to finish, starting from the time {@link #start()}
+ * is called. {@link #DURATION_INFINITE} will be returned if the animation or any
+ * child animation repeats infinite times.
+ */
+ public long getTotalDuration() {
+ long duration = getDuration();
+ if (duration == DURATION_INFINITE) {
+ return DURATION_INFINITE;
+ } else {
+ return getStartDelay() + duration;
+ }
+ }
+
+ /**
+ * The time interpolator used in calculating the elapsed fraction of the
+ * animation. The interpolator determines whether the animation runs with
+ * linear or non-linear motion, such as acceleration and deceleration. The
+ * default value is {@link android.view.animation.AccelerateDecelerateInterpolator}.
+ *
+ * @param value the interpolator to be used by this animation
+ */
+ public abstract void setInterpolator(TimeInterpolator value);
+
+ /**
+ * Returns the timing interpolator that this animation uses.
+ *
+ * @return The timing interpolator for this animation.
+ */
+ public TimeInterpolator getInterpolator() {
+ return null;
+ }
+
+ /**
+ * Returns whether this Animator is currently running (having been started and gone past any
+ * initial startDelay period and not yet ended).
+ *
+ * @return Whether the Animator is running.
+ */
+ public abstract boolean isRunning();
+
+ /**
+ * Returns whether this Animator has been started and not yet ended. For reusable
+ * Animators (which most Animators are, apart from the one-shot animator produced by
+ * {@link android.view.ViewAnimationUtils#createCircularReveal(
+ * android.view.View, int, int, float, float) createCircularReveal()}),
+ * this state is a superset of {@link #isRunning()}, because an Animator with a
+ * nonzero {@link #getStartDelay() startDelay} will return true for {@link #isStarted()} during
+ * the delay phase, whereas {@link #isRunning()} will return true only after the delay phase
+ * is complete. Non-reusable animators will always return true after they have been
+ * started, because they cannot return to a non-started state.
+ *
+ * @return Whether the Animator has been started and not yet ended.
+ */
+ public boolean isStarted() {
+ // Default method returns value for isRunning(). Subclasses should override to return a
+ // real value.
+ return isRunning();
+ }
+
+ /**
+ * Adds a listener to the set of listeners that are sent events through the life of an
+ * animation, such as start, repeat, and end.
+ *
+ * @param listener the listener to be added to the current set of listeners for this animation.
+ */
+ public void addListener(AnimatorListener listener) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<AnimatorListener>();
+ }
+ mListeners.add(listener);
+ }
+
+ /**
+ * Removes a listener from the set listening to this animation.
+ *
+ * @param listener the listener to be removed from the current set of listeners for this
+ * animation.
+ */
+ public void removeListener(AnimatorListener listener) {
+ if (mListeners == null) {
+ return;
+ }
+ mListeners.remove(listener);
+ if (mListeners.size() == 0) {
+ mListeners = null;
+ }
+ }
+
+ /**
+ * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently
+ * listening for events on this <code>Animator</code> object.
+ *
+ * @return ArrayList<AnimatorListener> The set of listeners.
+ */
+ public ArrayList<AnimatorListener> getListeners() {
+ return mListeners;
+ }
+
+ /**
+ * Adds a pause listener to this animator.
+ *
+ * @param listener the listener to be added to the current set of pause listeners
+ * for this animation.
+ */
+ public void addPauseListener(AnimatorPauseListener listener) {
+ if (mPauseListeners == null) {
+ mPauseListeners = new ArrayList<AnimatorPauseListener>();
+ }
+ mPauseListeners.add(listener);
+ }
+
+ /**
+ * Removes a pause listener from the set listening to this animation.
+ *
+ * @param listener the listener to be removed from the current set of pause
+ * listeners for this animation.
+ */
+ public void removePauseListener(AnimatorPauseListener li