• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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