| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.accessibilityservice; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.util.ArrayMap; |
| import android.util.Slog; |
| |
| import com.android.internal.util.Preconditions; |
| |
| /** |
| * Controller for the accessibility button within the system's navigation area |
| * <p> |
| * This class may be used to query the accessibility button's state and register |
| * callbacks for interactions with and state changes to the accessibility button when |
| * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} is set. |
| * </p> |
| * <p> |
| * <strong>Note:</strong> This class and |
| * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} should not be used as |
| * the sole means for offering functionality to users via an {@link AccessibilityService}. |
| * Some device implementations may choose not to provide a software-rendered system |
| * navigation area, making this affordance permanently unavailable. |
| * </p> |
| * <p> |
| * <strong>Note:</strong> On device implementations where the accessibility button is |
| * supported, it may not be available at all times, such as when a foreground application uses |
| * {@link android.view.View#SYSTEM_UI_FLAG_HIDE_NAVIGATION}. A user may also choose to assign |
| * this button to another accessibility service or feature. In each of these cases, a |
| * registered {@link AccessibilityButtonCallback}'s |
| * {@link AccessibilityButtonCallback#onAvailabilityChanged(AccessibilityButtonController, boolean)} |
| * method will be invoked to provide notifications of changes in the accessibility button's |
| * availability to the registering service. |
| * </p> |
| */ |
| public final class AccessibilityButtonController { |
| private static final String LOG_TAG = "A11yButtonController"; |
| |
| private final IAccessibilityServiceConnection mServiceConnection; |
| private final Object mLock; |
| private ArrayMap<AccessibilityButtonCallback, Handler> mCallbacks; |
| |
| AccessibilityButtonController(@NonNull IAccessibilityServiceConnection serviceConnection) { |
| mServiceConnection = serviceConnection; |
| mLock = new Object(); |
| } |
| |
| /** |
| * Retrieves whether the accessibility button in the system's navigation area is |
| * available to the calling service. |
| * <p> |
| * <strong>Note:</strong> If the service is not yet connected (e.g. |
| * {@link AccessibilityService#onServiceConnected()} has not yet been called) or the |
| * service has been disconnected, this method will have no effect and return {@code false}. |
| * </p> |
| * |
| * @return {@code true} if the accessibility button in the system's navigation area is |
| * available to the calling service, {@code false} otherwise |
| */ |
| public boolean isAccessibilityButtonAvailable() { |
| try { |
| return mServiceConnection.isAccessibilityButtonAvailable(); |
| } catch (RemoteException re) { |
| Slog.w(LOG_TAG, "Failed to get accessibility button availability.", re); |
| re.rethrowFromSystemServer(); |
| return false; |
| } |
| } |
| |
| /** |
| * Registers the provided {@link AccessibilityButtonCallback} for interaction and state |
| * changes callbacks related to the accessibility button. |
| * |
| * @param callback the callback to add, must be non-null |
| */ |
| public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) { |
| registerAccessibilityButtonCallback(callback, new Handler(Looper.getMainLooper())); |
| } |
| |
| /** |
| * Registers the provided {@link AccessibilityButtonCallback} for interaction and state |
| * change callbacks related to the accessibility button. The callback will occur on the |
| * specified {@link Handler}'s thread, or on the services's main thread if the handler is |
| * {@code null}. |
| * |
| * @param callback the callback to add, must be non-null |
| * @param handler the handler on which the callback should execute, must be non-null |
| */ |
| public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback, |
| @NonNull Handler handler) { |
| Preconditions.checkNotNull(callback); |
| Preconditions.checkNotNull(handler); |
| synchronized (mLock) { |
| if (mCallbacks == null) { |
| mCallbacks = new ArrayMap<>(); |
| } |
| |
| mCallbacks.put(callback, handler); |
| } |
| } |
| |
| /** |
| * Unregisters the provided {@link AccessibilityButtonCallback} for interaction and state |
| * change callbacks related to the accessibility button. |
| * |
| * @param callback the callback to remove, must be non-null |
| */ |
| public void unregisterAccessibilityButtonCallback( |
| @NonNull AccessibilityButtonCallback callback) { |
| Preconditions.checkNotNull(callback); |
| synchronized (mLock) { |
| if (mCallbacks == null) { |
| return; |
| } |
| |
| final int keyIndex = mCallbacks.indexOfKey(callback); |
| final boolean hasKey = keyIndex >= 0; |
| if (hasKey) { |
| mCallbacks.removeAt(keyIndex); |
| } |
| } |
| } |
| |
| /** |
| * Dispatches the accessibility button click to any registered callbacks. This should |
| * be called on the service's main thread. |
| */ |
| void dispatchAccessibilityButtonClicked() { |
| final ArrayMap<AccessibilityButtonCallback, Handler> entries; |
| synchronized (mLock) { |
| if (mCallbacks == null || mCallbacks.isEmpty()) { |
| Slog.w(LOG_TAG, "Received accessibility button click with no callbacks!"); |
| return; |
| } |
| |
| // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent |
| // modification. |
| entries = new ArrayMap<>(mCallbacks); |
| } |
| |
| for (int i = 0, count = entries.size(); i < count; i++) { |
| final AccessibilityButtonCallback callback = entries.keyAt(i); |
| final Handler handler = entries.valueAt(i); |
| handler.post(() -> callback.onClicked(this)); |
| } |
| } |
| |
| /** |
| * Dispatches the accessibility button availability changes to any registered callbacks. |
| * This should be called on the service's main thread. |
| */ |
| void dispatchAccessibilityButtonAvailabilityChanged(boolean available) { |
| final ArrayMap<AccessibilityButtonCallback, Handler> entries; |
| synchronized (mLock) { |
| if (mCallbacks == null || mCallbacks.isEmpty()) { |
| Slog.w(LOG_TAG, |
| "Received accessibility button availability change with no callbacks!"); |
| return; |
| } |
| |
| // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent |
| // modification. |
| entries = new ArrayMap<>(mCallbacks); |
| } |
| |
| for (int i = 0, count = entries.size(); i < count; i++) { |
| final AccessibilityButtonCallback callback = entries.keyAt(i); |
| final Handler handler = entries.valueAt(i); |
| handler.post(() -> callback.onAvailabilityChanged(this, available)); |
| } |
| } |
| |
| /** |
| * Callback for interaction with and changes to state of the accessibility button |
| * within the system's navigation area. |
| */ |
| public static abstract class AccessibilityButtonCallback { |
| |
| /** |
| * Called when the accessibility button in the system's navigation area is clicked. |
| * |
| * @param controller the controller used to register for this callback |
| */ |
| public void onClicked(AccessibilityButtonController controller) {} |
| |
| /** |
| * Called when the availability of the accessibility button in the system's |
| * navigation area has changed. The accessibility button may become unavailable |
| * because the device shopped showing the button, the button was assigned to another |
| * service, or for other reasons. |
| * |
| * @param controller the controller used to register for this callback |
| * @param available {@code true} if the accessibility button is available to this |
| * service, {@code false} otherwise |
| */ |
| public void onAvailabilityChanged(AccessibilityButtonController controller, |
| boolean available) { |
| } |
| } |
| } |