diff options
29 files changed, 710 insertions, 47 deletions
diff --git a/api/current.txt b/api/current.txt index e315958badeb..18ece9148fc7 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 ae145dc9b037..2b9f931374b0 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 1e3b6db92185..f19d959d777d 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 000000000000..c3a5daba4cfc --- /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 + * <p> + * This class may be used to query the accessibility button's state and register + * callbacks for interactions with and state changes to the accessibility button when + * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} is set. + * </p> + * <p> + * <strong>Note:</strong> This class and + * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} should not be used as + * the sole means for offering functionality to users via an {@link AccessibilityService}. + * Some device implementations may choose not to provide a software-rendered system + * navigation area, making this affordance permanently unavailable. + * </p> + * <p> + * <strong>Note:</strong> On device implementations where the accessibility button is + * supported, it may not be available at all times, such as when a foreground application uses + * {@link android.view.View#SYSTEM_UI_FLAG_HIDE_NAVIGATION}. A user may also choose to assign + * this button to another accessibility service or feature. In each of these cases, a + * registered {@link AccessibilityButtonCallback}'s + * {@link AccessibilityButtonCallback#onAvailabilityChanged(AccessibilityButtonController, boolean)} + * method will be invoked to provide notifications of changes in the accessibility button's + * availability to the registering service. + * </p> + */ +public final class AccessibilityButtonController { + private static final String LOG_TAG = "A11yButtonController"; + + private final IAccessibilityServiceConnection mServiceConnection; + private final Object mLock; + private ArrayMap<AccessibilityButtonCallback, Handler> mCallbacks; + + AccessibilityButtonController(@NonNull IAccessibilityServiceConnection serviceConnection) { + mServiceConnection = serviceConnection; + mLock = new Object(); + } + + /** + * Retrieves whether the accessibility button in the system's navigation area is + * available to the calling service. + * <p> + * <strong>Note:</strong> If the service is not yet connected (e.g. + * {@link AccessibilityService#onServiceConnected()} has not yet been called) or the + * service has been disconnected, this method will have no effect and return {@code false}. + * </p> + * + * @return {@code true} if the accessibility button in the system's navigation area is + * available to the calling service, {@code false} otherwise + */ + public boolean isAccessibilityButtonAvailable() { + try { + return mServiceConnection.isAccessibilityButtonAvailable(); + } catch (RemoteException re) { + Slog.w(LOG_TAG, "Failed to get accessibility button availability.", re); + re.rethrowFromSystemServer(); + return false; + } + } + + /** + * Registers the provided {@link AccessibilityButtonCallback} for interaction and state + * changes callbacks related to the accessibility button. + * + * @param callback the callback to add, must be non-null + */ + public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) { + registerAccessibilityButtonCallback(callback, 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<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); + 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<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); + 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 a036b6a6dfab..9e486d544195 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<OnShowModeChangedListener, Handler> 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). * <p> - * <strong>Note:</strong> If the service is not yet conected (e.g. + * <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 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 @@ -1349,6 +1353,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. + * <p> + * <strong>Note:</strong> Not all devices are capable of displaying the accessibility button + * within a navigation area, and as such, use of this class should be considered only as an + * optional feature or shortcut on supported device implementations. + * </p> + * + * @return the accessibility button controller for this {@link AccessibilityService} + */ + @NonNull + public final AccessibilityButtonController getAccessibilityButtonController() { + synchronized (mLock) { + if (mAccessibilityButtonController == null) { + mAccessibilityButtonController = new AccessibilityButtonController( + AccessibilityInteractionClient.getInstance().getConnection(mConnectionId)); + } + return mAccessibilityButtonController; + } + } + + private void onAccessibilityButtonClicked() { + getAccessibilityButtonController().dispatchAccessibilityButtonClicked(); + } + + private void onAccessibilityButtonAvailabilityChanged(boolean available) { + getAccessibilityButtonController().dispatchAccessibilityButtonAvailabilityChanged( + available); + } + + /** * Performs a global action. Such an action can be performed * at any moment regardless of the current application or user * location in that application. For example going back, going @@ -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 18e57cb5694c..e135ffdf9b13 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 3f778ad0e148..4e96b8f11628 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 5499bd562901..5bd372208f6f 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 6d1d1a331b61..1d6f42ecdb0b 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 1ef0d1799c2c..45302b6cbdce 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 136bbbe39c75..8fde47a30b2b 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 c548219b5206..7f49f056d3bf 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -3392,6 +3392,8 @@ <flag name="flagRetrieveInteractiveWindows" value="0x00000040" /> <!-- Has flag {@link android.accessibilityservice.AccessibilityServiceInfo#FLAG_ENABLE_ACCESSIBILITY_VOLUME} --> <flag name="flagEnableAccessibilityVolume" value="0x00000080" /> + <!-- Has flag {@link android.accessibilityservice.AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} --> + <flag name="flagRequestAccessibilityButton" value="0x00000100" /> <!-- Has flag {@link android.accessibilityservice.AccessibilityServiceInfo#FLAG_CAPTURE_FINGERPRINT_GESTURES} --> <flag name="flagCaptureFingerprintGestures" value="0x00000200" /> </attr> @@ -3429,18 +3431,9 @@ <attr name="canRequestFilterKeyEvents" format="boolean" /> <!-- Attribute whether the accessibility service wants to be able to control display magnification. - <p> - Required to allow setting the {@link android.accessibilityservice - #AccessibilityServiceInfo#FLAG_CAN_CONTROL_MAGNIFICATION} flag. - </p> --> <attr name="canControlMagnification" format="boolean" /> - <!-- Attribute whether the accessibility service wants to be able to perform gestures. - <p> - Required to allow setting the {@link android.accessibilityservice - #AccessibilityServiceInfo#FLAG_CAN_PERFORM_GESTURES} flag. - </p> - --> + <!-- Attribute whether the accessibility service wants to be able to perform gestures. --> <attr name="canPerformGestures" format="boolean" /> <!-- Attribute whether the accessibility service wants to be able to capture gestures from the fingerprint sensor. diff --git a/packages/SystemUI/res/drawable-hdpi/ic_sysbar_accessibility_button.png b/packages/SystemUI/res/drawable-hdpi/ic_sysbar_accessibility_button.png Binary files differnew file mode 100644 index 000000000000..0615668b6465 --- /dev/null +++ b/packages/SystemUI/res/drawable-hdpi/ic_sysbar_accessibility_button.png diff --git a/packages/SystemUI/res/drawable-hdpi/ic_sysbar_accessibility_button_dark.png b/packages/SystemUI/res/drawable-hdpi/ic_sysbar_accessibility_button_dark.png Binary files differnew file mode 100644 index 000000000000..839e5edeead3 --- /dev/null +++ b/packages/SystemUI/res/drawable-hdpi/ic_sysbar_accessibility_button_dark.png diff --git a/packages/SystemUI/res/drawable-mdpi/ic_sysbar_accessibility_button.png b/packages/SystemUI/res/drawable-mdpi/ic_sysbar_accessibility_button.png Binary files differnew file mode 100644 index 000000000000..1480c865e2dd --- /dev/null +++ b/packages/SystemUI/res/drawable-mdpi/ic_sysbar_accessibility_button.png diff --git a/packages/SystemUI/res/drawable-mdpi/ic_sysbar_accessibility_button_dark.png b/packages/SystemUI/res/drawable-mdpi/ic_sysbar_accessibility_button_dark.png Binary files differnew file mode 100644 index 000000000000..66e11fb52b30 --- /dev/null +++ b/packages/SystemUI/res/drawable-mdpi/ic_sysbar_accessibility_button_dark.png diff --git a/packages/SystemUI/res/drawable-xhdpi/ic_sysbar_accessibility_button.png b/packages/SystemUI/res/drawable-xhdpi/ic_sysbar_accessibility_button.png Binary files differnew file mode 100644 index 000000000000..d2fe0c32cad3 --- /dev/null +++ b/packages/SystemUI/res/drawable-xhdpi/ic_sysbar_accessibility_button.png diff --git a/packages/SystemUI/res/drawable-xhdpi/ic_sysbar_accessibility_button_dark.png b/packages/SystemUI/res/drawable-xhdpi/ic_sysbar_accessibility_button_dark.png Binary files differnew file mode 100644 index 000000000000..5923269a882b --- /dev/null +++ b/packages/SystemUI/res/drawable-xhdpi/ic_sysbar_accessibility_button_dark.png diff --git a/packages/SystemUI/res/drawable-xxhdpi/ic_sysbar_accessibility_button.png b/packages/SystemUI/res/drawable-xxhdpi/ic_sysbar_accessibility_button.png Binary files differnew file mode 100644 index 000000000000..d0196e5f8ea3 --- /dev/null +++ b/packages/SystemUI/res/drawable-xxhdpi/ic_sysbar_accessibility_button.png diff --git a/packages/SystemUI/res/drawable-xxhdpi/ic_sysbar_accessibility_button_dark.png b/packages/SystemUI/res/drawable-xxhdpi/ic_sysbar_accessibility_button_dark.png Binary files differnew file mode 100644 index 000000000000..d3a2b393e685 --- /dev/null +++ b/packages/SystemUI/res/drawable-xxhdpi/ic_sysbar_accessibility_button_dark.png diff --git a/packages/SystemUI/res/drawable-xxxhdpi/ic_sysbar_accessibility_button.png b/packages/SystemUI/res/drawable-xxxhdpi/ic_sysbar_accessibility_button.png Binary files differnew file mode 100644 index 000000000000..726643ca1bb1 --- /dev/null +++ b/packages/SystemUI/res/drawable-xxxhdpi/ic_sysbar_accessibility_button.png diff --git a/packages/SystemUI/res/drawable-xxxhdpi/ic_sysbar_accessibility_button_dark.png b/packages/SystemUI/res/drawable-xxxhdpi/ic_sysbar_accessibility_button_dark.png Binary files differnew file mode 100644 index 000000000000..31078f6aa42c --- /dev/null +++ b/packages/SystemUI/res/drawable-xxxhdpi/ic_sysbar_accessibility_button_dark.png diff --git a/packages/SystemUI/res/layout/menu_ime.xml b/packages/SystemUI/res/layout/menu_ime.xml index 5e85ba06beea..6bd79a4b4ae4 100644 --- a/packages/SystemUI/res/layout/menu_ime.xml +++ b/packages/SystemUI/res/layout/menu_ime.xml @@ -39,4 +39,13 @@ android:contentDescription="@string/accessibility_ime_switch_button" android:scaleType="centerInside" /> + <com.android.systemui.statusbar.policy.KeyButtonView + android:id="@+id/accessibility_button" + android:layout_width="@dimen/navigation_extra_key_width" + android:layout_height="match_parent" + android:layout_marginEnd="2dp" + android:visibility="invisible" + android:contentDescription="@string/accessibility_accessibility_button" + android:scaleType="centerInside" + /> </FrameLayout> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 363b3e20449e..2cc2621f7c5c 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -207,6 +207,8 @@ <string name="accessibility_home">Home</string> <!-- Content description of the menu button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="accessibility_menu">Menu</string> + <!-- Content description of the accessibility button in the navigation bar (not shown on the screen). [CHAR LIMIT=NONE] --> + <string name="accessibility_accessibility_button">Accessibility</string> <!-- Content description of the recents button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="accessibility_recent">Overview</string> <!-- Content description of the search button for accessibility. [CHAR LIMIT=NONE] --> 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 62b536e85f85..808cd2108829 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<AccessibilityServiceInfo> 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 dced747968aa..5d13289ef9cc 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<Nav private int mCurrentRotation = -1; boolean mShowMenu; + boolean mShowAccessibilityButton; + boolean mLongClickableAccessibilityButton; int mDisabledFlags = 0; int mNavigationIconHints = 0; @@ -89,6 +91,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav private KeyButtonDrawable mDockedIcon; private KeyButtonDrawable mImeIcon; private KeyButtonDrawable mMenuIcon; + private KeyButtonDrawable mAccessibilityIcon; private GestureHelper mGestureHelper; private DeadZone mDeadZone; @@ -202,6 +205,9 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mVertical = false; mShowMenu = false; + mShowAccessibilityButton = false; + mLongClickableAccessibilityButton = false; + mConfiguration = new Configuration(); mConfiguration.updateFrom(context.getResources().getConfiguration()); updateIcons(context, Configuration.EMPTY, mConfiguration); @@ -213,6 +219,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps)); mButtonDispatchers.put(R.id.menu, new ButtonDispatcher(R.id.menu)); mButtonDispatchers.put(R.id.ime_switcher, new ButtonDispatcher(R.id.ime_switcher)); + mButtonDispatchers.put(R.id.accessibility_button, + new ButtonDispatcher(R.id.accessibility_button)); } public BarTransitions getBarTransitions() { @@ -287,6 +295,10 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav return mButtonDispatchers.get(R.id.ime_switcher); } + public ButtonDispatcher getAccessibilityButton() { + return mButtonDispatchers.get(R.id.accessibility_button); + } + public SparseArray<ButtonDispatcher> getButtonDispatchers() { return mButtonDispatchers; } @@ -320,6 +332,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mRecentIcon = getDrawable(ctx, R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_dark); mMenuIcon = getDrawable(ctx, R.drawable.ic_sysbar_menu, R.drawable.ic_sysbar_menu_dark); + mAccessibilityIcon = getDrawable(ctx, R.drawable.ic_sysbar_accessibility_button, + R.drawable.ic_sysbar_accessibility_button_dark); Context darkContext = new ContextThemeWrapper(ctx, R.style.DualToneDarkTheme); Context lightContext = new ContextThemeWrapper(ctx, R.style.DualToneLightTheme); @@ -411,6 +425,9 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav setMenuVisibility(mShowMenu, true); getMenuButton().setImageDrawable(mMenuIcon); + setAccessibilityButtonState(mShowAccessibilityButton, mLongClickableAccessibilityButton); + getAccessibilityButton().setImageDrawable(mAccessibilityIcon); + setDisabledFlags(mDisabledFlags, true); mBarTransitions.reapplyDarkIntensity(); @@ -517,13 +534,25 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mShowMenu = show; - // Only show Menu if IME switcher not shown. - final boolean shouldShow = mShowMenu && + // Only show Menu if IME switcher and Accessibility button not shown. + final boolean shouldShow = mShowMenu && !mShowAccessibilityButton && ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) == 0); getMenuButton().setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE); } + public void setAccessibilityButtonState(final boolean visible, final boolean longClickable) { + mShowAccessibilityButton = visible; + mLongClickableAccessibilityButton = longClickable; + if (visible) { + // Accessibility button overrides Menu button. + setMenuVisibility(false, true); + } + + getAccessibilityButton().setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + getAccessibilityButton().setLongClickable(longClickable); + } + @Override public void onFinishInflate() { mNavigationInflaterView = (NavigationBarInflaterView) findViewById( @@ -814,6 +843,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav dumpButton(pw, "home", getHomeButton()); dumpButton(pw, "rcnt", getRecentsButton()); dumpButton(pw, "menu", getMenuButton()); + dumpButton(pw, "a11y", getAccessibilityButton()); pw.println(" }"); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index a861522c0576..987e42665908 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -445,7 +445,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } return userState.getClientState(); } else { - userState.mClients.register(client); + userState.mUserClients.register(client); // If this client is not for the current user we do not // return a state since it is not for the foreground user. // We will send the state to the client on a user switch. @@ -813,6 +813,42 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + /** + * Invoked remotely over AIDL by SysUi when the accessibility button within the system's + * navigation area has been clicked. + */ + @Override + public void notifyAccessibilityButtonClicked() { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.STATUS_BAR) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Caller does not hold permission " + + android.Manifest.permission.STATUS_BAR); + } + synchronized (mLock) { + notifyAccessibilityButtonClickedLocked(); + } + } + + /** + * Invoked remotely over AIDL by SysUi when the availability of the accessibility + * button within the system's navigation area has changed. + * + * @param available {@code true} if the accessibility button is available to the + * user, {@code false} otherwise + */ + @Override + public void notifyAccessibilityButtonAvailabilityChanged(boolean available) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.STATUS_BAR) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Caller does not hold permission " + + android.Manifest.permission.STATUS_BAR); + } + synchronized (mLock) { + notifyAccessibilityButtonAvailabilityChangedLocked(available); + } + } + + boolean onGesture(int gestureId) { synchronized (mLock) { boolean handled = notifyGestureLocked(gestureId, false); @@ -927,7 +963,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { oldUserState.onSwitchToAnotherUser(); // Disable the local managers for the old user. - if (oldUserState.mClients.getRegisteredCallbackCount() > 0) { + if (oldUserState.mUserClients.getRegisteredCallbackCount() > 0) { mMainHandler.obtainMessage(MainHandler.MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER, oldUserState.mUserId, 0).sendToTarget(); } @@ -1044,6 +1080,28 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + private void notifyAccessibilityButtonClickedLocked() { + final UserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + final Service service = state.mBoundServices.get(i); + // TODO(b/34720082): Only notify a single user-defined service + if (service.mRequestAccessibilityButton) { + service.notifyAccessibilityButtonClickedLocked(); + } + } + } + + private void notifyAccessibilityButtonAvailabilityChangedLocked(boolean available) { + final UserState state = getCurrentUserStateLocked(); + state.mIsAccessibilityButtonAvailable = available; + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + final Service service = state.mBoundServices.get(i); + if (service.mRequestAccessibilityButton) { + service.notifyAccessibilityButtonAvailabilityChangedLocked(available); + } + } + } + /** * Removes an AccessibilityInteractionConnection. * @@ -1178,7 +1236,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { service.onAdded(); userState.mBoundServices.add(service); userState.mComponentNameToServiceMap.put(service.mComponentName, service); - scheduleNotifyClientsOfServicesStateChange(); + scheduleNotifyClientsOfServicesStateChange(userState); } } catch (RemoteException re) { /* do nothing */ @@ -1200,7 +1258,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { Service boundService = userState.mBoundServices.get(i); userState.mComponentNameToServiceMap.put(boundService.mComponentName, boundService); } - scheduleNotifyClientsOfServicesStateChange(); + scheduleNotifyClientsOfServicesStateChange(userState); } /** @@ -1362,16 +1420,16 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { final int clientState = userState.getClientState(); if (userState.mLastSentClientState != clientState && (mGlobalClients.getRegisteredCallbackCount() > 0 - || userState.mClients.getRegisteredCallbackCount() > 0)) { + || userState.mUserClients.getRegisteredCallbackCount() > 0)) { userState.mLastSentClientState = clientState; mMainHandler.obtainMessage(MainHandler.MSG_SEND_STATE_TO_CLIENTS, clientState, userState.mUserId).sendToTarget(); } } - private void scheduleNotifyClientsOfServicesStateChange() { - mMainHandler.obtainMessage(MainHandler.MSG_SEND_SERVICES_STATE_CHANGED_TO_CLIENTS) - .sendToTarget(); + private void scheduleNotifyClientsOfServicesStateChange(UserState userState) { + mMainHandler.obtainMessage(MainHandler.MSG_SEND_SERVICES_STATE_CHANGED_TO_CLIENTS, + userState.mUserId).sendToTarget(); } private void scheduleUpdateInputFilter(UserState userState) { @@ -2225,12 +2283,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { final int clientState = msg.arg1; final int userId = msg.arg2; sendStateToClients(clientState, mGlobalClients); - sendStateToClientsForUser(clientState, userId); + sendStateToClients(clientState, getUserClientsForId(userId)); } break; case MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER: { final int userId = msg.arg1; - sendStateToClientsForUser(0, userId); + sendStateToClients(0, getUserClientsForId(userId)); } break; case MSG_ANNOUNCE_NEW_USER_IF_NEEDED: { @@ -2257,7 +2315,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } break; case MSG_SEND_SERVICES_STATE_CHANGED_TO_CLIENTS: { - notifyClientsOfServicesStateChange(); + final int userId = msg.arg1; + notifyClientsOfServicesStateChange(mGlobalClients); + notifyClientsOfServicesStateChange(getUserClientsForId(userId)); } break; case MSG_UPDATE_FINGERPRINT: { @@ -2282,12 +2342,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } - private void sendStateToClientsForUser(int clientState, int userId) { + private RemoteCallbackList<IAccessibilityManagerClient> getUserClientsForId(int userId) { final UserState userState; synchronized (mLock) { userState = getUserStateLocked(userId); } - sendStateToClients(clientState, userState.mClients); + return userState.mUserClients; } private void sendStateToClients(int clientState, @@ -2307,11 +2367,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } - private void notifyClientsOfServicesStateChange() { - RemoteCallbackList<IAccessibilityManagerClient> clients; - synchronized (mLock) { - clients = getCurrentUserStateLocked().mClients; - } + private void notifyClientsOfServicesStateChange( + RemoteCallbackList<IAccessibilityManagerClient> clients) { try { final int userClientCount = clients.beginBroadcast(); for (int i = 0; i < userClientCount; i++) { @@ -2426,6 +2483,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { boolean mCaptureFingerprintGestures; + boolean mRequestAccessibilityButton; + int mFetchFlags; long mNotificationTimeout; @@ -2581,6 +2640,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { & AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS) != 0; mCaptureFingerprintGestures = (info.flags & AccessibilityServiceInfo.FLAG_CAPTURE_FINGERPRINT_GESTURES) != 0; + mRequestAccessibilityButton = (info.flags + & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; } /** @@ -2694,7 +2755,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } UserState userState = getUserStateLocked(mUserId); onUserStateChangedLocked(userState); - scheduleNotifyClientsOfServicesStateChange(); + scheduleNotifyClientsOfServicesStateChange(userState); } } finally { Binder.restoreCallingIdentity(identity); @@ -3332,6 +3393,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } @Override + public boolean isAccessibilityButtonAvailable() { + final UserState userState; + synchronized (mLock) { + if (!isCalledForCurrentUserLocked()) { + return false; + } + userState = getCurrentUserStateLocked(); + } + + return mRequestAccessibilityButton && userState.mIsAccessibilityButtonAvailable; + } + + @Override public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP); synchronized (mLock) { @@ -3544,6 +3618,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { mInvocationHandler.notifySoftKeyboardShowModeChangedLocked(showState); } + public void notifyAccessibilityButtonClickedLocked() { + mInvocationHandler.notifyAccessibilityButtonClickedLocked(); + } + + public void notifyAccessibilityButtonAvailabilityChangedLocked(boolean available) { + mInvocationHandler.notifyAccessibilityButtonAvailabilityChangedLocked(available); + } + /** * Called by the invocation handler to notify the service that the * state of magnification has changed. @@ -3582,6 +3664,36 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + private void notifyAccessibilityButtonClickedInternal() { + final IAccessibilityServiceClient listener; + synchronized (mLock) { + listener = mServiceInterface; + } + if (listener != null) { + try { + listener.onAccessibilityButtonClicked(); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error sending accessibility button click to " + mService, re); + } + } + } + + private void notifyAccessibilityButtonAvailabilityChangedInternal(boolean available) { + final IAccessibilityServiceClient listener; + synchronized (mLock) { + listener = mServiceInterface; + } + if (listener != null) { + try { + listener.onAccessibilityButtonAvailabilityChanged(available); + } catch (RemoteException re) { + Slog.e(LOG_TAG, + "Error sending accessibility button availability change to " + mService, + re); + } + } + } + private void notifyGestureInternal(int gestureId) { final IAccessibilityServiceClient listener; synchronized (mLock) { @@ -3723,6 +3835,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { private static final int MSG_ON_MAGNIFICATION_CHANGED = 5; private static final int MSG_ON_SOFT_KEYBOARD_STATE_CHANGED = 6; + private static final int MSG_ON_ACCESSIBILITY_BUTTON_CLICKED = 7; + private static final int MSG_ON_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED = 8; private boolean mIsMagnificationCallbackEnabled = false; private boolean mIsSoftKeyboardCallbackEnabled = false; @@ -3758,6 +3872,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { notifySoftKeyboardShowModeChangedInternal(showState); } break; + case MSG_ON_ACCESSIBILITY_BUTTON_CLICKED: { + notifyAccessibilityButtonClickedInternal(); + } break; + + case MSG_ON_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED: { + final boolean available = (message.arg1 != 0); + notifyAccessibilityButtonAvailabilityChangedInternal(available); + } break; + default: { throw new IllegalArgumentException("Unknown message: " + type); } @@ -3797,6 +3920,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { public void setSoftKeyboardCallbackEnabled(boolean enabled) { mIsSoftKeyboardCallbackEnabled = enabled; } + + public void notifyAccessibilityButtonClickedLocked() { + final Message msg = obtainMessage(MSG_ON_ACCESSIBILITY_BUTTON_CLICKED); + msg.sendToTarget(); + } + + public void notifyAccessibilityButtonAvailabilityChangedLocked(boolean available) { + final Message msg = obtainMessage(MSG_ON_ACCESSIBILITY_BUTTON_AVAILABILITY_CHANGED, + (available ? 1 : 0), 0); + msg.sendToTarget(); + } } } @@ -4467,7 +4601,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { // Non-transient state. - public final RemoteCallbackList<IAccessibilityManagerClient> mClients = + public final RemoteCallbackList<IAccessibilityManagerClient> mUserClients = new RemoteCallbackList<>(); public final SparseArray<AccessibilityConnectionWrapper> mInteractionConnections = @@ -4501,6 +4635,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { public int mSoftKeyboardShowMode = 0; + public boolean mIsAccessibilityButtonAvailable; + public boolean mIsTouchExplorationEnabled; public boolean mIsTextHighContrastEnabled; public boolean mIsEnhancedWebAccessibilityEnabled; @@ -4575,6 +4711,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { mIsDisplayMagnificationEnabled = false; mIsAutoclickEnabled = false; mSoftKeyboardShowMode = 0; + + // Clear state tracked from system UI + mIsAccessibilityButtonAvailable = false; } public void destroyUiAutomationService() { diff --git a/services/core/java/com/android/server/policy/BarController.java b/services/core/java/com/android/server/policy/BarController.java index 6edb4d2d2ac1..7a2808146f62 100644 --- a/services/core/java/com/android/server/policy/BarController.java +++ b/services/core/java/com/android/server/policy/BarController.java @@ -18,6 +18,7 @@ package com.android.server.policy; import android.app.StatusBarManager; import android.os.Handler; +import android.os.Message; import android.os.SystemClock; import android.util.Slog; import android.view.View; @@ -43,6 +44,8 @@ public class BarController { private static final int TRANSLUCENT_ANIMATION_DELAY_MS = 1000; + private static final int MSG_NAV_BAR_VISIBILITY_CHANGED = 1; + protected final String mTag; private final int mTransientFlag; private final int mUnhideFlag; @@ -63,6 +66,8 @@ public class BarController { private boolean mSetUnHideFlagWhenNextTransparent; private boolean mNoAnimationOnNextShow; + private OnBarVisibilityChangedListener mVisibilityChangeListener; + public BarController(String tag, int transientFlag, int unhideFlag, int translucentFlag, int statusBarManagerId, int translucentWmFlag, int transparentFlag) { mTag = "BarController." + tag; @@ -72,7 +77,7 @@ public class BarController { mStatusBarManagerId = statusBarManagerId; mTranslucentWmFlag = translucentWmFlag; mTransparentFlag = transparentFlag; - mHandler = new Handler(); + mHandler = new BarHandler(); } public void setWindow(WindowState win) { @@ -153,9 +158,18 @@ public class BarController { mNoAnimationOnNextShow = false; final int state = computeStateLw(wasVis, wasAnim, mWin, change); final boolean stateChanged = updateStateLw(state); + + if (change && (mVisibilityChangeListener != null)) { + mHandler.obtainMessage(MSG_NAV_BAR_VISIBILITY_CHANGED, show ? 1 : 0, 0).sendToTarget(); + } + return change || stateChanged; } + void setOnBarVisibilityChangedListener(OnBarVisibilityChangedListener listener) { + mVisibilityChangeListener = listener; + } + protected boolean skipAnimation() { return false; } @@ -300,4 +314,22 @@ public class BarController { pw.println(transientBarStateToString(mTransientBarState)); } } + + private class BarHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_NAV_BAR_VISIBILITY_CHANGED: + final boolean visible = msg.arg1 != 0; + if (mVisibilityChangeListener != null) { + mVisibilityChangeListener.onBarVisibilityChanged(visible); + } + break; + } + } + } + + interface OnBarVisibilityChangedListener { + void onBarVisibilityChanged(boolean visible); + } } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 180f6c9ae963..c795676eddcf 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -990,6 +990,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, View.NAVIGATION_BAR_TRANSPARENT); + private final BarController.OnBarVisibilityChangedListener mNavBarVisibilityListener = + new BarController.OnBarVisibilityChangedListener() { + @Override + public void onBarVisibilityChanged(boolean visible) { + mAccessibilityManager.notifyAccessibilityButtonAvailabilityChanged(visible); + } + }; + private ImmersiveModeConfirmation mImmersiveModeConfirmation; private SystemGesturesPointerEventListener mSystemGestures; @@ -2900,6 +2908,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } mNavigationBar = win; mNavigationBarController.setWindow(win); + mNavigationBarController.setOnBarVisibilityChangedListener( + mNavBarVisibilityListener); if (DEBUG_LAYOUT) Slog.i(TAG, "NAVIGATION BAR: " + mNavigationBar); break; case TYPE_NAVIGATION_BAR_PANEL: |