1/*
2 * Copyright (C) 2009 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.view.accessibility;
18
19import android.Manifest;
20import android.accessibilityservice.AccessibilityServiceInfo;
21import android.content.Context;
22import android.content.pm.PackageManager;
23import android.content.pm.ServiceInfo;
24import android.os.Binder;
25import android.os.Handler;
26import android.os.IBinder;
27import android.os.Looper;
28import android.os.Message;
29import android.os.Process;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.os.SystemClock;
33import android.os.UserHandle;
34import android.util.Log;
35import android.view.IWindow;
36import android.view.View;
37
38import java.util.ArrayList;
39import java.util.Collections;
40import java.util.List;
41import java.util.concurrent.CopyOnWriteArrayList;
42
43/**
44 * System level service that serves as an event dispatch for {@link AccessibilityEvent}s,
45 * and provides facilities for querying the accessibility state of the system.
46 * Accessibility events are generated when something notable happens in the user interface,
47 * for example an {@link android.app.Activity} starts, the focus or selection of a
48 * {@link android.view.View} changes etc. Parties interested in handling accessibility
49 * events implement and register an accessibility service which extends
50 * {@link android.accessibilityservice.AccessibilityService}.
51 * <p>
52 * To obtain a handle to the accessibility manager do the following:
53 * </p>
54 * <p>
55 * <code>
56 * <pre>AccessibilityManager accessibilityManager =
57 *        (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);</pre>
58 * </code>
59 * </p>
60 *
61 * @see AccessibilityEvent
62 * @see AccessibilityNodeInfo
63 * @see android.accessibilityservice.AccessibilityService
64 * @see Context#getSystemService
65 * @see Context#ACCESSIBILITY_SERVICE
66 */
67public final class AccessibilityManager {
68    private static final boolean DEBUG = false;
69
70    private static final String LOG_TAG = "AccessibilityManager";
71
72    /** @hide */
73    public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001;
74
75    /** @hide */
76    public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002;
77
78    static final Object sInstanceSync = new Object();
79
80    private static AccessibilityManager sInstance;
81
82    private static final int DO_SET_STATE = 10;
83
84    final IAccessibilityManager mService;
85
86    final int mUserId;
87
88    final Handler mHandler;
89
90    boolean mIsEnabled;
91
92    boolean mIsTouchExplorationEnabled;
93
94    private final CopyOnWriteArrayList<AccessibilityStateChangeListener>
95            mAccessibilityStateChangeListeners = new CopyOnWriteArrayList<
96                    AccessibilityStateChangeListener>();
97
98    private final CopyOnWriteArrayList<TouchExplorationStateChangeListener>
99            mTouchExplorationStateChangeListeners = new CopyOnWriteArrayList<
100                    TouchExplorationStateChangeListener>();
101
102    /**
103     * Listener for the system accessibility state. To listen for changes to the
104     * accessibility state on the device, implement this interface and register
105     * it with the system by calling {@link #addAccessibilityStateChangeListener}.
106     */
107    public interface AccessibilityStateChangeListener {
108
109        /**
110         * Called when the accessibility enabled state changes.
111         *
112         * @param enabled Whether accessibility is enabled.
113         */
114        public void onAccessibilityStateChanged(boolean enabled);
115    }
116
117    /**
118     * Listener for the system touch exploration state. To listen for changes to
119     * the touch exploration state on the device, implement this interface and
120     * register it with the system by calling
121     * {@link #addTouchExplorationStateChangeListener}.
122     */
123    public interface TouchExplorationStateChangeListener {
124
125        /**
126         * Called when the touch exploration enabled state changes.
127         *
128         * @param enabled Whether touch exploration is enabled.
129         */
130        public void onTouchExplorationStateChanged(boolean enabled);
131    }
132
133    final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() {
134        public void setState(int state) {
135            mHandler.obtainMessage(DO_SET_STATE, state, 0).sendToTarget();
136        }
137    };
138
139    class MyHandler extends Handler {
140
141        MyHandler(Looper mainLooper) {
142            super(mainLooper);
143        }
144
145        @Override
146        public void handleMessage(Message message) {
147            switch (message.what) {
148                case DO_SET_STATE :
149                    setState(message.arg1);
150                    return;
151                default :
152                    Log.w(LOG_TAG, "Unknown message type: " + message.what);
153            }
154        }
155    }
156
157    /**
158     * Get an AccessibilityManager instance (create one if necessary).
159     *
160     * @param context Context in which this manager operates.
161     *
162     * @hide
163     */
164    public static AccessibilityManager getInstance(Context context) {
165        synchronized (sInstanceSync) {
166            if (sInstance == null) {
167                final int userId;
168                if (Binder.getCallingUid() == Process.SYSTEM_UID
169                        || context.checkCallingOrSelfPermission(
170                                Manifest.permission.INTERACT_ACROSS_USERS)
171                                        == PackageManager.PERMISSION_GRANTED
172                        || context.checkCallingOrSelfPermission(
173                                Manifest.permission.INTERACT_ACROSS_USERS_FULL)
174                                        == PackageManager.PERMISSION_GRANTED) {
175                    userId = UserHandle.USER_CURRENT;
176                } else {
177                    userId = UserHandle.myUserId();
178                }
179                IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE);
180                IAccessibilityManager service = IAccessibilityManager.Stub.asInterface(iBinder);
181                sInstance = new AccessibilityManager(context, service, userId);
182            }
183        }
184        return sInstance;
185    }
186
187    /**
188     * Create an instance.
189     *
190     * @param context A {@link Context}.
191     * @param service An interface to the backing service.
192     * @param userId User id under which to run.
193     *
194     * @hide
195     */
196    public AccessibilityManager(Context context, IAccessibilityManager service, int userId) {
197        mHandler = new MyHandler(context.getMainLooper());
198        mService = service;
199        mUserId = userId;
200
201        try {
202            final int stateFlags = mService.addClient(mClient, userId);
203            setState(stateFlags);
204        } catch (RemoteException re) {
205            Log.e(LOG_TAG, "AccessibilityManagerService is dead", re);
206        }
207    }
208
209    /**
210     * Returns if the accessibility in the system is enabled.
211     *
212     * @return True if accessibility is enabled, false otherwise.
213     */
214    public boolean isEnabled() {
215        synchronized (mHandler) {
216            return mIsEnabled;
217        }
218    }
219
220    /**
221     * Returns if the touch exploration in the system is enabled.
222     *
223     * @return True if touch exploration is enabled, false otherwise.
224     */
225    public boolean isTouchExplorationEnabled() {
226        synchronized (mHandler) {
227            return mIsTouchExplorationEnabled;
228        }
229    }
230
231    /**
232     * Returns the client interface this instance registers in
233     * the centralized accessibility manager service.
234     *
235     * @return The client.
236     *
237     * @hide
238     */
239    public IAccessibilityManagerClient getClient() {
240       return (IAccessibilityManagerClient) mClient.asBinder();
241    }
242
243    /**
244     * Sends an {@link AccessibilityEvent}.
245     *
246     * @param event The event to send.
247     *
248     * @throws IllegalStateException if accessibility is not enabled.
249     *
250     * <strong>Note:</strong> The preferred mechanism for sending custom accessibility
251     * events is through calling
252     * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)}
253     * instead of this method to allow predecessors to augment/filter events sent by
254     * their descendants.
255     */
256    public void sendAccessibilityEvent(AccessibilityEvent event) {
257        if (!mIsEnabled) {
258            throw new IllegalStateException("Accessibility off. Did you forget to check that?");
259        }
260        boolean doRecycle = false;
261        try {
262            event.setEventTime(SystemClock.uptimeMillis());
263            // it is possible that this manager is in the same process as the service but
264            // client using it is called through Binder from another process. Example: MMS
265            // app adds a SMS notification and the NotificationManagerService calls this method
266            long identityToken = Binder.clearCallingIdentity();
267            doRecycle = mService.sendAccessibilityEvent(event, mUserId);
268            Binder.restoreCallingIdentity(identityToken);
269            if (DEBUG) {
270                Log.i(LOG_TAG, event + " sent");
271            }
272        } catch (RemoteException re) {
273            Log.e(LOG_TAG, "Error during sending " + event + " ", re);
274        } finally {
275            if (doRecycle) {
276                event.recycle();
277            }
278        }
279    }
280
281    /**
282     * Requests feedback interruption from all accessibility services.
283     */
284    public void interrupt() {
285        if (!mIsEnabled) {
286            throw new IllegalStateException("Accessibility off. Did you forget to check that?");
287        }
288        try {
289            mService.interrupt(mUserId);
290            if (DEBUG) {
291                Log.i(LOG_TAG, "Requested interrupt from all services");
292            }
293        } catch (RemoteException re) {
294            Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re);
295        }
296    }
297
298    /**
299     * Returns the {@link ServiceInfo}s of the installed accessibility services.
300     *
301     * @return An unmodifiable list with {@link ServiceInfo}s.
302     *
303     * @deprecated Use {@link #getInstalledAccessibilityServiceList()}
304     */
305    @Deprecated
306    public List<ServiceInfo> getAccessibilityServiceList() {
307        List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList();
308        List<ServiceInfo> services = new ArrayList<ServiceInfo>();
309        final int infoCount = infos.size();
310        for (int i = 0; i < infoCount; i++) {
311            AccessibilityServiceInfo info = infos.get(i);
312            services.add(info.getResolveInfo().serviceInfo);
313        }
314        return Collections.unmodifiableList(services);
315    }
316
317    /**
318     * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services.
319     *
320     * @return An unmodifiable list with {@link AccessibilityServiceInfo}s.
321     */
322    public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() {
323        List<AccessibilityServiceInfo> services = null;
324        try {
325            services = mService.getInstalledAccessibilityServiceList(mUserId);
326            if (DEBUG) {
327                Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
328            }
329        } catch (RemoteException re) {
330            Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
331        }
332        return Collections.unmodifiableList(services);
333    }
334
335    /**
336     * Returns the {@link AccessibilityServiceInfo}s of the enabled accessibility services
337     * for a given feedback type.
338     *
339     * @param feedbackTypeFlags The feedback type flags.
340     * @return An unmodifiable list with {@link AccessibilityServiceInfo}s.
341     *
342     * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE
343     * @see AccessibilityServiceInfo#FEEDBACK_GENERIC
344     * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC
345     * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN
346     * @see AccessibilityServiceInfo#FEEDBACK_VISUAL
347     */
348    public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
349            int feedbackTypeFlags) {
350        List<AccessibilityServiceInfo> services = null;
351        try {
352            services = mService.getEnabledAccessibilityServiceList(feedbackTypeFlags, mUserId);
353            if (DEBUG) {
354                Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
355            }
356        } catch (RemoteException re) {
357            Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
358        }
359        return Collections.unmodifiableList(services);
360    }
361
362    /**
363     * Registers an {@link AccessibilityStateChangeListener} for changes in
364     * the global accessibility state of the system.
365     *
366     * @param listener The listener.
367     * @return True if successfully registered.
368     */
369    public boolean addAccessibilityStateChangeListener(
370            AccessibilityStateChangeListener listener) {
371        return mAccessibilityStateChangeListeners.add(listener);
372    }
373
374    /**
375     * Unregisters an {@link AccessibilityStateChangeListener}.
376     *
377     * @param listener The listener.
378     * @return True if successfully unregistered.
379     */
380    public boolean removeAccessibilityStateChangeListener(
381            AccessibilityStateChangeListener listener) {
382        return mAccessibilityStateChangeListeners.remove(listener);
383    }
384
385    /**
386     * Registers a {@link TouchExplorationStateChangeListener} for changes in
387     * the global touch exploration state of the system.
388     *
389     * @param listener The listener.
390     * @return True if successfully registered.
391     */
392    public boolean addTouchExplorationStateChangeListener(
393            TouchExplorationStateChangeListener listener) {
394        return mTouchExplorationStateChangeListeners.add(listener);
395    }
396
397    /**
398     * Unregisters a {@link TouchExplorationStateChangeListener}.
399     *
400     * @param listener The listener.
401     * @return True if successfully unregistered.
402     */
403    public boolean removeTouchExplorationStateChangeListener(
404            TouchExplorationStateChangeListener listener) {
405        return mTouchExplorationStateChangeListeners.remove(listener);
406    }
407
408    /**
409     * Sets the current state and notifies listeners, if necessary.
410     *
411     * @param stateFlags The state flags.
412     */
413    private void setState(int stateFlags) {
414        final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0;
415        final boolean touchExplorationEnabled =
416                (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0;
417        synchronized (mHandler) {
418            final boolean wasEnabled = mIsEnabled;
419            final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
420
421            // Ensure listeners get current state from isZzzEnabled() calls.
422            mIsEnabled = enabled;
423            mIsTouchExplorationEnabled = touchExplorationEnabled;
424
425            if (wasEnabled != enabled) {
426                notifyAccessibilityStateChangedLh();
427            }
428
429            if (wasTouchExplorationEnabled != touchExplorationEnabled) {
430                notifyTouchExplorationStateChangedLh();
431            }
432        }
433    }
434
435    /**
436     * Notifies the registered {@link AccessibilityStateChangeListener}s.
437     * <p>
438     * The caller must be locked on {@link #mHandler}.
439     */
440    private void notifyAccessibilityStateChangedLh() {
441        final int listenerCount = mAccessibilityStateChangeListeners.size();
442        for (int i = 0; i < listenerCount; i++) {
443            mAccessibilityStateChangeListeners.get(i).onAccessibilityStateChanged(mIsEnabled);
444        }
445    }
446
447    /**
448     * Notifies the registered {@link TouchExplorationStateChangeListener}s.
449     * <p>
450     * The caller must be locked on {@link #mHandler}.
451     */
452    private void notifyTouchExplorationStateChangedLh() {
453        final int listenerCount = mTouchExplorationStateChangeListeners.size();
454        for (int i = 0; i < listenerCount; i++) {
455            mTouchExplorationStateChangeListeners.get(i)
456                    .onTouchExplorationStateChanged(mIsTouchExplorationEnabled);
457        }
458    }
459
460    /**
461     * Adds an accessibility interaction connection interface for a given window.
462     * @param windowToken The window token to which a connection is added.
463     * @param connection The connection.
464     *
465     * @hide
466     */
467    public int addAccessibilityInteractionConnection(IWindow windowToken,
468            IAccessibilityInteractionConnection connection) {
469        try {
470            return mService.addAccessibilityInteractionConnection(windowToken, connection, mUserId);
471        } catch (RemoteException re) {
472            Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re);
473        }
474        return View.NO_ID;
475    }
476
477    /**
478     * Removed an accessibility interaction connection interface for a given window.
479     * @param windowToken The window token to which a connection is removed.
480     *
481     * @hide
482     */
483    public void removeAccessibilityInteractionConnection(IWindow windowToken) {
484        try {
485            mService.removeAccessibilityInteractionConnection(windowToken);
486        } catch (RemoteException re) {
487            Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re);
488        }
489    }
490}
491