RichInputConnectionAndTextRangeTests.java revision 4569a734adffe1d12a4a1e8ff751608c1e4b2faf
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, 0);
237        assertTrue(TextUtils.equals("word", r.mWord));
238
239        // more than one word
240        r = ic.getWordRangeAtCursor(SPACE, 1);
241        assertTrue(TextUtils.equals("word word", r.mWord));
242        ic.endBatchEdit();
243
244        // tab character instead of space
245        mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et));
246        ic.beginBatchEdit();
247        r = ic.getWordRangeAtCursor(TAB, 1);
248        ic.endBatchEdit();
249        assertTrue(TextUtils.equals("word\tword", r.mWord));
250
251        // only one word doesn't go too far
252        mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et));
253        ic.beginBatchEdit();
254        r = ic.getWordRangeAtCursor(TAB, 1);
255        ic.endBatchEdit();
256        assertTrue(TextUtils.equals("word\tword", r.mWord));
257
258        // tab or space
259        mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et));
260        ic.beginBatchEdit();
261        r = ic.getWordRangeAtCursor(SPACE_TAB, 1);
262        ic.endBatchEdit();
263        assertTrue(TextUtils.equals("word\tword", r.mWord));
264
265        // tab or space multiword
266        mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et));
267        ic.beginBatchEdit();
268        r = ic.getWordRangeAtCursor(SPACE_TAB, 2);
269        ic.endBatchEdit();
270        assertTrue(TextUtils.equals("one word\tword", r.mWord));
271
272        // splitting on supplementary character
273        mockInputMethodService.setInputConnection(
274                new MockConnection("one word" + SUPPLEMENTARY_CHAR + "wo", "rd", et));
275        ic.beginBatchEdit();
276        r = ic.getWordRangeAtCursor(StringUtils.toSortedCodePointArray(SUPPLEMENTARY_CHAR), 0);
277        ic.endBatchEdit();
278        assertTrue(TextUtils.equals("word", r.mWord));
279    }
280
281    /**
282     * Test logic in getting the word range at the cursor.
283     */
284    public void testGetSuggestionSpansAtWord() {
285        helpTestGetSuggestionSpansAtWord(10);
286        helpTestGetSuggestionSpansAtWord(12);
287        helpTestGetSuggestionSpansAtWord(15);
288        helpTestGetSuggestionSpansAtWord(16);
289    }
290
291    private void helpTestGetSuggestionSpansAtWord(final int cursorPos) {
292        final MockInputMethodService mockInputMethodService = new MockInputMethodService();
293        final RichInputConnection ic = new RichInputConnection(mockInputMethodService);
294
295        final String[] SUGGESTIONS1 = { "swing", "strong" };
296        final String[] SUGGESTIONS2 = { "storing", "strung" };
297
298        // Test the usual case.
299        SpannableString 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        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
303        TextRange r;
304        SuggestionSpan[] suggestions;
305
306        r = ic.getWordRangeAtCursor(SPACE, 0);
307        suggestions = r.getSuggestionSpansAtWord();
308        assertEquals(suggestions.length, 1);
309        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
310
311        // Test the case with 2 suggestion spans in the same place.
312        text = new SpannableString("This is a string for test");
313        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
314                10 /* start */, 16 /* end */, 0 /* flags */);
315        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
316                10 /* start */, 16 /* end */, 0 /* flags */);
317        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
318        r = ic.getWordRangeAtCursor(SPACE, 0);
319        suggestions = r.getSuggestionSpansAtWord();
320        assertEquals(suggestions.length, 2);
321        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
322        MoreAsserts.assertEquals(suggestions[1].getSuggestions(), SUGGESTIONS2);
323
324        // Test a case with overlapping spans, 2nd extending past the start of the word
325        text = new SpannableString("This is a string for test");
326        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
327                10 /* start */, 16 /* end */, 0 /* flags */);
328        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
329                5 /* start */, 16 /* end */, 0 /* flags */);
330        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
331        r = ic.getWordRangeAtCursor(SPACE, 0);
332        suggestions = r.getSuggestionSpansAtWord();
333        assertEquals(suggestions.length, 1);
334        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
335
336        // Test a case with overlapping spans, 2nd extending past the end of the word
337        text = new SpannableString("This is a string for test");
338        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
339                10 /* start */, 16 /* end */, 0 /* flags */);
340        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
341                10 /* start */, 20 /* end */, 0 /* flags */);
342        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
343        r = ic.getWordRangeAtCursor(SPACE, 0);
344        suggestions = r.getSuggestionSpansAtWord();
345        assertEquals(suggestions.length, 1);
346        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
347
348        // Test a case with overlapping spans, 2nd extending past both ends of the word
349        text = new SpannableString("This is a string for test");
350        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
351                10 /* start */, 16 /* end */, 0 /* flags */);
352        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
353                5 /* start */, 20 /* end */, 0 /* flags */);
354        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
355        r = ic.getWordRangeAtCursor(SPACE, 0);
356        suggestions = r.getSuggestionSpansAtWord();
357        assertEquals(suggestions.length, 1);
358        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
359
360        // Test a case with overlapping spans, none right on the word
361        text = new SpannableString("This is a string for test");
362        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
363                5 /* start */, 16 /* end */, 0 /* flags */);
364        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
365                5 /* start */, 20 /* end */, 0 /* flags */);
366        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
367        r = ic.getWordRangeAtCursor(SPACE, 0);
368        suggestions = r.getSuggestionSpansAtWord();
369        assertEquals(suggestions.length, 0);
370    }
371
372    public void testCursorTouchingWord() {
373        final MockInputMethodService ims = new MockInputMethodService();
374        final RichInputConnection ic = new RichInputConnection(ims);
375        final SpacingAndPunctuations sap = mSpacingAndPunctuations;
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'", 5));
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'", 6));
390        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
391        assertTrue(ic.isCursorTouchingWord(sap));
392
393        ims.setInputConnection(new MockConnection("'users'", 7));
394        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
395        assertTrue(ic.isCursorTouchingWord(sap));
396
397        ims.setInputConnection(new MockConnection("users '", 6));
398        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
399        assertFalse(ic.isCursorTouchingWord(sap));
400
401        ims.setInputConnection(new MockConnection("users '", 7));
402        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
403        assertFalse(ic.isCursorTouchingWord(sap));
404
405        ims.setInputConnection(new MockConnection("re-", 3));
406        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
407        assertTrue(ic.isCursorTouchingWord(sap));
408
409        ims.setInputConnection(new MockConnection("re--", 4));
410        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
411        assertFalse(ic.isCursorTouchingWord(sap));
412
413        ims.setInputConnection(new MockConnection("-", 1));
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(" -", 2));
422        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
423        assertFalse(ic.isCursorTouchingWord(sap));
424
425        ims.setInputConnection(new MockConnection(" --", 3));
426        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
427        assertFalse(ic.isCursorTouchingWord(sap));
428
429        ims.setInputConnection(new MockConnection(" users '", 1));
430        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
431        assertTrue(ic.isCursorTouchingWord(sap));
432
433        ims.setInputConnection(new MockConnection(" users '", 3));
434        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
435        assertTrue(ic.isCursorTouchingWord(sap));
436
437        ims.setInputConnection(new MockConnection(" users '", 7));
438        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
439        assertFalse(ic.isCursorTouchingWord(sap));
440
441        ims.setInputConnection(new MockConnection(" users are", 7));
442        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
443        assertTrue(ic.isCursorTouchingWord(sap));
444
445        ims.setInputConnection(new MockConnection(" users 'are", 7));
446        ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true);
447        assertFalse(ic.isCursorTouchingWord(sap));
448    }
449}
450