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.Looper;
23import android.os.RemoteException;
24import android.util.ArrayMap;
25import android.util.Slog;
26
27import 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 */
54public 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
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     */
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     */
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     */
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     */
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     */
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     */
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         */
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         */
214        public void onAvailabilityChanged(AccessibilityButtonController controller,
215                boolean available) {
216        }
217    }
218}
219