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