RichInputConnectionAndTextRangeTests.java revision 2f7f6257b66fc1ed19b600f3d55902fd0de2e338
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.inputmethodservice.InputMethodService;
20import android.os.Parcel;
21import android.test.AndroidTestCase;
22import android.test.MoreAsserts;
23import android.test.suitebuilder.annotation.SmallTest;
24import android.text.SpannableString;
25import android.text.Spanned;
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.RichInputConnection.Range;
34
35import java.util.Locale;
36
37@SmallTest
38public class RichInputConnectionAndTextRangeTests extends AndroidTestCase {
39
40    // The following is meant to be a reasonable default for
41    // the "word_separators" resource.
42    private static final String sSeparators = ".,:;!?-";
43
44    @Override
45    protected void setUp() throws Exception {
46        super.setUp();
47    }
48
49    private class MockConnection extends InputConnectionWrapper {
50        final CharSequence mTextBefore;
51        final CharSequence mTextAfter;
52        final ExtractedText mExtractedText;
53
54        public MockConnection(final CharSequence text, final int cursorPosition) {
55            super(null, false);
56            // Interaction of spans with Parcels is completely non-trivial, but in the actual case
57            // the CharSequences do go through Parcels because they go through IPC. There
58            // are some significant differences between the behavior of Spanned objects that
59            // have and that have not gone through parceling, so it's much easier to simulate
60            // the environment with Parcels than try to emulate things by hand.
61            final Parcel p = Parcel.obtain();
62            TextUtils.writeToParcel(text.subSequence(0, cursorPosition), p, 0 /* flags */);
63            TextUtils.writeToParcel(text.subSequence(cursorPosition, text.length()), p,
64                    0 /* flags */);
65            final byte[] marshalled = p.marshall();
66            p.unmarshall(marshalled, 0, marshalled.length);
67            p.setDataPosition(0);
68            mTextBefore = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
69            mTextAfter = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
70            mExtractedText = null;
71            p.recycle();
72        }
73
74        public MockConnection(String textBefore, String textAfter, ExtractedText extractedText) {
75            super(null, false);
76            mTextBefore = textBefore;
77            mTextAfter = textAfter;
78            mExtractedText = extractedText;
79        }
80
81        /* (non-Javadoc)
82         * @see android.view.inputmethod.InputConnectionWrapper#getTextBeforeCursor(int, int)
83         */
84        @Override
85        public CharSequence getTextBeforeCursor(int n, int flags) {
86            return mTextBefore;
87        }
88
89        /* (non-Javadoc)
90         * @see android.view.inputmethod.InputConnectionWrapper#getTextAfterCursor(int, int)
91         */
92        @Override
93        public CharSequence getTextAfterCursor(int n, int flags) {
94            return mTextAfter;
95        }
96
97        /* (non-Javadoc)
98         * @see android.view.inputmethod.InputConnectionWrapper#getExtractedText(
99         *         ExtractedTextRequest, int)
100         */
101        @Override
102        public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
103            return mExtractedText;
104        }
105
106        @Override
107        public boolean beginBatchEdit() {
108            return true;
109        }
110
111        @Override
112        public boolean endBatchEdit() {
113            return true;
114        }
115
116        @Override
117        public boolean finishComposingText() {
118            return true;
119        }
120    }
121
122    private class MockInputMethodService extends InputMethodService {
123        InputConnection mInputConnection;
124        public void setInputConnection(final InputConnection inputConnection) {
125            mInputConnection = inputConnection;
126        }
127        @Override
128        public InputConnection getCurrentInputConnection() {
129            return mInputConnection;
130        }
131    }
132
133    /************************** Tests ************************/
134
135    /**
136     * Test for getting previous word (for bigram suggestions)
137     */
138    public void testGetPreviousWord() {
139        // If one of the following cases breaks, the bigram suggestions won't work.
140        assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSeparators, 2), "abc");
141        assertNull(RichInputConnection.getNthPreviousWord("abc", sSeparators, 2));
142        assertNull(RichInputConnection.getNthPreviousWord("abc. def", sSeparators, 2));
143
144        // The following tests reflect the current behavior of the function
145        // RichInputConnection#getNthPreviousWord.
146        // TODO: However at this time, the code does never go
147        // into such a path, so it should be safe to change the behavior of
148        // this function if needed - especially since it does not seem very
149        // logical. These tests are just there to catch any unintentional
150        // changes in the behavior of the RichInputConnection#getPreviousWord method.
151        assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSeparators, 2), "abc");
152        assertEquals(RichInputConnection.getNthPreviousWord("abc def.", sSeparators, 2), "abc");
153        assertEquals(RichInputConnection.getNthPreviousWord("abc def .", sSeparators, 2), "def");
154        assertNull(RichInputConnection.getNthPreviousWord("abc ", sSeparators, 2));
155
156        assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSeparators, 1), "def");
157        assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSeparators, 1), "def");
158        assertNull(RichInputConnection.getNthPreviousWord("abc def.", sSeparators, 1));
159        assertNull(RichInputConnection.getNthPreviousWord("abc def .", sSeparators, 1));
160    }
161
162    /**
163     * Test logic in getting the word range at the cursor.
164     */
165    public void testGetWordRangeAtCursor() {
166        ExtractedText et = new ExtractedText();
167        final MockInputMethodService mockInputMethodService = new MockInputMethodService();
168        final RichInputConnection ic = new RichInputConnection(mockInputMethodService);
169        mockInputMethodService.setInputConnection(new MockConnection("word wo", "rd", et));
170        et.startOffset = 0;
171        et.selectionStart = 7;
172        Range r;
173
174        ic.beginBatchEdit();
175        // basic case
176        r = ic.getWordRangeAtCursor(" ", 0);
177        assertTrue(TextUtils.equals("word", r.mWord));
178
179        // more than one word
180        r = ic.getWordRangeAtCursor(" ", 1);
181        assertTrue(TextUtils.equals("word word", r.mWord));
182        ic.endBatchEdit();
183
184        // tab character instead of space
185        mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et));
186        ic.beginBatchEdit();
187        r = ic.getWordRangeAtCursor("\t", 1);
188        ic.endBatchEdit();
189        assertTrue(TextUtils.equals("word\tword", r.mWord));
190
191        // only one word doesn't go too far
192        mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et));
193        ic.beginBatchEdit();
194        r = ic.getWordRangeAtCursor("\t", 1);
195        ic.endBatchEdit();
196        assertTrue(TextUtils.equals("word\tword", r.mWord));
197
198        // tab or space
199        mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et));
200        ic.beginBatchEdit();
201        r = ic.getWordRangeAtCursor(" \t", 1);
202        ic.endBatchEdit();
203        assertTrue(TextUtils.equals("word\tword", r.mWord));
204
205        // tab or space multiword
206        mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et));
207        ic.beginBatchEdit();
208        r = ic.getWordRangeAtCursor(" \t", 2);
209        ic.endBatchEdit();
210        assertTrue(TextUtils.equals("one word\tword", r.mWord));
211
212        // splitting on supplementary character
213        final String supplementaryChar = "\uD840\uDC8A";
214        mockInputMethodService.setInputConnection(
215                new MockConnection("one word" + supplementaryChar + "wo", "rd", et));
216        ic.beginBatchEdit();
217        r = ic.getWordRangeAtCursor(supplementaryChar, 0);
218        ic.endBatchEdit();
219        assertTrue(TextUtils.equals("word", r.mWord));
220    }
221
222    /**
223     * Test logic in getting the word range at the cursor.
224     */
225    public void testGetSuggestionSpansAtWord() {
226        helpTestGetSuggestionSpansAtWord(10);
227        helpTestGetSuggestionSpansAtWord(12);
228        helpTestGetSuggestionSpansAtWord(15);
229        helpTestGetSuggestionSpansAtWord(16);
230    }
231
232    private void helpTestGetSuggestionSpansAtWord(final int cursorPos) {
233        final MockInputMethodService mockInputMethodService = new MockInputMethodService();
234        final RichInputConnection ic = new RichInputConnection(mockInputMethodService);
235
236        final String[] SUGGESTIONS1 = { "swing", "strong" };
237        final String[] SUGGESTIONS2 = { "storing", "strung" };
238
239        // Test the usual case.
240        SpannableString text = new SpannableString("This is a string for test");
241        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
242                10 /* start */, 16 /* end */, 0 /* flags */);
243        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
244        Range r;
245        SuggestionSpan[] suggestions;
246
247        r = ic.getWordRangeAtCursor(" ", 0);
248        suggestions = r.getSuggestionSpansAtWord();
249        assertEquals(suggestions.length, 1);
250        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
251
252        // Test the case with 2 suggestion spans in the same place.
253        text = new SpannableString("This is a string for test");
254        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
255                10 /* start */, 16 /* end */, 0 /* flags */);
256        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
257                10 /* start */, 16 /* end */, 0 /* flags */);
258        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
259        r = ic.getWordRangeAtCursor(" ", 0);
260        suggestions = r.getSuggestionSpansAtWord();
261        assertEquals(suggestions.length, 2);
262        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
263        MoreAsserts.assertEquals(suggestions[1].getSuggestions(), SUGGESTIONS2);
264
265        // Test a case with overlapping spans, 2nd extending past the start of the word
266        text = new SpannableString("This is a string for test");
267        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
268                10 /* start */, 16 /* end */, 0 /* flags */);
269        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
270                5 /* start */, 16 /* end */, 0 /* flags */);
271        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
272        r = ic.getWordRangeAtCursor(" ", 0);
273        suggestions = r.getSuggestionSpansAtWord();
274        assertEquals(suggestions.length, 1);
275        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
276
277        // Test a case with overlapping spans, 2nd extending past the end of the word
278        text = new SpannableString("This is a string for test");
279        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
280                10 /* start */, 16 /* end */, 0 /* flags */);
281        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
282                10 /* start */, 20 /* end */, 0 /* flags */);
283        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
284        r = ic.getWordRangeAtCursor(" ", 0);
285        suggestions = r.getSuggestionSpansAtWord();
286        assertEquals(suggestions.length, 1);
287        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
288
289        // Test a case with overlapping spans, 2nd extending past both ends of the word
290        text = new SpannableString("This is a string for test");
291        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
292                10 /* start */, 16 /* end */, 0 /* flags */);
293        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
294                5 /* start */, 20 /* end */, 0 /* flags */);
295        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
296        r = ic.getWordRangeAtCursor(" ", 0);
297        suggestions = r.getSuggestionSpansAtWord();
298        assertEquals(suggestions.length, 1);
299        MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
300
301        // Test a case with overlapping spans, none right on the word
302        text = new SpannableString("This is a string for test");
303        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
304                5 /* start */, 16 /* end */, 0 /* flags */);
305        text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
306                5 /* start */, 20 /* end */, 0 /* flags */);
307        mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
308        r = ic.getWordRangeAtCursor(" ", 0);
309        suggestions = r.getSuggestionSpansAtWord();
310        assertEquals(suggestions.length, 0);
311    }
312}
313