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