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