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

http://eyes-free.googlecode.com/ · Java · 752 lines · 453 code · 90 blank · 209 comment · 162 complexity · de99c397a17eec86fa71718457fd5807 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 android.content.Context;
  18. import android.graphics.Rect;
  19. import android.os.Parcelable;
  20. import android.os.SystemClock;
  21. import android.text.TextUtils;
  22. import android.util.Log;
  23. import android.view.accessibility.AccessibilityEvent;
  24. import android.view.accessibility.AccessibilityManager;
  25. import android.view.inputmethod.ExtractedText;
  26. import android.view.inputmethod.ExtractedTextRequest;
  27. import android.view.inputmethod.InputConnection;
  28. import android.view.inputmethod.InputConnectionWrapper;
  29. import java.text.BreakIterator;
  30. import java.util.HashSet;
  31. import java.util.Locale;
  32. /**
  33. * Basic implementation of TextNavigation. Also sends events to accesibility framework.
  34. *
  35. * @author hiteshk@google.com (Hitesh Khandelwal)
  36. */
  37. public class AccessibleInputConnection extends InputConnectionWrapper implements TextNavigation {
  38. /** Tag used for logging. */
  39. private static final String TAG = "AccessibleInputConnection";
  40. /** Debug flag. Set this to {@code false} for release. */
  41. private static final boolean DEBUG = false;
  42. /** Flag for navigating to next unit. */
  43. private static final int NAVIGATE_NEXT = 1;
  44. /** Flag for navigating to previous unit. */
  45. private static final int NAVIGATE_PREVIOUS = 2;
  46. /**
  47. * This is an arbitrary parcelable that's sent with an AccessibilityEvent to
  48. * prevent elimination of events with identical text.
  49. */
  50. private static final Parcelable JUNK_PARCELABLE = new Rect();
  51. /** String to speak when the cursor is at the end */
  52. private final String mCursorAtEnd;
  53. /** Handle to IME context. */
  54. private final Context mContext;
  55. /** Handle to current InputConnection to the editor. */
  56. private final InputConnection mIC;
  57. /** Extracted text from editor. */
  58. private ExtractedText mExtractedText;
  59. /*
  60. * Difference between Java and Android BreakIterator:<br>
  61. * - In Java all Iterator instances can share a common instance of CharacterIterator, while in
  62. * Android each Iterator instance keeps its own copy of CharacterIterator.<br>
  63. * - In Java setText(CharacterIterator newText) keeps argument CharacterIterator intact, while
  64. * Android resets the its position 0.<br>
  65. * - In Java last() is a valid index for preceding(), while in Android it is not a valid index
  66. * for preceding().<br>
  67. */
  68. /*
  69. * Why there is no logical line navigation in API?<br>
  70. * - No information about markups for logical line breaks. TextView uses Layout for fetching
  71. * line information.<br>
  72. * - InputConnection don't provide access to the dimension and statistics about TextView.<br>
  73. * - One workaround, is to emulate dpad_down key, but that is asynchronous, with no simple way
  74. * to know when key event is actually executed. Also not scalable with large text in View.
  75. */
  76. /*
  77. * Note: For Java line iterator, a line boundary occurs after the termination of a sequence of
  78. * whitespace characters. But we want to iterate over hard line breaks only. Hence we need to
  79. * implement wrapper navigation methods for line iterator, which ignores pre-specified list of
  80. * characters.
  81. */
  82. /** List of characters ignored by word iterator. */
  83. private final HashSet<Character> mIgnoredCharsForWord = new HashSet<Character>();
  84. /** List of characters ignored by Line iterator. */
  85. private final HashSet<Character> mIgnoredCharsForLine = new HashSet<Character>();
  86. /** Character iterator instance. */
  87. private BreakIterator mCharIterator;
  88. /** Word iterator instance. */
  89. private BreakIterator mWordIterator;
  90. /** Sentence iterator instance. */
  91. private BreakIterator mSentenceIterator;
  92. /** Line iterator instance. */
  93. private BreakIterator mLineIterator;
  94. /** AccessibilityManager instance, for sending events to accesibility framework */
  95. private AccessibilityManager mAccessibilityManager;
  96. /** Condition for enabling and disabling sending of accessibility events. */
  97. private boolean mSendAccessibilityEvents;
  98. /** Request sent to editor, for extracting text. */
  99. private ExtractedTextRequest mRequest;
  100. /**
  101. * Creates an instance of AccessibleInputConnection using the default
  102. * {@link Locale}.
  103. *
  104. * @param context IME's context.
  105. * @param inputConnection Handle to current input connection. It is
  106. * responsibility of user to keep the input connection updated.
  107. * @param sendAccessibilityEvents Set <code>true</code> to enable sending
  108. * accessibility events.
  109. * @param ignoredCharsForWord List of ignored characters for word iteration.
  110. */
  111. public AccessibleInputConnection(Context context, InputConnection inputConnection,
  112. boolean sendAccessibilityEvents, char[] ignoredCharsForWord) {
  113. this(context, inputConnection, sendAccessibilityEvents, ignoredCharsForWord,
  114. Locale.getDefault());
  115. }
  116. /**
  117. * Creates an instance of AccessibleInputConnection based on the specified
  118. * {@link Locale}.
  119. *
  120. * @param context IME's context.
  121. * @param inputConnection Handle to current input connection. It is
  122. * responsibility of user to keep the input connection updated.
  123. * @param sendAccessibilityEvents Set <code>true</code> to enable sending
  124. * accessibility events.
  125. * @param ignoredCharsForWord List of ignored characters for word iteration.
  126. */
  127. public AccessibleInputConnection(Context context, InputConnection inputConnection,
  128. boolean sendAccessibilityEvents, char[] ignoredCharsForWord, Locale locale) {
  129. super(inputConnection, true);
  130. if (inputConnection == null) {
  131. throw new IllegalArgumentException("Input connection must be non-null");
  132. }
  133. mIC = inputConnection;
  134. mContext = context;
  135. mAccessibilityManager = (AccessibilityManager) mContext
  136. .getSystemService(Context.ACCESSIBILITY_SERVICE);
  137. mSendAccessibilityEvents = sendAccessibilityEvents;
  138. mCharIterator = BreakIterator.getCharacterInstance(locale);
  139. mWordIterator = BreakIterator.getWordInstance(locale);
  140. mSentenceIterator = BreakIterator.getSentenceInstance(locale);
  141. mLineIterator = BreakIterator.getLineInstance(locale);
  142. for (int i = 0; i < ignoredCharsForWord.length; i++) {
  143. mIgnoredCharsForWord.add(ignoredCharsForWord[i]);
  144. }
  145. // Escape whitespace for line iterator
  146. mIgnoredCharsForLine.add(' ');
  147. mRequest = new ExtractedTextRequest();
  148. mRequest.hintMaxLines = Integer.MAX_VALUE;
  149. mRequest.flags = InputConnection.GET_TEXT_WITH_STYLES;
  150. mCursorAtEnd = mContext.getResources().getString(R.string.cursor_at_end_position);
  151. }
  152. /**
  153. * Checks validity of a <code>granularity</code>.
  154. *
  155. * @param granularity Value could be either {@link TextNavigation#GRANULARITY_CHAR},
  156. * {@link TextNavigation#GRANULARITY_WORD},
  157. * {@link TextNavigation#GRANULARITY_SENTENCE},
  158. * {@link TextNavigation#GRANULARITY_PARAGRAPH} or
  159. * {@link TextNavigation#GRANULARITY_ENTIRE_TEXT}
  160. * @throws IllegalArgumentException If granularity is invalid.
  161. */
  162. public static void checkValidGranularity(int granularity) {
  163. boolean correctGranularity = (granularity == TextNavigation.GRANULARITY_CHAR
  164. || granularity == TextNavigation.GRANULARITY_WORD
  165. || granularity == TextNavigation.GRANULARITY_SENTENCE
  166. || granularity == TextNavigation.GRANULARITY_PARAGRAPH
  167. || granularity == TextNavigation.GRANULARITY_ENTIRE_TEXT);
  168. if (!correctGranularity) {
  169. throw new IllegalArgumentException("granularity");
  170. }
  171. }
  172. /**
  173. * Checks validity of an <code>action</code>.
  174. *
  175. * @param action Value could be either {@link TextNavigation#ACTION_MOVE} or
  176. * {@link TextNavigation#ACTION_EXTEND}.
  177. * @throws IllegalArgumentException If action is invalid.
  178. */
  179. public static void checkValidAction(int action) {
  180. boolean correctAction = (action == ACTION_MOVE || action == ACTION_EXTEND);
  181. if (!correctAction) {
  182. throw new IllegalArgumentException("action");
  183. }
  184. }
  185. @Override
  186. public Position next(int granularity, int action) {
  187. if (DEBUG) {
  188. Log.i(TAG, "Next: " + granularity + " " + action);
  189. }
  190. checkValidGranularity(granularity);
  191. checkValidAction(action);
  192. if (granularity != TextNavigation.GRANULARITY_ENTIRE_TEXT) {
  193. return navigateNext(granularity, action);
  194. }
  195. // Text granularity = Entire text
  196. return navigateEntireText(NAVIGATE_NEXT, action);
  197. }
  198. @Override
  199. public Position previous(int granularity, int action) {
  200. if (DEBUG) {
  201. Log.i(TAG, "Previous: " + granularity + " " + action);
  202. }
  203. checkValidGranularity(granularity);
  204. checkValidAction(action);
  205. if (granularity != TextNavigation.GRANULARITY_ENTIRE_TEXT) {
  206. return navigatePrevious(granularity, action);
  207. }
  208. // Text granularity = Entire text
  209. return navigateEntireText(NAVIGATE_PREVIOUS, action);
  210. }
  211. @Override
  212. public Position get(int granularity) {
  213. if (DEBUG) {
  214. Log.i(TAG, "Get: " + granularity);
  215. }
  216. checkValidGranularity(granularity);
  217. if (granularity == TextNavigation.GRANULARITY_WORD
  218. || granularity == TextNavigation.GRANULARITY_SENTENCE
  219. || granularity == TextNavigation.GRANULARITY_PARAGRAPH) {
  220. return getCurrentUnit(granularity);
  221. } else if (granularity == TextNavigation.GRANULARITY_CHAR) {
  222. return getNextChar();
  223. } else if (granularity == TextNavigation.GRANULARITY_ENTIRE_TEXT) {
  224. return getContent();
  225. }
  226. return null;
  227. }
  228. /**
  229. * @return Returns True, if sending accessibility events, is enabled.
  230. */
  231. public boolean isSendAccessibilityEvents() {
  232. return mSendAccessibilityEvents;
  233. }
  234. /**
  235. * To enable or disable sending accessibility events.
  236. */
  237. public void setSendAccessibilityEvents(boolean sendAccessibilityEvents) {
  238. mSendAccessibilityEvents = sendAccessibilityEvents;
  239. }
  240. /**
  241. * Sends a character sequence to be read aloud.
  242. *
  243. * @param description The {@link CharSequence} to be read aloud.
  244. */
  245. public void trySendAccessiblityEvent(CharSequence description) {
  246. if (!mAccessibilityManager.isEnabled() || !mSendAccessibilityEvents
  247. || TextUtils.isEmpty(description)) {
  248. if (DEBUG) {
  249. Log.e(TAG, "Not sending accessiblity event");
  250. }
  251. return;
  252. }
  253. if (DEBUG) {
  254. Log.i(TAG, "Spell: " + description);
  255. }
  256. // TODO We need to add an AccessibilityEvent type for IMEs.
  257. AccessibilityEvent event = AccessibilityEvent.obtain(
  258. AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
  259. event.setPackageName(mContext.getPackageName());
  260. event.setClassName(getClass().getName());
  261. event.setAddedCount(description.length());
  262. event.setEventTime(SystemClock.uptimeMillis());
  263. event.getText().add(description);
  264. // TODO Do we still need to add parcelable data so that we don't get
  265. // eliminated by TalkBack as a duplicate event? Setting the event time
  266. // should be enough.
  267. event.setParcelableData(JUNK_PARCELABLE);
  268. mAccessibilityManager.sendAccessibilityEvent(event);
  269. }
  270. /**
  271. * Extract text from editor, by sending a request.
  272. */
  273. private void fetchTextFromView() {
  274. mExtractedText =
  275. mIC.getExtractedText(mRequest, InputConnection.GET_EXTRACTED_TEXT_MONITOR);
  276. }
  277. /**
  278. * Returns whether this input connection is currently connected to a text box.
  279. *
  280. * @return <code>true</code> if this input connection is currently connected to a text box.
  281. */
  282. public boolean hasExtractedText() {
  283. fetchTextFromView();
  284. return (mExtractedText != null && mExtractedText.text != null);
  285. }
  286. /**
  287. * Returns the current extracted text or <code>null</code> if not connected to a text box.
  288. *
  289. * @return the current extracted text or <code>null</code> if not connected to a text box.
  290. */
  291. public CharSequence getExtractedText() {
  292. return hasExtractedText() ? mExtractedText.text : null;
  293. }
  294. /**
  295. * Update text in editor, with new Selection.
  296. *
  297. * @param start Selection start.
  298. * @param end Selection end.
  299. */
  300. private void updateTextInView(int start, int end) {
  301. if (DEBUG) {
  302. Log.i(TAG, "Start: " + start + " End: " + end);
  303. }
  304. mIC.finishComposingText();
  305. mIC.setSelection(start, end);
  306. }
  307. /**
  308. * Get iterator based on <code>granularity</code> used.
  309. */
  310. private BreakIterator getCurrentIterator(int granularity) {
  311. if (granularity == TextNavigation.GRANULARITY_CHAR) {
  312. return mCharIterator;
  313. } else if (granularity == TextNavigation.GRANULARITY_WORD) {
  314. return mWordIterator;
  315. } else if (granularity == TextNavigation.GRANULARITY_SENTENCE) {
  316. return mSentenceIterator;
  317. } else if (granularity == TextNavigation.GRANULARITY_PARAGRAPH) {
  318. return mLineIterator;
  319. }
  320. return null;
  321. }
  322. /**
  323. * Get a list of ignored characters based on <code>granularity</code> used.
  324. */
  325. private HashSet<Character> getCurrentIgnoredChars(int granularity) {
  326. if (granularity == TextNavigation.GRANULARITY_WORD) {
  327. return mIgnoredCharsForWord;
  328. } else if (granularity == TextNavigation.GRANULARITY_PARAGRAPH) {
  329. return mIgnoredCharsForLine;
  330. }
  331. return null;
  332. }
  333. /**
  334. * Checks, if the character is present in the list of ignored characters for the specified
  335. * <code>granularity</code>.
  336. */
  337. private boolean isIgnoredChar(int granularity, int index) {
  338. if (DEBUG) {
  339. Log.i(TAG, "granularity: " + granularity + " index: " + index);
  340. }
  341. if (granularity == TextNavigation.GRANULARITY_WORD) {
  342. boolean validIndex = index < mExtractedText.text.length();
  343. char charOnRight = validIndex ? mExtractedText.text.charAt(index) : '0';
  344. boolean nullList = getCurrentIgnoredChars(granularity) == null;
  345. return validIndex && !nullList
  346. && getCurrentIgnoredChars(granularity).contains(charOnRight);
  347. } else if (granularity == TextNavigation.GRANULARITY_PARAGRAPH) {
  348. boolean validIndex = index > 0;
  349. char charOnLeft = validIndex ? mExtractedText.text.charAt(index - 1) : '0';
  350. boolean nullList = getCurrentIgnoredChars(granularity) == null;
  351. return validIndex && !nullList
  352. && getCurrentIgnoredChars(granularity).contains(charOnLeft);
  353. }
  354. return false;
  355. }
  356. /**
  357. * Returns the boundary following the current boundary, for line iterator.
  358. */
  359. private int nextLineIterator() {
  360. int currentIndex = getCurrentIterator(TextNavigation.GRANULARITY_PARAGRAPH).current();
  361. if (currentIndex == mExtractedText.text.length()) {
  362. return BreakIterator.DONE;
  363. }
  364. int nextIndex = getCurrentIterator(TextNavigation.GRANULARITY_PARAGRAPH).next();
  365. while (nextIndex != BreakIterator.DONE) {
  366. if (!isIgnoredChar(TextNavigation.GRANULARITY_PARAGRAPH, nextIndex)) {
  367. break;
  368. }
  369. nextIndex = getCurrentIterator(TextNavigation.GRANULARITY_PARAGRAPH).next();
  370. }
  371. return nextIndex;
  372. }
  373. /**
  374. * Returns the boundary preceding the current boundary, for line iterator.
  375. */
  376. private int previousLineIterator() {
  377. int currentIndex = getCurrentIterator(TextNavigation.GRANULARITY_PARAGRAPH).current();
  378. if (currentIndex == 0) {
  379. return BreakIterator.DONE;
  380. }
  381. int previousIndex = getCurrentIterator(TextNavigation.GRANULARITY_PARAGRAPH).previous();
  382. while (previousIndex != BreakIterator.DONE) {
  383. if (!isIgnoredChar(TextNavigation.GRANULARITY_PARAGRAPH, previousIndex)) {
  384. break;
  385. }
  386. previousIndex = getCurrentIterator(TextNavigation.GRANULARITY_PARAGRAPH).previous();
  387. }
  388. return previousIndex;
  389. }
  390. /**
  391. * Implementation of navigating to next unit.
  392. */
  393. private Position navigateNext(int granularity, int action) {
  394. // Each time, I need to fetch the text and set cursor position, because user can directly
  395. // change cursor position without using any iterator.
  396. // Fetch text from editor and update local variables
  397. fetchTextFromView();
  398. getCurrentIterator(granularity).setText(mExtractedText.text.toString());
  399. int selectionStart = mExtractedText.selectionStart;
  400. int selectionEnd = mExtractedText.selectionEnd;
  401. int textLength = mExtractedText.text.length();
  402. if (selectionEnd >= textLength) {
  403. // Handle corner case when cursor is at end of the text, Android implementation deviates
  404. // from standard Java implementation, check comments above.
  405. return null;
  406. }
  407. int nextIndex = getCurrentIterator(granularity).following(selectionEnd);
  408. // We dont need to loop when nextIndex == textLength
  409. while (nextIndex != BreakIterator.DONE) {
  410. if (!isIgnoredChar(granularity, nextIndex)) {
  411. if (action == ACTION_MOVE) {
  412. updateTextInView(nextIndex, nextIndex);
  413. // Be careful, if we are going to use mIterator again
  414. int unitEndIndex;
  415. if (granularity == TextNavigation.GRANULARITY_PARAGRAPH) {
  416. unitEndIndex = nextLineIterator();
  417. } else {
  418. unitEndIndex = getCurrentIterator(granularity).next();
  419. }
  420. if (unitEndIndex < nextIndex) {
  421. Log.e(TAG, "Failed to obtain unit end index!");
  422. nextIndex = BreakIterator.DONE;
  423. break;
  424. }
  425. if (DEBUG) {
  426. Log.i(TAG, "nextIndex: " + nextIndex + " unitEndIndex: " + unitEndIndex);
  427. }
  428. if (unitEndIndex != BreakIterator.DONE) {
  429. // Send new unit
  430. CharSequence spell = mExtractedText.text.subSequence(nextIndex,
  431. unitEndIndex);
  432. trySendAccessiblityEvent(spell.toString());
  433. } else {
  434. trySendAccessiblityEvent(mCursorAtEnd);
  435. }
  436. // Position of new unit encountered
  437. return Position.obtain(nextIndex, unitEndIndex, false);
  438. } else if (action == ACTION_EXTEND) {
  439. updateTextInView(selectionStart, nextIndex);
  440. if (DEBUG) {
  441. Log.i(TAG,
  442. "selectionStart: " + selectionStart + " nextIndex: " + nextIndex);
  443. }
  444. // Send additional text selected.
  445. CharSequence spell = mExtractedText.text.subSequence(selectionEnd, nextIndex);
  446. trySendAccessiblityEvent(spell.toString());
  447. // Position of selection
  448. return Position.obtain(selectionStart, nextIndex, true);
  449. }
  450. return null;
  451. }
  452. nextIndex = getCurrentIterator(granularity).next();
  453. }
  454. return null;
  455. }
  456. /**
  457. * Speaks the granular unit of text at the cursor.
  458. *
  459. * @param granularity The granular unit to speak.
  460. */
  461. public void speakCurrentUnit(int granularity) {
  462. if (!hasExtractedText()) {
  463. return;
  464. }
  465. final CharSequence extractedText = mExtractedText.text;
  466. final int cursorPos = mExtractedText.selectionEnd;
  467. final int textLength = extractedText.length();
  468. CharSequence description;
  469. if (cursorPos == textLength) {
  470. description = mCursorAtEnd;
  471. } else if (granularity == GRANULARITY_ENTIRE_TEXT) {
  472. description = extractedText.subSequence(cursorPos, extractedText.length());
  473. } else {
  474. final BreakIterator iterator = getCurrentIterator(granularity);
  475. iterator.setText(extractedText.toString());
  476. final int unitEndIndex = iterator.following(cursorPos);
  477. description = extractedText.subSequence(cursorPos, unitEndIndex);
  478. }
  479. trySendAccessiblityEvent(description);
  480. }
  481. /**
  482. * Implementation of navigating to previous unit.
  483. */
  484. private Position navigatePrevious(int granularity, int action) {
  485. // Fetch text from editor and update local variables
  486. fetchTextFromView();
  487. getCurrentIterator(granularity).setText(mExtractedText.text.toString());
  488. int selectionStart = mExtractedText.selectionStart;
  489. int selectionEnd = mExtractedText.selectionEnd;
  490. int textLength = mExtractedText.text.length();
  491. // Selection extension, always refers to moving selectionEnd only.
  492. int previousIndex;
  493. if (selectionEnd == textLength) {
  494. // Handle corner case when cursor is at end of the text, Android implementation deviates
  495. // from standard Java implementation, check comments above.
  496. getCurrentIterator(granularity).last();
  497. previousIndex = getCurrentIterator(granularity).previous();
  498. } else {
  499. previousIndex = getCurrentIterator(granularity).preceding(selectionEnd);
  500. }
  501. // We dont need to loop when previousIndex == 0
  502. while (previousIndex != BreakIterator.DONE) {
  503. if (!isIgnoredChar(granularity, previousIndex)) {
  504. if (action == ACTION_MOVE) {
  505. updateTextInView(previousIndex, previousIndex);
  506. // We are issuing next again because we don't want ignored chars
  507. int unitEndIndex;
  508. if (granularity == TextNavigation.GRANULARITY_PARAGRAPH) {
  509. unitEndIndex = previousLineIterator();
  510. } else {
  511. unitEndIndex = getCurrentIterator(granularity).next();
  512. }
  513. if (unitEndIndex < previousIndex) {
  514. Log.e(TAG, "Failed to obtain unit end index!");
  515. previousIndex = BreakIterator.DONE;
  516. break;
  517. }
  518. if (DEBUG) {
  519. Log.i(TAG, "previousIndex: " + previousIndex + " unitEndIndex: "
  520. + unitEndIndex);
  521. }
  522. if (unitEndIndex != BreakIterator.DONE) {
  523. CharSequence spell = mExtractedText.text.subSequence(previousIndex,
  524. unitEndIndex);
  525. trySendAccessiblityEvent(spell.toString());
  526. }
  527. return Position.obtain(previousIndex, unitEndIndex, false);
  528. } else if (action == ACTION_EXTEND) {
  529. updateTextInView(selectionStart, previousIndex);
  530. if (DEBUG) {
  531. Log.i(TAG, "selectionStart: " + selectionStart + " previousIndex: "
  532. + previousIndex);
  533. }
  534. // We dont need to issue next again, including ignored chars
  535. CharSequence spell = mExtractedText.text.subSequence(previousIndex,
  536. selectionEnd);
  537. trySendAccessiblityEvent(spell.toString());
  538. return Position.obtain(selectionStart, previousIndex, true);
  539. }
  540. }
  541. previousIndex = getCurrentIterator(granularity).previous();
  542. }
  543. return null;
  544. }
  545. /**
  546. * Navigating, when text unit is Entire text itself.<br>
  547. * It is implemented separately, because we don't have iterator for this
  548. * granularity.
  549. */
  550. private Position navigateEntireText(int direction, int action) {
  551. fetchTextFromView();
  552. final int selectionStart = mExtractedText.selectionStart;
  553. final int selectionEnd = mExtractedText.selectionEnd;
  554. final int textLength = mExtractedText.text.length();
  555. final int newPosition = direction == NAVIGATE_NEXT ? textLength : 0;
  556. if (selectionEnd == newPosition) {
  557. // Handle corner case when cursor is at edge of the text, Android
  558. // implementation deviates from standard Java implementation, check
  559. // comments above.
  560. return null;
  561. }
  562. if (DEBUG) {
  563. Log.i(TAG, "selectionStart: " + selectionStart + " selectionEnd: " + selectionEnd
  564. + " textLength: " + textLength + " newPosition: " + newPosition);
  565. }
  566. if (action == ACTION_MOVE) {
  567. updateTextInView(newPosition, newPosition);
  568. // No Accessibility event fired for next
  569. if (direction == NAVIGATE_PREVIOUS) {
  570. trySendAccessiblityEvent(mExtractedText.text.toString());
  571. }
  572. // Position of new unit encountered
  573. return Position.obtain(newPosition, newPosition, false);
  574. } else if (action == ACTION_EXTEND) {
  575. updateTextInView(selectionStart, newPosition);
  576. // Send additional text selected.
  577. final int lowerIndex = (direction == NAVIGATE_NEXT) ? selectionEnd : newPosition;
  578. final int higherUpper = (direction == NAVIGATE_NEXT) ? newPosition : selectionEnd;
  579. final CharSequence spell = mExtractedText.text.subSequence(lowerIndex, higherUpper);
  580. trySendAccessiblityEvent(spell.toString());
  581. // Position of selection
  582. return Position.obtain(selectionStart, newPosition, true);
  583. }
  584. return null;
  585. }
  586. /**
  587. * Fetch character on right of the cursor position.
  588. */
  589. private Position getNextChar() {
  590. fetchTextFromView();
  591. getCurrentIterator(TextNavigation.GRANULARITY_CHAR).setText(mExtractedText.text.toString());
  592. int selectionStart = mExtractedText.selectionStart;
  593. int selectionEnd = mExtractedText.selectionEnd;
  594. int textLength = mExtractedText.text.length();
  595. int nextPosition;
  596. if (selectionStart != selectionEnd || selectionStart == textLength) {
  597. // Return, if in selection mode, or at the end of the text
  598. return null;
  599. } else {
  600. nextPosition = getCurrentIterator(TextNavigation.GRANULARITY_CHAR)
  601. .following(selectionStart);
  602. }
  603. if (DEBUG) {
  604. Log.i(TAG, "selectionStart: " + selectionStart + " nextPosition: " + nextPosition);
  605. }
  606. if (nextPosition != BreakIterator.DONE) {
  607. CharSequence spell = mExtractedText.text.subSequence(selectionStart, nextPosition);
  608. trySendAccessiblityEvent(spell.toString());
  609. return Position.obtain(selectionStart, nextPosition, false);
  610. }
  611. return null;
  612. }
  613. /**
  614. * Implementation of getting current word or sentence. We need a different version for
  615. * getNextChar(), because of case when, selectionStart == textLength.
  616. */
  617. private Position getCurrentUnit(int granularity) {
  618. // Fetch text from editor and update local variables
  619. fetchTextFromView();
  620. getCurrentIterator(granularity).setText(mExtractedText.text.toString());
  621. int selectionStart = mExtractedText.selectionStart;
  622. int selectionEnd = mExtractedText.selectionEnd;
  623. int textLength = mExtractedText.text.length();
  624. // Return, if in selection mode
  625. if (selectionStart != selectionEnd) {
  626. return null;
  627. }
  628. int unitStartIndex = selectionStart;
  629. int unitEndIndex = selectionStart;
  630. if (selectionStart == textLength) {
  631. // Handle corner case when cursor is at end of the text.
  632. getCurrentIterator(granularity).last();
  633. unitStartIndex = getCurrentIterator(granularity).previous();
  634. unitEndIndex = getCurrentIterator(granularity).next();
  635. } else {
  636. boolean onRightEdgeOfWord = isIgnoredChar(granularity, unitStartIndex);
  637. boolean onRightEdgeOfSentence = getCurrentIterator(granularity)
  638. .isBoundary(unitStartIndex);
  639. if (granularity == TextNavigation.GRANULARITY_WORD && onRightEdgeOfWord) {
  640. unitStartIndex = getCurrentIterator(granularity).preceding(selectionStart);
  641. } else if (granularity == TextNavigation.GRANULARITY_SENTENCE
  642. && onRightEdgeOfSentence) {
  643. unitEndIndex = getCurrentIterator(granularity).following(selectionStart);
  644. } else {
  645. // In beginning or between the current unit
  646. unitEndIndex = getCurrentIterator(granularity).following(selectionStart);
  647. unitStartIndex = getCurrentIterator(granularity).previous();
  648. }
  649. }
  650. if (DEBUG) {
  651. Log.i(TAG, "startIndex: " + unitStartIndex + " endIndex: " + unitEndIndex);
  652. }
  653. CharSequence spell = mExtractedText.text.subSequence(unitStartIndex, unitEndIndex);
  654. trySendAccessiblityEvent(spell.toString());
  655. return Position.obtain(unitStartIndex, unitEndIndex, false);
  656. }
  657. private Position getContent() {
  658. fetchTextFromView();
  659. trySendAccessiblityEvent(mExtractedText.text.toString());
  660. return Position.obtain(0, mExtractedText.text.length() - 1, false);
  661. }
  662. }