/ime/aimelib/src/com/google/android/marvin/aime/AccessibleInputMethodService.java

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