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