AccessibilityEntityProvider.java revision 48ccd5528163383a46b597e9d5ea919ddc799f25
1/*
2 * Copyright (C) 2012 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 com.android.inputmethod.accessibility;
18
19import android.graphics.Rect;
20import android.inputmethodservice.InputMethodService;
21import android.os.Bundle;
22import android.os.SystemClock;
23import android.support.v4.view.ViewCompat;
24import android.support.v4.view.accessibility.AccessibilityEventCompat;
25import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
26import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
27import android.support.v4.view.accessibility.AccessibilityRecordCompat;
28import android.util.Log;
29import android.util.SparseArray;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.accessibility.AccessibilityEvent;
33import android.view.inputmethod.EditorInfo;
34
35import com.android.inputmethod.keyboard.Key;
36import com.android.inputmethod.keyboard.Keyboard;
37import com.android.inputmethod.keyboard.KeyboardView;
38
39/**
40 * Exposes a virtual view sub-tree for {@link KeyboardView} and generates
41 * {@link AccessibilityEvent}s for individual {@link Key}s.
42 * <p>
43 * A virtual sub-tree is composed of imaginary {@link View}s that are reported
44 * as a part of the view hierarchy for accessibility purposes. This enables
45 * custom views that draw complex content to report them selves as a tree of
46 * virtual views, thus conveying their logical structure.
47 * </p>
48 */
49public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
50    private static final String TAG = AccessibilityEntityProvider.class.getSimpleName();
51    private static final int UNDEFINED = Integer.MIN_VALUE;
52
53    private final InputMethodService mInputMethodService;
54    private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
55    private final AccessibilityUtils mAccessibilityUtils;
56
57    /** A map of integer IDs to {@link Key}s. */
58    private final SparseArray<Key> mVirtualViewIdToKey = new SparseArray<Key>();
59
60    /** Temporary rect used to calculate in-screen bounds. */
61    private final Rect mTempBoundsInScreen = new Rect();
62
63    /** The parent view's cached on-screen location. */
64    private final int[] mParentLocation = new int[2];
65
66    /** The virtual view identifier for the focused node. */
67    private int mAccessibilityFocusedView = UNDEFINED;
68
69    /** The current keyboard view. */
70    private KeyboardView mKeyboardView;
71
72    public AccessibilityEntityProvider(KeyboardView keyboardView, InputMethodService inputMethod) {
73        mInputMethodService = inputMethod;
74
75        mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
76        mAccessibilityUtils = AccessibilityUtils.getInstance();
77
78        setView(keyboardView);
79    }
80
81    /**
82     * Sets the keyboard view represented by this node provider.
83     *
84     * @param keyboardView The keyboard view to represent.
85     */
86    public void setView(KeyboardView keyboardView) {
87        mKeyboardView = keyboardView;
88
89        assignVirtualViewIds();
90        updateParentLocation();
91    }
92
93    /**
94     * Creates and populates an {@link AccessibilityEvent} for the specified key
95     * and event type.
96     *
97     * @param key A key on the host keyboard view.
98     * @param eventType The event type to create.
99     * @return A populated {@link AccessibilityEvent} for the key.
100     * @see AccessibilityEvent
101     */
102    public AccessibilityEvent createAccessibilityEvent(Key key, int eventType) {
103        final int virtualViewId = generateVirtualViewIdForKey(key);
104        final String keyDescription = getKeyDescription(key);
105
106        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
107        event.setPackageName(mKeyboardView.getContext().getPackageName());
108        event.setClassName(key.getClass().getName());
109        event.setContentDescription(keyDescription);
110        event.setEnabled(true);
111
112        final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
113        record.setSource(mKeyboardView, virtualViewId);
114
115        return event;
116    }
117
118    /**
119     * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
120     * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
121     * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
122     * <p>
123     * A virtual descendant is an imaginary View that is reported as a part of
124     * the view hierarchy for accessibility purposes. This enables custom views
125     * that draw complex content to report them selves as a tree of virtual
126     * views, thus conveying their logical structure.
127     * </p>
128     * <p>
129     * The implementer is responsible for obtaining an accessibility node info
130     * from the pool of reusable instances and setting the desired properties of
131     * the node info before returning it.
132     * </p>
133     *
134     * @param virtualViewId A client defined virtual view id.
135     * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual
136     *         descendant or the host View.
137     * @see AccessibilityNodeInfoCompat
138     */
139    @Override
140    public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
141        AccessibilityNodeInfoCompat info = null;
142
143        if (virtualViewId == UNDEFINED) {
144            return null;
145        } else  if (virtualViewId == View.NO_ID) {
146            // We are requested to create an AccessibilityNodeInfo describing
147            // this View, i.e. the root of the virtual sub-tree.
148            info = AccessibilityNodeInfoCompat.obtain(mKeyboardView);
149            ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, info);
150
151            // Add the virtual children of the root View.
152            final Keyboard keyboard = mKeyboardView.getKeyboard();
153            final Key[] keys = keyboard.mKeys;
154            for (Key key : keys) {
155                final int childVirtualViewId = generateVirtualViewIdForKey(key);
156                info.addChild(mKeyboardView, childVirtualViewId);
157            }
158        } else {
159            // Find the view that corresponds to the given id.
160            final Key key = mVirtualViewIdToKey.get(virtualViewId);
161            if (key == null) {
162                Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
163                return null;
164            }
165
166            final String keyDescription = getKeyDescription(key);
167            final Rect boundsInParent = key.mHitBox;
168
169            // Calculate the key's in-screen bounds.
170            mTempBoundsInScreen.set(boundsInParent);
171            mTempBoundsInScreen.offset(mParentLocation[0], mParentLocation[1]);
172
173            final Rect boundsInScreen = mTempBoundsInScreen;
174
175            // Obtain and initialize an AccessibilityNodeInfo with
176            // information about the virtual view.
177            info = AccessibilityNodeInfoCompat.obtain();
178            info.setPackageName(mKeyboardView.getContext().getPackageName());
179            info.setClassName(key.getClass().getName());
180            info.setContentDescription(keyDescription);
181            info.setBoundsInParent(boundsInParent);
182            info.setBoundsInScreen(boundsInScreen);
183            info.setParent(mKeyboardView);
184            info.setSource(mKeyboardView, virtualViewId);
185            info.setBoundsInScreen(boundsInScreen);
186            info.setEnabled(true);
187            info.setClickable(true);
188            info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
189
190            if (mAccessibilityFocusedView == virtualViewId) {
191                info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
192            } else {
193                info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
194            }
195        }
196
197        return info;
198    }
199
200    /**
201     * Simulates a key press by injecting touch events into the keyboard view.
202     * This avoids the complexity of trackers and listeners within the keyboard.
203     *
204     * @param key The key to press.
205     */
206    void simulateKeyPress(Key key) {
207        final int x = key.mHitBox.centerX();
208        final int y = key.mHitBox.centerY();
209        final long downTime = SystemClock.uptimeMillis();
210        final MotionEvent downEvent = MotionEvent.obtain(
211                downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0);
212        final MotionEvent upEvent = MotionEvent.obtain(
213                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0);
214
215        mKeyboardView.onTouchEvent(downEvent);
216        mKeyboardView.onTouchEvent(upEvent);
217    }
218
219    @Override
220    public boolean performAction(int virtualViewId, int action, Bundle arguments) {
221        final Key key = mVirtualViewIdToKey.get(virtualViewId);
222
223        if (key == null) {
224            return false;
225        }
226
227        return performActionForKey(key, action, arguments);
228    }
229
230    /**
231     * Performs the specified accessibility action for the given key.
232     *
233     * @param key The on which to perform the action.
234     * @param action The action to perform.
235     * @param arguments The action's arguments.
236     * @return The result of performing the action, or false if the action is
237     *         not supported.
238     */
239    boolean performActionForKey(Key key, int action, Bundle arguments) {
240        final int virtualViewId = generateVirtualViewIdForKey(key);
241
242        switch (action) {
243        case AccessibilityNodeInfoCompat.ACTION_CLICK:
244            simulateKeyPress(key);
245            return true;
246        case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
247            if (mAccessibilityFocusedView == virtualViewId) {
248                return false;
249            }
250            mAccessibilityFocusedView = virtualViewId;
251            sendAccessibilityEventForKey(
252                    key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
253            return true;
254        case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
255            if (mAccessibilityFocusedView != virtualViewId) {
256                return false;
257            }
258            mAccessibilityFocusedView = UNDEFINED;
259            sendAccessibilityEventForKey(
260                    key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
261            return true;
262        }
263
264        return false;
265    }
266
267    @Override
268    public AccessibilityNodeInfoCompat findAccessibilityFocus(int virtualViewId) {
269        return createAccessibilityNodeInfo(mAccessibilityFocusedView);
270    }
271
272    @Override
273    public AccessibilityNodeInfoCompat accessibilityFocusSearch(int direction, int virtualViewId) {
274        // Focus search is not currently supported for IMEs.
275        return null;
276    }
277
278    /**
279     * Sends an accessibility event for the given {@link Key}.
280     *
281     * @param key The key that's sending the event.
282     * @param eventType The type of event to send.
283     */
284    void sendAccessibilityEventForKey(Key key, int eventType) {
285        final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
286        mAccessibilityUtils.requestSendAccessibilityEvent(event);
287    }
288
289    /**
290     * Returns the context-specific description for a {@link Key}.
291     *
292     * @param key The key to describe.
293     * @return The context-specific description of the key.
294     */
295    private String getKeyDescription(Key key) {
296        final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
297        final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
298        final String keyDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
299                mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure);
300
301        return keyDescription;
302    }
303
304    /**
305     * Assigns virtual view IDs to keyboard keys and populates the related maps.
306     */
307    private void assignVirtualViewIds() {
308        final Keyboard keyboard = mKeyboardView.getKeyboard();
309        if (keyboard == null) {
310            return;
311        }
312
313        mVirtualViewIdToKey.clear();
314
315        final Key[] keys = keyboard.mKeys;
316        for (Key key : keys) {
317            final int virtualViewId = generateVirtualViewIdForKey(key);
318            mVirtualViewIdToKey.put(virtualViewId, key);
319        }
320    }
321
322    /**
323     * Updates the parent's on-screen location.
324     */
325    private void updateParentLocation() {
326        mKeyboardView.getLocationOnScreen(mParentLocation);
327    }
328
329    /**
330     * Generates a virtual view identifier for the given key. Returned
331     * identifiers are valid until the next global layout state change.
332     *
333     * @param key The key to identify.
334     * @return A virtual view identifier.
335     */
336    private static int generateVirtualViewIdForKey(Key key) {
337        // The key x- and y-coordinates are stable between layout changes.
338        // Generate an identifier by bit-shifting the x-coordinate to the
339        // left-half of the integer and OR'ing with the y-coordinate.
340        return ((0xFFFF & key.mX) << (Integer.SIZE / 2)) | (0xFFFF & key.mY);
341    }
342}
343