InputTestsBase.java revision 7b6e999cd174f63501ccabdade5489d61107b341
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. 206 runMessages(); 207 } 208 209 @Override 210 protected void tearDown() { 211 mLatinIME.mHandler.removeAllMessages(); 212 setStringPreference(PREF_AUTO_CORRECTION_THRESHOLD, mPreviousAutoCorrectSetting, 213 DEFAULT_AUTO_CORRECTION_THRESHOLD); 214 setDebugMode(mPreviousDebugSetting); 215 } 216 217 // We need to run the messages added to the handler from LatinIME. The only way to do 218 // that is to call Looper#loop() on the right looper, so we're going to get the looper 219 // object and call #loop() here. The messages in the handler actually run on the UI 220 // thread of the keyboard by design of the handler, so we want to call it synchronously 221 // on the same thread that the tests are running on to mimic the actual environment as 222 // closely as possible. 223 // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method 224 // is called, which has a lot of bad side effects. We can however just throw an exception 225 // in the runnable which will unwind the stack and allow us to exit. 226 private final class InterruptRunMessagesException extends RuntimeException { 227 // Empty class 228 } 229 protected void runMessages() { 230 mLatinIME.mHandler.post(new Runnable() { 231 @Override 232 public void run() { 233 throw new InterruptRunMessagesException(); 234 } 235 }); 236 try { 237 Looper.loop(); 238 } catch (InterruptRunMessagesException e) { 239 // Resume normal operation 240 } 241 } 242 243 // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME. 244 protected void type(final int codePoint) { 245 // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the 246 // code (although multitouch/slide input and other factors make the sequencing complicated). 247 // They are supposed to be entirely deconnected from the input logic from LatinIME point of 248 // view and only delegates to the parts of the code that care. So we don't include them here 249 // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies, 250 // but keep them in mind if something breaks. Commenting them out as is should work. 251 //mLatinIME.onPressKey(codePoint, 0 /* repeatCount */, true /* isSinglePointer */); 252 final Key key = mKeyboard.getKey(codePoint); 253 if (key != null) { 254 final int x = key.getX() + key.getWidth() / 2; 255 final int y = key.getY() + key.getHeight() / 2; 256 mLatinIME.onCodeInput(codePoint, x, y); 257 return; 258 } 259 mLatinIME.onCodeInput(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 260 //mLatinIME.onReleaseKey(codePoint, false /* withSliding */); 261 } 262 263 protected void type(final String stringToType) { 264 for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) { 265 type(stringToType.codePointAt(i)); 266 } 267 } 268 269 protected void waitForDictionaryToBeLoaded() { 270 try { 271 mLatinIME.waitForMainDictionary( 272 TIMEOUT_TO_WAIT_FOR_LOADING_MAIN_DICTIONARY_IN_SECONDS, TimeUnit.SECONDS); 273 } catch (InterruptedException e) { 274 Log.e(TAG, "Interrupted during waiting for loading main dictionary.", e); 275 } 276 } 277 278 protected void changeLanguage(final String locale) { 279 changeLanguageWithoutWait(locale); 280 waitForDictionaryToBeLoaded(); 281 } 282 283 protected void changeLanguageWithoutWait(final String locale) { 284 mEditText.mCurrentLocale = LocaleUtils.constructLocaleFromString(locale); 285 final InputMethodSubtype subtype = new InputMethodSubtype( 286 R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark, 287 locale, "keyboard", "KeyboardLayoutSet=" 288 // TODO: this is forcing a QWERTY keyboard for all locales, which is wrong. 289 // It's still better than using whatever keyboard is the current one, but we 290 // should actually use the default keyboard for this locale. 291 + SubtypeLocaleUtils.QWERTY 292 + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE 293 + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE 294 + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, 295 false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */); 296 SubtypeSwitcher.getInstance().forceSubtype(subtype); 297 mLatinIME.loadKeyboard(); 298 runMessages(); 299 mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard(); 300 } 301 302 protected void changeKeyboardLocaleAndDictLocale(final String keyboardLocale, 303 final String dictLocale) { 304 changeLanguage(keyboardLocale); 305 if (!keyboardLocale.equals(dictLocale)) { 306 mLatinIME.replaceDictionariesForTest(LocaleUtils.constructLocaleFromString(dictLocale)); 307 } 308 waitForDictionaryToBeLoaded(); 309 } 310 311 protected void pickSuggestionManually(final int index, final String suggestion) { 312 mLatinIME.pickSuggestionManually(index, new SuggestedWordInfo(suggestion, 1, 313 SuggestedWordInfo.KIND_CORRECTION, null /* sourceDict */, 314 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 315 SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); 316 } 317 318 // Helper to avoid writing the try{}catch block each time 319 protected static void sleep(final int milliseconds) { 320 try { 321 Thread.sleep(milliseconds); 322 } catch (InterruptedException e) {} 323 } 324} 325