AccessibilityButtonController.java revision a338b51e0232daadbee7fe9dd861cd78e9e6d9a9
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
26import 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 */
53public 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
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     */
77    public boolean isAccessibilityButtonAvailable() {
78        try {
79            return mServiceConnection.isAccessibilityButtonAvailable();
80        } catch (RemoteException re) {
81            Slog.w(LOG_TAG, "Failed to get accessibility button availability.", re);
82            re.rethrowFromSystemServer();
83            return false;
84        }
85    }
86
87    /**
88     * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
89     * changes callbacks related to the accessibility button.
90     *
91     * @param callback the callback to add, must be non-null
92     */
93    public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) {
94        registerAccessibilityButtonCallback(callback, new Handler());
95    }
96
97    /**
98     * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
99     * change callbacks related to the accessibility button. The callback will occur on the
100     * specified {@link Handler}'s thread, or on the services's main thread if the handler is
101     * {@code null}.
102     *
103     * @param callback the callback to add, must be non-null
104     * @param handler the handler on which the callback should execute, must be non-null
105     */
106    public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback,
107            @NonNull Handler handler) {
108        Preconditions.checkNotNull(callback);
109        Preconditions.checkNotNull(handler);
110        synchronized (mLock) {
111            if (mCallbacks == null) {
112                mCallbacks = new ArrayMap<>();
113            }
114
115            mCallbacks.put(callback, handler);
116        }
117    }
118
119    /**
120     * Unregisters the provided {@link AccessibilityButtonCallback} for interaction and state
121     * change callbacks related to the accessibility button.
122     *
123     * @param callback the callback to remove, must be non-null
124     */
125    public void unregisterAccessibilityButtonCallback(
126            @NonNull AccessibilityButtonCallback callback) {
127        Preconditions.checkNotNull(callback);
128        synchronized (mLock) {
129            if (mCallbacks == null) {
130                return;
131            }
132
133            final int keyIndex = mCallbacks.indexOfKey(callback);
134            final boolean hasKey = keyIndex >= 0;
135            if (hasKey) {
136                mCallbacks.removeAt(keyIndex);
137            }
138        }
139    }
140
141    /**
142     * Dispatches the accessibility button click to any registered callbacks. This should
143     * be called on the service's main thread.
144     */
145    void dispatchAccessibilityButtonClicked() {
146        final ArrayMap<AccessibilityButtonCallback, Handler> entries;
147        synchronized (mLock) {
148            if (mCallbacks == null || mCallbacks.isEmpty()) {
149                Slog.w(LOG_TAG, "Received accessibility button click with no callbacks!");
150                return;
151            }
152
153            // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent
154            // modification.
155            entries = new ArrayMap<>(mCallbacks);
156        }
157
158        for (int i = 0, count = entries.size(); i < count; i++) {
159            final AccessibilityButtonCallback callback = entries.keyAt(i);
160            final Handler handler = entries.valueAt(i);
161            handler.post(() -> callback.onClicked(this));
162        }
163    }
164
165    /**
166     * Dispatches the accessibility button availability changes to any registered callbacks.
167     * This should be called on the service's main thread.
168     */
169    void dispatchAccessibilityButtonAvailabilityChanged(boolean available) {
170        final ArrayMap<AccessibilityButtonCallback, Handler> entries;
171        synchronized (mLock) {
172            if (mCallbacks == null || mCallbacks.isEmpty()) {
173                Slog.w(LOG_TAG,
174                        "Received accessibility button availability change with no callbacks!");
175                return;
176            }
177
178            // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent
179            // modification.
180            entries = new ArrayMap<>(mCallbacks);
181        }
182
183        for (int i = 0, count = entries.size(); i < count; i++) {
184            final AccessibilityButtonCallback callback = entries.keyAt(i);
185            final Handler handler = entries.valueAt(i);
186            handler.post(() -> callback.onAvailabilityChanged(this, available));
187        }
188    }
189
190    /**
191     * Callback for interaction with and changes to state of the accessibility button
192     * within the system's navigation area.
193     */
194    public static abstract class AccessibilityButtonCallback {
195
196        /**
197         * Called when the accessibility button in the system's navigation area is clicked.
198         *
199         * @param controller the controller used to register for this callback
200         */
201        public void onClicked(AccessibilityButtonController controller) {}
202
203        /**
204         * Called when the availability of the accessibility button in the system's
205         * navigation area has changed. The accessibility button may become unavailable
206         * because the device shopped showing the button, the button was assigned to another
207         * service, or for other reasons.
208         *
209         * @param controller the controller used to register for this callback
210         * @param available {@code true} if the accessibility button is available to this
211         *                  service, {@code false} otherwise
212         */
213        public void onAvailabilityChanged(AccessibilityButtonController controller,
214                boolean available) {
215        }
216    }
217}
218