/* * 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 *

* 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, 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 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 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) { } } }