Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2008 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.app.AlertDialog;
     20 import android.content.BroadcastReceiver;
     21 import android.content.Context;
     22 import android.content.DialogInterface;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.content.SharedPreferences;
     26 import android.content.res.Configuration;
     27 import android.content.res.Resources;
     28 import android.content.res.XmlResourceParser;
     29 import android.inputmethodservice.InputMethodService;
     30 import android.inputmethodservice.Keyboard;
     31 import android.media.AudioManager;
     32 import android.os.Debug;
     33 import android.os.Handler;
     34 import android.os.Message;
     35 import android.os.SystemClock;
     36 import android.preference.PreferenceActivity;
     37 import android.preference.PreferenceManager;
     38 import android.speech.SpeechRecognizer;
     39 import android.text.ClipboardManager;
     40 import android.text.TextUtils;
     41 import android.util.DisplayMetrics;
     42 import android.util.Log;
     43 import android.util.PrintWriterPrinter;
     44 import android.util.Printer;
     45 import android.view.HapticFeedbackConstants;
     46 import android.view.KeyEvent;
     47 import android.view.LayoutInflater;
     48 import android.view.View;
     49 import android.view.ViewGroup;
     50 import android.view.ViewParent;
     51 import android.view.Window;
     52 import android.view.WindowManager;
     53 import android.view.inputmethod.CompletionInfo;
     54 import android.view.inputmethod.EditorInfo;
     55 import android.view.inputmethod.ExtractedText;
     56 import android.view.inputmethod.ExtractedTextRequest;
     57 import android.view.inputmethod.InputConnection;
     58 import android.view.inputmethod.InputMethodManager;
     59 import android.widget.LinearLayout;
     60 
     61 import com.android.inputmethod.latin.LatinIMEUtil.RingCharBuffer;
     62 import com.android.inputmethod.voice.FieldContext;
     63 import com.android.inputmethod.voice.SettingsUtil;
     64 import com.android.inputmethod.voice.VoiceInput;
     65 
     66 import org.xmlpull.v1.XmlPullParserException;
     67 
     68 import java.io.FileDescriptor;
     69 import java.io.IOException;
     70 import java.io.PrintWriter;
     71 import java.util.ArrayList;
     72 import java.util.Collections;
     73 import java.util.HashMap;
     74 import java.util.List;
     75 import java.util.Locale;
     76 import java.util.Map;
     77 
     78 /**
     79  * Input method implementation for Qwerty'ish keyboard.
     80  */
     81 public class LatinIME extends InputMethodService
     82         implements LatinKeyboardBaseView.OnKeyboardActionListener,
     83         VoiceInput.UiListener,
     84         SharedPreferences.OnSharedPreferenceChangeListener {
     85     private static final String TAG = "LatinIME";
     86     private static final boolean PERF_DEBUG = false;
     87     static final boolean DEBUG = false;
     88     static final boolean TRACE = false;
     89     static final boolean VOICE_INSTALLED = true;
     90     static final boolean ENABLE_VOICE_BUTTON = true;
     91 
     92     private static final String PREF_VIBRATE_ON = "vibrate_on";
     93     private static final String PREF_SOUND_ON = "sound_on";
     94     private static final String PREF_POPUP_ON = "popup_on";
     95     private static final String PREF_AUTO_CAP = "auto_cap";
     96     private static final String PREF_QUICK_FIXES = "quick_fixes";
     97     private static final String PREF_SHOW_SUGGESTIONS = "show_suggestions";
     98     private static final String PREF_AUTO_COMPLETE = "auto_complete";
     99     //private static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion";
    100     private static final String PREF_VOICE_MODE = "voice_mode";
    101 
    102     // Whether or not the user has used voice input before (and thus, whether to show the
    103     // first-run warning dialog or not).
    104     private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input";
    105 
    106     // Whether or not the user has used voice input from an unsupported locale UI before.
    107     // For example, the user has a Chinese UI but activates voice input.
    108     private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE =
    109             "has_used_voice_input_unsupported_locale";
    110 
    111     // A list of locales which are supported by default for voice input, unless we get a
    112     // different list from Gservices.
    113     public static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES =
    114             "en " +
    115             "en_US " +
    116             "en_GB " +
    117             "en_AU " +
    118             "en_CA " +
    119             "en_IE " +
    120             "en_IN " +
    121             "en_NZ " +
    122             "en_SG " +
    123             "en_ZA ";
    124 
    125     // The private IME option used to indicate that no microphone should be shown for a
    126     // given text field. For instance this is specified by the search dialog when the
    127     // dialog is already showing a voice search button.
    128     private static final String IME_OPTION_NO_MICROPHONE = "nm";
    129 
    130     public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
    131     public static final String PREF_INPUT_LANGUAGE = "input_language";
    132     private static final String PREF_RECORRECTION_ENABLED = "recorrection_enabled";
    133 
    134     private static final int MSG_UPDATE_SUGGESTIONS = 0;
    135     private static final int MSG_START_TUTORIAL = 1;
    136     private static final int MSG_UPDATE_SHIFT_STATE = 2;
    137     private static final int MSG_VOICE_RESULTS = 3;
    138     private static final int MSG_UPDATE_OLD_SUGGESTIONS = 4;
    139 
    140     // How many continuous deletes at which to start deleting at a higher speed.
    141     private static final int DELETE_ACCELERATE_AT = 20;
    142     // Key events coming any faster than this are long-presses.
    143     private static final int QUICK_PRESS = 200;
    144 
    145     static final int KEYCODE_ENTER = '\n';
    146     static final int KEYCODE_SPACE = ' ';
    147     static final int KEYCODE_PERIOD = '.';
    148 
    149     // Contextual menu positions
    150     private static final int POS_METHOD = 0;
    151     private static final int POS_SETTINGS = 1;
    152 
    153     //private LatinKeyboardView mInputView;
    154     private LinearLayout mCandidateViewContainer;
    155     private CandidateView mCandidateView;
    156     private Suggest mSuggest;
    157     private CompletionInfo[] mCompletions;
    158 
    159     private AlertDialog mOptionsDialog;
    160     private AlertDialog mVoiceWarningDialog;
    161 
    162     /* package */ KeyboardSwitcher mKeyboardSwitcher;
    163 
    164     private UserDictionary mUserDictionary;
    165     private UserBigramDictionary mUserBigramDictionary;
    166     private ContactsDictionary mContactsDictionary;
    167     private AutoDictionary mAutoDictionary;
    168 
    169     private Hints mHints;
    170 
    171     private Resources mResources;
    172 
    173     private String mInputLocale;
    174     private String mSystemLocale;
    175     private LanguageSwitcher mLanguageSwitcher;
    176 
    177     private StringBuilder mComposing = new StringBuilder();
    178     private WordComposer mWord = new WordComposer();
    179     private int mCommittedLength;
    180     private boolean mPredicting;
    181     private boolean mRecognizing;
    182     private boolean mAfterVoiceInput;
    183     private boolean mImmediatelyAfterVoiceInput;
    184     private boolean mShowingVoiceSuggestions;
    185     private boolean mVoiceInputHighlighted;
    186     private boolean mEnableVoiceButton;
    187     private CharSequence mBestWord;
    188     private boolean mPredictionOn;
    189     private boolean mCompletionOn;
    190     private boolean mHasDictionary;
    191     private boolean mAutoSpace;
    192     private boolean mJustAddedAutoSpace;
    193     private boolean mAutoCorrectEnabled;
    194     private boolean mReCorrectionEnabled;
    195     // Bigram Suggestion is disabled in this version.
    196     private final boolean mBigramSuggestionEnabled = false;
    197     private boolean mAutoCorrectOn;
    198     // TODO move this state variable outside LatinIME
    199     private boolean mCapsLock;
    200     private boolean mPasswordText;
    201     private boolean mVibrateOn;
    202     private boolean mSoundOn;
    203     private boolean mPopupOn;
    204     private boolean mAutoCap;
    205     private boolean mQuickFixes;
    206     private boolean mHasUsedVoiceInput;
    207     private boolean mHasUsedVoiceInputUnsupportedLocale;
    208     private boolean mLocaleSupportedForVoiceInput;
    209     private boolean mShowSuggestions;
    210     private boolean mIsShowingHint;
    211     private int     mCorrectionMode;
    212     private boolean mEnableVoice = true;
    213     private boolean mVoiceOnPrimary;
    214     private int     mOrientation;
    215     private List<CharSequence> mSuggestPuncList;
    216     // Keep track of the last selection range to decide if we need to show word alternatives
    217     private int     mLastSelectionStart;
    218     private int     mLastSelectionEnd;
    219 
    220     // Input type is such that we should not auto-correct
    221     private boolean mInputTypeNoAutoCorrect;
    222 
    223     // Indicates whether the suggestion strip is to be on in landscape
    224     private boolean mJustAccepted;
    225     private CharSequence mJustRevertedSeparator;
    226     private int mDeleteCount;
    227     private long mLastKeyTime;
    228 
    229     // Modifier keys state
    230     private ModifierKeyState mShiftKeyState = new ModifierKeyState();
    231     private ModifierKeyState mSymbolKeyState = new ModifierKeyState();
    232 
    233     private Tutorial mTutorial;
    234 
    235     private AudioManager mAudioManager;
    236     // Align sound effect volume on music volume
    237     private final float FX_VOLUME = -1.0f;
    238     private boolean mSilentMode;
    239 
    240     /* package */ String mWordSeparators;
    241     private String mSentenceSeparators;
    242     private String mSuggestPuncs;
    243     private VoiceInput mVoiceInput;
    244     private VoiceResults mVoiceResults = new VoiceResults();
    245     private boolean mConfigurationChanging;
    246 
    247     // Keeps track of most recently inserted text (multi-character key) for reverting
    248     private CharSequence mEnteredText;
    249     private boolean mRefreshKeyboardRequired;
    250 
    251     // For each word, a list of potential replacements, usually from voice.
    252     private Map<String, List<CharSequence>> mWordToSuggestions =
    253             new HashMap<String, List<CharSequence>>();
    254 
    255     private ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>();
    256 
    257     private class VoiceResults {
    258         List<String> candidates;
    259         Map<String, List<CharSequence>> alternatives;
    260     }
    261 
    262     public abstract static class WordAlternatives {
    263         protected CharSequence mChosenWord;
    264 
    265         public WordAlternatives() {
    266             // Nothing
    267         }
    268 
    269         public WordAlternatives(CharSequence chosenWord) {
    270             mChosenWord = chosenWord;
    271         }
    272 
    273         @Override
    274         public int hashCode() {
    275             return mChosenWord.hashCode();
    276         }
    277 
    278         public abstract CharSequence getOriginalWord();
    279 
    280         public CharSequence getChosenWord() {
    281             return mChosenWord;
    282         }
    283 
    284         public abstract List<CharSequence> getAlternatives();
    285     }
    286 
    287     public class TypedWordAlternatives extends WordAlternatives {
    288         private WordComposer word;
    289 
    290         public TypedWordAlternatives() {
    291             // Nothing
    292         }
    293 
    294         public TypedWordAlternatives(CharSequence chosenWord, WordComposer wordComposer) {
    295             super(chosenWord);
    296             word = wordComposer;
    297         }
    298 
    299         @Override
    300         public CharSequence getOriginalWord() {
    301             return word.getTypedWord();
    302         }
    303 
    304         @Override
    305         public List<CharSequence> getAlternatives() {
    306             return getTypedSuggestions(word);
    307         }
    308     }
    309 
    310     /* package */ Handler mHandler = new Handler() {
    311         @Override
    312         public void handleMessage(Message msg) {
    313             switch (msg.what) {
    314                 case MSG_UPDATE_SUGGESTIONS:
    315                     updateSuggestions();
    316                     break;
    317                 case MSG_UPDATE_OLD_SUGGESTIONS:
    318                     setOldSuggestions();
    319                     break;
    320                 case MSG_START_TUTORIAL:
    321                     if (mTutorial == null) {
    322                         if (mKeyboardSwitcher.getInputView().isShown()) {
    323                             mTutorial = new Tutorial(
    324                                     LatinIME.this, mKeyboardSwitcher.getInputView());
    325                             mTutorial.start();
    326                         } else {
    327                             // Try again soon if the view is not yet showing
    328                             sendMessageDelayed(obtainMessage(MSG_START_TUTORIAL), 100);
    329                         }
    330                     }
    331                     break;
    332                 case MSG_UPDATE_SHIFT_STATE:
    333                     updateShiftKeyState(getCurrentInputEditorInfo());
    334                     break;
    335                 case MSG_VOICE_RESULTS:
    336                     handleVoiceResults();
    337                     break;
    338             }
    339         }
    340     };
    341 
    342     @Override
    343     public void onCreate() {
    344         LatinImeLogger.init(this);
    345         KeyboardSwitcher.init(this);
    346         super.onCreate();
    347         //setStatusIcon(R.drawable.ime_qwerty);
    348         mResources = getResources();
    349         final Configuration conf = mResources.getConfiguration();
    350         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    351         mLanguageSwitcher = new LanguageSwitcher(this);
    352         mLanguageSwitcher.loadLocales(prefs);
    353         mKeyboardSwitcher = KeyboardSwitcher.getInstance();
    354         mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher);
    355         mSystemLocale = conf.locale.toString();
    356         mLanguageSwitcher.setSystemLocale(conf.locale);
    357         String inputLanguage = mLanguageSwitcher.getInputLanguage();
    358         if (inputLanguage == null) {
    359             inputLanguage = conf.locale.toString();
    360         }
    361         mReCorrectionEnabled = prefs.getBoolean(PREF_RECORRECTION_ENABLED,
    362                 getResources().getBoolean(R.bool.default_recorrection_enabled));
    363 
    364         LatinIMEUtil.GCUtils.getInstance().reset();
    365         boolean tryGC = true;
    366         for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
    367             try {
    368                 initSuggest(inputLanguage);
    369                 tryGC = false;
    370             } catch (OutOfMemoryError e) {
    371                 tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(inputLanguage, e);
    372             }
    373         }
    374 
    375         mOrientation = conf.orientation;
    376         initSuggestPuncList();
    377 
    378         // register to receive ringer mode changes for silent mode
    379         IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION);
    380         registerReceiver(mReceiver, filter);
    381         if (VOICE_INSTALLED) {
    382             mVoiceInput = new VoiceInput(this, this);
    383             mHints = new Hints(this, new Hints.Display() {
    384                 public void showHint(int viewResource) {
    385                     LayoutInflater inflater = (LayoutInflater) getSystemService(
    386                             Context.LAYOUT_INFLATER_SERVICE);
    387                     View view = inflater.inflate(viewResource, null);
    388                     setCandidatesView(view);
    389                     setCandidatesViewShown(true);
    390                     mIsShowingHint = true;
    391                 }
    392               });
    393         }
    394         prefs.registerOnSharedPreferenceChangeListener(this);
    395     }
    396 
    397     /**
    398      * Loads a dictionary or multiple separated dictionary
    399      * @return returns array of dictionary resource ids
    400      */
    401     /* package */ static int[] getDictionary(Resources res) {
    402         String packageName = LatinIME.class.getPackage().getName();
    403         XmlResourceParser xrp = res.getXml(R.xml.dictionary);
    404         ArrayList<Integer> dictionaries = new ArrayList<Integer>();
    405 
    406         try {
    407             int current = xrp.getEventType();
    408             while (current != XmlResourceParser.END_DOCUMENT) {
    409                 if (current == XmlResourceParser.START_TAG) {
    410                     String tag = xrp.getName();
    411                     if (tag != null) {
    412                         if (tag.equals("part")) {
    413                             String dictFileName = xrp.getAttributeValue(null, "name");
    414                             dictionaries.add(res.getIdentifier(dictFileName, "raw", packageName));
    415                         }
    416                     }
    417                 }
    418                 xrp.next();
    419                 current = xrp.getEventType();
    420             }
    421         } catch (XmlPullParserException e) {
    422             Log.e(TAG, "Dictionary XML parsing failure");
    423         } catch (IOException e) {
    424             Log.e(TAG, "Dictionary XML IOException");
    425         }
    426 
    427         int count = dictionaries.size();
    428         int[] dict = new int[count];
    429         for (int i = 0; i < count; i++) {
    430             dict[i] = dictionaries.get(i);
    431         }
    432 
    433         return dict;
    434     }
    435 
    436     private void initSuggest(String locale) {
    437         mInputLocale = locale;
    438 
    439         Resources orig = getResources();
    440         Configuration conf = orig.getConfiguration();
    441         Locale saveLocale = conf.locale;
    442         conf.locale = new Locale(locale);
    443         orig.updateConfiguration(conf, orig.getDisplayMetrics());
    444         if (mSuggest != null) {
    445             mSuggest.close();
    446         }
    447         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
    448         mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true);
    449 
    450         int[] dictionaries = getDictionary(orig);
    451         mSuggest = new Suggest(this, dictionaries);
    452         updateAutoTextEnabled(saveLocale);
    453         if (mUserDictionary != null) mUserDictionary.close();
    454         mUserDictionary = new UserDictionary(this, mInputLocale);
    455         if (mContactsDictionary == null) {
    456             mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
    457         }
    458         if (mAutoDictionary != null) {
    459             mAutoDictionary.close();
    460         }
    461         mAutoDictionary = new AutoDictionary(this, this, mInputLocale, Suggest.DIC_AUTO);
    462         if (mUserBigramDictionary != null) {
    463             mUserBigramDictionary.close();
    464         }
    465         mUserBigramDictionary = new UserBigramDictionary(this, this, mInputLocale,
    466                 Suggest.DIC_USER);
    467         mSuggest.setUserBigramDictionary(mUserBigramDictionary);
    468         mSuggest.setUserDictionary(mUserDictionary);
    469         mSuggest.setContactsDictionary(mContactsDictionary);
    470         mSuggest.setAutoDictionary(mAutoDictionary);
    471         updateCorrectionMode();
    472         mWordSeparators = mResources.getString(R.string.word_separators);
    473         mSentenceSeparators = mResources.getString(R.string.sentence_separators);
    474 
    475         conf.locale = saveLocale;
    476         orig.updateConfiguration(conf, orig.getDisplayMetrics());
    477     }
    478 
    479     @Override
    480     public void onDestroy() {
    481         if (mUserDictionary != null) {
    482             mUserDictionary.close();
    483         }
    484         if (mContactsDictionary != null) {
    485             mContactsDictionary.close();
    486         }
    487         unregisterReceiver(mReceiver);
    488         if (VOICE_INSTALLED && mVoiceInput != null) {
    489             mVoiceInput.destroy();
    490         }
    491         LatinImeLogger.commit();
    492         LatinImeLogger.onDestroy();
    493         super.onDestroy();
    494     }
    495 
    496     @Override
    497     public void onConfigurationChanged(Configuration conf) {
    498         // If the system locale changes and is different from the saved
    499         // locale (mSystemLocale), then reload the input locale list from the
    500         // latin ime settings (shared prefs) and reset the input locale
    501         // to the first one.
    502         final String systemLocale = conf.locale.toString();
    503         if (!TextUtils.equals(systemLocale, mSystemLocale)) {
    504             mSystemLocale = systemLocale;
    505             if (mLanguageSwitcher != null) {
    506                 mLanguageSwitcher.loadLocales(
    507                         PreferenceManager.getDefaultSharedPreferences(this));
    508                 mLanguageSwitcher.setSystemLocale(conf.locale);
    509                 toggleLanguage(true, true);
    510             } else {
    511                 reloadKeyboards();
    512             }
    513         }
    514         // If orientation changed while predicting, commit the change
    515         if (conf.orientation != mOrientation) {
    516             InputConnection ic = getCurrentInputConnection();
    517             commitTyped(ic);
    518             if (ic != null) ic.finishComposingText(); // For voice input
    519             mOrientation = conf.orientation;
    520             reloadKeyboards();
    521         }
    522         mConfigurationChanging = true;
    523         super.onConfigurationChanged(conf);
    524         if (mRecognizing) {
    525             switchToRecognitionStatusView();
    526         }
    527         mConfigurationChanging = false;
    528     }
    529 
    530     @Override
    531     public View onCreateInputView() {
    532         mKeyboardSwitcher.recreateInputView();
    533         mKeyboardSwitcher.makeKeyboards(true);
    534         mKeyboardSwitcher.setKeyboardMode(
    535                 KeyboardSwitcher.MODE_TEXT, 0,
    536                 shouldShowVoiceButton(makeFieldContext(), getCurrentInputEditorInfo()));
    537         return mKeyboardSwitcher.getInputView();
    538     }
    539 
    540     @Override
    541     public View onCreateCandidatesView() {
    542         mKeyboardSwitcher.makeKeyboards(true);
    543         mCandidateViewContainer = (LinearLayout) getLayoutInflater().inflate(
    544                 R.layout.candidates, null);
    545         mCandidateView = (CandidateView) mCandidateViewContainer.findViewById(R.id.candidates);
    546         mCandidateView.setService(this);
    547         setCandidatesViewShown(true);
    548         return mCandidateViewContainer;
    549     }
    550 
    551     @Override
    552     public void onStartInputView(EditorInfo attribute, boolean restarting) {
    553         LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
    554         // In landscape mode, this method gets called without the input view being created.
    555         if (inputView == null) {
    556             return;
    557         }
    558 
    559         if (mRefreshKeyboardRequired) {
    560             mRefreshKeyboardRequired = false;
    561             toggleLanguage(true, true);
    562         }
    563 
    564         mKeyboardSwitcher.makeKeyboards(false);
    565 
    566         TextEntryState.newSession(this);
    567 
    568         // Most such things we decide below in the switch statement, but we need to know
    569         // now whether this is a password text field, because we need to know now (before
    570         // the switch statement) whether we want to enable the voice button.
    571         mPasswordText = false;
    572         int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION;
    573         if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD ||
    574                 variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) {
    575             mPasswordText = true;
    576         }
    577 
    578         mEnableVoiceButton = shouldShowVoiceButton(makeFieldContext(), attribute);
    579         final boolean enableVoiceButton = mEnableVoiceButton && mEnableVoice;
    580 
    581         mAfterVoiceInput = false;
    582         mImmediatelyAfterVoiceInput = false;
    583         mShowingVoiceSuggestions = false;
    584         mVoiceInputHighlighted = false;
    585         mInputTypeNoAutoCorrect = false;
    586         mPredictionOn = false;
    587         mCompletionOn = false;
    588         mCompletions = null;
    589         mCapsLock = false;
    590         mEnteredText = null;
    591 
    592         switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) {
    593             case EditorInfo.TYPE_CLASS_NUMBER:
    594             case EditorInfo.TYPE_CLASS_DATETIME:
    595                 // fall through
    596                 // NOTE: For now, we use the phone keyboard for NUMBER and DATETIME until we get
    597                 // a dedicated number entry keypad.
    598                 // TODO: Use a dedicated number entry keypad here when we get one.
    599             case EditorInfo.TYPE_CLASS_PHONE:
    600                 mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_PHONE,
    601                         attribute.imeOptions, enableVoiceButton);
    602                 break;
    603             case EditorInfo.TYPE_CLASS_TEXT:
    604                 mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT,
    605                         attribute.imeOptions, enableVoiceButton);
    606                 //startPrediction();
    607                 mPredictionOn = true;
    608                 // Make sure that passwords are not displayed in candidate view
    609                 if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD ||
    610                         variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ) {
    611                     mPredictionOn = false;
    612                 }
    613                 if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
    614                         || variation == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME) {
    615                     mAutoSpace = false;
    616                 } else {
    617                     mAutoSpace = true;
    618                 }
    619                 if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) {
    620                     mPredictionOn = false;
    621                     mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_EMAIL,
    622                             attribute.imeOptions, enableVoiceButton);
    623                 } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) {
    624                     mPredictionOn = false;
    625                     mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_URL,
    626                             attribute.imeOptions, enableVoiceButton);
    627                 } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
    628                     mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_IM,
    629                             attribute.imeOptions, enableVoiceButton);
    630                 } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) {
    631                     mPredictionOn = false;
    632                 } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) {
    633                     mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_WEB,
    634                             attribute.imeOptions, enableVoiceButton);
    635                     // If it's a browser edit field and auto correct is not ON explicitly, then
    636                     // disable auto correction, but keep suggestions on.
    637                     if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) {
    638                         mInputTypeNoAutoCorrect = true;
    639                     }
    640                 }
    641 
    642                 // If NO_SUGGESTIONS is set, don't do prediction.
    643                 if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
    644                     mPredictionOn = false;
    645                     mInputTypeNoAutoCorrect = true;
    646                 }
    647                 // If it's not multiline and the autoCorrect flag is not set, then don't correct
    648                 if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 &&
    649                         (attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
    650                     mInputTypeNoAutoCorrect = true;
    651                 }
    652                 if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
    653                     mPredictionOn = false;
    654                     mCompletionOn = isFullscreenMode();
    655                 }
    656                 break;
    657             default:
    658                 mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT,
    659                         attribute.imeOptions, enableVoiceButton);
    660         }
    661         inputView.closing();
    662         mComposing.setLength(0);
    663         mPredicting = false;
    664         mDeleteCount = 0;
    665         mJustAddedAutoSpace = false;
    666         loadSettings();
    667         updateShiftKeyState(attribute);
    668 
    669         setCandidatesViewShownInternal(isCandidateStripVisible() || mCompletionOn,
    670                 false /* needsInputViewShown */ );
    671         updateSuggestions();
    672 
    673         // If the dictionary is not big enough, don't auto correct
    674         mHasDictionary = mSuggest.hasMainDictionary();
    675 
    676         updateCorrectionMode();
    677 
    678         inputView.setPreviewEnabled(mPopupOn);
    679         inputView.setProximityCorrectionEnabled(true);
    680         mPredictionOn = mPredictionOn && (mCorrectionMode > 0 || mShowSuggestions);
    681         // If we just entered a text field, maybe it has some old text that requires correction
    682         checkReCorrectionOnStart();
    683         checkTutorial(attribute.privateImeOptions);
    684         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
    685     }
    686 
    687     private void checkReCorrectionOnStart() {
    688         if (mReCorrectionEnabled && isPredictionOn()) {
    689             // First get the cursor position. This is required by setOldSuggestions(), so that
    690             // it can pass the correct range to setComposingRegion(). At this point, we don't
    691             // have valid values for mLastSelectionStart/Stop because onUpdateSelection() has
    692             // not been called yet.
    693             InputConnection ic = getCurrentInputConnection();
    694             if (ic == null) return;
    695             ExtractedTextRequest etr = new ExtractedTextRequest();
    696             etr.token = 0; // anything is fine here
    697             ExtractedText et = ic.getExtractedText(etr, 0);
    698             if (et == null) return;
    699 
    700             mLastSelectionStart = et.startOffset + et.selectionStart;
    701             mLastSelectionEnd = et.startOffset + et.selectionEnd;
    702 
    703             // Then look for possible corrections in a delayed fashion
    704             if (!TextUtils.isEmpty(et.text) && isCursorTouchingWord()) {
    705                 postUpdateOldSuggestions();
    706             }
    707         }
    708     }
    709 
    710     @Override
    711     public void onFinishInput() {
    712         super.onFinishInput();
    713 
    714         LatinImeLogger.commit();
    715         onAutoCompletionStateChanged(false);
    716 
    717         if (VOICE_INSTALLED && !mConfigurationChanging) {
    718             if (mAfterVoiceInput) {
    719                 mVoiceInput.flushAllTextModificationCounters();
    720                 mVoiceInput.logInputEnded();
    721             }
    722             mVoiceInput.flushLogs();
    723             mVoiceInput.cancel();
    724         }
    725         if (mKeyboardSwitcher.getInputView() != null) {
    726             mKeyboardSwitcher.getInputView().closing();
    727         }
    728         if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites();
    729         if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites();
    730     }
    731 
    732     @Override
    733     public void onFinishInputView(boolean finishingInput) {
    734         super.onFinishInputView(finishingInput);
    735         // Remove penging messages related to update suggestions
    736         mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS);
    737         mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
    738     }
    739 
    740     @Override
    741     public void onUpdateExtractedText(int token, ExtractedText text) {
    742         super.onUpdateExtractedText(token, text);
    743         InputConnection ic = getCurrentInputConnection();
    744         if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) {
    745             if (mHints.showPunctuationHintIfNecessary(ic)) {
    746                 mVoiceInput.logPunctuationHintDisplayed();
    747             }
    748         }
    749         mImmediatelyAfterVoiceInput = false;
    750     }
    751 
    752     @Override
    753     public void onUpdateSelection(int oldSelStart, int oldSelEnd,
    754             int newSelStart, int newSelEnd,
    755             int candidatesStart, int candidatesEnd) {
    756         super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
    757                 candidatesStart, candidatesEnd);
    758 
    759         if (DEBUG) {
    760             Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
    761                     + ", ose=" + oldSelEnd
    762                     + ", nss=" + newSelStart
    763                     + ", nse=" + newSelEnd
    764                     + ", cs=" + candidatesStart
    765                     + ", ce=" + candidatesEnd);
    766         }
    767 
    768         if (mAfterVoiceInput) {
    769             mVoiceInput.setCursorPos(newSelEnd);
    770             mVoiceInput.setSelectionSpan(newSelEnd - newSelStart);
    771         }
    772 
    773         // If the current selection in the text view changes, we should
    774         // clear whatever candidate text we have.
    775         if ((((mComposing.length() > 0 && mPredicting) || mVoiceInputHighlighted)
    776                 && (newSelStart != candidatesEnd
    777                     || newSelEnd != candidatesEnd)
    778                 && mLastSelectionStart != newSelStart)) {
    779             mComposing.setLength(0);
    780             mPredicting = false;
    781             postUpdateSuggestions();
    782             TextEntryState.reset();
    783             InputConnection ic = getCurrentInputConnection();
    784             if (ic != null) {
    785                 ic.finishComposingText();
    786             }
    787             mVoiceInputHighlighted = false;
    788         } else if (!mPredicting && !mJustAccepted) {
    789             switch (TextEntryState.getState()) {
    790                 case ACCEPTED_DEFAULT:
    791                     TextEntryState.reset();
    792                     // fall through
    793                 case SPACE_AFTER_PICKED:
    794                     mJustAddedAutoSpace = false;  // The user moved the cursor.
    795                     break;
    796             }
    797         }
    798         mJustAccepted = false;
    799         postUpdateShiftKeyState();
    800 
    801         // Make a note of the cursor position
    802         mLastSelectionStart = newSelStart;
    803         mLastSelectionEnd = newSelEnd;
    804 
    805         if (mReCorrectionEnabled) {
    806             // Don't look for corrections if the keyboard is not visible
    807             if (mKeyboardSwitcher != null && mKeyboardSwitcher.getInputView() != null
    808                     && mKeyboardSwitcher.getInputView().isShown()) {
    809                 // Check if we should go in or out of correction mode.
    810                 if (isPredictionOn()
    811                         && mJustRevertedSeparator == null
    812                         && (candidatesStart == candidatesEnd || newSelStart != oldSelStart
    813                                 || TextEntryState.isCorrecting())
    814                                 && (newSelStart < newSelEnd - 1 || (!mPredicting))
    815                                 && !mVoiceInputHighlighted) {
    816                     if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) {
    817                         postUpdateOldSuggestions();
    818                     } else {
    819                         abortCorrection(false);
    820                         // Show the punctuation suggestions list if the current one is not
    821                         // and if not showing "Touch again to save".
    822                         if (mCandidateView != null
    823                                 && !mSuggestPuncList.equals(mCandidateView.getSuggestions())
    824                                         && !mCandidateView.isShowingAddToDictionaryHint()) {
    825                             setNextSuggestions();
    826                         }
    827                     }
    828                 }
    829             }
    830         }
    831     }
    832 
    833     /**
    834      * This is called when the user has clicked on the extracted text view,
    835      * when running in fullscreen mode.  The default implementation hides
    836      * the candidates view when this happens, but only if the extracted text
    837      * editor has a vertical scroll bar because its text doesn't fit.
    838      * Here we override the behavior due to the possibility that a re-correction could
    839      * cause the candidate strip to disappear and re-appear.
    840      */
    841     @Override
    842     public void onExtractedTextClicked() {
    843         if (mReCorrectionEnabled && isPredictionOn()) return;
    844 
    845         super.onExtractedTextClicked();
    846     }
    847 
    848     /**
    849      * This is called when the user has performed a cursor movement in the
    850      * extracted text view, when it is running in fullscreen mode.  The default
    851      * implementation hides the candidates view when a vertical movement
    852      * happens, but only if the extracted text editor has a vertical scroll bar
    853      * because its text doesn't fit.
    854      * Here we override the behavior due to the possibility that a re-correction could
    855      * cause the candidate strip to disappear and re-appear.
    856      */
    857     @Override
    858     public void onExtractedCursorMovement(int dx, int dy) {
    859         if (mReCorrectionEnabled && isPredictionOn()) return;
    860 
    861         super.onExtractedCursorMovement(dx, dy);
    862     }
    863 
    864     @Override
    865     public void hideWindow() {
    866         LatinImeLogger.commit();
    867         onAutoCompletionStateChanged(false);
    868 
    869         if (TRACE) Debug.stopMethodTracing();
    870         if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
    871             mOptionsDialog.dismiss();
    872             mOptionsDialog = null;
    873         }
    874         if (!mConfigurationChanging) {
    875             if (mAfterVoiceInput) mVoiceInput.logInputEnded();
    876             if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
    877                 mVoiceInput.logKeyboardWarningDialogDismissed();
    878                 mVoiceWarningDialog.dismiss();
    879                 mVoiceWarningDialog = null;
    880             }
    881             if (VOICE_INSTALLED & mRecognizing) {
    882                 mVoiceInput.cancel();
    883             }
    884         }
    885         mWordToSuggestions.clear();
    886         mWordHistory.clear();
    887         super.hideWindow();
    888         TextEntryState.endSession();
    889     }
    890 
    891     @Override
    892     public void onDisplayCompletions(CompletionInfo[] completions) {
    893         if (DEBUG) {
    894             Log.i("foo", "Received completions:");
    895             for (int i=0; i<(completions != null ? completions.length : 0); i++) {
    896                 Log.i("foo", "  #" + i + ": " + completions[i]);
    897             }
    898         }
    899         if (mCompletionOn) {
    900             mCompletions = completions;
    901             if (completions == null) {
    902                 clearSuggestions();
    903                 return;
    904             }
    905 
    906             List<CharSequence> stringList = new ArrayList<CharSequence>();
    907             for (int i=0; i<(completions != null ? completions.length : 0); i++) {
    908                 CompletionInfo ci = completions[i];
    909                 if (ci != null) stringList.add(ci.getText());
    910             }
    911             // When in fullscreen mode, show completions generated by the application
    912             setSuggestions(stringList, true, true, true);
    913             mBestWord = null;
    914             setCandidatesViewShown(true);
    915         }
    916     }
    917 
    918     private void setCandidatesViewShownInternal(boolean shown, boolean needsInputViewShown) {
    919         // TODO: Remove this if we support candidates with hard keyboard
    920         if (onEvaluateInputViewShown()) {
    921             super.setCandidatesViewShown(shown && mKeyboardSwitcher.getInputView() != null
    922                     && (needsInputViewShown ? mKeyboardSwitcher.getInputView().isShown() : true));
    923         }
    924     }
    925 
    926     @Override
    927     public void setCandidatesViewShown(boolean shown) {
    928         setCandidatesViewShownInternal(shown, true /* needsInputViewShown */ );
    929     }
    930 
    931     @Override
    932     public void onComputeInsets(InputMethodService.Insets outInsets) {
    933         super.onComputeInsets(outInsets);
    934         if (!isFullscreenMode()) {
    935             outInsets.contentTopInsets = outInsets.visibleTopInsets;
    936         }
    937     }
    938 
    939     @Override
    940     public boolean onEvaluateFullscreenMode() {
    941         DisplayMetrics dm = getResources().getDisplayMetrics();
    942         float displayHeight = dm.heightPixels;
    943         // If the display is more than X inches high, don't go to fullscreen mode
    944         float dimen = getResources().getDimension(R.dimen.max_height_for_fullscreen);
    945         if (displayHeight > dimen) {
    946             return false;
    947         } else {
    948             return super.onEvaluateFullscreenMode();
    949         }
    950     }
    951 
    952     @Override
    953     public boolean onKeyDown(int keyCode, KeyEvent event) {
    954         switch (keyCode) {
    955             case KeyEvent.KEYCODE_BACK:
    956                 if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) {
    957                     if (mKeyboardSwitcher.getInputView().handleBack()) {
    958                         return true;
    959                     } else if (mTutorial != null) {
    960                         mTutorial.close();
    961                         mTutorial = null;
    962                     }
    963                 }
    964                 break;
    965             case KeyEvent.KEYCODE_DPAD_DOWN:
    966             case KeyEvent.KEYCODE_DPAD_UP:
    967             case KeyEvent.KEYCODE_DPAD_LEFT:
    968             case KeyEvent.KEYCODE_DPAD_RIGHT:
    969                 // If tutorial is visible, don't allow dpad to work
    970                 if (mTutorial != null) {
    971                     return true;
    972                 }
    973                 break;
    974         }
    975         return super.onKeyDown(keyCode, event);
    976     }
    977 
    978     @Override
    979     public boolean onKeyUp(int keyCode, KeyEvent event) {
    980         switch (keyCode) {
    981             case KeyEvent.KEYCODE_DPAD_DOWN:
    982             case KeyEvent.KEYCODE_DPAD_UP:
    983             case KeyEvent.KEYCODE_DPAD_LEFT:
    984             case KeyEvent.KEYCODE_DPAD_RIGHT:
    985                 // If tutorial is visible, don't allow dpad to work
    986                 if (mTutorial != null) {
    987                     return true;
    988                 }
    989                 LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
    990                 // Enable shift key and DPAD to do selections
    991                 if (inputView != null && inputView.isShown()
    992                         && inputView.isShifted()) {
    993                     event = new KeyEvent(event.getDownTime(), event.getEventTime(),
    994                             event.getAction(), event.getKeyCode(), event.getRepeatCount(),
    995                             event.getDeviceId(), event.getScanCode(),
    996                             KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
    997                     InputConnection ic = getCurrentInputConnection();
    998                     if (ic != null) ic.sendKeyEvent(event);
    999                     return true;
   1000                 }
   1001                 break;
   1002         }
   1003         return super.onKeyUp(keyCode, event);
   1004     }
   1005 
   1006     private void revertVoiceInput() {
   1007         InputConnection ic = getCurrentInputConnection();
   1008         if (ic != null) ic.commitText("", 1);
   1009         updateSuggestions();
   1010         mVoiceInputHighlighted = false;
   1011     }
   1012 
   1013     private void commitVoiceInput() {
   1014         InputConnection ic = getCurrentInputConnection();
   1015         if (ic != null) ic.finishComposingText();
   1016         updateSuggestions();
   1017         mVoiceInputHighlighted = false;
   1018     }
   1019 
   1020     private void reloadKeyboards() {
   1021         mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher);
   1022         if (mKeyboardSwitcher.getInputView() != null
   1023                 && mKeyboardSwitcher.getKeyboardMode() != KeyboardSwitcher.MODE_NONE) {
   1024             mKeyboardSwitcher.setVoiceMode(mEnableVoice && mEnableVoiceButton, mVoiceOnPrimary);
   1025         }
   1026         mKeyboardSwitcher.makeKeyboards(true);
   1027     }
   1028 
   1029     private void commitTyped(InputConnection inputConnection) {
   1030         if (mPredicting) {
   1031             mPredicting = false;
   1032             if (mComposing.length() > 0) {
   1033                 if (inputConnection != null) {
   1034                     inputConnection.commitText(mComposing, 1);
   1035                 }
   1036                 mCommittedLength = mComposing.length();
   1037                 TextEntryState.acceptedTyped(mComposing);
   1038                 addToDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED);
   1039             }
   1040             updateSuggestions();
   1041         }
   1042     }
   1043 
   1044     private void postUpdateShiftKeyState() {
   1045         mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE);
   1046         // TODO: Should remove this 300ms delay?
   1047         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SHIFT_STATE), 300);
   1048     }
   1049 
   1050     public void updateShiftKeyState(EditorInfo attr) {
   1051         InputConnection ic = getCurrentInputConnection();
   1052         if (ic != null && attr != null && mKeyboardSwitcher.isAlphabetMode()) {
   1053             mKeyboardSwitcher.setShifted(mShiftKeyState.isMomentary() || mCapsLock
   1054                     || getCursorCapsMode(ic, attr) != 0);
   1055         }
   1056     }
   1057 
   1058     private int getCursorCapsMode(InputConnection ic, EditorInfo attr) {
   1059         int caps = 0;
   1060         EditorInfo ei = getCurrentInputEditorInfo();
   1061         if (mAutoCap && ei != null && ei.inputType != EditorInfo.TYPE_NULL) {
   1062             caps = ic.getCursorCapsMode(attr.inputType);
   1063         }
   1064         return caps;
   1065     }
   1066 
   1067     private void swapPunctuationAndSpace() {
   1068         final InputConnection ic = getCurrentInputConnection();
   1069         if (ic == null) return;
   1070         CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
   1071         if (lastTwo != null && lastTwo.length() == 2
   1072                 && lastTwo.charAt(0) == KEYCODE_SPACE && isSentenceSeparator(lastTwo.charAt(1))) {
   1073             ic.beginBatchEdit();
   1074             ic.deleteSurroundingText(2, 0);
   1075             ic.commitText(lastTwo.charAt(1) + " ", 1);
   1076             ic.endBatchEdit();
   1077             updateShiftKeyState(getCurrentInputEditorInfo());
   1078             mJustAddedAutoSpace = true;
   1079         }
   1080     }
   1081 
   1082     private void reswapPeriodAndSpace() {
   1083         final InputConnection ic = getCurrentInputConnection();
   1084         if (ic == null) return;
   1085         CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
   1086         if (lastThree != null && lastThree.length() == 3
   1087                 && lastThree.charAt(0) == KEYCODE_PERIOD
   1088                 && lastThree.charAt(1) == KEYCODE_SPACE
   1089                 && lastThree.charAt(2) == KEYCODE_PERIOD) {
   1090             ic.beginBatchEdit();
   1091             ic.deleteSurroundingText(3, 0);
   1092             ic.commitText(" ..", 1);
   1093             ic.endBatchEdit();
   1094             updateShiftKeyState(getCurrentInputEditorInfo());
   1095         }
   1096     }
   1097 
   1098     private void doubleSpace() {
   1099         //if (!mAutoPunctuate) return;
   1100         if (mCorrectionMode == Suggest.CORRECTION_NONE) return;
   1101         final InputConnection ic = getCurrentInputConnection();
   1102         if (ic == null) return;
   1103         CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
   1104         if (lastThree != null && lastThree.length() == 3
   1105                 && Character.isLetterOrDigit(lastThree.charAt(0))
   1106                 && lastThree.charAt(1) == KEYCODE_SPACE && lastThree.charAt(2) == KEYCODE_SPACE) {
   1107             ic.beginBatchEdit();
   1108             ic.deleteSurroundingText(2, 0);
   1109             ic.commitText(". ", 1);
   1110             ic.endBatchEdit();
   1111             updateShiftKeyState(getCurrentInputEditorInfo());
   1112             mJustAddedAutoSpace = true;
   1113         }
   1114     }
   1115 
   1116     private void maybeRemovePreviousPeriod(CharSequence text) {
   1117         final InputConnection ic = getCurrentInputConnection();
   1118         if (ic == null) return;
   1119 
   1120         // When the text's first character is '.', remove the previous period
   1121         // if there is one.
   1122         CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
   1123         if (lastOne != null && lastOne.length() == 1
   1124                 && lastOne.charAt(0) == KEYCODE_PERIOD
   1125                 && text.charAt(0) == KEYCODE_PERIOD) {
   1126             ic.deleteSurroundingText(1, 0);
   1127         }
   1128     }
   1129 
   1130     private void removeTrailingSpace() {
   1131         final InputConnection ic = getCurrentInputConnection();
   1132         if (ic == null) return;
   1133 
   1134         CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
   1135         if (lastOne != null && lastOne.length() == 1
   1136                 && lastOne.charAt(0) == KEYCODE_SPACE) {
   1137             ic.deleteSurroundingText(1, 0);
   1138         }
   1139     }
   1140 
   1141     public boolean addWordToDictionary(String word) {
   1142         mUserDictionary.addWord(word, 128);
   1143         // Suggestion strip should be updated after the operation of adding word to the
   1144         // user dictionary
   1145         postUpdateSuggestions();
   1146         return true;
   1147     }
   1148 
   1149     private boolean isAlphabet(int code) {
   1150         if (Character.isLetter(code)) {
   1151             return true;
   1152         } else {
   1153             return false;
   1154         }
   1155     }
   1156 
   1157     private void showInputMethodPicker() {
   1158         ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE))
   1159                 .showInputMethodPicker();
   1160     }
   1161 
   1162     private void onOptionKeyPressed() {
   1163         if (!isShowingOptionDialog()) {
   1164             if (LatinIMEUtil.hasMultipleEnabledIMEs(this)) {
   1165                 showOptionsMenu();
   1166             } else {
   1167                 launchSettings();
   1168             }
   1169         }
   1170     }
   1171 
   1172     private void onOptionKeyLongPressed() {
   1173         if (!isShowingOptionDialog()) {
   1174             if (LatinIMEUtil.hasMultipleEnabledIMEs(this)) {
   1175                 showInputMethodPicker();
   1176             } else {
   1177                 launchSettings();
   1178             }
   1179         }
   1180     }
   1181 
   1182     private boolean isShowingOptionDialog() {
   1183         return mOptionsDialog != null && mOptionsDialog.isShowing();
   1184     }
   1185 
   1186     // Implementation of KeyboardViewListener
   1187 
   1188     public void onKey(int primaryCode, int[] keyCodes, int x, int y) {
   1189         long when = SystemClock.uptimeMillis();
   1190         if (primaryCode != Keyboard.KEYCODE_DELETE ||
   1191                 when > mLastKeyTime + QUICK_PRESS) {
   1192             mDeleteCount = 0;
   1193         }
   1194         mLastKeyTime = when;
   1195         final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch();
   1196         switch (primaryCode) {
   1197             case Keyboard.KEYCODE_DELETE:
   1198                 handleBackspace();
   1199                 mDeleteCount++;
   1200                 LatinImeLogger.logOnDelete();
   1201                 break;
   1202             case Keyboard.KEYCODE_SHIFT:
   1203                 // Shift key is handled in onPress() when device has distinct multi-touch panel.
   1204                 if (!distinctMultiTouch)
   1205                     handleShift();
   1206                 break;
   1207             case Keyboard.KEYCODE_MODE_CHANGE:
   1208                 // Symbol key is handled in onPress() when device has distinct multi-touch panel.
   1209                 if (!distinctMultiTouch)
   1210                     changeKeyboardMode();
   1211                 break;
   1212             case Keyboard.KEYCODE_CANCEL:
   1213                 if (!isShowingOptionDialog()) {
   1214                     handleClose();
   1215                 }
   1216                 break;
   1217             case LatinKeyboardView.KEYCODE_OPTIONS:
   1218                 onOptionKeyPressed();
   1219                 break;
   1220             case LatinKeyboardView.KEYCODE_OPTIONS_LONGPRESS:
   1221                 onOptionKeyLongPressed();
   1222                 break;
   1223             case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE:
   1224                 toggleLanguage(false, true);
   1225                 break;
   1226             case LatinKeyboardView.KEYCODE_PREV_LANGUAGE:
   1227                 toggleLanguage(false, false);
   1228                 break;
   1229             case LatinKeyboardView.KEYCODE_VOICE:
   1230                 if (VOICE_INSTALLED) {
   1231                     startListening(false /* was a button press, was not a swipe */);
   1232                 }
   1233                 break;
   1234             case 9 /*Tab*/:
   1235                 sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
   1236                 break;
   1237             default:
   1238                 if (primaryCode != KEYCODE_ENTER) {
   1239                     mJustAddedAutoSpace = false;
   1240                 }
   1241                 RingCharBuffer.getInstance().push((char)primaryCode, x, y);
   1242                 LatinImeLogger.logOnInputChar();
   1243                 if (isWordSeparator(primaryCode)) {
   1244                     handleSeparator(primaryCode);
   1245                 } else {
   1246                     handleCharacter(primaryCode, keyCodes);
   1247                 }
   1248                 // Cancel the just reverted state
   1249                 mJustRevertedSeparator = null;
   1250         }
   1251         mKeyboardSwitcher.onKey(primaryCode);
   1252         // Reset after any single keystroke
   1253         mEnteredText = null;
   1254     }
   1255 
   1256     public void onText(CharSequence text) {
   1257         if (VOICE_INSTALLED && mVoiceInputHighlighted) {
   1258             commitVoiceInput();
   1259         }
   1260         InputConnection ic = getCurrentInputConnection();
   1261         if (ic == null) return;
   1262         abortCorrection(false);
   1263         ic.beginBatchEdit();
   1264         if (mPredicting) {
   1265             commitTyped(ic);
   1266         }
   1267         maybeRemovePreviousPeriod(text);
   1268         ic.commitText(text, 1);
   1269         ic.endBatchEdit();
   1270         updateShiftKeyState(getCurrentInputEditorInfo());
   1271         mKeyboardSwitcher.onKey(0); // dummy key code.
   1272         mJustRevertedSeparator = null;
   1273         mJustAddedAutoSpace = false;
   1274         mEnteredText = text;
   1275     }
   1276 
   1277     public void onCancel() {
   1278         // User released a finger outside any key
   1279         mKeyboardSwitcher.onCancelInput();
   1280     }
   1281 
   1282     private void handleBackspace() {
   1283         if (VOICE_INSTALLED && mVoiceInputHighlighted) {
   1284             mVoiceInput.incrementTextModificationDeleteCount(
   1285                     mVoiceResults.candidates.get(0).toString().length());
   1286             revertVoiceInput();
   1287             return;
   1288         }
   1289         boolean deleteChar = false;
   1290         InputConnection ic = getCurrentInputConnection();
   1291         if (ic == null) return;
   1292 
   1293         ic.beginBatchEdit();
   1294 
   1295         if (mAfterVoiceInput) {
   1296             // Don't log delete if the user is pressing delete at
   1297             // the beginning of the text box (hence not deleting anything)
   1298             if (mVoiceInput.getCursorPos() > 0) {
   1299                 // If anything was selected before the delete was pressed, increment the
   1300                 // delete count by the length of the selection
   1301                 int deleteLen  =  mVoiceInput.getSelectionSpan() > 0 ?
   1302                         mVoiceInput.getSelectionSpan() : 1;
   1303                 mVoiceInput.incrementTextModificationDeleteCount(deleteLen);
   1304             }
   1305         }
   1306 
   1307         if (mPredicting) {
   1308             final int length = mComposing.length();
   1309             if (length > 0) {
   1310                 mComposing.delete(length - 1, length);
   1311                 mWord.deleteLast();
   1312                 ic.setComposingText(mComposing, 1);
   1313                 if (mComposing.length() == 0) {
   1314                     mPredicting = false;
   1315                 }
   1316                 postUpdateSuggestions();
   1317             } else {
   1318                 ic.deleteSurroundingText(1, 0);
   1319             }
   1320         } else {
   1321             deleteChar = true;
   1322         }
   1323         postUpdateShiftKeyState();
   1324         TextEntryState.backspace();
   1325         if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) {
   1326             revertLastWord(deleteChar);
   1327             ic.endBatchEdit();
   1328             return;
   1329         } else if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
   1330             ic.deleteSurroundingText(mEnteredText.length(), 0);
   1331         } else if (deleteChar) {
   1332             if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
   1333                 // Go back to the suggestion mode if the user canceled the
   1334                 // "Touch again to save".
   1335                 // NOTE: In gerenal, we don't revert the word when backspacing
   1336                 // from a manual suggestion pick.  We deliberately chose a
   1337                 // different behavior only in the case of picking the first
   1338                 // suggestion (typed word).  It's intentional to have made this
   1339                 // inconsistent with backspacing after selecting other suggestions.
   1340                 revertLastWord(deleteChar);
   1341             } else {
   1342                 sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
   1343                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
   1344                     sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
   1345                 }
   1346             }
   1347         }
   1348         mJustRevertedSeparator = null;
   1349         ic.endBatchEdit();
   1350     }
   1351 
   1352     private void resetShift() {
   1353         handleShiftInternal(true);
   1354     }
   1355 
   1356     private void handleShift() {
   1357         handleShiftInternal(false);
   1358     }
   1359 
   1360     private void handleShiftInternal(boolean forceNormal) {
   1361         mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE);
   1362         KeyboardSwitcher switcher = mKeyboardSwitcher;
   1363         LatinKeyboardView inputView = switcher.getInputView();
   1364         if (switcher.isAlphabetMode()) {
   1365             if (mCapsLock || forceNormal) {
   1366                 mCapsLock = false;
   1367                 switcher.setShifted(false);
   1368             } else if (inputView != null) {
   1369                 if (inputView.isShifted()) {
   1370                     mCapsLock = true;
   1371                     switcher.setShiftLocked(true);
   1372                 } else {
   1373                     switcher.setShifted(true);
   1374                 }
   1375             }
   1376         } else {
   1377             switcher.toggleShift();
   1378         }
   1379     }
   1380 
   1381     private void abortCorrection(boolean force) {
   1382         if (force || TextEntryState.isCorrecting()) {
   1383             getCurrentInputConnection().finishComposingText();
   1384             clearSuggestions();
   1385         }
   1386     }
   1387 
   1388     private void handleCharacter(int primaryCode, int[] keyCodes) {
   1389         if (VOICE_INSTALLED && mVoiceInputHighlighted) {
   1390             commitVoiceInput();
   1391         }
   1392 
   1393         if (mAfterVoiceInput) {
   1394             // Assume input length is 1. This assumption fails for smiley face insertions.
   1395             mVoiceInput.incrementTextModificationInsertCount(1);
   1396         }
   1397         if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) {
   1398             abortCorrection(false);
   1399         }
   1400 
   1401         if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) {
   1402             if (!mPredicting) {
   1403                 mPredicting = true;
   1404                 mComposing.setLength(0);
   1405                 saveWordInHistory(mBestWord);
   1406                 mWord.reset();
   1407             }
   1408         }
   1409         if (mKeyboardSwitcher.getInputView().isShifted()) {
   1410             if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
   1411                     || keyCodes[0] > Character.MAX_CODE_POINT) {
   1412                 return;
   1413             }
   1414             primaryCode = keyCodes[0];
   1415             if (mKeyboardSwitcher.isAlphabetMode() && Character.isLowerCase(primaryCode)) {
   1416                 // In some locales, such as Turkish, Character.toUpperCase() may return a wrong
   1417                 // character because it doesn't take care of locale.
   1418                 final String upperCaseString = new String(new int[] {primaryCode}, 0, 1)
   1419                         .toUpperCase(mLanguageSwitcher.getInputLocale());
   1420                 if (upperCaseString.codePointCount(0, upperCaseString.length()) == 1) {
   1421                     primaryCode = upperCaseString.codePointAt(0);
   1422                 } else {
   1423                     // Some keys, such as [eszett], have upper case as multi-characters.
   1424                     onText(upperCaseString);
   1425                     return;
   1426                 }
   1427             }
   1428         }
   1429         if (mPredicting) {
   1430             if (mKeyboardSwitcher.getInputView().isShifted()
   1431                     && mKeyboardSwitcher.isAlphabetMode()
   1432                     && mComposing.length() == 0) {
   1433                 mWord.setFirstCharCapitalized(true);
   1434             }
   1435             mComposing.append((char) primaryCode);
   1436             mWord.add(primaryCode, keyCodes);
   1437             InputConnection ic = getCurrentInputConnection();
   1438             if (ic != null) {
   1439                 // If it's the first letter, make note of auto-caps state
   1440                 if (mWord.size() == 1) {
   1441                     mWord.setAutoCapitalized(
   1442                             getCursorCapsMode(ic, getCurrentInputEditorInfo()) != 0);
   1443                 }
   1444                 ic.setComposingText(mComposing, 1);
   1445             }
   1446             postUpdateSuggestions();
   1447         } else {
   1448             sendKeyChar((char)primaryCode);
   1449         }
   1450         updateShiftKeyState(getCurrentInputEditorInfo());
   1451         if (LatinIME.PERF_DEBUG) measureCps();
   1452         TextEntryState.typedCharacter((char) primaryCode, isWordSeparator(primaryCode));
   1453     }
   1454 
   1455     private void handleSeparator(int primaryCode) {
   1456         if (VOICE_INSTALLED && mVoiceInputHighlighted) {
   1457             commitVoiceInput();
   1458         }
   1459 
   1460         if (mAfterVoiceInput){
   1461             // Assume input length is 1. This assumption fails for smiley face insertions.
   1462             mVoiceInput.incrementTextModificationInsertPunctuationCount(1);
   1463         }
   1464 
   1465         // Should dismiss the "Touch again to save" message when handling separator
   1466         if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
   1467             postUpdateSuggestions();
   1468         }
   1469 
   1470         boolean pickedDefault = false;
   1471         // Handle separator
   1472         InputConnection ic = getCurrentInputConnection();
   1473         if (ic != null) {
   1474             ic.beginBatchEdit();
   1475             abortCorrection(false);
   1476         }
   1477         if (mPredicting) {
   1478             // In certain languages where single quote is a separator, it's better
   1479             // not to auto correct, but accept the typed word. For instance,
   1480             // in Italian dov' should not be expanded to dove' because the elision
   1481             // requires the last vowel to be removed.
   1482             if (mAutoCorrectOn && primaryCode != '\'' &&
   1483                     (mJustRevertedSeparator == null
   1484                             || mJustRevertedSeparator.length() == 0
   1485                             || mJustRevertedSeparator.charAt(0) != primaryCode)) {
   1486                 pickedDefault = pickDefaultSuggestion();
   1487                 // Picked the suggestion by the space key.  We consider this
   1488                 // as "added an auto space".
   1489                 if (primaryCode == KEYCODE_SPACE) {
   1490                     mJustAddedAutoSpace = true;
   1491                 }
   1492             } else {
   1493                 commitTyped(ic);
   1494             }
   1495         }
   1496         if (mJustAddedAutoSpace && primaryCode == KEYCODE_ENTER) {
   1497             removeTrailingSpace();
   1498             mJustAddedAutoSpace = false;
   1499         }
   1500         sendKeyChar((char)primaryCode);
   1501 
   1502         // Handle the case of ". ." -> " .." with auto-space if necessary
   1503         // before changing the TextEntryState.
   1504         if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED
   1505                 && primaryCode == KEYCODE_PERIOD) {
   1506             reswapPeriodAndSpace();
   1507         }
   1508 
   1509         TextEntryState.typedCharacter((char) primaryCode, true);
   1510         if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED
   1511                 && primaryCode != KEYCODE_ENTER) {
   1512             swapPunctuationAndSpace();
   1513         } else if (isPredictionOn() && primaryCode == KEYCODE_SPACE) {
   1514             doubleSpace();
   1515         }
   1516         if (pickedDefault) {
   1517             TextEntryState.backToAcceptedDefault(mWord.getTypedWord());
   1518         }
   1519         updateShiftKeyState(getCurrentInputEditorInfo());
   1520         if (ic != null) {
   1521             ic.endBatchEdit();
   1522         }
   1523     }
   1524 
   1525     private void handleClose() {
   1526         commitTyped(getCurrentInputConnection());
   1527         if (VOICE_INSTALLED & mRecognizing) {
   1528             mVoiceInput.cancel();
   1529         }
   1530         requestHideSelf(0);
   1531         if (mKeyboardSwitcher != null) {
   1532             LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
   1533             if (inputView != null) {
   1534                 inputView.closing();
   1535             }
   1536         }
   1537         TextEntryState.endSession();
   1538     }
   1539 
   1540     private void saveWordInHistory(CharSequence result) {
   1541         if (mWord.size() <= 1) {
   1542             mWord.reset();
   1543             return;
   1544         }
   1545         // Skip if result is null. It happens in some edge case.
   1546         if (TextUtils.isEmpty(result)) {
   1547             return;
   1548         }
   1549 
   1550         // Make a copy of the CharSequence, since it is/could be a mutable CharSequence
   1551         final String resultCopy = result.toString();
   1552         TypedWordAlternatives entry = new TypedWordAlternatives(resultCopy,
   1553                 new WordComposer(mWord));
   1554         mWordHistory.add(entry);
   1555     }
   1556 
   1557     private void postUpdateSuggestions() {
   1558         mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS);
   1559         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100);
   1560     }
   1561 
   1562     private void postUpdateOldSuggestions() {
   1563         mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
   1564         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), 300);
   1565     }
   1566 
   1567     private boolean isPredictionOn() {
   1568         return mPredictionOn;
   1569     }
   1570 
   1571     private boolean isCandidateStripVisible() {
   1572         return isPredictionOn() && mShowSuggestions;
   1573     }
   1574 
   1575     public void onCancelVoice() {
   1576         if (mRecognizing) {
   1577             switchToKeyboardView();
   1578         }
   1579     }
   1580 
   1581     private void switchToKeyboardView() {
   1582       mHandler.post(new Runnable() {
   1583           public void run() {
   1584               mRecognizing = false;
   1585               if (mKeyboardSwitcher.getInputView() != null) {
   1586                 setInputView(mKeyboardSwitcher.getInputView());
   1587               }
   1588               setCandidatesViewShown(true);
   1589               updateInputViewShown();
   1590               postUpdateSuggestions();
   1591           }});
   1592     }
   1593 
   1594     private void switchToRecognitionStatusView() {
   1595         final boolean configChanged = mConfigurationChanging;
   1596         mHandler.post(new Runnable() {
   1597             public void run() {
   1598                 setCandidatesViewShown(false);
   1599                 mRecognizing = true;
   1600                 View v = mVoiceInput.getView();
   1601                 ViewParent p = v.getParent();
   1602                 if (p != null && p instanceof ViewGroup) {
   1603                     ((ViewGroup)v.getParent()).removeView(v);
   1604                 }
   1605                 setInputView(v);
   1606                 updateInputViewShown();
   1607                 if (configChanged) {
   1608                     mVoiceInput.onConfigurationChanged();
   1609                 }
   1610         }});
   1611     }
   1612 
   1613     private void startListening(boolean swipe) {
   1614         if (!mHasUsedVoiceInput ||
   1615                 (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale)) {
   1616             // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel.
   1617             showVoiceWarningDialog(swipe);
   1618         } else {
   1619             reallyStartListening(swipe);
   1620         }
   1621     }
   1622 
   1623     private void reallyStartListening(boolean swipe) {
   1624         if (!mHasUsedVoiceInput) {
   1625             // The user has started a voice input, so remember that in the
   1626             // future (so we don't show the warning dialog after the first run).
   1627             SharedPreferences.Editor editor =
   1628                     PreferenceManager.getDefaultSharedPreferences(this).edit();
   1629             editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true);
   1630             SharedPreferencesCompat.apply(editor);
   1631             mHasUsedVoiceInput = true;
   1632         }
   1633 
   1634         if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) {
   1635             // The user has started a voice input from an unsupported locale, so remember that
   1636             // in the future (so we don't show the warning dialog the next time they do this).
   1637             SharedPreferences.Editor editor =
   1638                     PreferenceManager.getDefaultSharedPreferences(this).edit();
   1639             editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true);
   1640             SharedPreferencesCompat.apply(editor);
   1641             mHasUsedVoiceInputUnsupportedLocale = true;
   1642         }
   1643 
   1644         // Clear N-best suggestions
   1645         clearSuggestions();
   1646 
   1647         FieldContext context = new FieldContext(
   1648             getCurrentInputConnection(),
   1649             getCurrentInputEditorInfo(),
   1650             mLanguageSwitcher.getInputLanguage(),
   1651             mLanguageSwitcher.getEnabledLanguages());
   1652         mVoiceInput.startListening(context, swipe);
   1653         switchToRecognitionStatusView();
   1654     }
   1655 
   1656     private void showVoiceWarningDialog(final boolean swipe) {
   1657         AlertDialog.Builder builder = new AlertDialog.Builder(this);
   1658         builder.setCancelable(true);
   1659         builder.setIcon(R.drawable.ic_mic_dialog);
   1660         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
   1661             public void onClick(DialogInterface dialog, int whichButton) {
   1662                 mVoiceInput.logKeyboardWarningDialogOk();
   1663                 reallyStartListening(swipe);
   1664             }
   1665         });
   1666         builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
   1667             public void onClick(DialogInterface dialog, int whichButton) {
   1668                 mVoiceInput.logKeyboardWarningDialogCancel();
   1669             }
   1670         });
   1671 
   1672         if (mLocaleSupportedForVoiceInput) {
   1673             String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" +
   1674                     getString(R.string.voice_warning_how_to_turn_off);
   1675             builder.setMessage(message);
   1676         } else {
   1677             String message = getString(R.string.voice_warning_locale_not_supported) + "\n\n" +
   1678                     getString(R.string.voice_warning_may_not_understand) + "\n\n" +
   1679                     getString(R.string.voice_warning_how_to_turn_off);
   1680             builder.setMessage(message);
   1681         }
   1682 
   1683         builder.setTitle(R.string.voice_warning_title);
   1684         mVoiceWarningDialog = builder.create();
   1685 
   1686         Window window = mVoiceWarningDialog.getWindow();
   1687         WindowManager.LayoutParams lp = window.getAttributes();
   1688         lp.token = mKeyboardSwitcher.getInputView().getWindowToken();
   1689         lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
   1690         window.setAttributes(lp);
   1691         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
   1692         mVoiceInput.logKeyboardWarningDialogShown();
   1693         mVoiceWarningDialog.show();
   1694     }
   1695 
   1696     public void onVoiceResults(List<String> candidates,
   1697             Map<String, List<CharSequence>> alternatives) {
   1698         if (!mRecognizing) {
   1699             return;
   1700         }
   1701         mVoiceResults.candidates = candidates;
   1702         mVoiceResults.alternatives = alternatives;
   1703         mHandler.sendMessage(mHandler.obtainMessage(MSG_VOICE_RESULTS));
   1704     }
   1705 
   1706     private void handleVoiceResults() {
   1707         mAfterVoiceInput = true;
   1708         mImmediatelyAfterVoiceInput = true;
   1709 
   1710         InputConnection ic = getCurrentInputConnection();
   1711         if (!isFullscreenMode()) {
   1712             // Start listening for updates to the text from typing, etc.
   1713             if (ic != null) {
   1714                 ExtractedTextRequest req = new ExtractedTextRequest();
   1715                 ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR);
   1716             }
   1717         }
   1718 
   1719         vibrate();
   1720         switchToKeyboardView();
   1721 
   1722         final List<CharSequence> nBest = new ArrayList<CharSequence>();
   1723         boolean capitalizeFirstWord = preferCapitalization()
   1724                 || (mKeyboardSwitcher.isAlphabetMode()
   1725                         && mKeyboardSwitcher.getInputView().isShifted());
   1726         for (String c : mVoiceResults.candidates) {
   1727             if (capitalizeFirstWord) {
   1728                 c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length());
   1729             }
   1730             nBest.add(c);
   1731         }
   1732 
   1733         if (nBest.size() == 0) {
   1734             return;
   1735         }
   1736 
   1737         String bestResult = nBest.get(0).toString();
   1738 
   1739         mVoiceInput.logVoiceInputDelivered(bestResult.length());
   1740 
   1741         mHints.registerVoiceResult(bestResult);
   1742 
   1743         if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text
   1744 
   1745         commitTyped(ic);
   1746         EditingUtil.appendText(ic, bestResult);
   1747 
   1748         if (ic != null) ic.endBatchEdit();
   1749 
   1750         mVoiceInputHighlighted = true;
   1751         mWordToSuggestions.putAll(mVoiceResults.alternatives);
   1752     }
   1753 
   1754     private void clearSuggestions() {
   1755         setSuggestions(null, false, false, false);
   1756     }
   1757 
   1758     private void setSuggestions(
   1759             List<CharSequence> suggestions,
   1760             boolean completions,
   1761             boolean typedWordValid,
   1762             boolean haveMinimalSuggestion) {
   1763 
   1764         if (mIsShowingHint) {
   1765              setCandidatesView(mCandidateViewContainer);
   1766              mIsShowingHint = false;
   1767         }
   1768 
   1769         if (mCandidateView != null) {
   1770             mCandidateView.setSuggestions(
   1771                     suggestions, completions, typedWordValid, haveMinimalSuggestion);
   1772         }
   1773     }
   1774 
   1775     private void updateSuggestions() {
   1776         LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
   1777         ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null);
   1778 
   1779         // Check if we have a suggestion engine attached.
   1780         if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) {
   1781             return;
   1782         }
   1783 
   1784         if (!mPredicting) {
   1785             setNextSuggestions();
   1786             return;
   1787         }
   1788         showSuggestions(mWord);
   1789     }
   1790 
   1791     private List<CharSequence> getTypedSuggestions(WordComposer word) {
   1792         List<CharSequence> stringList = mSuggest.getSuggestions(
   1793                 mKeyboardSwitcher.getInputView(), word, false, null);
   1794         return stringList;
   1795     }
   1796 
   1797     private void showCorrections(WordAlternatives alternatives) {
   1798         List<CharSequence> stringList = alternatives.getAlternatives();
   1799         ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters(null);
   1800         showSuggestions(stringList, alternatives.getOriginalWord(), false, false);
   1801     }
   1802 
   1803     private void showSuggestions(WordComposer word) {
   1804         // long startTime = System.currentTimeMillis(); // TIME MEASUREMENT!
   1805         // TODO Maybe need better way of retrieving previous word
   1806         CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(),
   1807                 mWordSeparators);
   1808         List<CharSequence> stringList = mSuggest.getSuggestions(
   1809                 mKeyboardSwitcher.getInputView(), word, false, prevWord);
   1810         // long stopTime = System.currentTimeMillis(); // TIME MEASUREMENT!
   1811         // Log.d("LatinIME","Suggest Total Time - " + (stopTime - startTime));
   1812 
   1813         int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies();
   1814 
   1815         ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters(
   1816                 nextLettersFrequencies);
   1817 
   1818         boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasMinimalCorrection();
   1819         //|| mCorrectionMode == mSuggest.CORRECTION_FULL;
   1820         CharSequence typedWord = word.getTypedWord();
   1821         // If we're in basic correct
   1822         boolean typedWordValid = mSuggest.isValidWord(typedWord) ||
   1823                 (preferCapitalization()
   1824                         && mSuggest.isValidWord(typedWord.toString().toLowerCase()));
   1825         if (mCorrectionMode == Suggest.CORRECTION_FULL
   1826                 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
   1827             correctionAvailable |= typedWordValid;
   1828         }
   1829         // Don't auto-correct words with multiple capital letter
   1830         correctionAvailable &= !word.isMostlyCaps();
   1831         correctionAvailable &= !TextEntryState.isCorrecting();
   1832 
   1833         showSuggestions(stringList, typedWord, typedWordValid, correctionAvailable);
   1834     }
   1835 
   1836     private void showSuggestions(List<CharSequence> stringList, CharSequence typedWord,
   1837             boolean typedWordValid, boolean correctionAvailable) {
   1838         setSuggestions(stringList, false, typedWordValid, correctionAvailable);
   1839         if (stringList.size() > 0) {
   1840             if (correctionAvailable && !typedWordValid && stringList.size() > 1) {
   1841                 mBestWord = stringList.get(1);
   1842             } else {
   1843                 mBestWord = typedWord;
   1844             }
   1845         } else {
   1846             mBestWord = null;
   1847         }
   1848         setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn);
   1849     }
   1850 
   1851     private boolean pickDefaultSuggestion() {
   1852         // Complete any pending candidate query first
   1853         if (mHandler.hasMessages(MSG_UPDATE_SUGGESTIONS)) {
   1854             mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS);
   1855             updateSuggestions();
   1856         }
   1857         if (mBestWord != null && mBestWord.length() > 0) {
   1858             TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord);
   1859             mJustAccepted = true;
   1860             pickSuggestion(mBestWord, false);
   1861             // Add the word to the auto dictionary if it's not a known word
   1862             addToDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED);
   1863             return true;
   1864 
   1865         }
   1866         return false;
   1867     }
   1868 
   1869     public void pickSuggestionManually(int index, CharSequence suggestion) {
   1870         List<CharSequence> suggestions = mCandidateView.getSuggestions();
   1871 
   1872         if (mAfterVoiceInput && mShowingVoiceSuggestions) {
   1873             mVoiceInput.flushAllTextModificationCounters();
   1874             // send this intent AFTER logging any prior aggregated edits.
   1875             mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.toString(), index,
   1876                                                           mWordSeparators,
   1877                                                           getCurrentInputConnection());
   1878         }
   1879 
   1880         final boolean correcting = TextEntryState.isCorrecting();
   1881         InputConnection ic = getCurrentInputConnection();
   1882         if (ic != null) {
   1883             ic.beginBatchEdit();
   1884         }
   1885         if (mCompletionOn && mCompletions != null && index >= 0
   1886                 && index < mCompletions.length) {
   1887             CompletionInfo ci = mCompletions[index];
   1888             if (ic != null) {
   1889                 ic.commitCompletion(ci);
   1890             }
   1891             mCommittedLength = suggestion.length();
   1892             if (mCandidateView != null) {
   1893                 mCandidateView.clear();
   1894             }
   1895             updateShiftKeyState(getCurrentInputEditorInfo());
   1896             if (ic != null) {
   1897                 ic.endBatchEdit();
   1898             }
   1899             return;
   1900         }
   1901 
   1902         // If this is a punctuation, apply it through the normal key press
   1903         if (suggestion.length() == 1 && (isWordSeparator(suggestion.charAt(0))
   1904                 || isSuggestedPunctuation(suggestion.charAt(0)))) {
   1905             // Word separators are suggested before the user inputs something.
   1906             // So, LatinImeLogger logs "" as a user's input.
   1907             LatinImeLogger.logOnManualSuggestion(
   1908                     "", suggestion.toString(), index, suggestions);
   1909             final char primaryCode = suggestion.charAt(0);
   1910             onKey(primaryCode, new int[]{primaryCode}, LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE,
   1911                     LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE);
   1912             if (ic != null) {
   1913                 ic.endBatchEdit();
   1914             }
   1915             return;
   1916         }
   1917         mJustAccepted = true;
   1918         pickSuggestion(suggestion, correcting);
   1919         // Add the word to the auto dictionary if it's not a known word
   1920         if (index == 0) {
   1921             addToDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED);
   1922         } else {
   1923             addToBigramDictionary(suggestion, 1);
   1924         }
   1925         LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(),
   1926                 index, suggestions);
   1927         TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion);
   1928         // Follow it with a space
   1929         if (mAutoSpace && !correcting) {
   1930             sendSpace();
   1931             mJustAddedAutoSpace = true;
   1932         }
   1933 
   1934         final boolean showingAddToDictionaryHint = index == 0 && mCorrectionMode > 0
   1935                 && !mSuggest.isValidWord(suggestion)
   1936                 && !mSuggest.isValidWord(suggestion.toString().toLowerCase());
   1937 
   1938         if (!correcting) {
   1939             // Fool the state watcher so that a subsequent backspace will not do a revert, unless
   1940             // we just did a correction, in which case we need to stay in
   1941             // TextEntryState.State.PICKED_SUGGESTION state.
   1942             TextEntryState.typedCharacter((char) KEYCODE_SPACE, true);
   1943             setNextSuggestions();
   1944         } else if (!showingAddToDictionaryHint) {
   1945             // If we're not showing the "Touch again to save", then show corrections again.
   1946             // In case the cursor position doesn't change, make sure we show the suggestions again.
   1947             clearSuggestions();
   1948             postUpdateOldSuggestions();
   1949         }
   1950         if (showingAddToDictionaryHint) {
   1951             mCandidateView.showAddToDictionaryHint(suggestion);
   1952         }
   1953         if (ic != null) {
   1954             ic.endBatchEdit();
   1955         }
   1956     }
   1957 
   1958     private void rememberReplacedWord(CharSequence suggestion) {
   1959         if (mShowingVoiceSuggestions) {
   1960             // Retain the replaced word in the alternatives array.
   1961             EditingUtil.Range range = new EditingUtil.Range();
   1962             String wordToBeReplaced = EditingUtil.getWordAtCursor(getCurrentInputConnection(),
   1963                     mWordSeparators, range);
   1964             if (!mWordToSuggestions.containsKey(wordToBeReplaced)) {
   1965                 wordToBeReplaced = wordToBeReplaced.toLowerCase();
   1966             }
   1967             if (mWordToSuggestions.containsKey(wordToBeReplaced)) {
   1968                 List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced);
   1969                 if (suggestions.contains(suggestion)) {
   1970                     suggestions.remove(suggestion);
   1971                 }
   1972                 suggestions.add(wordToBeReplaced);
   1973                 mWordToSuggestions.remove(wordToBeReplaced);
   1974                 mWordToSuggestions.put(suggestion.toString(), suggestions);
   1975             }
   1976         }
   1977     }
   1978 
   1979     /**
   1980      * Commits the chosen word to the text field and saves it for later
   1981      * retrieval.
   1982      * @param suggestion the suggestion picked by the user to be committed to
   1983      *            the text field
   1984      * @param correcting whether this is due to a correction of an existing
   1985      *            word.
   1986      */
   1987     private void pickSuggestion(CharSequence suggestion, boolean correcting) {
   1988         final LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
   1989         final Locale inputLocale = mLanguageSwitcher.getInputLocale();
   1990         if (mCapsLock) {
   1991             suggestion = suggestion.toString().toUpperCase(inputLocale);
   1992         } else if (preferCapitalization()
   1993                 || (mKeyboardSwitcher.isAlphabetMode()
   1994                         && inputView.isShifted())) {
   1995             suggestion = suggestion.toString().toUpperCase(inputLocale).charAt(0)
   1996                     + suggestion.subSequence(1, suggestion.length()).toString();
   1997         }
   1998         InputConnection ic = getCurrentInputConnection();
   1999         if (ic != null) {
   2000             rememberReplacedWord(suggestion);
   2001             ic.commitText(suggestion, 1);
   2002         }
   2003         saveWordInHistory(suggestion);
   2004         mPredicting = false;
   2005         mCommittedLength = suggestion.length();
   2006         ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null);
   2007         // If we just corrected a word, then don't show punctuations
   2008         if (!correcting) {
   2009             setNextSuggestions();
   2010         }
   2011         updateShiftKeyState(getCurrentInputEditorInfo());
   2012     }
   2013 
   2014     /**
   2015      * Tries to apply any voice alternatives for the word if this was a spoken word and
   2016      * there are voice alternatives.
   2017      * @param touching The word that the cursor is touching, with position information
   2018      * @return true if an alternative was found, false otherwise.
   2019      */
   2020     private boolean applyVoiceAlternatives(EditingUtil.SelectedWord touching) {
   2021         // Search for result in spoken word alternatives
   2022         String selectedWord = touching.word.toString().trim();
   2023         if (!mWordToSuggestions.containsKey(selectedWord)) {
   2024             selectedWord = selectedWord.toLowerCase();
   2025         }
   2026         if (mWordToSuggestions.containsKey(selectedWord)) {
   2027             mShowingVoiceSuggestions = true;
   2028             List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord);
   2029             // If the first letter of touching is capitalized, make all the suggestions
   2030             // start with a capital letter.
   2031             if (Character.isUpperCase(touching.word.charAt(0))) {
   2032                 final Locale inputLocale = mLanguageSwitcher.getInputLocale();
   2033                 for (int i = 0; i < suggestions.size(); i++) {
   2034                     String origSugg = (String) suggestions.get(i);
   2035                     String capsSugg = origSugg.toUpperCase(inputLocale).charAt(0)
   2036                             + origSugg.subSequence(1, origSugg.length()).toString();
   2037                     suggestions.set(i, capsSugg);
   2038                 }
   2039             }
   2040             setSuggestions(suggestions, false, true, true);
   2041             setCandidatesViewShown(true);
   2042             return true;
   2043         }
   2044         return false;
   2045     }
   2046 
   2047     /**
   2048      * Tries to apply any typed alternatives for the word if we have any cached alternatives,
   2049      * otherwise tries to find new corrections and completions for the word.
   2050      * @param touching The word that the cursor is touching, with position information
   2051      * @return true if an alternative was found, false otherwise.
   2052      */
   2053     private boolean applyTypedAlternatives(EditingUtil.SelectedWord touching) {
   2054         // If we didn't find a match, search for result in typed word history
   2055         WordComposer foundWord = null;
   2056         WordAlternatives alternatives = null;
   2057         for (WordAlternatives entry : mWordHistory) {
   2058             if (TextUtils.equals(entry.getChosenWord(), touching.word)) {
   2059                 if (entry instanceof TypedWordAlternatives) {
   2060                     foundWord = ((TypedWordAlternatives) entry).word;
   2061                 }
   2062                 alternatives = entry;
   2063                 break;
   2064             }
   2065         }
   2066         // If we didn't find a match, at least suggest completions
   2067         if (foundWord == null
   2068                 && (mSuggest.isValidWord(touching.word)
   2069                         || mSuggest.isValidWord(touching.word.toString().toLowerCase()))) {
   2070             foundWord = new WordComposer();
   2071             for (int i = 0; i < touching.word.length(); i++) {
   2072                 foundWord.add(touching.word.charAt(i), new int[] {
   2073                     touching.word.charAt(i)
   2074                 });
   2075             }
   2076             foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.word.charAt(0)));
   2077         }
   2078         // Found a match, show suggestions
   2079         if (foundWord != null || alternatives != null) {
   2080             if (alternatives == null) {
   2081                 alternatives = new TypedWordAlternatives(touching.word, foundWord);
   2082             }
   2083             showCorrections(alternatives);
   2084             if (foundWord != null) {
   2085                 mWord = new WordComposer(foundWord);
   2086             } else {
   2087                 mWord.reset();
   2088             }
   2089             return true;
   2090         }
   2091         return false;
   2092     }
   2093 
   2094     private void setOldSuggestions() {
   2095         mShowingVoiceSuggestions = false;
   2096         if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) {
   2097             return;
   2098         }
   2099         InputConnection ic = getCurrentInputConnection();
   2100         if (ic == null) return;
   2101         if (!mPredicting) {
   2102             // Extract the selected or touching text
   2103             EditingUtil.SelectedWord touching = EditingUtil.getWordAtCursorOrSelection(ic,
   2104                     mLastSelectionStart, mLastSelectionEnd, mWordSeparators);
   2105 
   2106             if (touching != null && touching.word.length() > 1) {
   2107                 ic.beginBatchEdit();
   2108 
   2109                 if (!applyVoiceAlternatives(touching) && !applyTypedAlternatives(touching)) {
   2110                     abortCorrection(true);
   2111                 } else {
   2112                     TextEntryState.selectedForCorrection();
   2113                     EditingUtil.underlineWord(ic, touching);
   2114                 }
   2115 
   2116                 ic.endBatchEdit();
   2117             } else {
   2118                 abortCorrection(true);
   2119                 setNextSuggestions();  // Show the punctuation suggestions list
   2120             }
   2121         } else {
   2122             abortCorrection(true);
   2123         }
   2124     }
   2125 
   2126     private void setNextSuggestions() {
   2127         setSuggestions(mSuggestPuncList, false, false, false);
   2128     }
   2129 
   2130     private void addToDictionaries(CharSequence suggestion, int frequencyDelta) {
   2131         checkAddToDictionary(suggestion, frequencyDelta, false);
   2132     }
   2133 
   2134     private void addToBigramDictionary(CharSequence suggestion, int frequencyDelta) {
   2135         checkAddToDictionary(suggestion, frequencyDelta, true);
   2136     }
   2137 
   2138     /**
   2139      * Adds to the UserBigramDictionary and/or AutoDictionary
   2140      * @param addToBigramDictionary true if it should be added to bigram dictionary if possible
   2141      */
   2142     private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta,
   2143             boolean addToBigramDictionary) {
   2144         if (suggestion == null || suggestion.length() < 1) return;
   2145         // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
   2146         // adding words in situations where the user or application really didn't
   2147         // want corrections enabled or learned.
   2148         if (!(mCorrectionMode == Suggest.CORRECTION_FULL
   2149                 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
   2150             return;
   2151         }
   2152         if (suggestion != null) {
   2153             if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion)
   2154                     || (!mSuggest.isValidWord(suggestion.toString())
   2155                     && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) {
   2156                 mAutoDictionary.addWord(suggestion.toString(), frequencyDelta);
   2157             }
   2158 
   2159             if (mUserBigramDictionary != null) {
   2160                 CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(),
   2161                         mSentenceSeparators);
   2162                 if (!TextUtils.isEmpty(prevWord)) {
   2163                     mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString());
   2164                 }
   2165             }
   2166         }
   2167     }
   2168 
   2169     private boolean isCursorTouchingWord() {
   2170         InputConnection ic = getCurrentInputConnection();
   2171         if (ic == null) return false;
   2172         CharSequence toLeft = ic.getTextBeforeCursor(1, 0);
   2173         CharSequence toRight = ic.getTextAfterCursor(1, 0);
   2174         if (!TextUtils.isEmpty(toLeft)
   2175                 && !isWordSeparator(toLeft.charAt(0))
   2176                 && !isSuggestedPunctuation(toLeft.charAt(0))) {
   2177             return true;
   2178         }
   2179         if (!TextUtils.isEmpty(toRight)
   2180                 && !isWordSeparator(toRight.charAt(0))
   2181                 && !isSuggestedPunctuation(toRight.charAt(0))) {
   2182             return true;
   2183         }
   2184         return false;
   2185     }
   2186 
   2187     private boolean sameAsTextBeforeCursor(InputConnection ic, CharSequence text) {
   2188         CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
   2189         return TextUtils.equals(text, beforeText);
   2190     }
   2191 
   2192     public void revertLastWord(boolean deleteChar) {
   2193         final int length = mComposing.length();
   2194         if (!mPredicting && length > 0) {
   2195             final InputConnection ic = getCurrentInputConnection();
   2196             mPredicting = true;
   2197             mJustRevertedSeparator = ic.getTextBeforeCursor(1, 0);
   2198             if (deleteChar) ic.deleteSurroundingText(1, 0);
   2199             int toDelete = mCommittedLength;
   2200             CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0);
   2201             if (toTheLeft != null && toTheLeft.length() > 0
   2202                     && isWordSeparator(toTheLeft.charAt(0))) {
   2203                 toDelete--;
   2204             }
   2205             ic.deleteSurroundingText(toDelete, 0);
   2206             ic.setComposingText(mComposing, 1);
   2207             TextEntryState.backspace();
   2208             postUpdateSuggestions();
   2209         } else {
   2210             sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
   2211             mJustRevertedSeparator = null;
   2212         }
   2213     }
   2214 
   2215     protected String getWordSeparators() {
   2216         return mWordSeparators;
   2217     }
   2218 
   2219     public boolean isWordSeparator(int code) {
   2220         String separators = getWordSeparators();
   2221         return separators.contains(String.valueOf((char)code));
   2222     }
   2223 
   2224     private boolean isSentenceSeparator(int code) {
   2225         return mSentenceSeparators.contains(String.valueOf((char)code));
   2226     }
   2227 
   2228     private void sendSpace() {
   2229         sendKeyChar((char)KEYCODE_SPACE);
   2230         updateShiftKeyState(getCurrentInputEditorInfo());
   2231         //onKey(KEY_SPACE[0], KEY_SPACE);
   2232     }
   2233 
   2234     public boolean preferCapitalization() {
   2235         return mWord.isFirstCharCapitalized();
   2236     }
   2237 
   2238     private void toggleLanguage(boolean reset, boolean next) {
   2239         if (reset) {
   2240             mLanguageSwitcher.reset();
   2241         } else {
   2242             if (next) {
   2243                 mLanguageSwitcher.next();
   2244             } else {
   2245                 mLanguageSwitcher.prev();
   2246             }
   2247         }
   2248         int currentKeyboardMode = mKeyboardSwitcher.getKeyboardMode();
   2249         reloadKeyboards();
   2250         mKeyboardSwitcher.makeKeyboards(true);
   2251         mKeyboardSwitcher.setKeyboardMode(currentKeyboardMode, 0,
   2252                 mEnableVoiceButton && mEnableVoice);
   2253         initSuggest(mLanguageSwitcher.getInputLanguage());
   2254         mLanguageSwitcher.persist();
   2255         updateShiftKeyState(getCurrentInputEditorInfo());
   2256     }
   2257 
   2258     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
   2259             String key) {
   2260         if (PREF_SELECTED_LANGUAGES.equals(key)) {
   2261             mLanguageSwitcher.loadLocales(sharedPreferences);
   2262             mRefreshKeyboardRequired = true;
   2263         } else if (PREF_RECORRECTION_ENABLED.equals(key)) {
   2264             mReCorrectionEnabled = sharedPreferences.getBoolean(PREF_RECORRECTION_ENABLED,
   2265                     getResources().getBoolean(R.bool.default_recorrection_enabled));
   2266         }
   2267     }
   2268 
   2269     public void swipeRight() {
   2270         if (LatinKeyboardView.DEBUG_AUTO_PLAY) {
   2271             ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE));
   2272             CharSequence text = cm.getText();
   2273             if (!TextUtils.isEmpty(text)) {
   2274                 mKeyboardSwitcher.getInputView().startPlaying(text.toString());
   2275             }
   2276         }
   2277     }
   2278 
   2279     public void swipeLeft() {
   2280     }
   2281 
   2282     public void swipeDown() {
   2283         handleClose();
   2284     }
   2285 
   2286     public void swipeUp() {
   2287         //launchSettings();
   2288     }
   2289 
   2290     public void onPress(int primaryCode) {
   2291         if (mKeyboardSwitcher.isVibrateAndSoundFeedbackRequired()) {
   2292             vibrate();
   2293             playKeyClick(primaryCode);
   2294         }
   2295         final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch();
   2296         if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_SHIFT) {
   2297             mShiftKeyState.onPress();
   2298             handleShift();
   2299         } else if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_MODE_CHANGE) {
   2300             changeKeyboardMode();
   2301             mSymbolKeyState.onPress();
   2302             mKeyboardSwitcher.setAutoModeSwitchStateMomentary();
   2303         } else {
   2304             mShiftKeyState.onOtherKeyPressed();
   2305             mSymbolKeyState.onOtherKeyPressed();
   2306         }
   2307     }
   2308 
   2309     public void onRelease(int primaryCode) {
   2310         // Reset any drag flags in the keyboard
   2311         ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).keyReleased();
   2312         //vibrate();
   2313         final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch();
   2314         if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_SHIFT) {
   2315             if (mShiftKeyState.isMomentary())
   2316                 resetShift();
   2317             mShiftKeyState.onRelease();
   2318         } else if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_MODE_CHANGE) {
   2319             // Snap back to the previous keyboard mode if the user chords the mode change key and
   2320             // other key, then released the mode change key.
   2321             if (mKeyboardSwitcher.isInChordingAutoModeSwitchState())
   2322                 changeKeyboardMode();
   2323             mSymbolKeyState.onRelease();
   2324         }
   2325     }
   2326 
   2327     private FieldContext makeFieldContext() {
   2328         return new FieldContext(
   2329                 getCurrentInputConnection(),
   2330                 getCurrentInputEditorInfo(),
   2331                 mLanguageSwitcher.getInputLanguage(),
   2332                 mLanguageSwitcher.getEnabledLanguages());
   2333     }
   2334 
   2335     private boolean fieldCanDoVoice(FieldContext fieldContext) {
   2336         return !mPasswordText
   2337                 && mVoiceInput != null
   2338                 && !mVoiceInput.isBlacklistedField(fieldContext);
   2339     }
   2340 
   2341     private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) {
   2342         return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext)
   2343                 && !(attribute != null
   2344                         && IME_OPTION_NO_MICROPHONE.equals(attribute.privateImeOptions))
   2345                 && SpeechRecognizer.isRecognitionAvailable(this);
   2346     }
   2347 
   2348     // receive ringer mode changes to detect silent mode
   2349     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
   2350         @Override
   2351         public void onReceive(Context context, Intent intent) {
   2352             updateRingerMode();
   2353         }
   2354     };
   2355 
   2356     // update flags for silent mode
   2357     private void updateRingerMode() {
   2358         if (mAudioManager == null) {
   2359             mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
   2360         }
   2361         if (mAudioManager != null) {
   2362             mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
   2363         }
   2364     }
   2365 
   2366     private void playKeyClick(int primaryCode) {
   2367         // if mAudioManager is null, we don't have the ringer state yet
   2368         // mAudioManager will be set by updateRingerMode
   2369         if (mAudioManager == null) {
   2370             if (mKeyboardSwitcher.getInputView() != null) {
   2371                 updateRingerMode();
   2372             }
   2373         }
   2374         if (mSoundOn && !mSilentMode) {
   2375             // FIXME: Volume and enable should come from UI settings
   2376             // FIXME: These should be triggered after auto-repeat logic
   2377             int sound = AudioManager.FX_KEYPRESS_STANDARD;
   2378             switch (primaryCode) {
   2379                 case Keyboard.KEYCODE_DELETE:
   2380                     sound = AudioManager.FX_KEYPRESS_DELETE;
   2381                     break;
   2382                 case KEYCODE_ENTER:
   2383                     sound = AudioManager.FX_KEYPRESS_RETURN;
   2384                     break;
   2385                 case KEYCODE_SPACE:
   2386                     sound = AudioManager.FX_KEYPRESS_SPACEBAR;
   2387                     break;
   2388             }
   2389             mAudioManager.playSoundEffect(sound, FX_VOLUME);
   2390         }
   2391     }
   2392 
   2393     private void vibrate() {
   2394         if (!mVibrateOn) {
   2395             return;
   2396         }
   2397         if (mKeyboardSwitcher.getInputView() != null) {
   2398             mKeyboardSwitcher.getInputView().performHapticFeedback(
   2399                     HapticFeedbackConstants.KEYBOARD_TAP,
   2400                     HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
   2401         }
   2402     }
   2403 
   2404     private void checkTutorial(String privateImeOptions) {
   2405         if (privateImeOptions == null) return;
   2406         if (privateImeOptions.equals("com.android.setupwizard:ShowTutorial")) {
   2407             if (mTutorial == null) startTutorial();
   2408         } else if (privateImeOptions.equals("com.android.setupwizard:HideTutorial")) {
   2409             if (mTutorial != null) {
   2410                 if (mTutorial.close()) {
   2411                     mTutorial = null;
   2412                 }
   2413             }
   2414         }
   2415     }
   2416 
   2417     private void startTutorial() {
   2418         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TUTORIAL), 500);
   2419     }
   2420 
   2421     /* package */ void tutorialDone() {
   2422         mTutorial = null;
   2423     }
   2424 
   2425     /* package */ void promoteToUserDictionary(String word, int frequency) {
   2426         if (mUserDictionary.isValidWord(word)) return;
   2427         mUserDictionary.addWord(word, frequency);
   2428     }
   2429 
   2430     /* package */ WordComposer getCurrentWord() {
   2431         return mWord;
   2432     }
   2433 
   2434     /* package */ boolean getPopupOn() {
   2435         return mPopupOn;
   2436     }
   2437 
   2438     private void updateCorrectionMode() {
   2439         mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false;
   2440         mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes)
   2441                 && !mInputTypeNoAutoCorrect && mHasDictionary;
   2442         mCorrectionMode = (mAutoCorrectOn && mAutoCorrectEnabled)
   2443                 ? Suggest.CORRECTION_FULL
   2444                 : (mAutoCorrectOn ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE);
   2445         mCorrectionMode = (mBigramSuggestionEnabled && mAutoCorrectOn && mAutoCorrectEnabled)
   2446                 ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
   2447         if (mSuggest != null) {
   2448             mSuggest.setCorrectionMode(mCorrectionMode);
   2449         }
   2450     }
   2451 
   2452     private void updateAutoTextEnabled(Locale systemLocale) {
   2453         if (mSuggest == null) return;
   2454         boolean different =
   2455                 !systemLocale.getLanguage().equalsIgnoreCase(mInputLocale.substring(0, 2));
   2456         mSuggest.setAutoTextEnabled(!different && mQuickFixes);
   2457     }
   2458 
   2459     protected void launchSettings() {
   2460         launchSettings(LatinIMESettings.class);
   2461     }
   2462 
   2463     public void launchDebugSettings() {
   2464         launchSettings(LatinIMEDebugSettings.class);
   2465     }
   2466 
   2467     protected void launchSettings (Class<? extends PreferenceActivity> settingsClass) {
   2468         handleClose();
   2469         Intent intent = new Intent();
   2470         intent.setClass(LatinIME.this, settingsClass);
   2471         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   2472         startActivity(intent);
   2473     }
   2474 
   2475     private void loadSettings() {
   2476         // Get the settings preferences
   2477         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
   2478         mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, false);
   2479         mSoundOn = sp.getBoolean(PREF_SOUND_ON, false);
   2480         mPopupOn = sp.getBoolean(PREF_POPUP_ON,
   2481                 mResources.getBoolean(R.bool.default_popup_preview));
   2482         mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true);
   2483         mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true);
   2484         mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false);
   2485         mHasUsedVoiceInputUnsupportedLocale =
   2486                 sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false);
   2487 
   2488         // Get the current list of supported locales and check the current locale against that
   2489         // list. We cache this value so as not to check it every time the user starts a voice
   2490         // input. Because this method is called by onStartInputView, this should mean that as
   2491         // long as the locale doesn't change while the user is keeping the IME open, the
   2492         // value should never be stale.
   2493         String supportedLocalesString = SettingsUtil.getSettingsString(
   2494                 getContentResolver(),
   2495                 SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES,
   2496                 DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES);
   2497         ArrayList<String> voiceInputSupportedLocales =
   2498                 newArrayList(supportedLocalesString.split("\\s+"));
   2499 
   2500         mLocaleSupportedForVoiceInput = voiceInputSupportedLocales.contains(mInputLocale);
   2501 
   2502         mShowSuggestions = sp.getBoolean(PREF_SHOW_SUGGESTIONS, true);
   2503 
   2504         if (VOICE_INSTALLED) {
   2505             final String voiceMode = sp.getString(PREF_VOICE_MODE,
   2506                     getString(R.string.voice_mode_main));
   2507             boolean enableVoice = !voiceMode.equals(getString(R.string.voice_mode_off))
   2508                     && mEnableVoiceButton;
   2509             boolean voiceOnPrimary = voiceMode.equals(getString(R.string.voice_mode_main));
   2510             if (mKeyboardSwitcher != null &&
   2511                     (enableVoice != mEnableVoice || voiceOnPrimary != mVoiceOnPrimary)) {
   2512                 mKeyboardSwitcher.setVoiceMode(enableVoice, voiceOnPrimary);
   2513             }
   2514             mEnableVoice = enableVoice;
   2515             mVoiceOnPrimary = voiceOnPrimary;
   2516         }
   2517         mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE,
   2518                 mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions;
   2519         //mBigramSuggestionEnabled = sp.getBoolean(
   2520         //        PREF_BIGRAM_SUGGESTIONS, true) & mShowSuggestions;
   2521         updateCorrectionMode();
   2522         updateAutoTextEnabled(mResources.getConfiguration().locale);
   2523         mLanguageSwitcher.loadLocales(sp);
   2524     }
   2525 
   2526     private void initSuggestPuncList() {
   2527         mSuggestPuncList = new ArrayList<CharSequence>();
   2528         mSuggestPuncs = mResources.getString(R.string.suggested_punctuations);
   2529         if (mSuggestPuncs != null) {
   2530             for (int i = 0; i < mSuggestPuncs.length(); i++) {
   2531                 mSuggestPuncList.add(mSuggestPuncs.subSequence(i, i + 1));
   2532             }
   2533         }
   2534     }
   2535 
   2536     private boolean isSuggestedPunctuation(int code) {
   2537         return mSuggestPuncs.contains(String.valueOf((char)code));
   2538     }
   2539 
   2540     private void showOptionsMenu() {
   2541         AlertDialog.Builder builder = new AlertDialog.Builder(this);
   2542         builder.setCancelable(true);
   2543         builder.setIcon(R.drawable.ic_dialog_keyboard);
   2544         builder.setNegativeButton(android.R.string.cancel, null);
   2545         CharSequence itemSettings = getString(R.string.english_ime_settings);
   2546         CharSequence itemInputMethod = getString(R.string.selectInputMethod);
   2547         builder.setItems(new CharSequence[] {
   2548                 itemInputMethod, itemSettings},
   2549                 new DialogInterface.OnClickListener() {
   2550 
   2551             public void onClick(DialogInterface di, int position) {
   2552                 di.dismiss();
   2553                 switch (position) {
   2554                     case POS_SETTINGS:
   2555                         launchSettings();
   2556                         break;
   2557                     case POS_METHOD:
   2558                         ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE))
   2559                             .showInputMethodPicker();
   2560                         break;
   2561                 }
   2562             }
   2563         });
   2564         builder.setTitle(mResources.getString(R.string.english_ime_input_options));
   2565         mOptionsDialog = builder.create();
   2566         Window window = mOptionsDialog.getWindow();
   2567         WindowManager.LayoutParams lp = window.getAttributes();
   2568         lp.token = mKeyboardSwitcher.getInputView().getWindowToken();
   2569         lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
   2570         window.setAttributes(lp);
   2571         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
   2572         mOptionsDialog.show();
   2573     }
   2574 
   2575     public void changeKeyboardMode() {
   2576         mKeyboardSwitcher.toggleSymbols();
   2577         if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) {
   2578             mKeyboardSwitcher.setShiftLocked(mCapsLock);
   2579         }
   2580 
   2581         updateShiftKeyState(getCurrentInputEditorInfo());
   2582     }
   2583 
   2584     public static <E> ArrayList<E> newArrayList(E... elements) {
   2585         int capacity = (elements.length * 110) / 100 + 5;
   2586         ArrayList<E> list = new ArrayList<E>(capacity);
   2587         Collections.addAll(list, elements);
   2588         return list;
   2589     }
   2590 
   2591     @Override
   2592     protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
   2593         super.dump(fd, fout, args);
   2594 
   2595         final Printer p = new PrintWriterPrinter(fout);
   2596         p.println("LatinIME state :");
   2597         p.println("  Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode());
   2598         p.println("  mCapsLock=" + mCapsLock);
   2599         p.println("  mComposing=" + mComposing.toString());
   2600         p.println("  mPredictionOn=" + mPredictionOn);
   2601         p.println("  mCorrectionMode=" + mCorrectionMode);
   2602         p.println("  mPredicting=" + mPredicting);
   2603         p.println("  mAutoCorrectOn=" + mAutoCorrectOn);
   2604         p.println("  mAutoSpace=" + mAutoSpace);
   2605         p.println("  mCompletionOn=" + mCompletionOn);
   2606         p.println("  TextEntryState.state=" + TextEntryState.getState());
   2607         p.println("  mSoundOn=" + mSoundOn);
   2608         p.println("  mVibrateOn=" + mVibrateOn);
   2609         p.println("  mPopupOn=" + mPopupOn);
   2610     }
   2611 
   2612     // Characters per second measurement
   2613 
   2614     private long mLastCpsTime;
   2615     private static final int CPS_BUFFER_SIZE = 16;
   2616     private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE];
   2617     private int mCpsIndex;
   2618 
   2619     private void measureCps() {
   2620         long now = System.currentTimeMillis();
   2621         if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial
   2622         mCpsIntervals[mCpsIndex] = now - mLastCpsTime;
   2623         mLastCpsTime = now;
   2624         mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE;
   2625         long total = 0;
   2626         for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i];
   2627         System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total));
   2628     }
   2629 
   2630     public void onAutoCompletionStateChanged(boolean isAutoCompletion) {
   2631         mKeyboardSwitcher.onAutoCompletionStateChanged(isAutoCompletion);
   2632     }
   2633 }
   2634