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.inputmethodservice.InputMethodService; 21 import android.os.Bundle; 22 import android.os.SystemClock; 23 import android.support.v4.view.ViewCompat; 24 import android.support.v4.view.accessibility.AccessibilityEventCompat; 25 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 26 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; 27 import android.support.v4.view.accessibility.AccessibilityRecordCompat; 28 import android.util.Log; 29 import android.util.SparseArray; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.accessibility.AccessibilityEvent; 33 import android.view.inputmethod.EditorInfo; 34 35 import com.android.inputmethod.keyboard.Key; 36 import com.android.inputmethod.keyboard.Keyboard; 37 import com.android.inputmethod.keyboard.KeyboardView; 38 import com.android.inputmethod.latin.settings.Settings; 39 import com.android.inputmethod.latin.settings.SettingsValues; 40 import com.android.inputmethod.latin.utils.CollectionUtils; 41 import com.android.inputmethod.latin.utils.CoordinateUtils; 42 43 /** 44 * Exposes a virtual view sub-tree for {@link KeyboardView} and generates 45 * {@link AccessibilityEvent}s for individual {@link Key}s. 46 * <p> 47 * A virtual sub-tree is composed of imaginary {@link View}s that are reported 48 * as a part of the view hierarchy for accessibility purposes. This enables 49 * custom views that draw complex content to report them selves as a tree of 50 * virtual views, thus conveying their logical structure. 51 * </p> 52 */ 53 public final class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat { 54 private static final String TAG = AccessibilityEntityProvider.class.getSimpleName(); 55 private static final int UNDEFINED = Integer.MIN_VALUE; 56 57 private final InputMethodService mInputMethodService; 58 private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper; 59 private final AccessibilityUtils mAccessibilityUtils; 60 61 /** A map of integer IDs to {@link Key}s. */ 62 private final SparseArray<Key> mVirtualViewIdToKey = CollectionUtils.newSparseArray(); 63 64 /** Temporary rect used to calculate in-screen bounds. */ 65 private final Rect mTempBoundsInScreen = new Rect(); 66 67 /** The parent view's cached on-screen location. */ 68 private final int[] mParentLocation = CoordinateUtils.newInstance(); 69 70 /** The virtual view identifier for the focused node. */ 71 private int mAccessibilityFocusedView = UNDEFINED; 72 73 /** The current keyboard view. */ 74 private KeyboardView mKeyboardView; 75 76 public AccessibilityEntityProvider(final KeyboardView keyboardView, 77 final InputMethodService inputMethod) { 78 mInputMethodService = inputMethod; 79 mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance(); 80 mAccessibilityUtils = AccessibilityUtils.getInstance(); 81 setView(keyboardView); 82 } 83 84 /** 85 * Sets the keyboard view represented by this node provider. 86 * 87 * @param keyboardView The keyboard view to represent. 88 */ 89 public void setView(final KeyboardView keyboardView) { 90 mKeyboardView = keyboardView; 91 updateParentLocation(); 92 93 // Since this class is constructed lazily, we might not get a subsequent 94 // call to setKeyboard() and therefore need to call it now. 95 setKeyboard(); 96 } 97 98 /** 99 * Sets the keyboard represented by this node provider. 100 */ 101 public void setKeyboard() { 102 assignVirtualViewIds(); 103 } 104 105 /** 106 * Creates and populates an {@link AccessibilityEvent} for the specified key 107 * and event type. 108 * 109 * @param key A key on the host keyboard view. 110 * @param eventType The event type to create. 111 * @return A populated {@link AccessibilityEvent} for the key. 112 * @see AccessibilityEvent 113 */ 114 public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) { 115 final int virtualViewId = generateVirtualViewIdForKey(key); 116 final String keyDescription = getKeyDescription(key); 117 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 118 event.setPackageName(mKeyboardView.getContext().getPackageName()); 119 event.setClassName(key.getClass().getName()); 120 event.setContentDescription(keyDescription); 121 event.setEnabled(true); 122 final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event); 123 record.setSource(mKeyboardView, virtualViewId); 124 return event; 125 } 126 127 /** 128 * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual 129 * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or 130 * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}. 131 * <p> 132 * A virtual descendant is an imaginary View that is reported as a part of 133 * the view hierarchy for accessibility purposes. This enables custom views 134 * that draw complex content to report them selves as a tree of virtual 135 * views, thus conveying their logical structure. 136 * </p> 137 * <p> 138 * The implementer is responsible for obtaining an accessibility node info 139 * from the pool of reusable instances and setting the desired properties of 140 * the node info before returning it. 141 * </p> 142 * 143 * @param virtualViewId A client defined virtual view id. 144 * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant or the host 145 * View. 146 * @see AccessibilityNodeInfoCompat 147 */ 148 @Override 149 public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(final int virtualViewId) { 150 if (virtualViewId == UNDEFINED) { 151 return null; 152 } 153 if (virtualViewId == View.NO_ID) { 154 // We are requested to create an AccessibilityNodeInfo describing 155 // this View, i.e. the root of the virtual sub-tree. 156 final AccessibilityNodeInfoCompat rootInfo = 157 AccessibilityNodeInfoCompat.obtain(mKeyboardView); 158 ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo); 159 160 // Add the virtual children of the root View. 161 final Keyboard keyboard = mKeyboardView.getKeyboard(); 162 final Key[] keys = keyboard.getKeys(); 163 for (Key key : keys) { 164 final int childVirtualViewId = generateVirtualViewIdForKey(key); 165 rootInfo.addChild(mKeyboardView, childVirtualViewId); 166 } 167 return rootInfo; 168 } 169 170 // Find the view that corresponds to the given id. 171 final Key key = mVirtualViewIdToKey.get(virtualViewId); 172 if (key == null) { 173 Log.e(TAG, "Invalid virtual view ID: " + virtualViewId); 174 return null; 175 } 176 final String keyDescription = getKeyDescription(key); 177 final Rect boundsInParent = key.getHitBox(); 178 179 // Calculate the key's in-screen bounds. 180 mTempBoundsInScreen.set(boundsInParent); 181 mTempBoundsInScreen.offset( 182 CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation)); 183 final Rect boundsInScreen = mTempBoundsInScreen; 184 185 // Obtain and initialize an AccessibilityNodeInfo with information about the virtual view. 186 final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); 187 info.setPackageName(mKeyboardView.getContext().getPackageName()); 188 info.setClassName(key.getClass().getName()); 189 info.setContentDescription(keyDescription); 190 info.setBoundsInParent(boundsInParent); 191 info.setBoundsInScreen(boundsInScreen); 192 info.setParent(mKeyboardView); 193 info.setSource(mKeyboardView, virtualViewId); 194 info.setBoundsInScreen(boundsInScreen); 195 info.setEnabled(true); 196 info.setVisibleToUser(true); 197 198 if (mAccessibilityFocusedView == virtualViewId) { 199 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 200 } else { 201 info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); 202 } 203 return info; 204 } 205 206 /** 207 * Simulates a key press by injecting touch events into the keyboard view. 208 * This avoids the complexity of trackers and listeners within the keyboard. 209 * 210 * @param key The key to press. 211 */ 212 void simulateKeyPress(final Key key) { 213 final int x = key.getHitBox().centerX(); 214 final int y = key.getHitBox().centerY(); 215 final long downTime = SystemClock.uptimeMillis(); 216 final MotionEvent downEvent = MotionEvent.obtain( 217 downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0); 218 final MotionEvent upEvent = MotionEvent.obtain( 219 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0); 220 221 mKeyboardView.onTouchEvent(downEvent); 222 mKeyboardView.onTouchEvent(upEvent); 223 downEvent.recycle(); 224 upEvent.recycle(); 225 } 226 227 @Override 228 public boolean performAction(final int virtualViewId, final int action, 229 final Bundle arguments) { 230 final Key key = mVirtualViewIdToKey.get(virtualViewId); 231 if (key == null) { 232 return false; 233 } 234 return performActionForKey(key, action, arguments); 235 } 236 237 /** 238 * Performs the specified accessibility action for the given key. 239 * 240 * @param key The on which to perform the action. 241 * @param action The action to perform. 242 * @param arguments The action's arguments. 243 * @return The result of performing the action, or false if the action is not supported. 244 */ 245 boolean performActionForKey(final Key key, final int action, final Bundle arguments) { 246 final int virtualViewId = generateVirtualViewIdForKey(key); 247 248 switch (action) { 249 case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: 250 if (mAccessibilityFocusedView == virtualViewId) { 251 return false; 252 } 253 mAccessibilityFocusedView = virtualViewId; 254 sendAccessibilityEventForKey( 255 key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 256 return true; 257 case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: 258 if (mAccessibilityFocusedView != virtualViewId) { 259 return false; 260 } 261 mAccessibilityFocusedView = UNDEFINED; 262 sendAccessibilityEventForKey( 263 key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 264 return true; 265 default: 266 return false; 267 } 268 } 269 270 /** 271 * Sends an accessibility event for the given {@link Key}. 272 * 273 * @param key The key that's sending the event. 274 * @param eventType The type of event to send. 275 */ 276 void sendAccessibilityEventForKey(final Key key, final int eventType) { 277 final AccessibilityEvent event = createAccessibilityEvent(key, eventType); 278 mAccessibilityUtils.requestSendAccessibilityEvent(event); 279 } 280 281 /** 282 * Returns the context-specific description for a {@link Key}. 283 * 284 * @param key The key to describe. 285 * @return The context-specific description of the key. 286 */ 287 private String getKeyDescription(final Key key) { 288 final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); 289 final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo); 290 final SettingsValues currentSettings = Settings.getInstance().getCurrent(); 291 final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey( 292 mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure); 293 if (currentSettings.isWordSeparator(key.getCode())) { 294 return mAccessibilityUtils.getAutoCorrectionDescription( 295 keyCodeDescription, shouldObscure); 296 } else { 297 return keyCodeDescription; 298 } 299 } 300 301 /** 302 * Assigns virtual view IDs to keyboard keys and populates the related maps. 303 */ 304 private void assignVirtualViewIds() { 305 final Keyboard keyboard = mKeyboardView.getKeyboard(); 306 if (keyboard == null) { 307 return; 308 } 309 mVirtualViewIdToKey.clear(); 310 311 final Key[] keys = keyboard.getKeys(); 312 for (Key key : keys) { 313 final int virtualViewId = generateVirtualViewIdForKey(key); 314 mVirtualViewIdToKey.put(virtualViewId, key); 315 } 316 } 317 318 /** 319 * Updates the parent's on-screen location. 320 */ 321 private void updateParentLocation() { 322 mKeyboardView.getLocationOnScreen(mParentLocation); 323 } 324 325 /** 326 * Generates a virtual view identifier for the given key. Returned 327 * identifiers are valid until the next global layout state change. 328 * 329 * @param key The key to identify. 330 * @return A virtual view identifier. 331 */ 332 private static int generateVirtualViewIdForKey(final Key key) { 333 // The key x- and y-coordinates are stable between layout changes. 334 // Generate an identifier by bit-shifting the x-coordinate to the 335 // left-half of the integer and OR'ing with the y-coordinate. 336 return ((0xFFFF & key.getX()) << (Integer.SIZE / 2)) | (0xFFFF & key.getY()); 337 } 338 } 339