InputTestsBase.java revision b8e2ae3bc312269897057fccc34cd736c05bcc90
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.content.Context; 20import android.content.SharedPreferences; 21import android.os.Looper; 22import android.os.MessageQueue; 23import android.preference.PreferenceManager; 24import android.test.ServiceTestCase; 25import android.text.InputType; 26import android.text.SpannableStringBuilder; 27import android.text.style.SuggestionSpan; 28import android.view.LayoutInflater; 29import android.view.View; 30import android.view.ViewGroup; 31import android.view.inputmethod.EditorInfo; 32import android.view.inputmethod.InputConnection; 33import android.view.inputmethod.InputMethodInfo; 34import android.view.inputmethod.InputMethodManager; 35import android.view.inputmethod.InputMethodSubtype; 36import android.widget.FrameLayout; 37import android.widget.TextView; 38 39import com.android.inputmethod.keyboard.Key; 40import com.android.inputmethod.keyboard.Keyboard; 41import com.android.inputmethod.keyboard.KeyboardActionListener; 42 43import java.util.HashMap; 44 45public class InputTestsBase extends ServiceTestCase<LatinIME> { 46 47 private static final String PREF_DEBUG_MODE = "debug_mode"; 48 49 // The message that sets the underline is posted with a 100 ms delay 50 protected static final int DELAY_TO_WAIT_FOR_UNDERLINE = 200; 51 52 protected LatinIME mLatinIME; 53 protected Keyboard mKeyboard; 54 protected TextView mTextView; 55 protected InputConnection mInputConnection; 56 private final HashMap<String, InputMethodSubtype> mSubtypeMap = 57 new HashMap<String, InputMethodSubtype>(); 58 59 // A helper class to ease span tests 60 public static class Span { 61 final SpannableStringBuilder mInputText; 62 final SuggestionSpan mSpan; 63 final int mStart; 64 final int mEnd; 65 // The supplied CharSequence should be an instance of SpannableStringBuilder, 66 // and it should contain exactly zero or one SuggestionSpan. Otherwise, an exception 67 // is thrown. 68 public Span(final CharSequence inputText) { 69 mInputText = (SpannableStringBuilder)inputText; 70 final SuggestionSpan[] spans = 71 mInputText.getSpans(0, mInputText.length(), SuggestionSpan.class); 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 SuggestionSpan, found " + spans.length); 82 } 83 } 84 public boolean isAutoCorrectionIndicator() { 85 return 0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & mSpan.getFlags()); 86 } 87 } 88 89 public InputTestsBase() { 90 super(LatinIME.class); 91 } 92 93 // returns the previous setting value 94 protected boolean setDebugMode(final boolean mode) { 95 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); 96 final boolean previousDebugSetting = prefs.getBoolean(PREF_DEBUG_MODE, false); 97 final SharedPreferences.Editor editor = prefs.edit(); 98 editor.putBoolean(PREF_DEBUG_MODE, mode); 99 editor.commit(); 100 return previousDebugSetting; 101 } 102 103 // overload this to configure preferences in a way specific to a subclass's tests 104 protected void configurePreferences() { 105 // please avoid changing preferences any more than is necessary, as an interruption 106 // during a test will leave the user's preferences in a bad state. 107 } 108 109 // restore any preferences set in configurePreferences() 110 protected void restorePreferences() { 111 // undo any effects from configurePreferences() 112 } 113 114 @Override 115 protected void setUp() { 116 try { 117 super.setUp(); 118 } catch (Exception e) { 119 e.printStackTrace(); 120 } 121 mTextView = new TextView(getContext()); 122 mTextView.setInputType(InputType.TYPE_CLASS_TEXT); 123 mTextView.setEnabled(true); 124 setupService(); 125 mLatinIME = getService(); 126 final boolean previousDebugSetting = setDebugMode(true); 127 configurePreferences(); 128 mLatinIME.onCreate(); 129 setDebugMode(previousDebugSetting); 130 initSubtypeMap(); 131 final EditorInfo ei = new EditorInfo(); 132 ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; 133 final InputConnection ic = mTextView.onCreateInputConnection(ei); 134 ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; 135 final LayoutInflater inflater = 136 (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 137 final ViewGroup vg = new FrameLayout(getContext()); 138 final View inputView = inflater.inflate(R.layout.input_view, vg); 139 mLatinIME.setInputView(inputView); 140 mLatinIME.onBindInput(); 141 mLatinIME.onCreateInputView(); 142 mLatinIME.onStartInputView(ei, false); 143 mLatinIME.onCreateInputMethodInterface().startInput(ic, ei); 144 mInputConnection = ic; 145 mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard(); 146 changeLanguage("en_US"); 147 } 148 149 @Override 150 protected void tearDown() throws Exception { 151 super.tearDown(); 152 restorePreferences(); 153 } 154 155 private void initSubtypeMap() { 156 final InputMethodManager imm = (InputMethodManager)mLatinIME.getSystemService( 157 Context.INPUT_METHOD_SERVICE); 158 final String packageName = mLatinIME.getPackageName(); 159 // The IMEs and subtypes don't need to be enabled to run this test because IMF isn't 160 // involved here. 161 for (final InputMethodInfo imi : imm.getInputMethodList()) { 162 if (imi.getPackageName().equals(packageName)) { 163 final int subtypeCount = imi.getSubtypeCount(); 164 for (int i = 0; i < subtypeCount; i++) { 165 final InputMethodSubtype ims = imi.getSubtypeAt(i); 166 final String locale = ims.getLocale(); 167 mSubtypeMap.put(locale, ims); 168 } 169 return; 170 } 171 } 172 fail("LatinIME is not found"); 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, so we need to do that at the right time so that #loop() returns at some 183 // point and we don't end up in an infinite loop. 184 // After we quit, the looper is still technically ready to process more messages but 185 // the handler will refuse to enqueue any because #quit() has been called and it 186 // explicitly tests for it on message enqueuing, so we'll have to reset it so that 187 // it lets us continue normal operation. 188 protected void runMessages() { 189 // Here begins deep magic. 190 final Looper looper = mLatinIME.mHandler.getLooper(); 191 mLatinIME.mHandler.post(new Runnable() { 192 @Override 193 public void run() { 194 looper.quit(); 195 } 196 }); 197 // The only way to get out of Looper#loop() is to call #quit() on it (or on its queue). 198 // Once #quit() is called remaining messages are not processed, which is why we post 199 // a message that calls it instead of calling it directly. 200 Looper.loop(); 201 202 // Once #quit() has been called, the message queue has an "mQuiting" field that prevents 203 // any subsequent post in this queue. However the queue itself is still fully functional! 204 // If we have a way of resetting "queue.mQuiting" then we can continue using it as normal, 205 // coming back to this method to run the messages. 206 MessageQueue queue = looper.getQueue(); 207 try { 208 // However there is no way of doing it externally, and mQuiting is private. 209 // So... get out the big guns. 210 java.lang.reflect.Field f = MessageQueue.class.getDeclaredField("mQuiting"); 211 f.setAccessible(true); // What do you mean "private"? 212 f.setBoolean(queue, false); 213 } catch (NoSuchFieldException e) { 214 throw new RuntimeException(e); 215 } catch (IllegalAccessException e) { 216 throw new RuntimeException(e); 217 } 218 } 219 220 // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME. 221 protected void type(final int codePoint) { 222 // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the 223 // code (although multitouch/slide input and other factors make the sequencing complicated). 224 // They are supposed to be entirely deconnected from the input logic from LatinIME point of 225 // view and only delegates to the parts of the code that care. So we don't include them here 226 // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies, 227 // but keep them in mind if something breaks. Commenting them out as is should work. 228 //mLatinIME.onPressKey(codePoint); 229 for (final Key key : mKeyboard.mKeys) { 230 if (key.mCode == codePoint) { 231 final int x = key.mX + key.mWidth / 2; 232 final int y = key.mY + key.mHeight / 2; 233 mLatinIME.onCodeInput(codePoint, x, y); 234 return; 235 } 236 } 237 mLatinIME.onCodeInput(codePoint, 238 KeyboardActionListener.NOT_A_TOUCH_COORDINATE, 239 KeyboardActionListener.NOT_A_TOUCH_COORDINATE); 240 //mLatinIME.onReleaseKey(codePoint, false); 241 } 242 243 protected void type(final String stringToType) { 244 for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) { 245 type(stringToType.codePointAt(i)); 246 } 247 } 248 249 protected void waitForDictionaryToBeLoaded() { 250 int remainingAttempts = 10; 251 while (remainingAttempts > 0 && !mLatinIME.mSuggest.hasMainDictionary()) { 252 try { 253 Thread.sleep(200); 254 } catch (InterruptedException e) { 255 // Don't do much 256 } finally { 257 --remainingAttempts; 258 } 259 } 260 if (!mLatinIME.mSuggest.hasMainDictionary()) { 261 throw new RuntimeException("Can't initialize the main dictionary"); 262 } 263 } 264 265 protected void changeLanguage(final String locale) { 266 final InputMethodSubtype subtype = mSubtypeMap.get(locale); 267 if (subtype == null) { 268 fail("InputMethodSubtype for locale " + locale + " is not enabled"); 269 } 270 SubtypeSwitcher.getInstance().updateSubtype(subtype); 271 waitForDictionaryToBeLoaded(); 272 } 273 274 275 // Helper to avoid writing the try{}catch block each time 276 protected static void sleep(final int milliseconds) { 277 try { 278 Thread.sleep(milliseconds); 279 } catch (InterruptedException e) {} 280 } 281} 282