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