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.graphics.Point; 22 import android.os.Looper; 23 import android.preference.PreferenceManager; 24 import android.test.ServiceTestCase; 25 import android.text.InputType; 26 import android.text.SpannableStringBuilder; 27 import android.text.style.CharacterStyle; 28 import android.text.style.SuggestionSpan; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.inputmethod.EditorInfo; 34 import android.view.inputmethod.InputConnection; 35 import android.view.inputmethod.InputMethodSubtype; 36 import android.widget.EditText; 37 import android.widget.FrameLayout; 38 39 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; 40 import com.android.inputmethod.event.Event; 41 import com.android.inputmethod.keyboard.Key; 42 import com.android.inputmethod.keyboard.Keyboard; 43 import com.android.inputmethod.latin.Dictionary.PhonyDictionary; 44 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 45 import com.android.inputmethod.latin.common.Constants; 46 import com.android.inputmethod.latin.common.InputPointers; 47 import com.android.inputmethod.latin.common.LocaleUtils; 48 import com.android.inputmethod.latin.common.StringUtils; 49 import com.android.inputmethod.latin.settings.DebugSettings; 50 import com.android.inputmethod.latin.settings.Settings; 51 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 52 53 import java.util.Locale; 54 import java.util.concurrent.TimeUnit; 55 56 public class InputTestsBase extends ServiceTestCase<LatinIMEForTests> { 57 private static final String TAG = InputTestsBase.class.getSimpleName(); 58 59 // Default value for auto-correction threshold. This is the string representation of the 60 // index in the resources array of auto-correction threshold settings. 61 private static final boolean DEFAULT_AUTO_CORRECTION = true; 62 63 // The message that sets the underline is posted with a 500 ms delay 64 protected static final int DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS = 500; 65 // The message that sets predictions is posted with a 200 ms delay 66 protected static final int DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS = 200; 67 // We wait for gesture computation for this delay 68 protected static final int DELAY_TO_WAIT_FOR_GESTURE_MILLIS = 200; 69 // If a dictionary takes longer to load, we could have serious problems. 70 private final int TIMEOUT_TO_WAIT_FOR_LOADING_MAIN_DICTIONARY_IN_SECONDS = 5; 71 72 // Type for a test phony dictionary 73 private static final String TYPE_TEST = "test"; 74 private static final PhonyDictionary DICTIONARY_TEST = new PhonyDictionary(TYPE_TEST); 75 76 protected LatinIME mLatinIME; 77 protected Keyboard mKeyboard; 78 protected MyEditText mEditText; 79 protected View mInputView; 80 protected InputConnection mInputConnection; 81 private boolean mPreviousAutoCorrectSetting; 82 private boolean mPreviousBigramPredictionSettings; 83 84 // A helper class to ease span tests 85 public static class SpanGetter { 86 final SpannableStringBuilder mInputText; 87 final CharacterStyle mSpan; 88 final int mStart; 89 final int mEnd; 90 // The supplied CharSequence should be an instance of SpannableStringBuilder, 91 // and it should contain exactly zero or one span. Otherwise, an exception 92 // is thrown. SpanGetter(final CharSequence inputText, final Class<? extends CharacterStyle> spanType)93 public SpanGetter(final CharSequence inputText, 94 final Class<? extends CharacterStyle> spanType) { 95 mInputText = (SpannableStringBuilder)inputText; 96 final CharacterStyle[] spans = 97 mInputText.getSpans(0, mInputText.length(), spanType); 98 if (0 == spans.length) { 99 mSpan = null; 100 mStart = -1; 101 mEnd = -1; 102 } else if (1 == spans.length) { 103 mSpan = spans[0]; 104 mStart = mInputText.getSpanStart(mSpan); 105 mEnd = mInputText.getSpanEnd(mSpan); 106 } else { 107 throw new RuntimeException("Expected one span, found " + spans.length); 108 } 109 } getSpan()110 public SuggestionSpan getSpan() { 111 return (SuggestionSpan) mSpan; 112 } isAutoCorrectionIndicator()113 public boolean isAutoCorrectionIndicator() { 114 return (mSpan instanceof SuggestionSpan) && 115 0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & getSpan().getFlags()); 116 } getSuggestions()117 public String[] getSuggestions() { 118 return getSpan().getSuggestions(); 119 } 120 } 121 122 // A helper class to increase control over the EditText 123 public static class MyEditText extends EditText { 124 public Locale mCurrentLocale; MyEditText(final Context c)125 public MyEditText(final Context c) { 126 super(c); 127 } 128 129 @Override onAttachedToWindow()130 public void onAttachedToWindow() { 131 // Make onAttachedToWindow "public" 132 super.onAttachedToWindow(); 133 } 134 135 // overriding hidden API in EditText getTextServicesLocale()136 public Locale getTextServicesLocale() { 137 // This method is necessary because EditText is asking this method for the language 138 // to check the spell in. If we don't override this, the spell checker will run in 139 // whatever language the keyboard is currently set on the test device, ignoring any 140 // settings we do inside the tests. 141 return mCurrentLocale; 142 } 143 144 // overriding hidden API in EditText getSpellCheckerLocale()145 public Locale getSpellCheckerLocale() { 146 // This method is necessary because EditText is asking this method for the language 147 // to check the spell in. If we don't override this, the spell checker will run in 148 // whatever language the keyboard is currently set on the test device, ignoring any 149 // settings we do inside the tests. 150 return mCurrentLocale; 151 } 152 153 } 154 InputTestsBase()155 public InputTestsBase() { 156 super(LatinIMEForTests.class); 157 } 158 setBooleanPreference(final String key, final boolean value, final boolean defaultValue)159 protected boolean setBooleanPreference(final String key, final boolean value, 160 final boolean defaultValue) { 161 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); 162 final boolean previousSetting = prefs.getBoolean(key, defaultValue); 163 final SharedPreferences.Editor editor = prefs.edit(); 164 editor.putBoolean(key, value); 165 editor.apply(); 166 return previousSetting; 167 } 168 getBooleanPreference(final String key, final boolean defaultValue)169 protected boolean getBooleanPreference(final String key, final boolean defaultValue) { 170 return PreferenceManager.getDefaultSharedPreferences(mLatinIME) 171 .getBoolean(key, defaultValue); 172 } 173 setStringPreference(final String key, final String value, final String defaultValue)174 protected String setStringPreference(final String key, final String value, 175 final String defaultValue) { 176 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); 177 final String previousSetting = prefs.getString(key, defaultValue); 178 final SharedPreferences.Editor editor = prefs.edit(); 179 editor.putString(key, value); 180 editor.apply(); 181 return previousSetting; 182 } 183 setDebugMode(final boolean value)184 protected void setDebugMode(final boolean value) { 185 setBooleanPreference(DebugSettings.PREF_DEBUG_MODE, value, false); 186 setBooleanPreference(Settings.PREF_KEY_IS_INTERNAL, value, false); 187 } 188 enrichEditorInfo(final EditorInfo ei)189 protected EditorInfo enrichEditorInfo(final EditorInfo ei) { 190 // Some tests that inherit from us need to add some data in the EditorInfo (see 191 // AppWorkaroundsTests#enrichEditorInfo() for a concrete example of this). Since we 192 // control the EditorInfo, we supply a hook here for children to override. 193 return ei; 194 } 195 196 @Override setUp()197 protected void setUp() throws Exception { 198 super.setUp(); 199 mEditText = new MyEditText(getContext()); 200 final int inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT 201 | InputType.TYPE_TEXT_FLAG_MULTI_LINE; 202 mEditText.setInputType(inputType); 203 mEditText.setEnabled(true); 204 mLastCursorPos = 0; 205 if (null == Looper.myLooper()) { 206 Looper.prepare(); 207 } 208 setupService(); 209 mLatinIME = getService(); 210 setDebugMode(true); 211 mPreviousBigramPredictionSettings = setBooleanPreference(Settings.PREF_BIGRAM_PREDICTIONS, 212 true, true /* defaultValue */); 213 mPreviousAutoCorrectSetting = setBooleanPreference(Settings.PREF_AUTO_CORRECTION, 214 DEFAULT_AUTO_CORRECTION, DEFAULT_AUTO_CORRECTION); 215 mLatinIME.onCreate(); 216 EditorInfo ei = new EditorInfo(); 217 final InputConnection ic = mEditText.onCreateInputConnection(ei); 218 final LayoutInflater inflater = 219 (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 220 final ViewGroup vg = new FrameLayout(getContext()); 221 mInputView = inflater.inflate(R.layout.input_view, vg); 222 ei = enrichEditorInfo(ei); 223 mLatinIME.onCreateInputMethodInterface().startInput(ic, ei); 224 mLatinIME.setInputView(mInputView); 225 mLatinIME.onBindInput(); 226 mLatinIME.onCreateInputView(); 227 mLatinIME.onStartInputView(ei, false); 228 mInputConnection = ic; 229 changeLanguage("en_US"); 230 // Run messages to avoid the messages enqueued by startInputView() and its friends 231 // to run on a later call and ruin things. We need to wait first because some of them 232 // can be posted with a delay (notably, MSG_RESUME_SUGGESTIONS) 233 sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); 234 runMessages(); 235 } 236 237 @Override tearDown()238 protected void tearDown() throws Exception { 239 mLatinIME.onFinishInputView(true); 240 mLatinIME.onFinishInput(); 241 runMessages(); 242 mLatinIME.mHandler.removeAllMessages(); 243 setBooleanPreference(Settings.PREF_BIGRAM_PREDICTIONS, mPreviousBigramPredictionSettings, 244 true /* defaultValue */); 245 setBooleanPreference(Settings.PREF_AUTO_CORRECTION, mPreviousAutoCorrectSetting, 246 DEFAULT_AUTO_CORRECTION); 247 setDebugMode(false); 248 mLatinIME.recycle(); 249 super.tearDown(); 250 mLatinIME = null; 251 } 252 253 // We need to run the messages added to the handler from LatinIME. The only way to do 254 // that is to call Looper#loop() on the right looper, so we're going to get the looper 255 // object and call #loop() here. The messages in the handler actually run on the UI 256 // thread of the keyboard by design of the handler, so we want to call it synchronously 257 // on the same thread that the tests are running on to mimic the actual environment as 258 // closely as possible. 259 // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method 260 // is called, which has a lot of bad side effects. We can however just throw an exception 261 // in the runnable which will unwind the stack and allow us to exit. 262 final class InterruptRunMessagesException extends RuntimeException { 263 // Empty class 264 } runMessages()265 protected void runMessages() { 266 mLatinIME.mHandler.post(new Runnable() { 267 @Override 268 public void run() { 269 throw new InterruptRunMessagesException(); 270 } 271 }); 272 try { 273 Looper.loop(); 274 } catch (InterruptRunMessagesException e) { 275 // Resume normal operation 276 } 277 } 278 279 // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME. typeInternal(final int codePoint, final boolean isKeyRepeat)280 protected void typeInternal(final int codePoint, final boolean isKeyRepeat) { 281 // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the 282 // code (although multitouch/slide input and other factors make the sequencing complicated). 283 // They are supposed to be entirely deconnected from the input logic from LatinIME point of 284 // view and only delegates to the parts of the code that care. So we don't include them here 285 // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies, 286 // but keep them in mind if something breaks. Commenting them out as is should work. 287 //mLatinIME.onPressKey(codePoint, 0 /* repeatCount */, true /* isSinglePointer */); 288 final Key key = mKeyboard.getKey(codePoint); 289 final Event event; 290 if (key == null) { 291 event = Event.createSoftwareKeypressEvent(codePoint, Event.NOT_A_KEY_CODE, 292 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, isKeyRepeat); 293 } else { 294 final int x = key.getX() + key.getWidth() / 2; 295 final int y = key.getY() + key.getHeight() / 2; 296 event = LatinIME.createSoftwareKeypressEvent(codePoint, x, y, isKeyRepeat); 297 } 298 mLatinIME.onEvent(event); 299 // Also see the comment at the top of this function about onReleaseKey 300 //mLatinIME.onReleaseKey(codePoint, false /* withSliding */); 301 } 302 type(final int codePoint)303 protected void type(final int codePoint) { 304 typeInternal(codePoint, false /* isKeyRepeat */); 305 } 306 repeatKey(final int codePoint)307 protected void repeatKey(final int codePoint) { 308 typeInternal(codePoint, true /* isKeyRepeat */); 309 } 310 type(final String stringToType)311 protected void type(final String stringToType) { 312 for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) { 313 type(stringToType.codePointAt(i)); 314 } 315 } 316 getXY(final int codePoint)317 protected Point getXY(final int codePoint) { 318 final Key key = mKeyboard.getKey(codePoint); 319 if (key == null) { 320 throw new RuntimeException("Code point not on the keyboard"); 321 } 322 return new Point(key.getX() + key.getWidth() / 2, key.getY() + key.getHeight() / 2); 323 } 324 gesture(final String stringToGesture)325 protected void gesture(final String stringToGesture) { 326 if (StringUtils.codePointCount(stringToGesture) < 2) { 327 throw new RuntimeException("Can't gesture strings less than 2 chars long"); 328 } 329 330 mLatinIME.onStartBatchInput(); 331 final int startCodePoint = stringToGesture.codePointAt(0); 332 Point oldPoint = getXY(startCodePoint); 333 int timestamp = 0; // In milliseconds since the start of the gesture 334 final InputPointers pointers = new InputPointers(Constants.DEFAULT_GESTURE_POINTS_CAPACITY); 335 pointers.addPointer(oldPoint.x, oldPoint.y, 0 /* pointerId */, timestamp); 336 337 for (int i = Character.charCount(startCodePoint); i < stringToGesture.length(); 338 i = stringToGesture.offsetByCodePoints(i, 1)) { 339 final Point newPoint = getXY(stringToGesture.codePointAt(i)); 340 // Arbitrarily 0.5s between letters and 0.1 between events. Refine this later if needed. 341 final int STEPS = 5; 342 for (int j = 0; j < STEPS; ++j) { 343 timestamp += 100; 344 pointers.addPointer(oldPoint.x + ((newPoint.x - oldPoint.x) * j) / STEPS, 345 oldPoint.y + ((newPoint.y - oldPoint.y) * j) / STEPS, 346 0 /* pointerId */, timestamp); 347 } 348 oldPoint.x = newPoint.x; 349 oldPoint.y = newPoint.y; 350 mLatinIME.onUpdateBatchInput(pointers); 351 } 352 mLatinIME.onEndBatchInput(pointers); 353 sleep(DELAY_TO_WAIT_FOR_GESTURE_MILLIS); 354 runMessages(); 355 } 356 waitForDictionariesToBeLoaded()357 protected void waitForDictionariesToBeLoaded() { 358 try { 359 mLatinIME.waitForLoadingDictionaries( 360 TIMEOUT_TO_WAIT_FOR_LOADING_MAIN_DICTIONARY_IN_SECONDS, TimeUnit.SECONDS); 361 } catch (InterruptedException e) { 362 Log.e(TAG, "Interrupted during waiting for loading main dictionary.", e); 363 } 364 } 365 changeLanguage(final String locale)366 protected void changeLanguage(final String locale) { 367 changeLanguage(locale, null); 368 } 369 changeLanguage(final String locale, final String combiningSpec)370 protected void changeLanguage(final String locale, final String combiningSpec) { 371 changeLanguageWithoutWait(locale, combiningSpec); 372 waitForDictionariesToBeLoaded(); 373 } 374 changeLanguageWithoutWait(final String locale, final String combiningSpec)375 protected void changeLanguageWithoutWait(final String locale, final String combiningSpec) { 376 mEditText.mCurrentLocale = LocaleUtils.constructLocaleFromString(locale); 377 // TODO: this is forcing a QWERTY keyboard for all locales, which is wrong. 378 // It's still better than using whatever keyboard is the current one, but we 379 // should actually use the default keyboard for this locale. 380 // TODO: Use {@link InputMethodSubtype.InputMethodSubtypeBuilder} directly or indirectly so 381 // that {@link InputMethodSubtype#isAsciiCapable} can return the correct value. 382 final String EXTRA_VALUE_FOR_TEST = 383 "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY 384 + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE 385 + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE 386 + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE 387 + null == combiningSpec ? "" : ("," + combiningSpec); 388 final InputMethodSubtype subtype = InputMethodSubtypeCompatUtils.newInputMethodSubtype( 389 R.string.subtype_no_language_qwerty, 390 R.drawable.ic_ime_switcher_dark, 391 locale, 392 Constants.Subtype.KEYBOARD_MODE, 393 EXTRA_VALUE_FOR_TEST, 394 false /* isAuxiliary */, 395 false /* overridesImplicitlyEnabledSubtype */, 396 0 /* id */); 397 RichInputMethodManager.forceSubtype(subtype); 398 mLatinIME.onCurrentInputMethodSubtypeChanged(subtype); 399 runMessages(); 400 mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard(); 401 mLatinIME.clearPersonalizedDictionariesForTest(); 402 } 403 changeKeyboardLocaleAndDictLocale(final String keyboardLocale, final String dictLocale)404 protected void changeKeyboardLocaleAndDictLocale(final String keyboardLocale, 405 final String dictLocale) { 406 changeLanguage(keyboardLocale); 407 if (!keyboardLocale.equals(dictLocale)) { 408 mLatinIME.replaceDictionariesForTest(LocaleUtils.constructLocaleFromString(dictLocale)); 409 } 410 waitForDictionariesToBeLoaded(); 411 } 412 pickSuggestionManually(final String suggestion)413 protected void pickSuggestionManually(final String suggestion) { 414 mLatinIME.pickSuggestionManually(new SuggestedWordInfo(suggestion, 415 "" /* prevWordsContext */, 1 /* score */, 416 SuggestedWordInfo.KIND_CORRECTION, DICTIONARY_TEST, 417 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 418 SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); 419 } 420 421 // Helper to avoid writing the try{}catch block each time sleep(final int milliseconds)422 protected static void sleep(final int milliseconds) { 423 try { 424 Thread.sleep(milliseconds); 425 } catch (InterruptedException e) {} 426 } 427 428 // Some helper methods to manage the mock cursor position 429 // DO NOT CALL LatinIME#onUpdateSelection IF YOU WANT TO USE THOSE 430 int mLastCursorPos = 0; 431 /** 432 * Move the cached cursor position to the passed position and send onUpdateSelection to LatinIME 433 */ sendUpdateForCursorMoveTo(final int position)434 protected int sendUpdateForCursorMoveTo(final int position) { 435 mInputConnection.setSelection(position, position); 436 mLatinIME.onUpdateSelection(mLastCursorPos, mLastCursorPos, position, position, -1, -1); 437 mLastCursorPos = position; 438 return position; 439 } 440 441 /** 442 * Move the cached cursor position by the passed amount and send onUpdateSelection to LatinIME 443 */ sendUpdateForCursorMoveBy(final int offset)444 protected int sendUpdateForCursorMoveBy(final int offset) { 445 final int lastPos = mEditText.getText().length(); 446 final int requestedPosition = mLastCursorPos + offset; 447 if (requestedPosition < 0) { 448 return sendUpdateForCursorMoveTo(0); 449 } else if (requestedPosition > lastPos) { 450 return sendUpdateForCursorMoveTo(lastPos); 451 } else { 452 return sendUpdateForCursorMoveTo(requestedPosition); 453 } 454 } 455 456 /** 457 * Move the cached cursor position to the end of the line and send onUpdateSelection to LatinIME 458 */ sendUpdateForCursorMoveToEndOfLine()459 protected int sendUpdateForCursorMoveToEndOfLine() { 460 final int lastPos = mEditText.getText().length(); 461 return sendUpdateForCursorMoveTo(lastPos); 462 } 463 } 464