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.view.LayoutInflater; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.inputmethod.EditorInfo; 32 import android.view.inputmethod.InputConnection; 33 import android.widget.EditText; 34 import android.widget.FrameLayout; 35 36 import com.android.inputmethod.keyboard.Key; 37 import com.android.inputmethod.keyboard.Keyboard; 38 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 39 import com.android.inputmethod.latin.utils.LocaleUtils; 40 41 import java.util.Locale; 42 43 public class InputTestsBase extends ServiceTestCase<LatinIMEForTests> { 44 45 private static final String PREF_DEBUG_MODE = "debug_mode"; 46 47 // The message that sets the underline is posted with a 200 ms delay 48 protected static final int DELAY_TO_WAIT_FOR_UNDERLINE = 200; 49 // The message that sets predictions is posted with a 200 ms delay 50 protected static final int DELAY_TO_WAIT_FOR_PREDICTIONS = 200; 51 52 protected LatinIME mLatinIME; 53 protected Keyboard mKeyboard; 54 protected MyEditText mEditText; 55 protected View mInputView; 56 protected InputConnection mInputConnection; 57 58 // A helper class to ease span tests 59 public static class SpanGetter { 60 final SpannableStringBuilder mInputText; 61 final CharacterStyle mSpan; 62 final int mStart; 63 final int mEnd; 64 // The supplied CharSequence should be an instance of SpannableStringBuilder, 65 // and it should contain exactly zero or one span. Otherwise, an exception 66 // is thrown. 67 public SpanGetter(final CharSequence inputText, 68 final Class<? extends CharacterStyle> spanType) { 69 mInputText = (SpannableStringBuilder)inputText; 70 final CharacterStyle[] spans = 71 mInputText.getSpans(0, mInputText.length(), spanType); 72 if (0 == spans.length) { 73 mSpan = null; 74 mStart = -1; 75 mEnd = -1; 76 } else if (1 == spans.length) { 77 mSpan = spans[0]; 78 mStart = mInputText.getSpanStart(mSpan); 79 mEnd = mInputText.getSpanEnd(mSpan); 80 } else { 81 throw new RuntimeException("Expected one span, found " + spans.length); 82 } 83 } 84 public boolean isAutoCorrectionIndicator() { 85 return (mSpan instanceof SuggestionSpan) && 86 0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & ((SuggestionSpan)mSpan).getFlags()); 87 } 88 public String[] getSuggestions() { 89 return ((SuggestionSpan)mSpan).getSuggestions(); 90 } 91 } 92 93 // A helper class to increase control over the EditText 94 public static class MyEditText extends EditText { 95 public Locale mCurrentLocale; 96 public MyEditText(final Context c) { 97 super(c); 98 } 99 100 @Override 101 public void onAttachedToWindow() { 102 // Make onAttachedToWindow "public" 103 super.onAttachedToWindow(); 104 } 105 106 // overriding hidden API in EditText 107 public Locale getTextServicesLocale() { 108 // This method is necessary because EditText is asking this method for the language 109 // to check the spell in. If we don't override this, the spell checker will run in 110 // whatever language the keyboard is currently set on the test device, ignoring any 111 // settings we do inside the tests. 112 return mCurrentLocale; 113 } 114 115 // overriding hidden API in EditText 116 public Locale getSpellCheckerLocale() { 117 // This method is necessary because EditText is asking this method for the language 118 // to check the spell in. If we don't override this, the spell checker will run in 119 // whatever language the keyboard is currently set on the test device, ignoring any 120 // settings we do inside the tests. 121 return mCurrentLocale; 122 } 123 124 } 125 126 public InputTestsBase() { 127 super(LatinIMEForTests.class); 128 } 129 130 // TODO: Isn't there a way to make this generic somehow? We can take a <T> and return a <T> 131 // but we'd have to dispatch types on editor.put...() functions 132 protected boolean setBooleanPreference(final String key, final boolean value, 133 final boolean defaultValue) { 134 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); 135 final boolean previousSetting = prefs.getBoolean(key, defaultValue); 136 final SharedPreferences.Editor editor = prefs.edit(); 137 editor.putBoolean(key, value); 138 editor.commit(); 139 return previousSetting; 140 } 141 142 // returns the previous setting value 143 protected boolean setDebugMode(final boolean value) { 144 return setBooleanPreference(PREF_DEBUG_MODE, value, false); 145 } 146 147 @Override 148 protected void setUp() throws Exception { 149 super.setUp(); 150 mEditText = new MyEditText(getContext()); 151 final int inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT 152 | InputType.TYPE_TEXT_FLAG_MULTI_LINE; 153 mEditText.setInputType(inputType); 154 mEditText.setEnabled(true); 155 setupService(); 156 mLatinIME = getService(); 157 final boolean previousDebugSetting = setDebugMode(true); 158 mLatinIME.onCreate(); 159 setDebugMode(previousDebugSetting); 160 final EditorInfo ei = new EditorInfo(); 161 final InputConnection ic = mEditText.onCreateInputConnection(ei); 162 final LayoutInflater inflater = 163 (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 164 final ViewGroup vg = new FrameLayout(getContext()); 165 mInputView = inflater.inflate(R.layout.input_view, vg); 166 mLatinIME.onCreateInputMethodInterface().startInput(ic, ei); 167 mLatinIME.setInputView(mInputView); 168 mLatinIME.onBindInput(); 169 mLatinIME.onCreateInputView(); 170 mLatinIME.onStartInputView(ei, false); 171 mInputConnection = ic; 172 changeLanguage("en_US"); 173 } 174 175 // We need to run the messages added to the handler from LatinIME. The only way to do 176 // that is to call Looper#loop() on the right looper, so we're going to get the looper 177 // object and call #loop() here. The messages in the handler actually run on the UI 178 // thread of the keyboard by design of the handler, so we want to call it synchronously 179 // on the same thread that the tests are running on to mimic the actual environment as 180 // closely as possible. 181 // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method 182 // is called, which has a lot of bad side effects. We can however just throw an exception 183 // in the runnable which will unwind the stack and allow us to exit. 184 private final class InterruptRunMessagesException extends RuntimeException { 185 // Empty class 186 } 187 protected void runMessages() { 188 mLatinIME.mHandler.post(new Runnable() { 189 @Override 190 public void run() { 191 throw new InterruptRunMessagesException(); 192 } 193 }); 194 try { 195 Looper.loop(); 196 } catch (InterruptRunMessagesException e) { 197 // Resume normal operation 198 } 199 } 200 201 // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME. 202 protected void type(final int codePoint) { 203 // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the 204 // code (although multitouch/slide input and other factors make the sequencing complicated). 205 // They are supposed to be entirely deconnected from the input logic from LatinIME point of 206 // view and only delegates to the parts of the code that care. So we don't include them here 207 // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies, 208 // but keep them in mind if something breaks. Commenting them out as is should work. 209 //mLatinIME.onPressKey(codePoint, 0 /* repeatCount */, true /* isSinglePointer */); 210 final Key key = mKeyboard.getKey(codePoint); 211 if (key != null) { 212 final int x = key.getX() + key.getWidth() / 2; 213 final int y = key.getY() + key.getHeight() / 2; 214 mLatinIME.onCodeInput(codePoint, x, y); 215 return; 216 } 217 mLatinIME.onCodeInput(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 218 //mLatinIME.onReleaseKey(codePoint, false /* withSliding */); 219 } 220 221 protected void type(final String stringToType) { 222 for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) { 223 type(stringToType.codePointAt(i)); 224 } 225 } 226 227 protected void waitForDictionaryToBeLoaded() { 228 int remainingAttempts = 300; 229 while (remainingAttempts > 0 && mLatinIME.isCurrentlyWaitingForMainDictionary()) { 230 try { 231 Thread.sleep(200); 232 } catch (InterruptedException e) { 233 // Don't do much 234 } finally { 235 --remainingAttempts; 236 } 237 } 238 } 239 240 protected void changeLanguage(final String locale) { 241 changeLanguageWithoutWait(locale); 242 waitForDictionaryToBeLoaded(); 243 } 244 245 protected void changeLanguageWithoutWait(final String locale) { 246 mEditText.mCurrentLocale = LocaleUtils.constructLocaleFromString(locale); 247 SubtypeSwitcher.getInstance().forceLocale(mEditText.mCurrentLocale); 248 mLatinIME.loadKeyboard(); 249 runMessages(); 250 mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard(); 251 } 252 253 protected void changeKeyboardLocaleAndDictLocale(final String keyboardLocale, 254 final String dictLocale) { 255 changeLanguage(keyboardLocale); 256 if (!keyboardLocale.equals(dictLocale)) { 257 mLatinIME.replaceMainDictionaryForTest( 258 LocaleUtils.constructLocaleFromString(dictLocale)); 259 } 260 waitForDictionaryToBeLoaded(); 261 } 262 263 protected void pickSuggestionManually(final int index, final String suggestion) { 264 mLatinIME.pickSuggestionManually(index, new SuggestedWordInfo(suggestion, 1, 265 SuggestedWordInfo.KIND_CORRECTION, null /* sourceDict */, 266 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 267 SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); 268 } 269 270 // Helper to avoid writing the try{}catch block each time 271 protected static void sleep(final int milliseconds) { 272 try { 273 Thread.sleep(milliseconds); 274 } catch (InterruptedException e) {} 275 } 276 } 277