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 
17 package com.android.inputmethod.accessibility;
18 
19 import android.graphics.Rect;
20 import android.os.Bundle;
21 import android.support.v4.view.ViewCompat;
22 import android.support.v4.view.accessibility.AccessibilityEventCompat;
23 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
24 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
25 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
26 import android.util.Log;
27 import android.view.View;
28 import android.view.accessibility.AccessibilityEvent;
29 import android.view.inputmethod.EditorInfo;
30 
31 import com.android.inputmethod.keyboard.Key;
32 import com.android.inputmethod.keyboard.Keyboard;
33 import com.android.inputmethod.keyboard.KeyboardView;
34 import com.android.inputmethod.latin.common.CoordinateUtils;
35 import com.android.inputmethod.latin.settings.Settings;
36 import com.android.inputmethod.latin.settings.SettingsValues;
37 
38 import 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  */
50 final 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 
KeyboardAccessibilityNodeProvider(final KV keyboardView, final KeyboardAccessibilityDelegate<KV> delegate)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      */
setKeyboard(final Keyboard keyboard)98     public void setKeyboard(final Keyboard keyboard) {
99         mKeyboard = keyboard;
100     }
101 
getKeyOf(final int virtualViewId)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 
getVirtualViewIdOf(final Key key)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      */
createAccessibilityEvent(final Key key, final int eventType)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 
onHoverEnterTo(final Key key)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 
onHoverExitFrom(final Key key)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
createAccessibilityNodeInfo(final int virtualViewId)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
performAction(final int virtualViewId, final int action, final Bundle arguments)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      */
performActionForKey(final Key key, final int action)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      */
sendAccessibilityEventForKey(final Key key, final int eventType)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      */
getKeyDescription(final Key key)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         }
333         return keyCodeDescription;
334     }
335 
336     /**
337      * Updates the parent's on-screen location.
338      */
updateParentLocation()339     private void updateParentLocation() {
340         mKeyboardView.getLocationOnScreen(mParentLocation);
341     }
342 }
343