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