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