InputTestsBase.java revision bac89ecc508052161704ef02c26e4e1d4d5060fa
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.content.Context; 20import android.content.SharedPreferences; 21import android.os.Looper; 22import android.preference.PreferenceManager; 23import android.test.ServiceTestCase; 24import android.text.InputType; 25import android.text.SpannableStringBuilder; 26import android.text.style.CharacterStyle; 27import android.text.style.SuggestionSpan; 28import android.util.Log; 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.InputMethodSubtype; 35import android.widget.EditText; 36import android.widget.FrameLayout; 37 38import com.android.inputmethod.keyboard.Key; 39import com.android.inputmethod.keyboard.Keyboard; 40import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 41import com.android.inputmethod.latin.utils.LocaleUtils; 42import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 43 44import java.util.Locale; 45import java.util.concurrent.TimeUnit; 46 47public class InputTestsBase extends ServiceTestCase<LatinIMEForTests> { 48 private static final String TAG = InputTestsBase.class.getSimpleName(); 49 50 private static final String PREF_DEBUG_MODE = "debug_mode"; 51 private static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold"; 52 // Default value for auto-correction threshold. This is the string representation of the 53 // index in the resources array of auto-correction threshold settings. 54 private static final String DEFAULT_AUTO_CORRECTION_THRESHOLD = "1"; 55 56 // The message that sets the underline is posted with a 500 ms delay 57 protected static final int DELAY_TO_WAIT_FOR_UNDERLINE = 500; 58 // The message that sets predictions is posted with a 200 ms delay 59 protected static final int DELAY_TO_WAIT_FOR_PREDICTIONS = 200; 60 private final int TIMEOUT_TO_WAIT_FOR_LOADING_MAIN_DICTIONARY_IN_SECONDS = 60; 61 62 protected LatinIME mLatinIME; 63 protected Keyboard mKeyboard; 64 protected MyEditText mEditText; 65 protected View mInputView; 66 protected InputConnection mInputConnection; 67 private boolean mPreviousDebugSetting; 68 private String mPreviousAutoCorrectSetting; 69 70 // A helper class to ease span tests 71 public static class SpanGetter { 72 final SpannableStringBuilder mInputText; 73 final CharacterStyle mSpan; 74 final int mStart; 75 final int mEnd; 76 // The supplied CharSequence should be an instance of SpannableStringBuilder, 77 // and it should contain exactly zero or one span. Otherwise, an exception 78 // is thrown. 79 public SpanGetter(final CharSequence inputText, 80 final Class<? extends CharacterStyle> spanType) { 81 mInputText = (SpannableStringBuilder)inputText; 82 final CharacterStyle[] spans = 83 mInputText.getSpans(0, mInputText.length(), spanType); 84 if (0 == spans.length) { 85 mSpan = null; 86 mStart = -1; 87 mEnd = -1; 88 } else if (1 == spans.length) { 89 mSpan = spans[0]; 90 mStart = mInputText.getSpanStart(mSpan); 91 mEnd = mInputText.getSpanEnd(mSpan); 92 } else { 93 throw new RuntimeException("Expected one span, found " + spans.length); 94 } 95 } 96 public boolean isAutoCorrectionIndicator() { 97 return (mSpan instanceof SuggestionSpan) && 98 0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & ((SuggestionSpan)mSpan).getFlags()); 99 } 100 public String[] getSuggestions() { 101 return ((SuggestionSpan)mSpan).getSuggestions(); 102 } 103 } 104 105 // A helper class to increase control over the EditText 106 public static class MyEditText extends EditText { 107 public Locale mCurrentLocale; 108 public MyEditText(final Context c) { 109 super(c); 110 } 111 112 @Override 113 public void onAttachedToWindow() { 114 // Make onAttachedToWindow "public" 115 super.onAttachedToWindow(); 116 } 117 118 // overriding hidden API in EditText 119 public Locale getTextServicesLocale() { 120 // This method is necessary because EditText is asking this method for the language 121 // to check the spell in. If we don't override this, the spell checker will run in 122 // whatever language the keyboard is currently set on the test device, ignoring any 123 // settings we do inside the tests. 124 return mCurrentLocale; 125 } 126 127 // overriding hidden API in EditText 128 public Locale getSpellCheckerLocale() { 129 // This method is necessary because EditText is asking this method for the language 130 // to check the spell in. If we don't override this, the spell checker will run in 131 // whatever language the keyboard is currently set on the test device, ignoring any 132 // settings we do inside the tests. 133 return mCurrentLocale; 134 } 135 136 } 137 138 public InputTestsBase() { 139 super(LatinIMEForTests.class); 140 } 141 142 // TODO: Isn't there a way to make this generic somehow? We can take a <T> and return a <T> 143 // but we'd have to dispatch types on editor.put...() functions 144 protected boolean setBooleanPreference(final String key, final boolean value, 145 final boolean defaultValue) { 146 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); 147 final boolean previousSetting = prefs.getBoolean(key, defaultValue); 148 final SharedPreferences.Editor editor = prefs.edit(); 149 editor.putBoolean(key, value); 150 editor.apply(); 151 return previousSetting; 152 } 153 154 protected String setStringPreference(final String key, final String value, 155 final String defaultValue) { 156 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); 157 final String previousSetting = prefs.getString(key, defaultValue); 158 final SharedPreferences.Editor editor = prefs.edit(); 159 editor.putString(key, value); 160 editor.apply(); 161 return previousSetting; 162 } 163 164 // returns the previous setting value 165 protected boolean setDebugMode(final boolean value) { 166 return setBooleanPreference(PREF_DEBUG_MODE, value, false); 167 } 168 169 protected EditorInfo enrichEditorInfo(final EditorInfo ei) { 170 // Some tests that inherit from us need to add some data in the EditorInfo (see 171 // AppWorkaroundsTests#enrichEditorInfo() for a concrete example of this). Since we 172 // control the EditorInfo, we supply a hook here for children to override. 173 return ei; 174 } 175 176 @Override 177 protected void setUp() throws Exception { 178 super.setUp(); 179 mEditText = new MyEditText(getContext()); 180 final int inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT 181 | InputType.TYPE_TEXT_FLAG_MULTI_LINE; 182 mEditText.setInputType(inputType); 183 mEditText.setEnabled(true); 184 setupService(); 185 mLatinIME = getService(); 186 mPreviousDebugSetting = setDebugMode(true); 187 mPreviousAutoCorrectSetting = setStringPreference(PREF_AUTO_CORRECTION_THRESHOLD, 188 DEFAULT_AUTO_CORRECTION_THRESHOLD, DEFAULT_AUTO_CORRECTION_THRESHOLD); 189 mLatinIME.onCreate(); 190 EditorInfo ei = new EditorInfo(); 191 final InputConnection ic = mEditText.onCreateInputConnection(ei); 192 final LayoutInflater inflater = 193 (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 194 final ViewGroup vg = new FrameLayout(getContext()); 195 mInputView = inflater.inflate(R.layout.input_view, vg); 196 ei = enrichEditorInfo(ei); 197 mLatinIME.onCreateInputMethodInterface().startInput(ic, ei); 198 mLatinIME.setInputView(mInputView); 199 mLatinIME.onBindInput(); 200 mLatinIME.onCreateInputView(); 201 mLatinIME.onStartInputView(ei, false); 202 mInputConnection = ic; 203 changeLanguage("en_US"); 204 // Run messages to avoid the messages enqueued by startInputView() and its friends 205 // to run on a later call and ruin things. We need to wait first because some of them 206 // can be posted with a delay (notably, MSG_RESUME_SUGGESTIONS) 207 sleep(DELAY_TO_WAIT_FOR_PREDICTIONS); 208 runMessages(); 209 } 210 211 @Override 212 protected void tearDown() { 213 mLatinIME.mHandler.removeAllMessages(); 214 setStringPreference(PREF_AUTO_CORRECTION_THRESHOLD, mPreviousAutoCorrectSetting, 215 DEFAULT_AUTO_CORRECTION_THRESHOLD); 216 setDebugMode(mPreviousDebugSetting); 217 } 218 219 // We need to run the messages added to the handler from LatinIME. The only way to do 220 // that is to call Looper#loop() on the right looper, so we're going to get the looper 221 // object and call #loop() here. The messages in the handler actually run on the UI 222 // thread of the keyboard by design of the handler, so we want to call it synchronously 223 // on the same thread that the tests are running on to mimic the actual environment as 224 // closely as possible. 225 // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method 226 // is called, which has a lot of bad side effects. We can however just throw an exception 227 // in the runnable which will unwind the stack and allow us to exit. 228 private final class InterruptRunMessagesException extends RuntimeException { 229 // Empty class 230 } 231 protected void runMessages() { 232 mLatinIME.mHandler.post(new Runnable() { 233 @Override 234 public void run() { 235 throw new InterruptRunMessagesException(); 236 } 237 }); 238 try { 239 Looper.loop(); 240 } catch (InterruptRunMessagesException e) { 241 // Resume normal operation 242 } 243 } 244 245 // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME. 246 protected void type(final int codePoint) { 247 // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the 248 // code (although multitouch/slide input and other factors make the sequencing complicated). 249 // They are supposed to be entirely deconnected from the input logic from LatinIME point of 250 // view and only delegates to the parts of the code that care. So we don't include them here 251 // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies, 252 // but keep them in mind if something breaks. Commenting them out as is should work. 253 //mLatinIME.onPressKey(codePoint, 0 /* repeatCount */, true /* isSinglePointer */); 254 final Key key = mKeyboard.getKey(codePoint); 255 if (key != null) { 256 final int x = key.getX() + key.getWidth() / 2; 257 final int y = key.getY() + key.getHeight() / 2; 258 mLatinIME.onCodeInput(codePoint, x, y); 259 return; 260 } 261 mLatinIME.onCodeInput(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 262 //mLatinIME.onReleaseKey(codePoint, false /* withSliding */); 263 } 264 265 protected void type(final String stringToType) { 266 for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) { 267 type(stringToType.codePointAt(i)); 268 } 269 } 270 271 protected void waitForDictionaryToBeLoaded() { 272 try { 273 mLatinIME.waitForMainDictionary( 274 TIMEOUT_TO_WAIT_FOR_LOADING_MAIN_DICTIONARY_IN_SECONDS, TimeUnit.SECONDS); 275 } catch (InterruptedException e) { 276 Log.e(TAG, "Interrupted during waiting for loading main dictionary.", e); 277 } 278 } 279 280 protected void changeLanguage(final String locale) { 281 changeLanguageWithoutWait(locale); 282 waitForDictionaryToBeLoaded(); 283 } 284 285 protected void changeLanguageWithoutWait(final String locale) { 286 mEditText.mCurrentLocale = LocaleUtils.constructLocaleFromString(locale); 287 final InputMethodSubtype subtype = new InputMethodSubtype( 288 R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark, 289 locale, "keyboard", "KeyboardLayoutSet=" 290 // TODO: this is forcing a QWERTY keyboard for all locales, which is wrong. 291 // It's still better than using whatever keyboard is the current one, but we 292 // should actually use the default keyboard for this locale. 293 + SubtypeLocaleUtils.QWERTY 294 + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE 295 + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE 296 + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, 297 false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */); 298 SubtypeSwitcher.getInstance().forceSubtype(subtype); 299 mLatinIME.loadKeyboard(); 300 runMessages(); 301 mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard(); 302 } 303 304 protected void changeKeyboardLocaleAndDictLocale(final String keyboardLocale, 305 final String dictLocale) { 306 changeLanguage(keyboardLocale); 307 if (!keyboardLocale.equals(dictLocale)) { 308 mLatinIME.replaceDictionariesForTest(LocaleUtils.constructLocaleFromString(dictLocale)); 309 } 310 waitForDictionaryToBeLoaded(); 311 } 312 313 protected void pickSuggestionManually(final int index, final String suggestion) { 314 mLatinIME.pickSuggestionManually(index, new SuggestedWordInfo(suggestion, 1, 315 SuggestedWordInfo.KIND_CORRECTION, null /* sourceDict */, 316 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 317 SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); 318 } 319 320 // Helper to avoid writing the try{}catch block each time 321 protected static void sleep(final int milliseconds) { 322 try { 323 Thread.sleep(milliseconds); 324 } catch (InterruptedException e) {} 325 } 326} 327