PageRenderTime 49ms CodeModel.GetById 9ms app.highlight 34ms RepoModel.GetById 1ms app.codeStats 0ms

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