InputTestsBase.java revision b8e2ae3bc312269897057fccc34cd736c05bcc90
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.SharedPreferences;
21import android.os.Looper;
22import android.os.MessageQueue;
23import android.preference.PreferenceManager;
24import android.test.ServiceTestCase;
25import android.text.InputType;
26import android.text.SpannableStringBuilder;
27import android.text.style.SuggestionSpan;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.inputmethod.EditorInfo;
32import android.view.inputmethod.InputConnection;
33import android.view.inputmethod.InputMethodInfo;
34import android.view.inputmethod.InputMethodManager;
35import android.view.inputmethod.InputMethodSubtype;
36import android.widget.FrameLayout;
37import android.widget.TextView;
38
39import com.android.inputmethod.keyboard.Key;
40import com.android.inputmethod.keyboard.Keyboard;
41import com.android.inputmethod.keyboard.KeyboardActionListener;
42
43import java.util.HashMap;
44
45public class InputTestsBase extends ServiceTestCase<LatinIME> {
46
47    private static final String PREF_DEBUG_MODE = "debug_mode";
48
49    // The message that sets the underline is posted with a 100 ms delay
50    protected static final int DELAY_TO_WAIT_FOR_UNDERLINE = 200;
51
52    protected LatinIME mLatinIME;
53    protected Keyboard mKeyboard;
54    protected TextView mTextView;
55    protected InputConnection mInputConnection;
56    private final HashMap<String, InputMethodSubtype> mSubtypeMap =
57            new HashMap<String, InputMethodSubtype>();
58
59    // A helper class to ease span tests
60    public static class Span {
61        final SpannableStringBuilder mInputText;
62        final SuggestionSpan mSpan;
63        final int mStart;
64        final int mEnd;
65        // The supplied CharSequence should be an instance of SpannableStringBuilder,
66        // and it should contain exactly zero or one SuggestionSpan. Otherwise, an exception
67        // is thrown.
68        public Span(final CharSequence inputText) {
69            mInputText = (SpannableStringBuilder)inputText;
70            final SuggestionSpan[] spans =
71                    mInputText.getSpans(0, mInputText.length(), SuggestionSpan.class);
72            if (0 == spans.length) {
73                mSpan = null;
74                mStart = -1;
75                mEnd = -1;
76            } else if (1 == spans.length) {
77                mSpan = spans[0];
78                mStart = mInputText.getSpanStart(mSpan);
79                mEnd = mInputText.getSpanEnd(mSpan);
80            } else {
81                throw new RuntimeException("Expected one SuggestionSpan, found " + spans.length);
82            }
83        }
84        public boolean isAutoCorrectionIndicator() {
85            return 0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & mSpan.getFlags());
86        }
87    }
88
89    public InputTestsBase() {
90        super(LatinIME.class);
91    }
92
93    // returns the previous setting value
94    protected boolean setDebugMode(final boolean mode) {
95        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME);
96        final boolean previousDebugSetting = prefs.getBoolean(PREF_DEBUG_MODE, false);
97        final SharedPreferences.Editor editor = prefs.edit();
98        editor.putBoolean(PREF_DEBUG_MODE, mode);
99        editor.commit();
100        return previousDebugSetting;
101    }
102
103    // overload this to configure preferences in a way specific to a subclass's tests
104    protected void configurePreferences() {
105        // please avoid changing preferences any more than is necessary, as an interruption
106        // during a test will leave the user's preferences in a bad state.
107    }
108
109    // restore any preferences set in configurePreferences()
110    protected void restorePreferences() {
111        // undo any effects from configurePreferences()
112    }
113
114    @Override
115    protected void setUp() {
116        try {
117            super.setUp();
118        } catch (Exception e) {
119            e.printStackTrace();
120        }
121        mTextView = new TextView(getContext());
122        mTextView.setInputType(InputType.TYPE_CLASS_TEXT);
123        mTextView.setEnabled(true);
124        setupService();
125        mLatinIME = getService();
126        final boolean previousDebugSetting = setDebugMode(true);
127        configurePreferences();
128        mLatinIME.onCreate();
129        setDebugMode(previousDebugSetting);
130        initSubtypeMap();
131        final EditorInfo ei = new EditorInfo();
132        ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
133        final InputConnection ic = mTextView.onCreateInputConnection(ei);
134        ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
135        final LayoutInflater inflater =
136                (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
137        final ViewGroup vg = new FrameLayout(getContext());
138        final View inputView = inflater.inflate(R.layout.input_view, vg);
139        mLatinIME.setInputView(inputView);
140        mLatinIME.onBindInput();
141        mLatinIME.onCreateInputView();
142        mLatinIME.onStartInputView(ei, false);
143        mLatinIME.onCreateInputMethodInterface().startInput(ic, ei);
144        mInputConnection = ic;
145        mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard();
146        changeLanguage("en_US");
147    }
148
149    @Override
150    protected void tearDown() throws Exception {
151        super.tearDown();
152        restorePreferences();
153    }
154
155    private void initSubtypeMap() {
156        final InputMethodManager imm = (InputMethodManager)mLatinIME.getSystemService(
157                Context.INPUT_METHOD_SERVICE);
158        final String packageName = mLatinIME.getPackageName();
159        // The IMEs and subtypes don't need to be enabled to run this test because IMF isn't
160        // involved here.
161        for (final InputMethodInfo imi : imm.getInputMethodList()) {
162            if (imi.getPackageName().equals(packageName)) {
163                final int subtypeCount = imi.getSubtypeCount();
164                for (int i = 0; i < subtypeCount; i++) {
165                    final InputMethodSubtype ims = imi.getSubtypeAt(i);
166                    final String locale = ims.getLocale();
167                    mSubtypeMap.put(locale, ims);
168                }
169                return;
170            }
171        }
172        fail("LatinIME is not found");
173    }
174
175    // We need to run the messages added to the handler from LatinIME. The only way to do
176    // that is to call Looper#loop() on the right looper, so we're going to get the looper
177    // object and call #loop() here. The messages in the handler actually run on the UI
178    // thread of the keyboard by design of the handler, so we want to call it synchronously
179    // on the same thread that the tests are running on to mimic the actual environment as
180    // closely as possible.
181    // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method
182    // is called, so we need to do that at the right time so that #loop() returns at some
183    // point and we don't end up in an infinite loop.
184    // After we quit, the looper is still technically ready to process more messages but
185    // the handler will refuse to enqueue any because #quit() has been called and it
186    // explicitly tests for it on message enqueuing, so we'll have to reset it so that
187    // it lets us continue normal operation.
188    protected void runMessages() {
189        // Here begins deep magic.
190        final Looper looper = mLatinIME.mHandler.getLooper();
191        mLatinIME.mHandler.post(new Runnable() {
192                @Override
193                public void run() {
194                    looper.quit();
195                }
196            });
197        // The only way to get out of Looper#loop() is to call #quit() on it (or on its queue).
198        // Once #quit() is called remaining messages are not processed, which is why we post
199        // a message that calls it instead of calling it directly.
200        Looper.loop();
201
202        // Once #quit() has been called, the message queue has an "mQuiting" field that prevents
203        // any subsequent post in this queue. However the queue itself is still fully functional!
204        // If we have a way of resetting "queue.mQuiting" then we can continue using it as normal,
205        // coming back to this method to run the messages.
206        MessageQueue queue = looper.getQueue();
207        try {
208            // However there is no way of doing it externally, and mQuiting is private.
209            // So... get out the big guns.
210            java.lang.reflect.Field f = MessageQueue.class.getDeclaredField("mQuiting");
211            f.setAccessible(true); // What do you mean "private"?
212            f.setBoolean(queue, false);
213        } catch (NoSuchFieldException e) {
214            throw new RuntimeException(e);
215        } catch (IllegalAccessException e) {
216            throw new RuntimeException(e);
217        }
218    }
219
220    // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME.
221    protected void type(final int codePoint) {
222        // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the
223        // code (although multitouch/slide input and other factors make the sequencing complicated).
224        // They are supposed to be entirely deconnected from the input logic from LatinIME point of
225        // view and only delegates to the parts of the code that care. So we don't include them here
226        // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies,
227        // but keep them in mind if something breaks. Commenting them out as is should work.
228        //mLatinIME.onPressKey(codePoint);
229        for (final Key key : mKeyboard.mKeys) {
230            if (key.mCode == codePoint) {
231                final int x = key.mX + key.mWidth / 2;
232                final int y = key.mY + key.mHeight / 2;
233                mLatinIME.onCodeInput(codePoint, x, y);
234                return;
235            }
236        }
237        mLatinIME.onCodeInput(codePoint,
238                KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
239                KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
240        //mLatinIME.onReleaseKey(codePoint, false);
241    }
242
243    protected void type(final String stringToType) {
244        for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) {
245            type(stringToType.codePointAt(i));
246        }
247    }
248
249    protected void waitForDictionaryToBeLoaded() {
250        int remainingAttempts = 10;
251        while (remainingAttempts > 0 && !mLatinIME.mSuggest.hasMainDictionary()) {
252            try {
253                Thread.sleep(200);
254            } catch (InterruptedException e) {
255                // Don't do much
256            } finally {
257                --remainingAttempts;
258            }
259        }
260        if (!mLatinIME.mSuggest.hasMainDictionary()) {
261            throw new RuntimeException("Can't initialize the main dictionary");
262        }
263    }
264
265    protected void changeLanguage(final String locale) {
266        final InputMethodSubtype subtype = mSubtypeMap.get(locale);
267        if (subtype == null) {
268            fail("InputMethodSubtype for locale " + locale + " is not enabled");
269        }
270        SubtypeSwitcher.getInstance().updateSubtype(subtype);
271        waitForDictionaryToBeLoaded();
272    }
273
274
275    // Helper to avoid writing the try{}catch block each time
276    protected static void sleep(final int milliseconds) {
277        try {
278            Thread.sleep(milliseconds);
279        } catch (InterruptedException e) {}
280    }
281}
282