Home | History | Annotate | Download | only in latin
      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