1 /*
2  * Copyright (C) 2012 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;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertTrue;
22 
23 import android.content.res.Resources;
24 import android.inputmethodservice.InputMethodService;
25 import android.os.Parcel;
26 import android.text.SpannableString;
27 import android.text.TextUtils;
28 import android.text.style.SuggestionSpan;
29 import android.view.inputmethod.ExtractedText;
30 import android.view.inputmethod.ExtractedTextRequest;
31 import android.view.inputmethod.InputConnection;
32 import android.view.inputmethod.InputConnectionWrapper;
33 
34 import androidx.test.InstrumentationRegistry;
35 import androidx.test.filters.SmallTest;
36 import androidx.test.runner.AndroidJUnit4;
37 
38 import com.android.inputmethod.latin.common.Constants;
39 import com.android.inputmethod.latin.common.StringUtils;
40 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
41 import com.android.inputmethod.latin.utils.NgramContextUtils;
42 import com.android.inputmethod.latin.utils.RunInLocale;
43 import com.android.inputmethod.latin.utils.ScriptUtils;
44 import com.android.inputmethod.latin.utils.TextRange;
45 
46 import org.junit.Before;
47 import org.junit.Test;
48 import org.junit.runner.RunWith;
49 
50 import java.util.Locale;
51 
52 @SmallTest
53 @RunWith(AndroidJUnit4.class)
54 public class RichInputConnectionAndTextRangeTests {
55 
56     // The following is meant to be a reasonable default for
57     // the "word_separators" resource.
58     private SpacingAndPunctuations mSpacingAndPunctuations;
59 
60     @Before
setUp()61     public void setUp() throws Exception {
62         final RunInLocale<SpacingAndPunctuations> job = new RunInLocale<SpacingAndPunctuations>() {
63             @Override
64             protected SpacingAndPunctuations job(final Resources res) {
65                 return new SpacingAndPunctuations(res);
66             }
67         };
68         final Resources res = InstrumentationRegistry.getTargetContext().getResources();
69         mSpacingAndPunctuations = job.runInLocale(res, Locale.ENGLISH);
70     }
71 
72     private class MockConnection extends InputConnectionWrapper {
73         final CharSequence mTextBefore;
74         final CharSequence mTextAfter;
75         final ExtractedText mExtractedText;
76 
MockConnection(final CharSequence text, final int cursorPosition)77         public MockConnection(final CharSequence text, final int cursorPosition) {
78             super(null, false);
79             // Interaction of spans with Parcels is completely non-trivial, but in the actual case
80             // the CharSequences do go through Parcels because they go through IPC. There
81             // are some significant differences between the behavior of Spanned objects that
82             // have and that have not gone through parceling, so it's much easier to simulate
83             // the environment with Parcels than try to emulate things by hand.
84             final Parcel p = Parcel.obtain();
85             TextUtils.writeToParcel(text.subSequence(0, cursorPosition), p, 0 /* flags */);
86             TextUtils.writeToParcel(text.subSequence(cursorPosition, text.length()), p,
87                     0 /* flags */);
88             final byte[] marshalled = p.marshall();
89             p.unmarshall(marshalled, 0, marshalled.length);
90             p.setDataPosition(0);
91             mTextBefore = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
92             mTextAfter = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
93             mExtractedText = null;
94             p.recycle();
95         }
96 
MockConnection(String textBefore, String textAfter, ExtractedText extractedText)97         public MockConnection(String textBefore, String textAfter, ExtractedText extractedText) {
98             super(null, false);
99             mTextBefore = textBefore;
100             mTextAfter = textAfter;
101             mExtractedText = extractedText;
102         }
103 
cursorPos()104         public int cursorPos() {
105             return mTextBefore.length();
106         }
107 
108         /* (non-Javadoc)
109          * @see android.view.inputmethod.InputConnectionWrapper#getTextBeforeCursor(int, int)
110          */
111         @Override
getTextBeforeCursor(int n, int flags)112         public CharSequence getTextBeforeCursor(int n, int flags) {
113             return mTextBefore;
114         }
115 
116         /* (non-Javadoc)
117          * @see android.view.inputmethod.InputConnectionWrapper#getTextAfterCursor(int, int)
118          */
119         @Override
getTextAfterCursor(int n, int flags)120         public CharSequence getTextAfterCursor(int n, int flags) {
121             return mTextAfter;
122         }
123 
124         /* (non-Javadoc)
125          * @see android.view.inputmethod.InputConnectionWrapper#getExtractedText(
126          *         ExtractedTextRequest, int)
127          */
128         @Override
getExtractedText(ExtractedTextRequest request, int flags)129         public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
130             return mExtractedText;
131         }
132 
133         @Override
beginBatchEdit()134         public boolean beginBatchEdit() {
135             return true;
136         }
137 
138         @Override
endBatchEdit()139         public boolean endBatchEdit() {
140             return true;
141         }
142 
143         @Override
finishComposingText()144         public boolean finishComposingText() {
145             return true;
146         }
147     }
148 
149     static class MockInputMethodService extends InputMethodService {
150         private MockConnection mMockConnection;
setInputConnection(final MockConnection mockConnection)151         public void setInputConnection(final MockConnection mockConnection) {
152             mMockConnection = mockConnection;
153         }
cursorPos()154         public int cursorPos() {
155             return mMockConnection.cursorPos();
156         }
157         @Override
getCurrentInputConnection()158         public InputConnection getCurrentInputConnection() {
159             return mMockConnection;
160         }
161     }
162 
163     /************************** Tests ************************/
164 
165     /**
166      * Test for getting previous word (for bigram suggestions)
167      */
168     @Test
testGetPreviousWord()169     public void testGetPreviousWord() {
170         // If one of the following cases breaks, the bigram suggestions won't work.
171         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
172                 "abc def", mSpacingAndPunctuations, 2).getNthPrevWord(1), "abc");
173         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
174                 "abc", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE);
175         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
176                 "abc. def", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE);
177 
178         assertFalse(NgramContextUtils.getNgramContextFromNthPreviousWord(
179                 "abc def", mSpacingAndPunctuations, 2).isBeginningOfSentenceContext());
180         assertTrue(NgramContextUtils.getNgramContextFromNthPreviousWord(
181                 "abc", mSpacingAndPunctuations, 2).isBeginningOfSentenceContext());
182 
183         // For n-gram
184         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
185                 "abc def", mSpacingAndPunctuations, 1).getNthPrevWord(1), "def");
186         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
187                 "abc def", mSpacingAndPunctuations, 1).getNthPrevWord(2), "abc");
188         assertTrue(NgramContextUtils.getNgramContextFromNthPreviousWord(
189                 "abc def", mSpacingAndPunctuations, 2).isNthPrevWordBeginningOfSentence(2));
190 
191         // The following tests reflect the current behavior of the function
192         // RichInputConnection#getNthPreviousWord.
193         // TODO: However at this time, the code does never go
194         // into such a path, so it should be safe to change the behavior of
195         // this function if needed - especially since it does not seem very
196         // logical. These tests are just there to catch any unintentional
197         // changes in the behavior of the RichInputConnection#getPreviousWord method.
198         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
199                 "abc def ", mSpacingAndPunctuations, 2).getNthPrevWord(1), "abc");
200         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
201                 "abc def.", mSpacingAndPunctuations, 2).getNthPrevWord(1), "abc");
202         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
203                 "abc def .", mSpacingAndPunctuations, 2).getNthPrevWord(1), "def");
204         assertTrue(NgramContextUtils.getNgramContextFromNthPreviousWord(
205                 "abc ", mSpacingAndPunctuations, 2).isBeginningOfSentenceContext());
206 
207         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
208                 "abc def", mSpacingAndPunctuations, 1).getNthPrevWord(1), "def");
209         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
210                 "abc def ", mSpacingAndPunctuations, 1).getNthPrevWord(1), "def");
211         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
212                 "abc 'def", mSpacingAndPunctuations, 1).getNthPrevWord(1), "'def");
213         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
214                 "abc def.", mSpacingAndPunctuations, 1), NgramContext.BEGINNING_OF_SENTENCE);
215         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
216                 "abc def .", mSpacingAndPunctuations, 1), NgramContext.BEGINNING_OF_SENTENCE);
217         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
218                 "abc, def", mSpacingAndPunctuations, 2), NgramContext.EMPTY_PREV_WORDS_INFO);
219         // question mark is treated as the end of the sentence. Hence, beginning of the
220         // sentence is expected.
221         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
222                 "abc? def", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE);
223         // Exclamation mark is treated as the end of the sentence. Hence, beginning of the
224         // sentence is expected.
225         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
226                 "abc! def", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE);
227         assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord(
228                 "abc 'def", mSpacingAndPunctuations, 2), NgramContext.EMPTY_PREV_WORDS_INFO);
229     }
230 
231     @Test
testGetWordRangeAtCursor()232     public void testGetWordRangeAtCursor() {
233         /**
234          * Test logic in getting the word range at the cursor.
235          */
236         final SpacingAndPunctuations SPACE = new SpacingAndPunctuations(
237                 mSpacingAndPunctuations, new int[] { Constants.CODE_SPACE });
238         final SpacingAndPunctuations TAB = new SpacingAndPunctuations(
239                 mSpacingAndPunctuations, new int[] { Constants.CODE_TAB });
240         // A character that needs surrogate pair to represent its code point (U+2008A).
241         final String SUPPLEMENTARY_CHAR_STRING = "\uD840\uDC8A";
242         final SpacingAndPunctuations SUPPLEMENTARY_CHAR = new SpacingAndPunctuations(
243                 mSpacingAndPunctuations, StringUtils.toSortedCodePointArray(
244                         SUPPLEMENTARY_CHAR_STRING));
245         final String HIRAGANA_WORD = "\u3042\u3044\u3046\u3048\u304A"; // あいうえお
246         final String GREEK_WORD = "\u03BA\u03B1\u03B9"; // και
247 
248         ExtractedText et = new ExtractedText();
249         final MockInputMethodService mockInputMethodService = new MockInputMethodService();
250         final RichInputConnection ic = new RichInputConnection(mockInputMethodService);
251         mockInputMethodService.setInputConnection(new MockConnection("word wo", "rd", et));
252         et.startOffset = 0;
253         et.selectionStart = 7;
254         TextRange r;
255 
256         ic.beginBatchEdit();
257         // basic case
258         r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN);
259         assertTrue(TextUtils.equals("word", r.mWord));
260 
261         // tab character instead of space
262         mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et));
263         ic.beginBatchEdit();
264         r = ic.getWordRangeAtCursor(TAB, ScriptUtils.SCRIPT_LATIN);
265         ic.endBatchEdit();
266         assertTrue(TextUtils.equals("word", r.mWord));
267 
268         // splitting on supplementary character
269         mockInputMethodService.setInputConnection(
270                 new MockConnection("one word" + SUPPLEMENTARY_CHAR_STRING + "wo", "rd", et));
271         ic.beginBatchEdit();
272         r = ic.getWordRangeAtCursor(SUPPLEMENTARY_CHAR, ScriptUtils.SCRIPT_LATIN);
273         ic.endBatchEdit();
274         assertTrue(TextUtils.equals("word", r.mWord));
275 
276         // split on chars outside the specified script
277         mockInputMethodService.setInputConnection(
278                 new MockConnection(HIRAGANA_WORD + "wo", "rd" + GREEK_WORD, et));
279         ic.beginBatchEdit();
280         r = ic.getWordRangeAtCursor(SUPPLEMENTARY_CHAR, ScriptUtils.SCRIPT_LATIN);
281         ic.endBatchEdit();
282         assertTrue(TextUtils.equals("word", r.mWord));
283 
284         // likewise for greek
285         mockInputMethodService.setInputConnection(
286                 new MockConnection("text" + GREEK_WORD, "text", et));
287         ic.beginBatchEdit();
288         r = ic.getWordRangeAtCursor(SUPPLEMENTARY_CHAR, ScriptUtils.SCRIPT_GREEK);
289         ic.endBatchEdit();
290         assertTrue(TextUtils.equals(GREEK_WORD, r.mWord));
291     }
292 
293     /**
294      * Test logic in getting the word range at the cursor.
295      */
296     @Test
testGetSuggestionSpansAtWord()297     public void testGetSuggestionSpansAtWord() {
298         helpTestGetSuggestionSpansAtWord(10);
299         helpTestGetSuggestionSpansAtWord(12);
300         helpTestGetSuggestionSpansAtWord(15);
301         helpTestGetSuggestionSpansAtWord(16);
302     }
303 
helpTestGetSuggestionSpansAtWord(final int cursorPos)304     private void helpTestGetSuggestionSpansAtWord(final int cursorPos) {
305         final SpacingAndPunctuations SPACE = new SpacingAndPunctuations(
306                 mSpacingAndPunctuations, new int[] { Constants.CODE_SPACE });
307         final MockInputMethodService mockInputMethodService = new MockInputMethodService();
308         final RichInputConnection ic = new RichInputConnection(mockInputMethodService);
309 
310         final String[] SUGGESTIONS1 = { "swing", "strong" };
311         final String[] SUGGESTIONS2 = { "storing", "strung" };
312 
313         // Test the usual case.
314         SpannableString text = new SpannableString("This is a string for test");
315         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
316                 10 /* start */, 16 /* end */, 0 /* flags */);
317         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
318         TextRange r;
319         SuggestionSpan[] suggestions;
320 
321         r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN);
322         suggestions = r.getSuggestionSpansAtWord();
323         assertEquals(suggestions.length, 1);
324         assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
325 
326         // Test the case with 2 suggestion spans in the same place.
327         text = new SpannableString("This is a string for test");
328         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
329                 10 /* start */, 16 /* end */, 0 /* flags */);
330         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
331                 10 /* start */, 16 /* end */, 0 /* flags */);
332         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
333         r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN);
334         suggestions = r.getSuggestionSpansAtWord();
335         assertEquals(suggestions.length, 2);
336         assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
337         assertEquals(suggestions[1].getSuggestions(), SUGGESTIONS2);
338 
339         // Test a case with overlapping spans, 2nd extending past the start of the word
340         text = new SpannableString("This is a string for test");
341         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
342                 10 /* start */, 16 /* end */, 0 /* flags */);
343         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
344                 5 /* start */, 16 /* end */, 0 /* flags */);
345         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
346         r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN);
347         suggestions = r.getSuggestionSpansAtWord();
348         assertEquals(suggestions.length, 1);
349         assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
350 
351         // Test a case with overlapping spans, 2nd extending past the end of the word
352         text = new SpannableString("This is a string for test");
353         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
354                 10 /* start */, 16 /* end */, 0 /* flags */);
355         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
356                 10 /* start */, 20 /* end */, 0 /* flags */);
357         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
358         r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN);
359         suggestions = r.getSuggestionSpansAtWord();
360         assertEquals(suggestions.length, 1);
361         assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
362 
363         // Test a case with overlapping spans, 2nd extending past both ends of the word
364         text = new SpannableString("This is a string for test");
365         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
366                 10 /* start */, 16 /* end */, 0 /* flags */);
367         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
368                 5 /* start */, 20 /* end */, 0 /* flags */);
369         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
370         r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN);
371         suggestions = r.getSuggestionSpansAtWord();
372         assertEquals(suggestions.length, 1);
373         assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
374 
375         // Test a case with overlapping spans, none right on the word
376         text = new SpannableString("This is a string for test");
377         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
378                 5 /* start */, 16 /* end */, 0 /* flags */);
379         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
380                 5 /* start */, 20 /* end */, 0 /* flags */);
381         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
382         r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN);
383         suggestions = r.getSuggestionSpansAtWord();
384         assertEquals(suggestions.length, 0);
385     }
386 
387     @Test
testCursorTouchingWord()388     public void testCursorTouchingWord() {
389         final MockInputMethodService ims = new MockInputMethodService();
390         final RichInputConnection ic = new RichInputConnection(ims);
391         final SpacingAndPunctuations sap = mSpacingAndPunctuations;
392 
393         ims.setInputConnection(new MockConnection("users", 5));
394         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
395         assertTrue(ic.isCursorTouchingWord(sap, true /* checkTextAfter */));
396 
397         ims.setInputConnection(new MockConnection("users'", 5));
398         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
399         assertTrue(ic.isCursorTouchingWord(sap, true));
400 
401         ims.setInputConnection(new MockConnection("users'", 6));
402         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
403         assertTrue(ic.isCursorTouchingWord(sap, true));
404 
405         ims.setInputConnection(new MockConnection("'users'", 6));
406         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
407         assertTrue(ic.isCursorTouchingWord(sap, true));
408 
409         ims.setInputConnection(new MockConnection("'users'", 7));
410         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
411         assertTrue(ic.isCursorTouchingWord(sap, true));
412 
413         ims.setInputConnection(new MockConnection("users '", 6));
414         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
415         assertFalse(ic.isCursorTouchingWord(sap, true));
416 
417         ims.setInputConnection(new MockConnection("users '", 7));
418         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
419         assertFalse(ic.isCursorTouchingWord(sap, true));
420 
421         ims.setInputConnection(new MockConnection("re-", 3));
422         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
423         assertTrue(ic.isCursorTouchingWord(sap, true));
424 
425         ims.setInputConnection(new MockConnection("re--", 4));
426         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
427         assertFalse(ic.isCursorTouchingWord(sap, true));
428 
429         ims.setInputConnection(new MockConnection("-", 1));
430         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
431         assertFalse(ic.isCursorTouchingWord(sap, true));
432 
433         ims.setInputConnection(new MockConnection("--", 2));
434         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
435         assertFalse(ic.isCursorTouchingWord(sap, true));
436 
437         ims.setInputConnection(new MockConnection(" -", 2));
438         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
439         assertFalse(ic.isCursorTouchingWord(sap, true));
440 
441         ims.setInputConnection(new MockConnection(" --", 3));
442         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
443         assertFalse(ic.isCursorTouchingWord(sap, true));
444 
445         ims.setInputConnection(new MockConnection(" users '", 1));
446         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
447         assertTrue(ic.isCursorTouchingWord(sap, true));
448 
449         ims.setInputConnection(new MockConnection(" users '", 3));
450         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
451         assertTrue(ic.isCursorTouchingWord(sap, true));
452 
453         ims.setInputConnection(new MockConnection(" users '", 7));
454         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
455         assertFalse(ic.isCursorTouchingWord(sap, true));
456 
457         ims.setInputConnection(new MockConnection(" users are", 7));
458         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
459         assertTrue(ic.isCursorTouchingWord(sap, true));
460 
461         ims.setInputConnection(new MockConnection(" users 'are", 7));
462         ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
463         assertFalse(ic.isCursorTouchingWord(sap, true));
464     }
465 }
466