diff --git a/api/current.txt b/api/current.txt index e315958badebd..18ece9148fc7f 100644 --- a/api/current.txt +++ b/api/current.txt @@ -2688,11 +2688,25 @@ package android { package android.accessibilityservice { + public final class AccessibilityButtonController { + method public boolean isAccessibilityButtonAvailable(); + method public void registerAccessibilityButtonCallback(android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback); + method public void registerAccessibilityButtonCallback(android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback, android.os.Handler); + method public void unregisterAccessibilityButtonCallback(android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback); + } + + public static abstract class AccessibilityButtonController.AccessibilityButtonCallback { + ctor public AccessibilityButtonController.AccessibilityButtonCallback(); + method public void onAvailabilityChanged(android.accessibilityservice.AccessibilityButtonController, boolean); + method public void onClicked(android.accessibilityservice.AccessibilityButtonController); + } + public abstract class AccessibilityService extends android.app.Service { ctor public AccessibilityService(); method public final void disableSelf(); method public final boolean dispatchGesture(android.accessibilityservice.GestureDescription, android.accessibilityservice.AccessibilityService.GestureResultCallback, android.os.Handler); method public android.view.accessibility.AccessibilityNodeInfo findFocus(int); + method public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(); method public final android.accessibilityservice.FingerprintGestureController getFingerprintGestureController(); method public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController(); method public android.view.accessibility.AccessibilityNodeInfo getRootInActiveWindow(); @@ -2805,6 +2819,7 @@ package android.accessibilityservice { field public static final int FLAG_ENABLE_ACCESSIBILITY_VOLUME = 128; // 0x80 field public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 2; // 0x2 field public static final int FLAG_REPORT_VIEW_IDS = 16; // 0x10 + field public static final int FLAG_REQUEST_ACCESSIBILITY_BUTTON = 256; // 0x100 field public static final int FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 8; // 0x8 field public static final int FLAG_REQUEST_FILTER_KEY_EVENTS = 32; // 0x20 field public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 4; // 0x4 diff --git a/api/system-current.txt b/api/system-current.txt index ae145dc9b0375..2b9f931374b0d 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -2807,11 +2807,25 @@ package android { package android.accessibilityservice { + public final class AccessibilityButtonController { + method public boolean isAccessibilityButtonAvailable(); + method public void registerAccessibilityButtonCallback(android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback); + method public void registerAccessibilityButtonCallback(android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback, android.os.Handler); + method public void unregisterAccessibilityButtonCallback(android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback); + } + + public static abstract class AccessibilityButtonController.AccessibilityButtonCallback { + ctor public AccessibilityButtonController.AccessibilityButtonCallback(); + method public void onAvailabilityChanged(android.accessibilityservice.AccessibilityButtonController, boolean); + method public void onClicked(android.accessibilityservice.AccessibilityButtonController); + } + public abstract class AccessibilityService extends android.app.Service { ctor public AccessibilityService(); method public final void disableSelf(); method public final boolean dispatchGesture(android.accessibilityservice.GestureDescription, android.accessibilityservice.AccessibilityService.GestureResultCallback, android.os.Handler); method public android.view.accessibility.AccessibilityNodeInfo findFocus(int); + method public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(); method public final android.accessibilityservice.FingerprintGestureController getFingerprintGestureController(); method public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController(); method public android.view.accessibility.AccessibilityNodeInfo getRootInActiveWindow(); @@ -2924,6 +2938,7 @@ package android.accessibilityservice { field public static final int FLAG_ENABLE_ACCESSIBILITY_VOLUME = 128; // 0x80 field public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 2; // 0x2 field public static final int FLAG_REPORT_VIEW_IDS = 16; // 0x10 + field public static final int FLAG_REQUEST_ACCESSIBILITY_BUTTON = 256; // 0x100 field public static final int FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 8; // 0x8 field public static final int FLAG_REQUEST_FILTER_KEY_EVENTS = 32; // 0x20 field public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 4; // 0x4 diff --git a/api/test-current.txt b/api/test-current.txt index 1e3b6db92185d..f19d959d777d5 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -2688,11 +2688,25 @@ package android { package android.accessibilityservice { + public final class AccessibilityButtonController { + method public boolean isAccessibilityButtonAvailable(); + method public void registerAccessibilityButtonCallback(android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback); + method public void registerAccessibilityButtonCallback(android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback, android.os.Handler); + method public void unregisterAccessibilityButtonCallback(android.accessibilityservice.AccessibilityButtonController.AccessibilityButtonCallback); + } + + public static abstract class AccessibilityButtonController.AccessibilityButtonCallback { + ctor public AccessibilityButtonController.AccessibilityButtonCallback(); + method public void onAvailabilityChanged(android.accessibilityservice.AccessibilityButtonController, boolean); + method public void onClicked(android.accessibilityservice.AccessibilityButtonController); + } + public abstract class AccessibilityService extends android.app.Service { ctor public AccessibilityService(); method public final void disableSelf(); method public final boolean dispatchGesture(android.accessibilityservice.GestureDescription, android.accessibilityservice.AccessibilityService.GestureResultCallback, android.os.Handler); method public android.view.accessibility.AccessibilityNodeInfo findFocus(int); + method public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(); method public final android.accessibilityservice.FingerprintGestureController getFingerprintGestureController(); method public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController(); method public android.view.accessibility.AccessibilityNodeInfo getRootInActiveWindow(); @@ -2805,6 +2819,7 @@ package android.accessibilityservice { field public static final int FLAG_ENABLE_ACCESSIBILITY_VOLUME = 128; // 0x80 field public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 2; // 0x2 field public static final int FLAG_REPORT_VIEW_IDS = 16; // 0x10 + field public static final int FLAG_REQUEST_ACCESSIBILITY_BUTTON = 256; // 0x100 field public static final int FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 8; // 0x8 field public static final int FLAG_REQUEST_FILTER_KEY_EVENTS = 32; // 0x20 field public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 4; // 0x4 diff --git a/core/java/android/accessibilityservice/AccessibilityButtonController.java b/core/java/android/accessibilityservice/AccessibilityButtonController.java new file mode 100644 index 0000000000000..c3a5daba4cfce --- /dev/null +++ b/core/java/android/accessibilityservice/AccessibilityButtonController.java @@ -0,0 +1,223 @@ +/* + * 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.Slog; + +/** + * Controller for the accessibility button within the system's navigation area + *

+ * 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. + *

+ *

+ * Note: 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. + *

+ *

+ * Note: 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. + *

+ */ +public final class AccessibilityButtonController { + private static final String LOG_TAG = "A11yButtonController"; + + private final IAccessibilityServiceConnection mServiceConnection; + private final Object mLock; + private ArrayMap 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. + *

+ * Note: 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}. + *

+ * + * @return {@code true} if the accessibility button in the system's navigation area is + * available to the calling service, {@code false} otherwise + */ + public boolean isAccessibilityButtonAvailable() { + try { + return mServiceConnection.isAccessibilityButtonAvailable(); + } catch (RemoteException re) { + Slog.w(LOG_TAG, "Failed to get accessibility button availability.", re); + re.rethrowFromSystemServer(); + return false; + } + } + + /** + * Registers the provided {@link AccessibilityButtonCallback} for interaction and state + * changes callbacks related to the accessibility button. + * + * @param callback the callback to add, must be non-null + */ + public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) { + registerAccessibilityButtonCallback(callback, null); + } + + /** + * 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 to callback should execute, or {@code null} to + * execute on the service's main thread + */ + public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback, + @Nullable Handler 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) { + 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 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); + if (handler != null) { + handler.post(() -> callback.onClicked(this)); + } else { + // We're already on the main thread, just run the callback. + 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 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); + if (handler != null) { + handler.post(() -> callback.onAvailabilityChanged(this, available)); + } else { + // We're already on the main thread, just run the callback. + 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/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index a036b6a6dfabd..9e486d544195e 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -378,6 +378,8 @@ public abstract class AccessibilityService extends Service { void onPerformGestureResult(int sequence, boolean completedSuccessfully); void onFingerprintCapturingGesturesChanged(boolean active); void onFingerprintGesture(int gesture); + void onAccessibilityButtonClicked(); + void onAccessibilityButtonAvailabilityChanged(boolean available); } /** @@ -400,6 +402,7 @@ public abstract class AccessibilityService extends Service { private MagnificationController mMagnificationController; private SoftKeyboardController mSoftKeyboardController; + private AccessibilityButtonController mAccessibilityButtonController; private int mGestureStatusCallbackSequence; @@ -431,6 +434,9 @@ public abstract class AccessibilityService extends Service { if (mMagnificationController != null) { mMagnificationController.onServiceConnected(); } + if (mSoftKeyboardController != null) { + mSoftKeyboardController.onServiceConnected(); + } // The client gets to handle service connection last, after we've set // up any state upon which their code may rely. @@ -809,12 +815,10 @@ public abstract class AccessibilityService extends Service { } /** - * Removes all instances of the specified change listener from the list - * of magnification change listeners. + * 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 at least one instance of the listener was - * removed + * @return {@code true} if the listener was removed, {@code false} otherwise */ public boolean removeListener(@NonNull OnMagnificationChangedListener listener) { if (mListeners == null) { @@ -1203,11 +1207,11 @@ public abstract class AccessibilityService extends Service { } /** - * Removes all instances of the specified change listener from the list of magnification - * change listeners. + * 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 at least one instance of the listener was removed + * @return {@code true} if the listener was removed, {@code false} otherwise */ public boolean removeOnShowModeChangedListener(@NonNull OnShowModeChangedListener listener) { if (mListeners == null) { @@ -1252,7 +1256,7 @@ public abstract class AccessibilityService extends Service { final ArrayMap entries; synchronized (mLock) { if (mListeners == null || mListeners.isEmpty()) { - Slog.d(LOG_TAG, "Received soft keyboard show mode changed callback" + Slog.w(LOG_TAG, "Received soft keyboard show mode changed callback" + " with no listeners registered!"); setSoftKeyboardCallbackEnabled(false); return; @@ -1308,9 +1312,9 @@ public abstract class AccessibilityService extends Service { * The lastto this method will be honored, regardless of any previous calls (including those * made by other AccessibilityServices). *

- * Note: If the service is not yet conected (e.g. + * Note: 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 hav no effect and return {@code false}. + * 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 @@ -1348,6 +1352,39 @@ public abstract class AccessibilityService extends Service { } } + /** + * 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. + *

+ * Note: 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. + *

+ * + * @return the accessibility button controller for this {@link AccessibilityService} + */ + @NonNull + public final AccessibilityButtonController getAccessibilityButtonController() { + synchronized (mLock) { + if (mAccessibilityButtonController == null) { + mAccessibilityButtonController = new AccessibilityButtonController( + AccessibilityInteractionClient.getInstance().getConnection(mConnectionId)); + } + return mAccessibilityButtonController; + } + } + + private void onAccessibilityButtonClicked() { + getAccessibilityButtonController().dispatchAccessibilityButtonClicked(); + } + + private void onAccessibilityButtonAvailabilityChanged(boolean available) { + getAccessibilityButtonController().dispatchAccessibilityButtonAvailabilityChanged( + available); + } + /** * Performs a global action. Such an action can be performed * at any moment regardless of the current application or user @@ -1543,6 +1580,16 @@ public abstract class AccessibilityService extends Service { public void onFingerprintGesture(int gesture) { AccessibilityService.this.onFingerprintGesture(gesture); } + + @Override + public void onAccessibilityButtonClicked() { + AccessibilityService.this.onAccessibilityButtonClicked(); + } + + @Override + public void onAccessibilityButtonAvailabilityChanged(boolean available) { + AccessibilityService.this.onAccessibilityButtonAvailabilityChanged(available); + } }); } @@ -1565,6 +1612,8 @@ public abstract class AccessibilityService extends Service { private static final int DO_GESTURE_COMPLETE = 9; private static final int DO_ON_FINGERPRINT_ACTIVE_CHANGED = 10; private static final int DO_ON_FINGERPRINT_GESTURE = 11; + private static final int DO_ACCESSIBILITY_BUTTON_CLICKED = 12; + private static final int DO_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED = 13; private final HandlerCaller mCaller; @@ -1645,6 +1694,17 @@ public abstract class AccessibilityService extends Service { mCaller.sendMessage(mCaller.obtainMessageI(DO_ON_FINGERPRINT_GESTURE, gesture)); } + public void onAccessibilityButtonClicked() { + final Message message = mCaller.obtainMessage(DO_ACCESSIBILITY_BUTTON_CLICKED); + mCaller.sendMessage(message); + } + + public void onAccessibilityButtonAvailabilityChanged(boolean available) { + final Message message = mCaller.obtainMessageI( + DO_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED, (available ? 1 : 0)); + mCaller.sendMessage(message); + } + @Override public void executeMessage(Message message) { switch (message.what) { @@ -1750,6 +1810,15 @@ public abstract class AccessibilityService extends Service { mCallback.onFingerprintGesture(message.arg1); } return; + case (DO_ACCESSIBILITY_BUTTON_CLICKED): { + mCallback.onAccessibilityButtonClicked(); + } return; + + case (DO_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED): { + final boolean available = (message.arg1 != 0); + mCallback.onAccessibilityButtonAvailabilityChanged(available); + } return; + default : Log.w(LOG_TAG, "Unknown message type " + message.what); } diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index 18e57cb5694c3..e135ffdf9b132 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -306,6 +306,12 @@ public class AccessibilityServiceInfo implements Parcelable { */ public static final int FLAG_ENABLE_ACCESSIBILITY_VOLUME = 0x00000080; + /** + * This flag indicates to the system that the accessibility service requests that an + * accessibility button be shown within the system's navigation area, if available. + */ + public static final int FLAG_REQUEST_ACCESSIBILITY_BUTTON = 0x00000100; + /** * This flag requests that all fingerprint gestures be sent to the accessibility service. * It is handled in {@link FingerprintGestureController} @@ -396,6 +402,8 @@ public class AccessibilityServiceInfo implements Parcelable { * @see #FLAG_REQUEST_FILTER_KEY_EVENTS * @see #FLAG_REPORT_VIEW_IDS * @see #FLAG_RETRIEVE_INTERACTIVE_WINDOWS + * @see #FLAG_ENABLE_ACCESSIBILITY_VOLUME + * @see #FLAG_REQUEST_ACCESSIBILITY_BUTTON */ public int flags; @@ -631,7 +639,7 @@ public class AccessibilityServiceInfo implements Parcelable { * @see #CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT * @see #CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION * @see #CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY - * @see #CAPABILITY_FILTER_KEY_EVENTS + * @see #CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS * @see #CAPABILITY_CAN_CONTROL_MAGNIFICATION * @see #CAPABILITY_CAN_PERFORM_GESTURES */ @@ -648,7 +656,7 @@ public class AccessibilityServiceInfo implements Parcelable { * @see #CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT * @see #CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION * @see #CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY - * @see #CAPABILITY_FILTER_KEY_EVENTS + * @see #CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS * @see #CAPABILITY_CAN_CONTROL_MAGNIFICATION * @see #CAPABILITY_CAN_PERFORM_GESTURES * @@ -936,6 +944,8 @@ public class AccessibilityServiceInfo implements Parcelable { 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_CAPTURE_FINGERPRINT_GESTURES: return "FLAG_CAPTURE_FINGERPRINT_GESTURES"; default: @@ -960,7 +970,7 @@ public class AccessibilityServiceInfo implements Parcelable { case CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY: return "CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY"; case CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS: - return "CAPABILITY_CAN_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: diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl index 3f778ad0e1482..4e96b8f116289 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl @@ -50,4 +50,8 @@ import android.view.KeyEvent; void onFingerprintCapturingGesturesChanged(boolean capturing); void onFingerprintGesture(int gesture); + + void onAccessibilityButtonClicked(); + + void onAccessibilityButtonAvailabilityChanged(boolean available); } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 5499bd5629016..5bd372208f6fc 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -88,6 +88,8 @@ interface IAccessibilityServiceConnection { void setSoftKeyboardCallbackEnabled(boolean enabled); + boolean isAccessibilityButtonAvailable(); + void sendGesture(int sequence, in ParceledListSlice gestureSteps); boolean isFingerprintGestureDetectionAvailable(); diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 6d1d1a331b612..1d6f42ecdb0b1 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -1118,6 +1118,16 @@ public final class UiAutomation { public void onFingerprintGesture(int gesture) { /* do nothing */ } + + @Override + public void onAccessibilityButtonClicked() { + /* do nothing */ + } + + @Override + public void onAccessibilityButtonAvailabilityChanged(boolean available) { + /* do nothing */ + } }); } } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 1ef0d1799c2cc..45302b6cbdce6 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -779,6 +779,49 @@ public final class AccessibilityManager { } } + /** + * Notifies that the accessibility button in the system's navigation area has been clicked + * + * @hide + */ + public void notifyAccessibilityButtonClicked() { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.notifyAccessibilityButtonClicked(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while dispatching accessibility button click", re); + } + } + + /** + * Notifies that the availability of the accessibility button in the system's navigation area + * has changed. + * + * @param available {@code true} if the accessibility button is available within the system + * navigation area, {@code false} otherwise + * @hide + */ + public void notifyAccessibilityButtonAvailabilityChanged(boolean available) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.notifyAccessibilityButtonAvailabilityChanged(available); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while dispatching accessibility button availability change", re); + } + } + private IAccessibilityManager getServiceLocked() { if (mService == null) { tryConnectToServiceLocked(null); diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 136bbbe39c75c..8fde47a30b2b7 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -59,6 +59,10 @@ interface IAccessibilityManager { IBinder getWindowToken(int windowId, int userId); + void notifyAccessibilityButtonClicked(); + + void notifyAccessibilityButtonAvailabilityChanged(boolean available); + // Requires WRITE_SECURE_SETTINGS void performAccessibilityShortcut(); diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index c548219b52066..7f49f056d3bfa 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -3392,6 +3392,8 @@ + + @@ -3429,18 +3431,9 @@ - + Menu + + Accessibility Overview diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java index 62b536e85f859..808cd21088296 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java @@ -23,6 +23,7 @@ import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRAN import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_WINDOW_STATE; import static com.android.systemui.statusbar.phone.StatusBar.dumpBarTransitions; +import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManagerNative; @@ -79,6 +80,7 @@ import com.android.systemui.statusbar.stack.StackStateAnimator; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.List; import java.util.Locale; /** @@ -101,7 +103,7 @@ public class NavigationBarFragment extends Fragment implements Callbacks { private int mNavigationIconHints = 0; private int mNavigationBarMode; - protected AccessibilityManager mAccessibilityManager; + private AccessibilityManager mAccessibilityManager; private int mDisabledFlags1; private StatusBar mStatusBar; @@ -132,6 +134,8 @@ public class NavigationBarFragment extends Fragment implements Callbacks { mDivider = SysUiServiceProvider.getComponent(getContext(), Divider.class); mWindowManager = getContext().getSystemService(WindowManager.class); mAccessibilityManager = getContext().getSystemService(AccessibilityManager.class); + mAccessibilityManager.addAccessibilityServicesStateChangeListener( + this::updateAccessibilityServicesState); if (savedInstanceState != null) { mDisabledFlags1 = savedInstanceState.getInt(EXTRA_DISABLE_STATE, 0); } @@ -149,6 +153,8 @@ public class NavigationBarFragment extends Fragment implements Callbacks { public void onDestroy() { super.onDestroy(); mCommandQueue.removeCallbacks(this); + mAccessibilityManager.removeAccessibilityServicesStateChangeListener( + this::updateAccessibilityServicesState); try { WindowManagerGlobal.getWindowManagerService() .removeRotationWatcher(mRotationWatcher); @@ -402,6 +408,10 @@ public class NavigationBarFragment extends Fragment implements Callbacks { ButtonDispatcher homeButton = mNavigationBarView.getHomeButton(); homeButton.setOnTouchListener(this::onHomeTouch); homeButton.setOnLongClickListener(this::onHomeLongClick); + + ButtonDispatcher accessibilityButton = mNavigationBarView.getAccessibilityButton(); + accessibilityButton.setOnClickListener(this::onAccessibilityClick); + accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick); } private boolean onHomeTouch(View v, MotionEvent event) { @@ -553,6 +563,34 @@ public class NavigationBarFragment extends Fragment implements Callbacks { MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS); } + private void onAccessibilityClick(View v) { + mAccessibilityManager.notifyAccessibilityButtonClicked(); + } + + private boolean onAccessibilityLongClick(View v) { + // TODO(b/34720082): Target service selection via long click + android.widget.Toast.makeText(getContext(), "Service selection coming soon...", + android.widget.Toast.LENGTH_LONG).show(); + return true; + } + + private void updateAccessibilityServicesState() { + final List services = + mAccessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + int requestingServices = 0; + for (int i = services.size() - 1; i >= 0; --i) { + AccessibilityServiceInfo info = services.get(i); + if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) { + requestingServices++; + } + } + + final boolean showAccessibilityButton = requestingServices >= 1; + final boolean targetSelection = requestingServices >= 2; + mNavigationBarView.setAccessibilityButtonState(showAccessibilityButton, targetSelection); + } + // ----- Methods that StatusBar talks to (should be minimized) ----- public void setLightBarController(LightBarController lightBarController) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index dced747968aa6..5d13289ef9ccc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -78,6 +78,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener