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