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 com.android.internal.util.Preconditions;
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         Preconditions.checkNotNull(callback);
112         Preconditions.checkNotNull(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         Preconditions.checkNotNull(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