AccessibilityButtonController.java revision 048c2bc19af4cfcb5e959f1bc07fbfa03a72e330
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.accessibilityservice;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.os.Handler;
22import android.os.RemoteException;
23import android.util.ArrayMap;
24import android.util.Slog;
25
26/**
27 * Controller for the accessibility button within the system's navigation area
28 * <p>
29 * This class may be used to query the accessibility button's state and register
30 * callbacks for interactions with and state changes to the accessibility button when
31 * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} is set.
32 * </p>
33 * <p>
34 * <strong>Note:</strong> This class and
35 * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} should not be used as
36 * the sole means for offering functionality to users via an {@link AccessibilityService}.
37 * Some device implementations may choose not to provide a software-rendered system
38 * navigation area, making this affordance permanently unavailable.
39 * </p>
40 * <p>
41 * <strong>Note:</strong> On device implementations where the accessibility button is
42 * supported, it may not be available at all times, such as when a foreground application uses
43 * {@link android.view.View#SYSTEM_UI_FLAG_HIDE_NAVIGATION}. A user may also choose to assign
44 * this button to another accessibility service or feature. In each of these cases, a
45 * registered {@link AccessibilityButtonCallback}'s
46 * {@link AccessibilityButtonCallback#onAvailabilityChanged(AccessibilityButtonController, boolean)}
47 * method will be invoked to provide notifications of changes in the accessibility button's
48 * availability to the registering service.
49 * </p>
50 */
51public final class AccessibilityButtonController {
52    private static final String LOG_TAG = "A11yButtonController";
53
54    private final IAccessibilityServiceConnection mServiceConnection;
55    private final Object mLock;
56    private ArrayMap<AccessibilityButtonCallback, Handler> mCallbacks;
57
58    AccessibilityButtonController(@NonNull IAccessibilityServiceConnection serviceConnection) {
59        mServiceConnection = serviceConnection;
60        mLock = new Object();
61    }
62
63    /**
64     * Retrieves whether the accessibility button in the system's navigation area is
65     * available to the calling service.
66     * <p>
67     * <strong>Note:</strong> If the service is not yet connected (e.g.
68     * {@link AccessibilityService#onServiceConnected()} has not yet been called) or the
69     * service has been disconnected, this method will have no effect and return {@code false}.
70     * </p>
71     *
72     * @return {@code true} if the accessibility button in the system's navigation area is
73     * available to the calling service, {@code false} otherwise
74     */
75    public boolean isAccessibilityButtonAvailable() {
76        try {
77            return mServiceConnection.isAccessibilityButtonAvailable();
78        } catch (RemoteException re) {
79            Slog.w(LOG_TAG, "Failed to get accessibility button availability.", re);
80            re.rethrowFromSystemServer();
81            return false;
82        }
83    }
84
85    /**
86     * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
87     * changes callbacks related to the accessibility button.
88     *
89     * @param callback the callback to add, must be non-null
90     */
91    public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) {
92        registerAccessibilityButtonCallback(callback, null);
93    }
94
95    /**
96     * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
97     * change callbacks related to the accessibility button. The callback will occur on the
98     * specified {@link Handler}'s thread, or on the services's main thread if the handler is
99     * {@code null}.
100     *
101     * @param callback the callback to add, must be non-null
102     * @param handler the handler on which to callback should execute, or {@code null} to
103     *                execute on the service's main thread
104     */
105    public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback,
106            @Nullable Handler handler) {
107        synchronized (mLock) {
108            if (mCallbacks == null) {
109                mCallbacks = new ArrayMap<>();
110            }
111
112            mCallbacks.put(callback, handler);
113        }
114    }
115
116    /**
117     * Unregisters the provided {@link AccessibilityButtonCallback} for interaction and state
118     * change callbacks related to the accessibility button.
119     *
120     * @param callback the callback to remove, must be non-null
121     */
122    public void unregisterAccessibilityButtonCallback(
123            @NonNull AccessibilityButtonCallback callback) {
124        synchronized (mLock) {
125            if (mCallbacks == null) {
126                return;
127            }
128
129            final int keyIndex = mCallbacks.indexOfKey(callback);
130            final boolean hasKey = keyIndex >= 0;
131            if (hasKey) {
132                mCallbacks.removeAt(keyIndex);
133            }
134        }
135    }
136
137    /**
138     * Dispatches the accessibility button click to any registered callbacks. This should
139     * be called on the service's main thread.
140     */
141    void dispatchAccessibilityButtonClicked() {
142        final ArrayMap<AccessibilityButtonCallback, Handler> entries;
143        synchronized (mLock) {
144            if (mCallbacks == null || mCallbacks.isEmpty()) {
145                Slog.w(LOG_TAG, "Received accessibility button click with no callbacks!");
146                return;
147            }
148
149            // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent
150            // modification.
151            entries = new ArrayMap<>(mCallbacks);
152        }
153
154        for (int i = 0, count = entries.size(); i < count; i++) {
155            final AccessibilityButtonCallback callback = entries.keyAt(i);
156            final Handler handler = entries.valueAt(i);
157            if (handler != null) {
158                handler.post(() -> callback.onClicked(this));
159            } else {
160                // We're already on the main thread, just run the callback.
161                callback.onClicked(this);
162            }
163        }
164    }
165
166    /**
167     * Dispatches the accessibility button availability changes to any registered callbacks.
168     * This should be called on the service's main thread.
169     */
170    void dispatchAccessibilityButtonAvailabilityChanged(boolean available) {
171        final ArrayMap<AccessibilityButtonCallback, Handler> entries;
172        synchronized (mLock) {
173            if (mCallbacks == null || mCallbacks.isEmpty()) {
174                Slog.w(LOG_TAG,
175                        "Received accessibility button availability change with no callbacks!");
176                return;
177            }
178
179            // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent
180            // modification.
181            entries = new ArrayMap<>(mCallbacks);
182        }
183
184        for (int i = 0, count = entries.size(); i < count; i++) {
185            final AccessibilityButtonCallback callback = entries.keyAt(i);
186            final Handler handler = entries.valueAt(i);
187            if (handler != null) {
188                handler.post(() -> callback.onAvailabilityChanged(this, available));
189            } else {
190                // We're already on the main thread, just run the callback.
191                callback.onAvailabilityChanged(this, available);
192            }
193        }
194    }
195
196    /**
197     * Callback for interaction with and changes to state of the accessibility button
198     * within the system's navigation area.
199     */
200    public static abstract class AccessibilityButtonCallback {
201
202        /**
203         * Called when the accessibility button in the system's navigation area is clicked.
204         *
205         * @param controller the controller used to register for this callback
206         */
207        public void onClicked(AccessibilityButtonController controller) {}
208
209        /**
210         * Called when the availability of the accessibility button in the system's
211         * navigation area has changed. The accessibility button may become unavailable
212         * because the device shopped showing the button, the button was assigned to another
213         * service, or for other reasons.
214         *
215         * @param controller the controller used to register for this callback
216         * @param available {@code true} if the accessibility button is available to this
217         *                  service, {@code false} otherwise
218         */
219        public void onAvailabilityChanged(AccessibilityButtonController controller,
220                boolean available) {
221        }
222    }
223}
224