1 /* 2 * Copyright (C) 2011 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.content.Context; 20 import android.inputmethodservice.InputMethodService; 21 import android.support.v4.view.AccessibilityDelegateCompat; 22 import android.support.v4.view.ViewCompat; 23 import android.support.v4.view.accessibility.AccessibilityEventCompat; 24 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 25 import android.util.SparseIntArray; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewParent; 29 import android.view.accessibility.AccessibilityEvent; 30 31 import com.android.inputmethod.keyboard.Key; 32 import com.android.inputmethod.keyboard.Keyboard; 33 import com.android.inputmethod.keyboard.KeyboardId; 34 import com.android.inputmethod.keyboard.MainKeyboardView; 35 import com.android.inputmethod.keyboard.PointerTracker; 36 import com.android.inputmethod.latin.R; 37 38 public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { 39 private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy(); 40 41 /** Map of keyboard modes to resource IDs. */ 42 private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray(); 43 44 static { 45 KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date); 46 KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time); 47 KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email); 48 KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im); 49 KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number); 50 KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone); 51 KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text); 52 KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time); 53 KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url); 54 } 55 56 private InputMethodService mInputMethod; 57 private MainKeyboardView mView; 58 private AccessibilityEntityProvider mAccessibilityNodeProvider; 59 60 private Key mLastHoverKey = null; 61 62 /** 63 * Inset in pixels to look for keys when the user's finger exits the keyboard area. 64 */ 65 private int mEdgeSlop; 66 67 /** The most recently set keyboard mode. */ 68 private int mLastKeyboardMode; 69 70 public static void init(final InputMethodService inputMethod) { 71 sInstance.initInternal(inputMethod); 72 } 73 74 public static AccessibleKeyboardViewProxy getInstance() { 75 return sInstance; 76 } 77 78 private AccessibleKeyboardViewProxy() { 79 // Not publicly instantiable. 80 } 81 82 private void initInternal(final InputMethodService inputMethod) { 83 mInputMethod = inputMethod; 84 mEdgeSlop = inputMethod.getResources().getDimensionPixelSize( 85 R.dimen.accessibility_edge_slop); 86 } 87 88 /** 89 * Sets the view wrapped by this proxy. 90 * 91 * @param view The view to wrap. 92 */ 93 public void setView(final MainKeyboardView view) { 94 if (view == null) { 95 // Ignore null views. 96 return; 97 } 98 mView = view; 99 100 // Ensure that the view has an accessibility delegate. 101 ViewCompat.setAccessibilityDelegate(view, this); 102 103 if (mAccessibilityNodeProvider == null) { 104 return; 105 } 106 mAccessibilityNodeProvider.setView(view); 107 } 108 109 /** 110 * Called when the keyboard layout changes. 111 * <p> 112 * <b>Note:</b> This method will be called even if accessibility is not 113 * enabled. 114 */ 115 public void setKeyboard() { 116 if (mView == null) { 117 return; 118 } 119 if (mAccessibilityNodeProvider != null) { 120 mAccessibilityNodeProvider.setKeyboard(); 121 } 122 final int keyboardMode = mView.getKeyboard().mId.mMode; 123 124 // Since this method is called even when accessibility is off, make sure 125 // to check the state before announcing anything. Also, don't announce 126 // changes within the same mode. 127 if (AccessibilityUtils.getInstance().isAccessibilityEnabled() 128 && (mLastKeyboardMode != keyboardMode)) { 129 announceKeyboardMode(keyboardMode); 130 } 131 mLastKeyboardMode = keyboardMode; 132 } 133 134 /** 135 * Called when the keyboard is hidden and accessibility is enabled. 136 */ 137 public void onHideWindow() { 138 if (mView == null) { 139 return; 140 } 141 announceKeyboardHidden(); 142 mLastKeyboardMode = -1; 143 } 144 145 /** 146 * Announces which type of keyboard is being displayed. If the keyboard type 147 * is unknown, no announcement is made. 148 * 149 * @param mode The new keyboard mode. 150 */ 151 private void announceKeyboardMode(int mode) { 152 final int resId = KEYBOARD_MODE_RES_IDS.get(mode); 153 if (resId == 0) { 154 return; 155 } 156 final Context context = mView.getContext(); 157 final String keyboardMode = context.getString(resId); 158 final String text = context.getString(R.string.announce_keyboard_mode, keyboardMode); 159 sendWindowStateChanged(text); 160 } 161 162 /** 163 * Announces that the keyboard has been hidden. 164 */ 165 private void announceKeyboardHidden() { 166 final Context context = mView.getContext(); 167 final String text = context.getString(R.string.announce_keyboard_hidden); 168 169 sendWindowStateChanged(text); 170 } 171 172 /** 173 * Sends a window state change event with the specified text. 174 * 175 * @param text The text to send with the event. 176 */ 177 private void sendWindowStateChanged(final String text) { 178 final AccessibilityEvent stateChange = AccessibilityEvent.obtain( 179 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 180 mView.onInitializeAccessibilityEvent(stateChange); 181 stateChange.getText().add(text); 182 stateChange.setContentDescription(null); 183 184 final ViewParent parent = mView.getParent(); 185 if (parent != null) { 186 parent.requestSendAccessibilityEvent(mView, stateChange); 187 } 188 } 189 190 /** 191 * Proxy method for View.getAccessibilityNodeProvider(). This method is called in SDK 192 * version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual 193 * node hierarchy provider. 194 * 195 * @param host The host view for the provider. 196 * @return The accessibility node provider for the current keyboard. 197 */ 198 @Override 199 public AccessibilityEntityProvider getAccessibilityNodeProvider(final View host) { 200 if (mView == null) { 201 return null; 202 } 203 return getAccessibilityNodeProvider(); 204 } 205 206 /** 207 * Intercepts touch events before dispatch when touch exploration is turned on in ICS and 208 * higher. 209 * 210 * @param event The motion event being dispatched. 211 * @return {@code true} if the event is handled 212 */ 213 public boolean dispatchTouchEvent(final MotionEvent event) { 214 // To avoid accidental key presses during touch exploration, always drop 215 // touch events generated by the user. 216 return false; 217 } 218 219 /** 220 * Receives hover events when touch exploration is turned on in SDK versions ICS and higher. 221 * 222 * @param event The hover event. 223 * @return {@code true} if the event is handled 224 */ 225 public boolean dispatchHoverEvent(final MotionEvent event, final PointerTracker tracker) { 226 if (mView == null) { 227 return false; 228 } 229 230 final int x = (int) event.getX(); 231 final int y = (int) event.getY(); 232 final Key previousKey = mLastHoverKey; 233 final Key key; 234 235 if (pointInView(x, y)) { 236 key = tracker.getKeyOn(x, y); 237 } else { 238 key = null; 239 } 240 mLastHoverKey = key; 241 242 switch (event.getAction()) { 243 case MotionEvent.ACTION_HOVER_EXIT: 244 // Make sure we're not getting an EXIT event because the user slid 245 // off the keyboard area, then force a key press. 246 if (key != null) { 247 getAccessibilityNodeProvider().simulateKeyPress(key); 248 } 249 //$FALL-THROUGH$ 250 case MotionEvent.ACTION_HOVER_ENTER: 251 return onHoverKey(key, event); 252 case MotionEvent.ACTION_HOVER_MOVE: 253 if (key != previousKey) { 254 return onTransitionKey(key, previousKey, event); 255 } 256 return onHoverKey(key, event); 257 } 258 return false; 259 } 260 261 /** 262 * @return A lazily-instantiated node provider for this view proxy. 263 */ 264 private AccessibilityEntityProvider getAccessibilityNodeProvider() { 265 // Instantiate the provide only when requested. Since the system 266 // will call this method multiple times it is a good practice to 267 // cache the provider instance. 268 if (mAccessibilityNodeProvider == null) { 269 mAccessibilityNodeProvider = new AccessibilityEntityProvider(mView, mInputMethod); 270 } 271 return mAccessibilityNodeProvider; 272 } 273 274 /** 275 * Utility method to determine whether the given point, in local coordinates, is inside the 276 * view, where the area of the view is contracted by the edge slop factor. 277 * 278 * @param localX The local x-coordinate. 279 * @param localY The local y-coordinate. 280 */ 281 private boolean pointInView(final int localX, final int localY) { 282 return (localX >= mEdgeSlop) && (localY >= mEdgeSlop) 283 && (localX < (mView.getWidth() - mEdgeSlop)) 284 && (localY < (mView.getHeight() - mEdgeSlop)); 285 } 286 287 /** 288 * Simulates a transition between two {@link Key}s by sending a HOVER_EXIT on the previous key, 289 * a HOVER_ENTER on the current key, and a HOVER_MOVE on the current key. 290 * 291 * @param currentKey The currently hovered key. 292 * @param previousKey The previously hovered key. 293 * @param event The event that triggered the transition. 294 * @return {@code true} if the event was handled. 295 */ 296 private boolean onTransitionKey(final Key currentKey, final Key previousKey, 297 final MotionEvent event) { 298 final int savedAction = event.getAction(); 299 event.setAction(MotionEvent.ACTION_HOVER_EXIT); 300 onHoverKey(previousKey, event); 301 event.setAction(MotionEvent.ACTION_HOVER_ENTER); 302 onHoverKey(currentKey, event); 303 event.setAction(MotionEvent.ACTION_HOVER_MOVE); 304 final boolean handled = onHoverKey(currentKey, event); 305 event.setAction(savedAction); 306 return handled; 307 } 308 309 /** 310 * Handles a hover event on a key. If {@link Key} extended View, this would be analogous to 311 * calling View.onHoverEvent(MotionEvent). 312 * 313 * @param key The currently hovered key. 314 * @param event The hover event. 315 * @return {@code true} if the event was handled. 316 */ 317 private boolean onHoverKey(final Key key, final MotionEvent event) { 318 // Null keys can't receive events. 319 if (key == null) { 320 return false; 321 } 322 final AccessibilityEntityProvider provider = getAccessibilityNodeProvider(); 323 324 switch (event.getAction()) { 325 case MotionEvent.ACTION_HOVER_ENTER: 326 provider.sendAccessibilityEventForKey( 327 key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); 328 provider.performActionForKey( 329 key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); 330 break; 331 case MotionEvent.ACTION_HOVER_EXIT: 332 provider.sendAccessibilityEventForKey( 333 key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); 334 break; 335 } 336 return true; 337 } 338 339 /** 340 * Notifies the user of changes in the keyboard shift state. 341 */ 342 public void notifyShiftState() { 343 if (mView == null) { 344 return; 345 } 346 347 final Keyboard keyboard = mView.getKeyboard(); 348 final KeyboardId keyboardId = keyboard.mId; 349 final int elementId = keyboardId.mElementId; 350 final Context context = mView.getContext(); 351 final CharSequence text; 352 353 switch (elementId) { 354 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 355 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 356 text = context.getText(R.string.spoken_description_shiftmode_locked); 357 break; 358 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 359 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 360 case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: 361 text = context.getText(R.string.spoken_description_shiftmode_on); 362 break; 363 default: 364 text = context.getText(R.string.spoken_description_shiftmode_off); 365 } 366 AccessibilityUtils.getInstance().announceForAccessibility(mView, text); 367 } 368 369 /** 370 * Notifies the user of changes in the keyboard symbols state. 371 */ 372 public void notifySymbolsState() { 373 if (mView == null) { 374 return; 375 } 376 377 final Keyboard keyboard = mView.getKeyboard(); 378 final Context context = mView.getContext(); 379 final KeyboardId keyboardId = keyboard.mId; 380 final int elementId = keyboardId.mElementId; 381 final int resId; 382 383 switch (elementId) { 384 case KeyboardId.ELEMENT_ALPHABET: 385 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 386 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 387 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 388 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 389 resId = R.string.spoken_description_mode_alpha; 390 break; 391 case KeyboardId.ELEMENT_SYMBOLS: 392 case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: 393 resId = R.string.spoken_description_mode_symbol; 394 break; 395 case KeyboardId.ELEMENT_PHONE: 396 resId = R.string.spoken_description_mode_phone; 397 break; 398 case KeyboardId.ELEMENT_PHONE_SYMBOLS: 399 resId = R.string.spoken_description_mode_phone_shift; 400 break; 401 default: 402 resId = -1; 403 } 404 405 if (resId < 0) { 406 return; 407 } 408 final String text = context.getString(resId); 409 AccessibilityUtils.getInstance().announceForAccessibility(mView, text); 410 } 411 } 412