blob: a70085cbde4f132d3c16e1cf35e3b896f09e670f [file] [log] [blame]
Casey Burkhardt048c2bc2016-12-08 16:09:20 -08001/*
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;
Casey Burkhardt2402943a2017-04-28 17:09:57 -070022import android.os.Looper;
Casey Burkhardt048c2bc2016-12-08 16:09:20 -080023import android.os.RemoteException;
24import android.util.ArrayMap;
25import android.util.Slog;
26
Casey Burkhardta338b512017-04-27 11:09:50 -070027import com.android.internal.util.Preconditions;
28
Casey Burkhardt048c2bc2016-12-08 16:09:20 -080029/**
30 * Controller for the accessibility button within the system's navigation area
31 * <p>
32 * This class may be used to query the accessibility button's state and register
33 * callbacks for interactions with and state changes to the accessibility button when
34 * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} is set.
35 * </p>
36 * <p>
37 * <strong>Note:</strong> This class and
38 * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} should not be used as
39 * the sole means for offering functionality to users via an {@link AccessibilityService}.
40 * Some device implementations may choose not to provide a software-rendered system
41 * navigation area, making this affordance permanently unavailable.
42 * </p>
43 * <p>
44 * <strong>Note:</strong> On device implementations where the accessibility button is
45 * supported, it may not be available at all times, such as when a foreground application uses
46 * {@link android.view.View#SYSTEM_UI_FLAG_HIDE_NAVIGATION}. A user may also choose to assign
47 * this button to another accessibility service or feature. In each of these cases, a
48 * registered {@link AccessibilityButtonCallback}'s
49 * {@link AccessibilityButtonCallback#onAvailabilityChanged(AccessibilityButtonController, boolean)}
50 * method will be invoked to provide notifications of changes in the accessibility button's
51 * availability to the registering service.
52 * </p>
53 */
54public final class AccessibilityButtonController {
55 private static final String LOG_TAG = "A11yButtonController";
56
57 private final IAccessibilityServiceConnection mServiceConnection;
58 private final Object mLock;
59 private ArrayMap<AccessibilityButtonCallback, Handler> mCallbacks;
60
61 AccessibilityButtonController(@NonNull IAccessibilityServiceConnection serviceConnection) {
62 mServiceConnection = serviceConnection;
63 mLock = new Object();
64 }
65
66 /**
67 * Retrieves whether the accessibility button in the system's navigation area is
68 * available to the calling service.
69 * <p>
70 * <strong>Note:</strong> If the service is not yet connected (e.g.
71 * {@link AccessibilityService#onServiceConnected()} has not yet been called) or the
72 * service has been disconnected, this method will have no effect and return {@code false}.
73 * </p>
74 *
75 * @return {@code true} if the accessibility button in the system's navigation area is
76 * available to the calling service, {@code false} otherwise
77 */
78 public boolean isAccessibilityButtonAvailable() {
79 try {
80 return mServiceConnection.isAccessibilityButtonAvailable();
81 } catch (RemoteException re) {
82 Slog.w(LOG_TAG, "Failed to get accessibility button availability.", re);
83 re.rethrowFromSystemServer();
84 return false;
85 }
86 }
87
88 /**
89 * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
90 * changes callbacks related to the accessibility button.
91 *
92 * @param callback the callback to add, must be non-null
93 */
94 public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) {
Casey Burkhardt2402943a2017-04-28 17:09:57 -070095 registerAccessibilityButtonCallback(callback, new Handler(Looper.getMainLooper()));
Casey Burkhardt048c2bc2016-12-08 16:09:20 -080096 }
97
98 /**
99 * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
100 * change callbacks related to the accessibility button. The callback will occur on the
101 * specified {@link Handler}'s thread, or on the services's main thread if the handler is
102 * {@code null}.
103 *
104 * @param callback the callback to add, must be non-null
Casey Burkhardta338b512017-04-27 11:09:50 -0700105 * @param handler the handler on which the callback should execute, must be non-null
Casey Burkhardt048c2bc2016-12-08 16:09:20 -0800106 */
107 public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback,
Casey Burkhardta338b512017-04-27 11:09:50 -0700108 @NonNull Handler handler) {
109 Preconditions.checkNotNull(callback);
110 Preconditions.checkNotNull(handler);
Casey Burkhardt048c2bc2016-12-08 16:09:20 -0800111 synchronized (mLock) {
112 if (mCallbacks == null) {
113 mCallbacks = new ArrayMap<>();
114 }
115
116 mCallbacks.put(callback, handler);
117 }
118 }
119
120 /**
121 * Unregisters the provided {@link AccessibilityButtonCallback} for interaction and state
122 * change callbacks related to the accessibility button.
123 *
124 * @param callback the callback to remove, must be non-null
125 */
126 public void unregisterAccessibilityButtonCallback(
127 @NonNull AccessibilityButtonCallback callback) {
Casey Burkhardta338b512017-04-27 11:09:50 -0700128 Preconditions.checkNotNull(callback);
Casey Burkhardt048c2bc2016-12-08 16:09:20 -0800129 synchronized (mLock) {
130 if (mCallbacks == null) {
131 return;
132 }
133
134 final int keyIndex = mCallbacks.indexOfKey(callback);
135 final boolean hasKey = keyIndex >= 0;
136 if (hasKey) {
137 mCallbacks.removeAt(keyIndex);
138 }
139 }
140 }
141
142 /**
143 * Dispatches the accessibility button click to any registered callbacks. This should
144 * be called on the service's main thread.
145 */
146 void dispatchAccessibilityButtonClicked() {
147 final ArrayMap<AccessibilityButtonCallback, Handler> entries;
148 synchronized (mLock) {
149 if (mCallbacks == null || mCallbacks.isEmpty()) {
150 Slog.w(LOG_TAG, "Received accessibility button click with no callbacks!");
151 return;
152 }
153
154 // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent
155 // modification.
156 entries = new ArrayMap<>(mCallbacks);
157 }
158
159 for (int i = 0, count = entries.size(); i < count; i++) {
160 final AccessibilityButtonCallback callback = entries.keyAt(i);
161 final Handler handler = entries.valueAt(i);
Casey Burkhardta338b512017-04-27 11:09:50 -0700162 handler.post(() -> callback.onClicked(this));
Casey Burkhardt048c2bc2016-12-08 16:09:20 -0800163 }
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);
Casey Burkhardta338b512017-04-27 11:09:50 -0700187 handler.post(() -> callback.onAvailabilityChanged(this, available));
Casey Burkhardt048c2bc2016-12-08 16:09:20 -0800188 }
189 }
190
191 /**
192 * Callback for interaction with and changes to state of the accessibility button
193 * within the system's navigation area.
194 */
195 public static abstract class AccessibilityButtonCallback {
196
197 /**
198 * Called when the accessibility button in the system's navigation area is clicked.
199 *
200 * @param controller the controller used to register for this callback
201 */
202 public void onClicked(AccessibilityButtonController controller) {}
203
204 /**
205 * Called when the availability of the accessibility button in the system's
206 * navigation area has changed. The accessibility button may become unavailable
207 * because the device shopped showing the button, the button was assigned to another
208 * service, or for other reasons.
209 *
210 * @param controller the controller used to register for this callback
211 * @param available {@code true} if the accessibility button is available to this
212 * service, {@code false} otherwise
213 */
214 public void onAvailabilityChanged(AccessibilityButtonController controller,
215 boolean available) {
216 }
217 }
218}