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