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