1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.inputmethod.latin.utils;
18 
19 import android.text.Spanned;
20 import android.text.style.SuggestionSpan;
21 
22 import java.util.Arrays;
23 
24 /**
25  * Represents a range of text, relative to the current cursor position.
26  */
27 public final class TextRange {
28     private final CharSequence mTextAtCursor;
29     private final int mWordAtCursorStartIndex;
30     private final int mWordAtCursorEndIndex;
31     private final int mCursorIndex;
32 
33     public final CharSequence mWord;
34     public final boolean mHasUrlSpans;
35 
getNumberOfCharsInWordBeforeCursor()36     public int getNumberOfCharsInWordBeforeCursor() {
37         return mCursorIndex - mWordAtCursorStartIndex;
38     }
39 
getNumberOfCharsInWordAfterCursor()40     public int getNumberOfCharsInWordAfterCursor() {
41         return mWordAtCursorEndIndex - mCursorIndex;
42     }
43 
length()44     public int length() {
45         return mWord.length();
46     }
47 
48     /**
49      * Gets the suggestion spans that are put squarely on the word, with the exact start
50      * and end of the span matching the boundaries of the word.
51      * @return the list of spans.
52      */
getSuggestionSpansAtWord()53     public SuggestionSpan[] getSuggestionSpansAtWord() {
54         if (!(mTextAtCursor instanceof Spanned && mWord instanceof Spanned)) {
55             return new SuggestionSpan[0];
56         }
57         final Spanned text = (Spanned)mTextAtCursor;
58         // Note: it's fine to pass indices negative or greater than the length of the string
59         // to the #getSpans() method. The reason we need to get from -1 to +1 is that, the
60         // spans were cut at the cursor position, and #getSpans(start, end) does not return
61         // spans that end at `start' or begin at `end'. Consider the following case:
62         //              this| is          (The | symbolizes the cursor position
63         //              ---- ---
64         // In this case, the cursor is in position 4, so the 0~7 span has been split into
65         // a 0~4 part and a 4~7 part.
66         // If we called #getSpans(0, 4) in this case, we would only get the part from 0 to 4
67         // of the span, and not the part from 4 to 7, so we would not realize the span actually
68         // extends from 0 to 7. But if we call #getSpans(-1, 5) we'll get both the 0~4 and
69         // the 4~7 spans and we can merge them accordingly.
70         // Any span starting more than 1 char away from the word boundaries in any direction
71         // does not touch the word, so we don't need to consider it. That's why requesting
72         // -1 ~ +1 is enough.
73         // Of course this is only relevant if the cursor is at one end of the word. If it's
74         // in the middle, the -1 and +1 are not necessary, but they are harmless.
75         final SuggestionSpan[] spans = text.getSpans(mWordAtCursorStartIndex - 1,
76                 mWordAtCursorEndIndex + 1, SuggestionSpan.class);
77         int readIndex = 0;
78         int writeIndex = 0;
79         for (; readIndex < spans.length; ++readIndex) {
80             final SuggestionSpan span = spans[readIndex];
81             // The span may be null, as we null them when we find duplicates. Cf a few lines
82             // down.
83             if (null == span) continue;
84             // Tentative span start and end. This may be modified later if we realize the
85             // same span is also applied to other parts of the string.
86             int spanStart = text.getSpanStart(span);
87             int spanEnd = text.getSpanEnd(span);
88             for (int i = readIndex + 1; i < spans.length; ++i) {
89                 if (span.equals(spans[i])) {
90                     // We found the same span somewhere else. Read the new extent of this
91                     // span, and adjust our values accordingly.
92                     spanStart = Math.min(spanStart, text.getSpanStart(spans[i]));
93                     spanEnd = Math.max(spanEnd, text.getSpanEnd(spans[i]));
94                     // ...and mark the span as processed.
95                     spans[i] = null;
96                 }
97             }
98             if (spanStart == mWordAtCursorStartIndex && spanEnd == mWordAtCursorEndIndex) {
99                 // If the span does not start and stop here, ignore it. It probably extends
100                 // past the start or end of the word, as happens in missing space correction
101                 // or EasyEditSpans put by voice input.
102                 spans[writeIndex++] = spans[readIndex];
103             }
104         }
105         return writeIndex == readIndex ? spans : Arrays.copyOfRange(spans, 0, writeIndex);
106     }
107 
TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex, final int wordAtCursorEndIndex, final int cursorIndex, final boolean hasUrlSpans)108     public TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex,
109             final int wordAtCursorEndIndex, final int cursorIndex, final boolean hasUrlSpans) {
110         if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex
111                 || cursorIndex > wordAtCursorEndIndex
112                 || wordAtCursorEndIndex > textAtCursor.length()) {
113             throw new IndexOutOfBoundsException();
114         }
115         mTextAtCursor = textAtCursor;
116         mWordAtCursorStartIndex = wordAtCursorStartIndex;
117         mWordAtCursorEndIndex = wordAtCursorEndIndex;
118         mCursorIndex = cursorIndex;
119         mHasUrlSpans = hasUrlSpans;
120         mWord = mTextAtCursor.subSequence(mWordAtCursorStartIndex, mWordAtCursorEndIndex);
121     }
122 }