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.annotation.Nullable; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.RemoteException; 24 import android.util.ArrayMap; 25 import android.util.Slog; 26 27 import com.android.internal.util.Preconditions; 28 29 /** 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 */ 54 public 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 AccessibilityButtonController(@onNull IAccessibilityServiceConnection serviceConnection)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 */ isAccessibilityButtonAvailable()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 */ registerAccessibilityButtonCallback(@onNull AccessibilityButtonCallback callback)94 public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) { 95 registerAccessibilityButtonCallback(callback, new Handler(Looper.getMainLooper())); 96 } 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 105 * @param handler the handler on which the callback should execute, must be non-null 106 */ registerAccessibilityButtonCallback(@onNull AccessibilityButtonCallback callback, @NonNull Handler handler)107 public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback, 108 @NonNull Handler handler) { 109 Preconditions.checkNotNull(callback); 110 Preconditions.checkNotNull(handler); 111 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 */ unregisterAccessibilityButtonCallback( @onNull AccessibilityButtonCallback callback)126 public void unregisterAccessibilityButtonCallback( 127 @NonNull AccessibilityButtonCallback callback) { 128 Preconditions.checkNotNull(callback); 129 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 */ dispatchAccessibilityButtonClicked()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); 162 handler.post(() -> callback.onClicked(this)); 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 */ dispatchAccessibilityButtonAvailabilityChanged(boolean available)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 handler.post(() -> callback.onAvailabilityChanged(this, available)); 188 } 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 */ onClicked(AccessibilityButtonController controller)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 */ onAvailabilityChanged(AccessibilityButtonController controller, boolean available)214 public void onAvailabilityChanged(AccessibilityButtonController controller, 215 boolean available) { 216 } 217 } 218 } 219