InputLogicTests.java revision 845b24d9d31072b98958c557366617ad1c34f1b7
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.SharedPreferences;
22import android.os.Looper;
23import android.os.MessageQueue;
24import android.preference.PreferenceManager;
25import android.test.ServiceTestCase;
26import android.text.InputType;
27import android.text.SpannableStringBuilder;
28import android.text.style.SuggestionSpan;
29import android.util.Log;
30import android.view.LayoutInflater;
31import android.view.ViewGroup;
32import android.view.View;
33import android.view.inputmethod.BaseInputConnection;
34import android.view.inputmethod.EditorInfo;
35import android.view.inputmethod.InputConnection;
36import android.widget.FrameLayout;
37import android.widget.TextView;
38
39import com.android.inputmethod.keyboard.Keyboard;
40import com.android.inputmethod.keyboard.KeyboardActionListener;
41import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService; // for proximity info
42import com.android.inputmethod.latin.spellcheck.SpellCheckerProximityInfo;
43
44import java.util.Arrays;
45import java.util.HashMap;
46
47public class InputLogicTests extends ServiceTestCase<LatinIME> {
48
49    private static final String PREF_DEBUG_MODE = "debug_mode";
50
51    private LatinIME mLatinIME;
52    private TextView mTextView;
53    private InputConnection mInputConnection;
54
55    public InputLogicTests() {
56        super(LatinIME.class);
57    }
58
59    // returns the previous setting value
60    private boolean setDebugMode(final boolean mode) {
61        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME);
62        final boolean previousDebugSetting = prefs.getBoolean(PREF_DEBUG_MODE, false);
63        final SharedPreferences.Editor editor = prefs.edit();
64        editor.putBoolean(PREF_DEBUG_MODE, true);
65        editor.commit();
66        return previousDebugSetting;
67    }
68
69    @Override
70    protected void setUp() {
71        try {
72            super.setUp();
73        } catch (Exception e) {
74            e.printStackTrace();
75        }
76        mTextView = new TextView(getContext());
77        mTextView.setInputType(InputType.TYPE_CLASS_TEXT);
78        mTextView.setEnabled(true);
79        setupService();
80        mLatinIME = getService();
81        final boolean previousDebugSetting = setDebugMode(true);
82        mLatinIME.onCreate();
83        setDebugMode(previousDebugSetting);
84        final EditorInfo ei = new EditorInfo();
85        ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
86        final InputConnection ic = mTextView.onCreateInputConnection(ei);
87        ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
88        final LayoutInflater inflater =
89                (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
90        final ViewGroup vg = new FrameLayout(getContext());
91        final View inputView = inflater.inflate(R.layout.input_view, vg);
92        mLatinIME.setInputView(inputView);
93        mLatinIME.onBindInput();
94        mLatinIME.onCreateInputView();
95        mLatinIME.onStartInputView(ei, false);
96        mLatinIME.onCreateInputMethodInterface().startInput(ic, ei);
97        mInputConnection = ic;
98        // Wait for the main dictionary to be loaded (we need it for auto-correction tests)
99        int remainingAttempts = 10;
100        while (remainingAttempts > 0 && !mLatinIME.mSuggest.hasMainDictionary()) {
101            try {
102                Thread.sleep(100);
103            } catch (InterruptedException e) {
104                // Don't do much
105            } finally {
106                --remainingAttempts;
107            }
108        }
109        if (!mLatinIME.mSuggest.hasMainDictionary()) {
110            throw new RuntimeException("Can't initialize the main dictionary");
111        }
112    }
113
114    // We need to run the messages added to the handler from LatinIME. The only way to do
115    // that is to call Looper#loop() on the right looper, so we're going to get the looper
116    // object and call #loop() here. The messages in the handler actually run on the UI
117    // thread of the keyboard by design of the handler, so we want to call it synchronously
118    // on the same thread that the tests are running on to mimic the actual environment as
119    // closely as possible.
120    // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method
121    // is called, so we need to do that at the right time so that #loop() returns at some
122    // point and we don't end up in an infinite loop.
123    // After we quit, the looper is still technically ready to process more messages but
124    // the handler will refuse to enqueue any because #quit() has been called and it
125    // explicitly tests for it on message enqueuing, so we'll have to reset it so that
126    // it lets us continue normal operation.
127    private void runMessages() {
128        // Here begins deep magic.
129        final Looper looper = mLatinIME.mHandler.getLooper();
130        mLatinIME.mHandler.post(new Runnable() {
131                @Override
132                public void run() {
133                    looper.quit();
134                }
135            });
136        // The only way to get out of Looper#loop() is to call #quit() on it (or on its queue).
137        // Once #quit() is called remaining messages are not processed, which is why we post
138        // a message that calls it instead of calling it directly.
139        looper.loop();
140
141        // Once #quit() has been called, the message queue has an "mQuiting" field that prevents
142        // any subsequent post in this queue. However the queue itself is still fully functional!
143        // If we have a way of resetting "queue.mQuiting" then we can continue using it as normal,
144        // coming back to this method to run the messages.
145        MessageQueue queue = looper.getQueue();
146        try {
147            // However there is no way of doing it externally, and mQuiting is private.
148            // So... get out the big guns.
149            java.lang.reflect.Field f = MessageQueue.class.getDeclaredField("mQuiting");
150            f.setAccessible(true); // What do you mean "private"?
151            f.setBoolean(queue, false);
152        } catch (NoSuchFieldException e) {
153            throw new RuntimeException(e);
154        } catch (IllegalAccessException e) {
155            throw new RuntimeException(e);
156        }
157    }
158
159    // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME.
160    private void type(final int codePoint) {
161        // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the
162        // code (although multitouch/slide input and other factors make the sequencing complicated).
163        // They are supposed to be entirely deconnected from the input logic from LatinIME point of
164        // view and only delegates to the parts of the code that care. So we don't include them here
165        // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies,
166        // but keep them in mind if something breaks. Commenting them out as is should work.
167        //mLatinIME.onPressKey(codePoint);
168        mLatinIME.onCodeInput(codePoint,
169                KeyboardActionListener.SPELL_CHECKER_COORDINATE,
170                KeyboardActionListener.SPELL_CHECKER_COORDINATE);
171        //mLatinIME.onReleaseKey(codePoint, false);
172    }
173
174    private void type(final String stringToType) {
175        for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) {
176            type(stringToType.codePointAt(i));
177        }
178    }
179
180    // Helper to avoid writing the try{}catch block each time
181    private static void sleep(final int milliseconds) {
182        try {
183            Thread.sleep(milliseconds);
184        } catch (InterruptedException e) {}
185    }
186
187    public void testTypeWord() {
188        final String WORD_TO_TYPE = "abcd";
189        type(WORD_TO_TYPE);
190        assertEquals("type word", WORD_TO_TYPE, mTextView.getText().toString());
191    }
192
193    public void testPickSuggestionThenBackspace() {
194        final String WORD_TO_TYPE = "this";
195        final String EXPECTED_RESULT = "this";
196        type(WORD_TO_TYPE);
197        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
198        mLatinIME.onUpdateSelection(0, 0, WORD_TO_TYPE.length(), WORD_TO_TYPE.length(), -1, -1);
199        type(Keyboard.CODE_DELETE);
200        assertEquals("press suggestion then backspace", EXPECTED_RESULT,
201                mTextView.getText().toString());
202    }
203
204    public void testPickAutoCorrectionThenBackspace() {
205        final String WORD_TO_TYPE = "tgis";
206        final String WORD_TO_PICK = "this";
207        final String EXPECTED_RESULT = "tgis";
208        type(WORD_TO_TYPE);
209        // Choose the auto-correction, which is always in position 0. For "tgis", the
210        // auto-correction should be "this".
211        mLatinIME.pickSuggestionManually(0, WORD_TO_PICK);
212        mLatinIME.onUpdateSelection(0, 0, WORD_TO_TYPE.length(), WORD_TO_TYPE.length(), -1, -1);
213        assertEquals("pick typed word over auto-correction then backspace", WORD_TO_PICK,
214                mTextView.getText().toString());
215        type(Keyboard.CODE_DELETE);
216        assertEquals("pick typed word over auto-correction then backspace", EXPECTED_RESULT,
217                mTextView.getText().toString());
218    }
219
220    public void testPickTypedWordOverAutoCorrectionThenBackspace() {
221        final String WORD_TO_TYPE = "tgis";
222        final String EXPECTED_RESULT = "tgis";
223        type(WORD_TO_TYPE);
224        // Choose the typed word, which should be in position 1 (because position 0 should
225        // be occupied by the "this" auto-correction, as checked by testAutoCorrect())
226        mLatinIME.pickSuggestionManually(1, WORD_TO_TYPE);
227        mLatinIME.onUpdateSelection(0, 0, WORD_TO_TYPE.length(), WORD_TO_TYPE.length(), -1, -1);
228        assertEquals("pick typed word over auto-correction then backspace", WORD_TO_TYPE,
229                mTextView.getText().toString());
230        type(Keyboard.CODE_DELETE);
231        assertEquals("pick typed word over auto-correction then backspace", EXPECTED_RESULT,
232                mTextView.getText().toString());
233    }
234
235    public void testPickDifferentSuggestionThenBackspace() {
236        final String WORD_TO_TYPE = "tgis";
237        final String WORD_TO_PICK = "thus";
238        final String EXPECTED_RESULT = "tgis";
239        type(WORD_TO_TYPE);
240        // Choose the second suggestion, which should be in position 2 and should be "thus"
241        // when "tgis is typed.
242        mLatinIME.pickSuggestionManually(2, WORD_TO_PICK);
243        mLatinIME.onUpdateSelection(0, 0, WORD_TO_TYPE.length(), WORD_TO_TYPE.length(), -1, -1);
244        assertEquals("pick different suggestion then backspace", WORD_TO_PICK,
245                mTextView.getText().toString());
246        type(Keyboard.CODE_DELETE);
247        assertEquals("pick different suggestion then backspace", EXPECTED_RESULT,
248                mTextView.getText().toString());
249    }
250
251    public void testDeleteSelection() {
252        final String STRING_TO_TYPE = "some text delete me some text";
253        final int SELECTION_START = 10;
254        final int SELECTION_END = 19;
255        final String EXPECTED_RESULT = "some text  some text";
256        type(STRING_TO_TYPE);
257        // There is no IMF to call onUpdateSelection for us so we must do it by hand.
258        // Send once to simulate the cursor actually responding to the move caused by typing.
259        // This is necessary because LatinIME is bookkeeping to avoid confusing a real cursor
260        // move with a move triggered by LatinIME inputting stuff.
261        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
262        mInputConnection.setSelection(SELECTION_START, SELECTION_END);
263        // And now we simulate the user actually selecting some text.
264        mLatinIME.onUpdateSelection(0, 0, SELECTION_START, SELECTION_END, -1, -1);
265        type(Keyboard.CODE_DELETE);
266        assertEquals("delete selection", EXPECTED_RESULT, mTextView.getText().toString());
267    }
268
269    public void testAutoCorrect() {
270        final String STRING_TO_TYPE = "tgis ";
271        final String EXPECTED_RESULT = "this ";
272        type(STRING_TO_TYPE);
273        assertEquals("simple auto-correct", EXPECTED_RESULT, mTextView.getText().toString());
274    }
275
276    public void testAutoCorrectWithPeriod() {
277        final String STRING_TO_TYPE = "tgis.";
278        final String EXPECTED_RESULT = "this.";
279        type(STRING_TO_TYPE);
280        assertEquals("auto-correct with period", EXPECTED_RESULT, mTextView.getText().toString());
281    }
282
283    public void testAutoCorrectWithPeriodThenRevert() {
284        final String STRING_TO_TYPE = "tgis.";
285        final String EXPECTED_RESULT = "tgis.";
286        type(STRING_TO_TYPE);
287        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
288        type(Keyboard.CODE_DELETE);
289        assertEquals("auto-correct with period then revert", EXPECTED_RESULT,
290                mTextView.getText().toString());
291    }
292
293    public void testDoubleSpace() {
294        final String STRING_TO_TYPE = "this  ";
295        final String EXPECTED_RESULT = "this. ";
296        type(STRING_TO_TYPE);
297        assertEquals("double space make a period", EXPECTED_RESULT, mTextView.getText().toString());
298    }
299
300    public void testCancelDoubleSpace() {
301        final String STRING_TO_TYPE = "this  ";
302        final String EXPECTED_RESULT = "this  ";
303        type(STRING_TO_TYPE);
304        type(Keyboard.CODE_DELETE);
305        assertEquals("double space make a period", EXPECTED_RESULT, mTextView.getText().toString());
306    }
307
308    public void testBackspaceAtStartAfterAutocorrect() {
309        final String STRING_TO_TYPE = "tgis ";
310        final String EXPECTED_RESULT = "this ";
311        final int NEW_CURSOR_POSITION = 0;
312        type(STRING_TO_TYPE);
313        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
314        mInputConnection.setSelection(NEW_CURSOR_POSITION, NEW_CURSOR_POSITION);
315        mLatinIME.onUpdateSelection(0, 0, NEW_CURSOR_POSITION, NEW_CURSOR_POSITION, -1, -1);
316        type(Keyboard.CODE_DELETE);
317        assertEquals("auto correct then move cursor to start of line then backspace",
318                EXPECTED_RESULT, mTextView.getText().toString());
319    }
320
321    public void testAutoCorrectThenMoveCursorThenBackspace() {
322        final String STRING_TO_TYPE = "and tgis ";
323        final String EXPECTED_RESULT = "andthis ";
324        final int NEW_CURSOR_POSITION = STRING_TO_TYPE.indexOf('t');
325        type(STRING_TO_TYPE);
326        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
327        mInputConnection.setSelection(NEW_CURSOR_POSITION, NEW_CURSOR_POSITION);
328        mLatinIME.onUpdateSelection(0, 0, NEW_CURSOR_POSITION, NEW_CURSOR_POSITION, -1, -1);
329        type(Keyboard.CODE_DELETE);
330        assertEquals("auto correct then move cursor then backspace",
331                EXPECTED_RESULT, mTextView.getText().toString());
332    }
333
334    public void testNoSpaceAfterManualPick() {
335        final String WORD_TO_TYPE = "this";
336        final String EXPECTED_RESULT = WORD_TO_TYPE;
337        type(WORD_TO_TYPE);
338        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
339        assertEquals("no space after manual pick", EXPECTED_RESULT,
340                mTextView.getText().toString());
341    }
342
343    public void testManualPickThenType() {
344        final String WORD1_TO_TYPE = "this";
345        final String WORD2_TO_TYPE = "is";
346        final String EXPECTED_RESULT = "this is";
347        type(WORD1_TO_TYPE);
348        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
349        type(WORD2_TO_TYPE);
350        assertEquals("manual pick then type", EXPECTED_RESULT, mTextView.getText().toString());
351    }
352
353    public void testManualPickThenSeparator() {
354        final String WORD1_TO_TYPE = "this";
355        final String WORD2_TO_TYPE = "!";
356        final String EXPECTED_RESULT = "this!";
357        type(WORD1_TO_TYPE);
358        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
359        type(WORD2_TO_TYPE);
360        assertEquals("manual pick then separator", EXPECTED_RESULT, mTextView.getText().toString());
361    }
362
363    public void testWordThenSpaceThenPunctuationFromStripTwice() {
364        final String WORD_TO_TYPE = "this ";
365        final String PUNCTUATION_FROM_STRIP = "!";
366        final String EXPECTED_RESULT = "this!! ";
367        type(WORD_TO_TYPE);
368        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
369        runMessages();
370        assertTrue("type word then type space should display punctuation strip",
371                mLatinIME.isShowingPunctuationList());
372        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
373        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
374        assertEquals("type word then type space then punctuation from strip twice", EXPECTED_RESULT,
375                mTextView.getText().toString());
376    }
377
378    public void testWordThenSpaceThenPunctuationFromKeyboardTwice() {
379        final String WORD_TO_TYPE = "this !!";
380        final String EXPECTED_RESULT = "this !!";
381        type(WORD_TO_TYPE);
382        assertEquals("manual pick then space then punctuation from keyboard twice", EXPECTED_RESULT,
383                mTextView.getText().toString());
384    }
385
386    public void testManualPickThenPunctuationFromStripTwiceThenType() {
387        final String WORD1_TO_TYPE = "this";
388        final String WORD2_TO_TYPE = "is";
389        final String PUNCTUATION_FROM_STRIP = "!";
390        final String EXPECTED_RESULT = "this!! is";
391        type(WORD1_TO_TYPE);
392        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
393        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
394        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
395        type(WORD2_TO_TYPE);
396        assertEquals("pick word then pick punctuation twice then type", EXPECTED_RESULT,
397                mTextView.getText().toString());
398    }
399
400    public void testManualPickThenSpaceThenType() {
401        final String WORD1_TO_TYPE = "this";
402        final String WORD2_TO_TYPE = " is";
403        final String EXPECTED_RESULT = "this is";
404        type(WORD1_TO_TYPE);
405        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
406        type(WORD2_TO_TYPE);
407        assertEquals("manual pick then space then type", EXPECTED_RESULT,
408                mTextView.getText().toString());
409    }
410
411    public void testManualPickThenManualPick() {
412        final String WORD1_TO_TYPE = "this";
413        final String WORD2_TO_PICK = "is";
414        final String EXPECTED_RESULT = "this is";
415        type(WORD1_TO_TYPE);
416        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
417        // Here we fake picking a word through bigram prediction. This test is taking
418        // advantage of the fact that Latin IME blindly trusts the caller of #pickSuggestionManually
419        // to actually pass the right string.
420        mLatinIME.pickSuggestionManually(1, WORD2_TO_PICK);
421        assertEquals("manual pick then manual pick", EXPECTED_RESULT,
422                mTextView.getText().toString());
423    }
424
425    public void testManualPickThenManualPickWithPunctAtStart() {
426        final String WORD1_TO_TYPE = "this";
427        final String WORD2_TO_PICK = "!is";
428        final String EXPECTED_RESULT = "this!is";
429        type(WORD1_TO_TYPE);
430        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
431        mLatinIME.pickSuggestionManually(1, WORD2_TO_PICK);
432        assertEquals("manual pick then manual pick a word with punct at start", EXPECTED_RESULT,
433                mTextView.getText().toString());
434    }
435
436    public void testDeleteWholeComposingWord() {
437        final String WORD_TO_TYPE = "this";
438        type(WORD_TO_TYPE);
439        for (int i = 0; i < WORD_TO_TYPE.length(); ++i) {
440            type(Keyboard.CODE_DELETE);
441        }
442        assertEquals("delete whole composing word", "", mTextView.getText().toString());
443    }
444
445    public void testManuallyPickedWordThenColon() {
446        final String WORD_TO_TYPE = "this";
447        final String PUNCTUATION = ":";
448        final String EXPECTED_RESULT = "this:";
449        type(WORD_TO_TYPE);
450        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
451        type(PUNCTUATION);
452        assertEquals("manually pick word then colon",
453                EXPECTED_RESULT, mTextView.getText().toString());
454    }
455
456    public void testManuallyPickedWordThenOpenParen() {
457        final String WORD_TO_TYPE = "this";
458        final String PUNCTUATION = "(";
459        final String EXPECTED_RESULT = "this (";
460        type(WORD_TO_TYPE);
461        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
462        type(PUNCTUATION);
463        assertEquals("manually pick word then open paren",
464                EXPECTED_RESULT, mTextView.getText().toString());
465    }
466
467    public void testManuallyPickedWordThenCloseParen() {
468        final String WORD_TO_TYPE = "this";
469        final String PUNCTUATION = ")";
470        final String EXPECTED_RESULT = "this)";
471        type(WORD_TO_TYPE);
472        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
473        type(PUNCTUATION);
474        assertEquals("manually pick word then close paren",
475                EXPECTED_RESULT, mTextView.getText().toString());
476    }
477
478    public void testManuallyPickedWordThenSmiley() {
479        final String WORD_TO_TYPE = "this";
480        final String SPECIAL_KEY = ":-)";
481        final String EXPECTED_RESULT = "this :-)";
482        type(WORD_TO_TYPE);
483        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
484        mLatinIME.onTextInput(SPECIAL_KEY);
485        assertEquals("manually pick word then press the smiley key",
486                EXPECTED_RESULT, mTextView.getText().toString());
487    }
488
489    public void testManuallyPickedWordThenDotCom() {
490        final String WORD_TO_TYPE = "this";
491        final String SPECIAL_KEY = ".com";
492        final String EXPECTED_RESULT = "this.com";
493        type(WORD_TO_TYPE);
494        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
495        mLatinIME.onTextInput(SPECIAL_KEY);
496        assertEquals("manually pick word then press the .com key",
497                EXPECTED_RESULT, mTextView.getText().toString());
498    }
499
500    public void testTypeWordTypeDotThenPressDotCom() {
501        final String WORD_TO_TYPE = "this.";
502        final String SPECIAL_KEY = ".com";
503        final String EXPECTED_RESULT = "this.com";
504        type(WORD_TO_TYPE);
505        mLatinIME.onTextInput(SPECIAL_KEY);
506        assertEquals("type word type dot then press the .com key",
507                EXPECTED_RESULT, mTextView.getText().toString());
508    }
509
510    // A helper class to ease span tests
511    private static class Span {
512        final SpannableStringBuilder mInputText;
513        final SuggestionSpan mSpan;
514        final int mStart;
515        final int mEnd;
516        // The supplied CharSequence should be an instance of SpannableStringBuilder,
517        // and it should contain exactly zero or one SuggestionSpan. Otherwise, an exception
518        // is thrown.
519        public Span(final CharSequence inputText) {
520            mInputText = (SpannableStringBuilder)inputText;
521            final SuggestionSpan[] spans =
522                    mInputText.getSpans(0, mInputText.length(), SuggestionSpan.class);
523            if (0 == spans.length) {
524                mSpan = null;
525                mStart = -1;
526                mEnd = -1;
527            } else if (1 == spans.length) {
528                mSpan = spans[0];
529                mStart = mInputText.getSpanStart(mSpan);
530                mEnd = mInputText.getSpanEnd(mSpan);
531            } else {
532                throw new RuntimeException("Expected one SuggestionSpan, found " + spans.length);
533            }
534        }
535        public boolean isAutoCorrectionIndicator() {
536            return 0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & mSpan.getFlags());
537        }
538    }
539
540    static final int DELAY_TO_WAIT_FOR_UNDERLINE = 200; // The message is posted with a 100 ms delay
541    public void testBlueUnderline() {
542        final String STRING_TO_TYPE = "tgis";
543        final int EXPECTED_SPAN_START = 0;
544        final int EXPECTED_SPAN_END = 4;
545        type(STRING_TO_TYPE);
546        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
547        runMessages();
548        final Span span = new Span(mTextView.getText());
549        assertEquals("show blue underline, span start", EXPECTED_SPAN_START, span.mStart);
550        assertEquals("show blue underline, span end", EXPECTED_SPAN_END, span.mEnd);
551        assertEquals("show blue underline, span color", true, span.isAutoCorrectionIndicator());
552    }
553
554    public void testBlueUnderlineDisappears() {
555        final String STRING_1_TO_TYPE = "tgis";
556        final String STRING_2_TO_TYPE = "q";
557        final int EXPECTED_SPAN_START = 0;
558        final int EXPECTED_SPAN_END = 5;
559        type(STRING_1_TO_TYPE);
560        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
561        runMessages();
562        type(STRING_2_TO_TYPE);
563        // We haven't have time to look into the dictionary yet, so the line should still be
564        // blue to avoid any flicker.
565        final Span spanBefore = new Span(mTextView.getText());
566        assertEquals("extend blue underline, span start", EXPECTED_SPAN_START, spanBefore.mStart);
567        assertEquals("extend blue underline, span end", EXPECTED_SPAN_END, spanBefore.mEnd);
568        assertEquals("extend blue underline, span color", true,
569                spanBefore.isAutoCorrectionIndicator());
570        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
571        runMessages();
572        // Now we have been able to re-evaluate the word, there shouldn't be an auto-correction span
573        final Span spanAfter = new Span(mTextView.getText());
574        assertNull("hide blue underline", spanAfter.mSpan);
575    }
576
577    public void testBlueUnderlineOnBackspace() {
578        final String STRING_TO_TYPE = "tgis";
579        final int EXPECTED_SPAN_START = 0;
580        final int EXPECTED_SPAN_END = 4;
581        type(STRING_TO_TYPE);
582        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
583        runMessages();
584        type(Keyboard.CODE_SPACE);
585        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
586        runMessages();
587        type(Keyboard.CODE_DELETE);
588        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
589        runMessages();
590        type(Keyboard.CODE_DELETE);
591        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
592        runMessages();
593        final Span span = new Span(mTextView.getText());
594        assertEquals("show blue underline after backspace, span start",
595                EXPECTED_SPAN_START, span.mStart);
596        assertEquals("show blue underline after backspace, span end",
597                EXPECTED_SPAN_END, span.mEnd);
598        assertEquals("show blue underline after backspace, span color", true,
599                span.isAutoCorrectionIndicator());
600    }
601
602    public void testBlueUnderlineDisappearsWhenCursorMoved() {
603        final String STRING_TO_TYPE = "tgis";
604        final int NEW_CURSOR_POSITION = 0;
605        type(STRING_TO_TYPE);
606        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
607        // Simulate the onUpdateSelection() event
608        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
609        runMessages();
610        // Here the blue underline has been set. testBlueUnderline() is testing for this already,
611        // so let's not test it here again.
612        // Now simulate the user moving the cursor.
613        mInputConnection.setSelection(NEW_CURSOR_POSITION, NEW_CURSOR_POSITION);
614        mLatinIME.onUpdateSelection(0, 0, NEW_CURSOR_POSITION, NEW_CURSOR_POSITION, -1, -1);
615        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
616        runMessages();
617        final Span span = new Span(mTextView.getText());
618        assertNull("blue underline removed when cursor is moved", span.mSpan);
619    }
620    // TODO: Add some tests for non-BMP characters
621}
622