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