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