KeyboardAccessibilityNodeProvider.java revision 7b90d2c432fd7ffbf0022fac9db921cf39197ac6
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.os.Bundle;
21import android.support.v4.view.ViewCompat;
22import android.support.v4.view.accessibility.AccessibilityEventCompat;
23import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
24import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
25import android.support.v4.view.accessibility.AccessibilityRecordCompat;
26import android.util.Log;
27import android.view.View;
28import android.view.accessibility.AccessibilityEvent;
29import android.view.inputmethod.EditorInfo;
30
31import com.android.inputmethod.keyboard.Key;
32import com.android.inputmethod.keyboard.Keyboard;
33import com.android.inputmethod.keyboard.KeyboardView;
34import com.android.inputmethod.latin.settings.Settings;
35import com.android.inputmethod.latin.settings.SettingsValues;
36import com.android.inputmethod.latin.utils.CoordinateUtils;
37
38import java.util.List;
39
40/**
41 * Exposes a virtual view sub-tree for {@link KeyboardView} and generates
42 * {@link AccessibilityEvent}s for individual {@link Key}s.
43 * <p>
44 * A virtual sub-tree is composed of imaginary {@link View}s that are reported
45 * as a part of the view hierarchy for accessibility purposes. This enables
46 * custom views that draw complex content to report them selves as a tree of
47 * virtual views, thus conveying their logical structure.
48 * </p>
49 */
50public final class KeyboardAccessibilityNodeProvider extends AccessibilityNodeProviderCompat {
51    private static final String TAG = KeyboardAccessibilityNodeProvider.class.getSimpleName();
52    private static final int UNDEFINED = Integer.MIN_VALUE;
53
54    private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
55    private final AccessibilityUtils mAccessibilityUtils;
56
57    /** Temporary rect used to calculate in-screen bounds. */
58    private final Rect mTempBoundsInScreen = new Rect();
59
60    /** The parent view's cached on-screen location. */
61    private final int[] mParentLocation = CoordinateUtils.newInstance();
62
63    /** The virtual view identifier for the focused node. */
64    private int mAccessibilityFocusedView = UNDEFINED;
65
66    /** The current keyboard view. */
67    private final KeyboardView mKeyboardView;
68
69    /** The current keyboard. */
70    private Keyboard mKeyboard;
71
72    public KeyboardAccessibilityNodeProvider(final KeyboardView keyboardView) {
73        super();
74        mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
75        mAccessibilityUtils = AccessibilityUtils.getInstance();
76        mKeyboardView = keyboardView;
77        updateParentLocation();
78
79        // Since this class is constructed lazily, we might not get a subsequent
80        // call to setKeyboard() and therefore need to call it now.
81        setKeyboard(keyboardView.getKeyboard());
82    }
83
84    /**
85     * Sets the keyboard represented by this node provider.
86     *
87     * @param keyboard The keyboard that is being set to the keyboard view.
88     */
89    public void setKeyboard(final Keyboard keyboard) {
90        mKeyboard = keyboard;
91    }
92
93    private Key getKeyOf(final int virtualViewId) {
94        if (mKeyboard == null) {
95            return null;
96        }
97        final List<Key> sortedKeys = mKeyboard.getSortedKeys();
98        // Use a virtual view id as an index of the sorted keys list.
99        if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) {
100            return sortedKeys.get(virtualViewId);
101        }
102        return null;
103    }
104
105    private int getVirtualViewIdOf(final Key key) {
106        if (mKeyboard == null) {
107            return View.NO_ID;
108        }
109        final List<Key> sortedKeys = mKeyboard.getSortedKeys();
110        final int size = sortedKeys.size();
111        for (int index = 0; index < size; index++) {
112            if (sortedKeys.get(index) == key) {
113                // Use an index of the sorted keys list as a virtual view id.
114                return index;
115            }
116        }
117        return View.NO_ID;
118    }
119
120    /**
121     * Creates and populates an {@link AccessibilityEvent} for the specified key
122     * and event type.
123     *
124     * @param key A key on the host keyboard view.
125     * @param eventType The event type to create.
126     * @return A populated {@link AccessibilityEvent} for the key.
127     * @see AccessibilityEvent
128     */
129    public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) {
130        final int virtualViewId = getVirtualViewIdOf(key);
131        final String keyDescription = getKeyDescription(key);
132        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
133        event.setPackageName(mKeyboardView.getContext().getPackageName());
134        event.setClassName(key.getClass().getName());
135        event.setContentDescription(keyDescription);
136        event.setEnabled(true);
137        final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
138        record.setSource(mKeyboardView, virtualViewId);
139        return event;
140    }
141
142    /**
143     * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
144     * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
145     * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
146     * <p>
147     * A virtual descendant is an imaginary View that is reported as a part of
148     * the view hierarchy for accessibility purposes. This enables custom views
149     * that draw complex content to report them selves as a tree of virtual
150     * views, thus conveying their logical structure.
151     * </p>
152     * <p>
153     * The implementer is responsible for obtaining an accessibility node info
154     * from the pool of reusable instances and setting the desired properties of
155     * the node info before returning it.
156     * </p>
157     *
158     * @param virtualViewId A client defined virtual view id.
159     * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant or the host
160     * View.
161     * @see AccessibilityNodeInfoCompat
162     */
163    @Override
164    public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(final int virtualViewId) {
165        if (virtualViewId == UNDEFINED) {
166            return null;
167        }
168        if (virtualViewId == View.NO_ID) {
169            // We are requested to create an AccessibilityNodeInfo describing
170            // this View, i.e. the root of the virtual sub-tree.
171            final AccessibilityNodeInfoCompat rootInfo =
172                    AccessibilityNodeInfoCompat.obtain(mKeyboardView);
173            ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo);
174
175            // Add the virtual children of the root View.
176            final List<Key> sortedKeys = mKeyboard.getSortedKeys();
177            final int size = sortedKeys.size();
178            for (int index = 0; index < size; index++) {
179                final Key key = sortedKeys.get(index);
180                if (key.isSpacer()) {
181                    continue;
182                }
183                // Use an index of the sorted keys list as a virtual view id.
184                rootInfo.addChild(mKeyboardView, index);
185            }
186            return rootInfo;
187        }
188
189        // Find the key that corresponds to the given virtual view id.
190        final Key key = getKeyOf(virtualViewId);
191        if (key == null) {
192            Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
193            return null;
194        }
195        final String keyDescription = getKeyDescription(key);
196        final Rect boundsInParent = key.getHitBox();
197
198        // Calculate the key's in-screen bounds.
199        mTempBoundsInScreen.set(boundsInParent);
200        mTempBoundsInScreen.offset(
201                CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation));
202        final Rect boundsInScreen = mTempBoundsInScreen;
203
204        // Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
205        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
206        info.setPackageName(mKeyboardView.getContext().getPackageName());
207        info.setClassName(key.getClass().getName());
208        info.setContentDescription(keyDescription);
209        info.setBoundsInParent(boundsInParent);
210        info.setBoundsInScreen(boundsInScreen);
211        info.setParent(mKeyboardView);
212        info.setSource(mKeyboardView, virtualViewId);
213        info.setBoundsInScreen(boundsInScreen);
214        info.setEnabled(true);
215        info.setVisibleToUser(true);
216
217        if (mAccessibilityFocusedView == virtualViewId) {
218            info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
219        } else {
220            info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
221        }
222        return info;
223    }
224
225    @Override
226    public boolean performAction(final int virtualViewId, final int action,
227            final Bundle arguments) {
228        final Key key = getKeyOf(virtualViewId);
229        if (key == null) {
230            return false;
231        }
232        return performActionForKey(key, action, arguments);
233    }
234
235    /**
236     * Performs the specified accessibility action for the given key.
237     *
238     * @param key The on which to perform the action.
239     * @param action The action to perform.
240     * @param arguments The action's arguments.
241     * @return The result of performing the action, or false if the action is not supported.
242     */
243    boolean performActionForKey(final Key key, final int action, final Bundle arguments) {
244        final int virtualViewId = getVirtualViewIdOf(key);
245
246        switch (action) {
247        case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
248            if (mAccessibilityFocusedView == virtualViewId) {
249                return false;
250            }
251            mAccessibilityFocusedView = virtualViewId;
252            sendAccessibilityEventForKey(
253                    key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
254            return true;
255        case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
256            if (mAccessibilityFocusedView != virtualViewId) {
257                return false;
258            }
259            mAccessibilityFocusedView = UNDEFINED;
260            sendAccessibilityEventForKey(
261                    key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
262            return true;
263        default:
264            return false;
265        }
266    }
267
268    /**
269     * Sends an accessibility event for the given {@link Key}.
270     *
271     * @param key The key that's sending the event.
272     * @param eventType The type of event to send.
273     */
274    void sendAccessibilityEventForKey(final Key key, final int eventType) {
275        final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
276        mAccessibilityUtils.requestSendAccessibilityEvent(event);
277    }
278
279    /**
280     * Returns the context-specific description for a {@link Key}.
281     *
282     * @param key The key to describe.
283     * @return The context-specific description of the key.
284     */
285    private String getKeyDescription(final Key key) {
286        final EditorInfo editorInfo = mKeyboard.mId.mEditorInfo;
287        final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
288        final SettingsValues currentSettings = Settings.getInstance().getCurrent();
289        final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
290                mKeyboardView.getContext(), mKeyboard, key, shouldObscure);
291        if (currentSettings.isWordSeparator(key.getCode())) {
292            return mAccessibilityUtils.getAutoCorrectionDescription(
293                    keyCodeDescription, shouldObscure);
294        } else {
295            return keyCodeDescription;
296        }
297    }
298
299    /**
300     * Updates the parent's on-screen location.
301     */
302    private void updateParentLocation() {
303        mKeyboardView.getLocationOnScreen(mParentLocation);
304    }
305}
306