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