/ime/aimelib/src/com/google/android/marvin/aime/AccessibleInputMethodService.java
Java | 647 lines | 329 code | 93 blank | 225 comment | 73 complexity | fd6d76123e3ae073e63408e650f8ebe3 MD5 | raw file
1/* 2 * Copyright (C) 2010 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.google.android.marvin.aime; 18 19import com.google.android.marvin.aime.usercommands.UserCommandHandler; 20 21import android.content.res.Configuration; 22import android.content.res.Resources; 23import android.database.ContentObserver; 24import android.inputmethodservice.InputMethodService; 25import android.os.Handler; 26import android.provider.Settings; 27import android.view.KeyEvent; 28import android.view.MotionEvent; 29import android.view.accessibility.AccessibilityManager; 30import android.view.inputmethod.EditorInfo; 31import android.view.inputmethod.InputConnection; 32 33import java.util.Locale; 34 35/** 36 * Accessible InputMethodService. Provides handle to 37 * {@link AccessibleInputConnection}, for powerful navigation capabilities. It 38 * also fires accessibility events. Extend this class to make your IME 39 * accessible. It overrides default behaviour of trackball and dpad for 40 * improving accessibility.<br> 41 * <br> 42 * Call {@link #setGranularity(int)} and {@link #setAction(int)}, everytime 43 * current granularity or action of IME changes, to reflect change in trackball 44 * and dpad behavior.<br> 45 * Default <code>granularity</code> is {@link TextNavigation#GRANULARITY_CHAR} 46 * and default <code>action</code> is {@link TextNavigation#ACTION_MOVE} for 47 * trackball and dpad motion. 48 * 49 * @author hiteshk@google.com (Hitesh Khandelwal) 50 * @author alanv@google.com (Alan Viverette) 51 */ 52public abstract class AccessibleInputMethodService extends InputMethodService { 53 /** Whether the trackball can be used to control granularity. */ 54 private static final boolean ENABLE_TRACKBALL = false; 55 56 /** List of characters ignored by word iterator. */ 57 private final char[] ignoredCharForWords = { 58 ' ' 59 }; 60 61 /** String to speak when granularity changes. */ 62 private String mGranularitySet; 63 64 /** String to speak when ALT key is pressed. */ 65 private String mAltString; 66 67 /** String to speak when SHIFT key is pressed. */ 68 private String mShiftString; 69 70 /** Strings used to describe granularity changes. */ 71 private String[] mGranularityTypes; 72 73 /** String to speak when action changes. */ 74 private String mActionSet; 75 76 /** Strings used to describe action changes. */ 77 private String[] mActionTypes; 78 79 /** Current granularity (unit type). */ 80 private int mGranularity = TextNavigation.GRANULARITY_CHAR; 81 82 /** Current action set. */ 83 private int mAction = TextNavigation.ACTION_MOVE; 84 85 /** Handle to AccessibleInputConnection. */ 86 private AccessibleInputConnection mAIC = null; 87 88 /** Handle to base InputConnection. */ 89 private InputConnection mIC = null; 90 91 /** Stored key down event. */ 92 private KeyEvent mPreviousDpadDownEvent; 93 94 /** Stored meta key down event. */ 95 private KeyEvent mPreviousMetaDownEvent; 96 97 /** Whether accessibility is enabled. */ 98 private boolean mAccessibilityEnabled; 99 100 /** Whether KEYCODE_UP or KEYCODE_DOWN was just pressed. */ 101 private boolean mWasUpDownPressed; 102 103 private UserCommandHandler mUserCommandHandler; 104 private AccessibilityManager mAccessibilityManager; 105 106 @Override 107 public void onCreate() { 108 super.onCreate(); 109 110 final Resources res = getResources(); 111 mGranularityTypes = res.getStringArray(R.array.granularity_types); 112 mGranularitySet = res.getString(R.string.set_granularity); 113 mActionTypes = res.getStringArray(R.array.action_types); 114 mActionSet = res.getString(R.string.set_action); 115 mAltString = res.getString(R.string.alt_pressed); 116 mShiftString = res.getString(R.string.shift_pressed); 117 118 mPreviousDpadDownEvent = null; 119 mWasUpDownPressed = false; 120 121 mUserCommandHandler = new UserCommandHandler(this); 122 mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE); 123 124 // Register content observer to receive accessibility status changes. 125 getContentResolver().registerContentObserver( 126 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_ENABLED), false, 127 mAccessibilityObserver); 128 129 updateAccessibilityState(); 130 } 131 132 @Override 133 public void onDestroy() { 134 super.onDestroy(); 135 136 mUserCommandHandler.release(); 137 } 138 139 /** 140 * Returns an {@link AccessibleInputConnection} bound to the current 141 * {@link InputConnection}. 142 * 143 * @return an instance of AccessibleInputConnection 144 */ 145 @Override 146 public AccessibleInputConnection getCurrentInputConnection() { 147 InputConnection currentIC = super.getCurrentInputConnection(); 148 149 if (currentIC == null) { 150 mAIC = null; 151 return null; 152 } 153 154 if (mAIC == null || (mIC != null && mIC != currentIC)) { 155 mIC = currentIC; 156 mAIC = new AccessibleInputConnection(this, mIC, true, ignoredCharForWords); 157 } 158 159 return mAIC; 160 } 161 162 @Override 163 public void onStartInputView(EditorInfo info, boolean restarting) { 164 super.onStartInputView(info, restarting); 165 166 if (mAccessibilityManager.isEnabled()) { 167 mAccessibilityManager.interrupt(); 168 } 169 } 170 171 @Override 172 public void onFinishInput() { 173 super.onFinishInput(); 174 175 // Reset state when leaving input field. 176 mWasUpDownPressed = false; 177 } 178 179 /** 180 * Overrides default trackball behavior: 181 * <ul> 182 * <li>Up/down: Increases/decreases text navigation granularity</li> 183 * <li>Left/right: Moves to previous/next unit of text</li> 184 * </ul> 185 * <br> 186 * Moving the trackball far to the left or right results in moving by 187 * multiple units. 188 * <p> 189 * If one of the following conditions is met, default behavior is preserved: 190 * <ul> 191 * <li>No input connection available</li> 192 * <li>Input view is hidden</li> 193 * <li>Not currently editing text</li> 194 * <li>Cannot move in the specified direction</li> 195 * </ul> 196 * </p> 197 */ 198 @SuppressWarnings("unused") 199 @Override 200 public boolean onTrackballEvent(MotionEvent event) { 201 if (!ENABLE_TRACKBALL) 202 return false; 203 204 AccessibleInputConnection aic = getCurrentInputConnection(); 205 if (aic == null || !isInputViewShown()) { 206 return super.onTrackballEvent(event); 207 } 208 209 float x = event.getX(); 210 float absX = Math.abs(event.getX()); 211 float y = event.getY(); 212 float absY = Math.abs(event.getY()); 213 214 if (absY > 2 * absX && absY >= 0.75) { 215 // Up and down switch granularities, but this is less common so it's 216 // less sensitive. 217 218 if (y < 0) { 219 adjustGranularity(1); 220 } else { 221 adjustGranularity(-1); 222 } 223 } else { 224 // If they moved the trackball really far, move by more than one but 225 // only announce for the last move. 226 227 int count = Math.max(1, (int) Math.floor(absX + 0.25)); 228 boolean isNext = (x > 0); 229 boolean isShiftPressed = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0; 230 231 moveUnit(mGranularity, count, isNext, isShiftPressed); 232 } 233 234 return true; 235 } 236 237 /** 238 * Overrides default directional pad behavior: 239 * <ul> 240 * <li>Up/down: Increases/decreases text navigation granularity</li> 241 * <li>Left/right: Moves to previous/next unit of text</li> 242 * </ul> 243 * <p> 244 * If one of the following conditions is met, default behavior is preserved: 245 * <ul> 246 * <li>No input connection available</li> 247 * <li>Input view is hidden</li> 248 * <li>Not currently editing text</li> 249 * <li>Cannot move in the specified direction</li> 250 * </ul> 251 * </p> 252 */ 253 @Override 254 public boolean onKeyUp(int keyCode, KeyEvent event) { 255 if (mUserCommandHandler.onKeyUp(event)) { 256 return true; 257 } 258 259 final AccessibleInputConnection aic = getCurrentInputConnection(); 260 if (aic == null || !aic.hasExtractedText()) { 261 return super.onKeyUp(keyCode, event); 262 } 263 264 final KeyEvent downEvent = mPreviousDpadDownEvent; 265 mPreviousDpadDownEvent = null; 266 267 final KeyEvent metaDownEvent = mPreviousMetaDownEvent; 268 mPreviousMetaDownEvent = null; 269 270 if (downEvent != null) { 271 boolean captureEvent = false; 272 273 switch (downEvent.getKeyCode()) { 274 case KeyEvent.KEYCODE_DPAD_LEFT: 275 if (!event.isAltPressed()) { 276 captureEvent = previousUnit(mGranularity, 1, event.isShiftPressed()); 277 } else { 278 mWasUpDownPressed = true; 279 } 280 break; 281 case KeyEvent.KEYCODE_DPAD_RIGHT: 282 if (!event.isAltPressed()) { 283 captureEvent = nextUnit(mGranularity, 1, event.isShiftPressed()); 284 } else { 285 mWasUpDownPressed = true; 286 } 287 break; 288 case KeyEvent.KEYCODE_DPAD_UP: 289 if (event.isAltPressed()) { 290 adjustGranularity(1); 291 captureEvent = true; 292 } else { 293 mWasUpDownPressed = true; 294 } 295 break; 296 case KeyEvent.KEYCODE_DPAD_DOWN: 297 if (event.isAltPressed()) { 298 adjustGranularity(-1); 299 captureEvent = true; 300 } else { 301 mWasUpDownPressed = true; 302 } 303 break; 304 } 305 306 if (captureEvent) { 307 return true; 308 } 309 } 310 311 // If we didn't capture the meta event, attempt to send the previous 312 // meta down event and then preserve default behavior. 313 if (metaDownEvent != null) { 314 if (!super.onKeyDown(metaDownEvent.getKeyCode(), metaDownEvent)) { 315 aic.sendKeyEvent(metaDownEvent); 316 } 317 } 318 319 // If we didn't capture the event, attempt to send the previous down 320 // event and then preserve default behavior. 321 if (downEvent != null) { 322 if (!super.onKeyDown(downEvent.getKeyCode(), downEvent)) { 323 aic.sendKeyEvent(downEvent); 324 } 325 } 326 327 if (!super.onKeyUp(keyCode, event)) { 328 aic.sendKeyEvent(event); 329 } 330 331 return true; 332 } 333 334 /** 335 * Captures and stores directional pad events. If onKeyUp() preserves 336 * default behavior, the original down event will be released. 337 */ 338 @Override 339 public boolean onKeyDown(int keyCode, KeyEvent event) { 340 if (mUserCommandHandler.onKeyDown(event)) { 341 return true; 342 } 343 344 AccessibleInputConnection aic = getCurrentInputConnection(); 345 if (aic == null || !aic.hasExtractedText()) { 346 return super.onKeyDown(keyCode, event); 347 } 348 349 // If we've captured a meta key, capture all subsequent keys. 350 if (mPreviousMetaDownEvent != null) { 351 mPreviousDpadDownEvent = event; 352 return true; 353 } 354 355 switch (event.getKeyCode()) { 356 case KeyEvent.KEYCODE_DPAD_DOWN: 357 case KeyEvent.KEYCODE_DPAD_UP: 358 case KeyEvent.KEYCODE_DPAD_LEFT: 359 case KeyEvent.KEYCODE_DPAD_RIGHT: 360 mPreviousDpadDownEvent = event; 361 return true; 362 case KeyEvent.KEYCODE_ALT_LEFT: 363 case KeyEvent.KEYCODE_ALT_RIGHT: { 364 mAIC.trySendAccessiblityEvent(mAltString); 365 mPreviousMetaDownEvent = event; 366 return true; 367 } 368 case KeyEvent.KEYCODE_SHIFT_LEFT: 369 case KeyEvent.KEYCODE_SHIFT_RIGHT: { 370 mAIC.trySendAccessiblityEvent(mShiftString); 371 mPreviousMetaDownEvent = event; 372 return true; 373 } 374 default: 375 return super.onKeyDown(keyCode, event); 376 } 377 } 378 379 /** 380 * Moves forward <code>count</code> units using the current granularity and 381 * action. Returns <code>true</code> if successful. Moving can fail if the 382 * carat is already at the end of the text or if there is no available input 383 * connection. 384 * 385 * @param granularity The granularity with which to move. 386 * @param count The number of units to move. 387 * @param isShiftPressed <code>true</code> if the shift key is pressed. 388 * @return <code>true</code> if successful. 389 * @see AccessibleInputMethodService#setGranularity(int) 390 * @see AccessibleInputMethodService#setAction(int) 391 */ 392 protected boolean nextUnit(int granularity, int count, boolean isShiftPressed) { 393 return moveUnit(granularity, count, true, isShiftPressed); 394 } 395 396 /** 397 * Moves backward <code>count</code> units using the current granularity and 398 * action. Returns <code>true</code> if successful. Moving can fail if the 399 * carat is already at the beginning of the text or if there is no available 400 * input connection. 401 * 402 * @param granularity The granularity with which to move. 403 * @param count The number of units to move. 404 * @param isShiftPressed <code>true</code> if the shift key is pressed. 405 * @return <code>true</code> if successful. 406 * @see AccessibleInputMethodService#setGranularity(int) 407 * @see AccessibleInputMethodService#setAction(int) 408 */ 409 protected boolean previousUnit(int granularity, int count, boolean isShiftPressed) { 410 return moveUnit(granularity, count, false, isShiftPressed); 411 } 412 413 /** 414 * Moves <code>count</code> units in the specified direction using the 415 * current granularity and action. 416 * 417 * @param count The number of units to move. 418 * @param forward <code>true</code> to move <code>count</code> units 419 * forward, <code>false</code> to move backward. 420 * @param isShiftPressed <code>true</code> if the shift key is pressed. 421 * @return <code>true</code> if successful or <code>false</code> if no input 422 * connection was available or the movement failed 423 */ 424 private boolean moveUnit(int granularity, int count, boolean forward, boolean isShiftPressed) { 425 // If the input connection is null or count is 0, no-op. 426 AccessibleInputConnection inputConnection = getCurrentInputConnection(); 427 if (inputConnection == null || !inputConnection.hasExtractedText()) { 428 return false; 429 } else if (count == 0) { 430 return true; 431 } 432 433 // If the shift key is held down, force ACTION_EXTEND mode. 434 int action = (isShiftPressed ? TextNavigation.ACTION_EXTEND : mAction); 435 436 // Disable sending accessibility events while we send multiple events, 437 // then announce only the last event. 438 boolean savedSendAccessibilityEvents = inputConnection.isSendAccessibilityEvents(); 439 inputConnection.setSendAccessibilityEvents(false); 440 for (int i = 0; i < count - 1; i++) { 441 if (forward) { 442 inputConnection.next(granularity, action); 443 } else { 444 inputConnection.previous(granularity, action); 445 } 446 } 447 inputConnection.setSendAccessibilityEvents(savedSendAccessibilityEvents); 448 449 // Obtain the new position from the final event. If the position is 450 // null, we failed to move and should return false. 451 Position newPosition = null; 452 if (forward) { 453 newPosition = inputConnection.next(granularity, action); 454 } else { 455 newPosition = inputConnection.previous(mGranularity, action); 456 } 457 return (newPosition != null); 458 } 459 460 /** 461 * Adjusts granularity up or down. Returns <code>true</code> if granularity 462 * is set to a different value. Wraps around if granularity is already at 463 * the minimum or maximum setting. 464 * 465 * @param direction The direction in which to change granularity. 466 * @return <code>true</code> if granularity is set to a different value. 467 * @see TextNavigation#NUM_GRANULARITY_TYPES 468 */ 469 protected boolean adjustGranularity(int direction) { 470 int oldGranularity = getGranularity(); 471 int granularity = oldGranularity + direction; 472 473 if (granularity < 0) { 474 granularity += TextNavigation.NUM_GRANULARITY_TYPES; 475 } else if (granularity >= TextNavigation.NUM_GRANULARITY_TYPES) { 476 granularity -= TextNavigation.NUM_GRANULARITY_TYPES; 477 } 478 479 setGranularity(granularity); 480 481 return (oldGranularity != granularity); 482 } 483 484 /** 485 * Sets granularity (unit type) for text navigation. 486 * 487 * @param granularity Value could be {@link TextNavigation#GRANULARITY_CHAR} 488 * , {@link TextNavigation#GRANULARITY_WORD}, 489 * {@link TextNavigation#GRANULARITY_SENTENCE}, 490 * {@link TextNavigation#GRANULARITY_PARAGRAPH} or 491 * {@link TextNavigation#GRANULARITY_ENTIRE_TEXT} 492 * @return <code>true</code> if granularity changed 493 */ 494 public boolean setGranularity(int granularity) { 495 AccessibleInputConnection.checkValidGranularity(granularity); 496 String speak = String.format(mGranularitySet, mGranularityTypes[granularity]); 497 getCurrentInputConnection().trySendAccessiblityEvent(speak); 498 499 if (mGranularity == granularity) { 500 return false; 501 } 502 503 mGranularity = granularity; 504 onGranularityChanged(granularity); 505 506 return true; 507 } 508 509 /** 510 * Returns the current granularity. 511 * 512 * @return the current granularity 513 * @see AccessibleInputMethodService#setGranularity(int) 514 */ 515 public int getGranularity() { 516 return mGranularity; 517 } 518 519 /** 520 * Callback for change in granularity. Override this method to update any 521 * internal state or GUI of IME. 522 * 523 * @param granularity The type of granularity. 524 * @see TextNavigation#GRANULARITY_CHAR 525 * @see TextNavigation#GRANULARITY_WORD 526 * @see TextNavigation#GRANULARITY_SENTENCE 527 * @see TextNavigation#GRANULARITY_PARAGRAPH 528 * @see TextNavigation#GRANULARITY_ENTIRE_TEXT 529 */ 530 public void onGranularityChanged(int granularity) { 531 } 532 533 /** 534 * Sets action to be performed on the current selection. 535 * 536 * @param action Value could be either {@link TextNavigation#ACTION_MOVE} or 537 * {@link TextNavigation#ACTION_EXTEND}. 538 */ 539 public boolean setAction(int action) { 540 AccessibleInputConnection.checkValidAction(action); 541 542 if (mAction == action) { 543 return false; 544 } 545 546 mAction = action; 547 String speak = String.format(mActionSet, mActionTypes[action]); 548 getCurrentInputConnection().trySendAccessiblityEvent(speak); 549 onActionChanged(action); 550 return true; 551 } 552 553 /** 554 * Returns the current action. 555 * 556 * @return the current action 557 * @see AccessibleInputMethodService#setAction(int) 558 */ 559 public int getAction() { 560 return mAction; 561 } 562 563 /** 564 * Callback for change in action. Override this method to update any 565 * internal state or GUI of IME. 566 * 567 * @param action The type of action. 568 * @see TextNavigation#ACTION_MOVE 569 * @see TextNavigation#ACTION_EXTEND 570 */ 571 public void onActionChanged(int action) { 572 } 573 574 /** 575 * Returns whether accessibility is enabled. 576 * 577 * @return whether accessibility is enabled 578 */ 579 protected boolean isAccessibilityEnabled() { 580 return mAccessibilityEnabled; 581 } 582 583 /** 584 * Updates the current accessibility enabled state. 585 */ 586 private void updateAccessibilityState() { 587 mAccessibilityEnabled = (Settings.Secure.getInt(getContentResolver(), 588 Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1); 589 590 // Reset text navigation when accessibility is disabled. 591 if (!mAccessibilityEnabled) { 592 mAction = TextNavigation.ACTION_MOVE; 593 mGranularity = TextNavigation.GRANULARITY_CHAR; 594 } 595 } 596 597 /** 598 * Callback for change in accessibility enabled state. 599 * 600 * @param accessibilityEnabled 601 */ 602 protected void onAccessibilityChanged(boolean accessibilityEnabled) { 603 // Placeholder. 604 } 605 606 @Override 607 public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, 608 int candidatesStart, int candidatesEnd) { 609 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, 610 candidatesEnd); 611 612 if (mWasUpDownPressed) { 613 mWasUpDownPressed = false; 614 615 android.util.Log.e("AIME", "updown was pressed, speaking"); 616 mAIC.speakCurrentUnit(mGranularity); 617 } 618 } 619 620 /** 621 * This handler is used by the {@link ContentObserver} below. 622 */ 623 private final Handler mHandler = new Handler(); 624 625 /** 626 * This observer listens for changes in the accessibility enabled state. 627 */ 628 private final ContentObserver mAccessibilityObserver = new ContentObserver(mHandler) { 629 @Override 630 public void onChange(boolean selfChange) { 631 if (selfChange) { 632 return; 633 } 634 635 updateAccessibilityState(); 636 637 // Force a configuration change. 638 Configuration newConfig = new Configuration(); 639 newConfig.setToDefaults(); 640 newConfig.locale = Locale.getDefault(); 641 Settings.System.getConfiguration(getContentResolver(), newConfig); 642 onConfigurationChanged(newConfig); 643 644 onAccessibilityChanged(mAccessibilityEnabled); 645 } 646 }; 647}