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 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.os.MessageQueue; 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.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.InputMethodInfo; 35 import android.view.inputmethod.InputMethodManager; 36 import android.view.inputmethod.InputMethodSubtype; 37 import android.widget.FrameLayout; 38 import android.widget.TextView; 39 40 import com.android.inputmethod.keyboard.Key; 41 import com.android.inputmethod.keyboard.Keyboard; 42 import com.android.inputmethod.keyboard.KeyboardActionListener; 43 44 import java.util.HashMap; 45 46 public class InputTestsBase extends ServiceTestCase<LatinIME> { 47 48 private static final String PREF_DEBUG_MODE = "debug_mode"; 49 50 // The message that sets the underline is posted with a 100 ms delay 51 protected static final int DELAY_TO_WAIT_FOR_UNDERLINE = 200; 52 53 protected LatinIME mLatinIME; 54 protected Keyboard mKeyboard; 55 protected TextView mTextView; 56 protected InputConnection mInputConnection; 57 private final HashMap<String, InputMethodSubtype> mSubtypeMap = 58 new HashMap<String, InputMethodSubtype>(); 59 60 // A helper class to ease span tests 61 public static class SpanGetter { 62 final SpannableStringBuilder mInputText; 63 final CharacterStyle mSpan; 64 final int mStart; 65 final int mEnd; 66 // The supplied CharSequence should be an instance of SpannableStringBuilder, 67 // and it should contain exactly zero or one span. Otherwise, an exception 68 // is thrown. 69 public SpanGetter(final CharSequence inputText, 70 final Class<? extends CharacterStyle> spanType) { 71 mInputText = (SpannableStringBuilder)inputText; 72 final CharacterStyle[] spans = 73 mInputText.getSpans(0, mInputText.length(), spanType); 74 if (0 == spans.length) { 75 mSpan = null; 76 mStart = -1; 77 mEnd = -1; 78 } else if (1 == spans.length) { 79 mSpan = spans[0]; 80 mStart = mInputText.getSpanStart(mSpan); 81 mEnd = mInputText.getSpanEnd(mSpan); 82 } else { 83 throw new RuntimeException("Expected one span, found " + spans.length); 84 } 85 } 86 public boolean isAutoCorrectionIndicator() { 87 return (mSpan instanceof SuggestionSpan) && 88 0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & ((SuggestionSpan)mSpan).getFlags()); 89 } 90 } 91 92 public InputTestsBase() { 93 super(LatinIME.class); 94 } 95 96 // TODO: Isn't there a way to make this generic somehow? We can take a <T> and return a <T> 97 // but we'd have to dispatch types on editor.put...() functions 98 protected boolean setBooleanPreference(final String key, final boolean value, 99 final boolean defaultValue) { 100 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); 101 final boolean previousSetting = prefs.getBoolean(key, defaultValue); 102 final SharedPreferences.Editor editor = prefs.edit(); 103 editor.putBoolean(key, value); 104 editor.commit(); 105 return previousSetting; 106 } 107 108 // returns the previous setting value 109 protected boolean setDebugMode(final boolean value) { 110 return setBooleanPreference(PREF_DEBUG_MODE, value, false); 111 } 112 113 @Override 114 protected void setUp() throws Exception { 115 super.setUp(); 116 mTextView = new TextView(getContext()); 117 mTextView.setInputType(InputType.TYPE_CLASS_TEXT); 118 mTextView.setEnabled(true); 119 setupService(); 120 mLatinIME = getService(); 121 final boolean previousDebugSetting = setDebugMode(true); 122 mLatinIME.onCreate(); 123 setDebugMode(previousDebugSetting); 124 initSubtypeMap(); 125 final EditorInfo ei = new EditorInfo(); 126 ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; 127 final InputConnection ic = mTextView.onCreateInputConnection(ei); 128 ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; 129 final LayoutInflater inflater = 130 (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 131 final ViewGroup vg = new FrameLayout(getContext()); 132 final View inputView = inflater.inflate(R.layout.input_view, vg); 133 mLatinIME.setInputView(inputView); 134 mLatinIME.onBindInput(); 135 mLatinIME.onCreateInputView(); 136 mLatinIME.onStartInputView(ei, false); 137 mLatinIME.onCreateInputMethodInterface().startInput(ic, ei); 138 mInputConnection = ic; 139 mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard(); 140 changeLanguage("en_US"); 141 } 142 143 private void initSubtypeMap() { 144 final InputMethodManager imm = (InputMethodManager)mLatinIME.getSystemService( 145 Context.INPUT_METHOD_SERVICE); 146 final String packageName = mLatinIME.getPackageName(); 147 // The IMEs and subtypes don't need to be enabled to run this test because IMF isn't 148 // involved here. 149 for (final InputMethodInfo imi : imm.getInputMethodList()) { 150 if (imi.getPackageName().equals(packageName)) { 151 final int subtypeCount = imi.getSubtypeCount(); 152 for (int i = 0; i < subtypeCount; i++) { 153 final InputMethodSubtype ims = imi.getSubtypeAt(i); 154 final String locale = ims.getLocale(); 155 mSubtypeMap.put(locale, ims); 156 } 157 return; 158 } 159 } 160 fail("LatinIME is not found"); 161 } 162 163 // We need to run the messages added to the handler from LatinIME. The only way to do 164 // that is to call Looper#loop() on the right looper, so we're going to get the looper 165 // object and call #loop() here. The messages in the handler actually run on the UI 166 // thread of the keyboard by design of the handler, so we want to call it synchronously 167 // on the same thread that the tests are running on to mimic the actual environment as 168 // closely as possible. 169 // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method 170 // is called, so we need to do that at the right time so that #loop() returns at some 171 // point and we don't end up in an infinite loop. 172 // After we quit, the looper is still technically ready to process more messages but 173 // the handler will refuse to enqueue any because #quit() has been called and it 174 // explicitly tests for it on message enqueuing, so we'll have to reset it so that 175 // it lets us continue normal operation. 176 protected void runMessages() { 177 // Here begins deep magic. 178 final Looper looper = mLatinIME.mHandler.getLooper(); 179 mLatinIME.mHandler.post(new Runnable() { 180 @Override 181 public void run() { 182 looper.quit(); 183 } 184 }); 185 // The only way to get out of Looper#loop() is to call #quit() on it (or on its queue). 186 // Once #quit() is called remaining messages are not processed, which is why we post 187 // a message that calls it instead of calling it directly. 188 Looper.loop(); 189 190 // Once #quit() has been called, the message queue has an "mQuiting" field that prevents 191 // any subsequent post in this queue. However the queue itself is still fully functional! 192 // If we have a way of resetting "queue.mQuiting" then we can continue using it as normal, 193 // coming back to this method to run the messages. 194 MessageQueue queue = looper.getQueue(); 195 try { 196 // However there is no way of doing it externally, and mQuiting is private. 197 // So... get out the big guns. 198 java.lang.reflect.Field f = MessageQueue.class.getDeclaredField("mQuiting"); 199 f.setAccessible(true); // What do you mean "private"? 200 f.setBoolean(queue, false); 201 } catch (NoSuchFieldException e) { 202 throw new RuntimeException(e); 203 } catch (IllegalAccessException e) { 204 throw new RuntimeException(e); 205 } 206 } 207 208 // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME. 209 protected void type(final int codePoint) { 210 // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the 211 // code (although multitouch/slide input and other factors make the sequencing complicated). 212 // They are supposed to be entirely deconnected from the input logic from LatinIME point of 213 // view and only delegates to the parts of the code that care. So we don't include them here 214 // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies, 215 // but keep them in mind if something breaks. Commenting them out as is should work. 216 //mLatinIME.onPressKey(codePoint); 217 for (final Key key : mKeyboard.mKeys) { 218 if (key.mCode == codePoint) { 219 final int x = key.mX + key.mWidth / 2; 220 final int y = key.mY + key.mHeight / 2; 221 mLatinIME.onCodeInput(codePoint, x, y); 222 return; 223 } 224 } 225 mLatinIME.onCodeInput(codePoint, 226 KeyboardActionListener.NOT_A_TOUCH_COORDINATE, 227 KeyboardActionListener.NOT_A_TOUCH_COORDINATE); 228 //mLatinIME.onReleaseKey(codePoint, false); 229 } 230 231 protected void type(final String stringToType) { 232 for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) { 233 type(stringToType.codePointAt(i)); 234 } 235 } 236 237 protected void waitForDictionaryToBeLoaded() { 238 int remainingAttempts = 10; 239 while (remainingAttempts > 0 && !mLatinIME.mSuggest.hasMainDictionary()) { 240 try { 241 Thread.sleep(200); 242 } catch (InterruptedException e) { 243 // Don't do much 244 } finally { 245 --remainingAttempts; 246 } 247 } 248 if (!mLatinIME.mSuggest.hasMainDictionary()) { 249 throw new RuntimeException("Can't initialize the main dictionary"); 250 } 251 } 252 253 protected void changeLanguage(final String locale) { 254 final InputMethodSubtype subtype = mSubtypeMap.get(locale); 255 if (subtype == null) { 256 fail("InputMethodSubtype for locale " + locale + " is not enabled"); 257 } 258 SubtypeSwitcher.getInstance().updateSubtype(subtype); 259 waitForDictionaryToBeLoaded(); 260 } 261 262 protected void pickSuggestionManually(final int index, final CharSequence suggestion) { 263 mLatinIME.pickSuggestionManually(index, suggestion, 264 KeyboardActionListener.NOT_A_TOUCH_COORDINATE, 265 KeyboardActionListener.NOT_A_TOUCH_COORDINATE); 266 } 267 268 // Helper to avoid writing the try{}catch block each time 269 protected static void sleep(final int milliseconds) { 270 try { 271 Thread.sleep(milliseconds); 272 } catch (InterruptedException e) {} 273 } 274 } 275