LatinIME.java revision 00eb3658734078df46b6fe88b8b6aa8b79d327c9
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
17package com.android.inputmethod.latin;
18
19import com.android.inputmethod.keyboard.Keyboard;
20import com.android.inputmethod.keyboard.KeyboardActionListener;
21import com.android.inputmethod.keyboard.KeyboardId;
22import com.android.inputmethod.keyboard.KeyboardSwitcher;
23import com.android.inputmethod.keyboard.KeyboardView;
24import com.android.inputmethod.keyboard.LatinKeyboard;
25import com.android.inputmethod.keyboard.LatinKeyboardView;
26import com.android.inputmethod.latin.Utils.RingCharBuffer;
27import com.android.inputmethod.voice.VoiceIMEConnector;
28
29import android.app.AlertDialog;
30import android.content.BroadcastReceiver;
31import android.content.Context;
32import android.content.DialogInterface;
33import android.content.Intent;
34import android.content.IntentFilter;
35import android.content.SharedPreferences;
36import android.content.res.Configuration;
37import android.content.res.Resources;
38import android.inputmethodservice.InputMethodService;
39import android.media.AudioManager;
40import android.os.Debug;
41import android.os.Handler;
42import android.os.Message;
43import android.os.SystemClock;
44import android.os.Vibrator;
45import android.preference.PreferenceActivity;
46import android.preference.PreferenceManager;
47import android.text.InputType;
48import android.text.TextUtils;
49import android.util.DisplayMetrics;
50import android.util.Log;
51import android.util.PrintWriterPrinter;
52import android.util.Printer;
53import android.view.Gravity;
54import android.view.HapticFeedbackConstants;
55import android.view.KeyEvent;
56import android.view.LayoutInflater;
57import android.view.View;
58import android.view.ViewGroup;
59import android.view.ViewParent;
60import android.view.Window;
61import android.view.WindowManager;
62import android.view.inputmethod.CompletionInfo;
63import android.view.inputmethod.CorrectionInfo;
64import android.view.inputmethod.EditorInfo;
65import android.view.inputmethod.ExtractedText;
66import android.view.inputmethod.ExtractedTextRequest;
67import android.view.inputmethod.InputConnection;
68import android.view.inputmethod.InputMethodManager;
69import android.view.inputmethod.InputMethodSubtype;
70import android.widget.FrameLayout;
71import android.widget.HorizontalScrollView;
72import android.widget.LinearLayout;
73
74import java.io.FileDescriptor;
75import java.io.PrintWriter;
76import java.util.ArrayList;
77import java.util.Arrays;
78import java.util.Locale;
79
80/**
81 * Input method implementation for Qwerty'ish keyboard.
82 */
83public class LatinIME extends InputMethodService implements KeyboardActionListener,
84        SharedPreferences.OnSharedPreferenceChangeListener {
85    private static final String TAG = "LatinIME";
86    private static final boolean PERF_DEBUG = false;
87    private static final boolean TRACE = false;
88    private static boolean DEBUG = LatinImeLogger.sDBG;
89
90    private static final int DELAY_UPDATE_SUGGESTIONS = 180;
91    private static final int DELAY_UPDATE_OLD_SUGGESTIONS = 300;
92    private static final int DELAY_UPDATE_SHIFT_STATE = 300;
93    private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
94
95    // How many continuous deletes at which to start deleting at a higher speed.
96    private static final int DELETE_ACCELERATE_AT = 20;
97    // Key events coming any faster than this are long-presses.
98    private static final int QUICK_PRESS = 200;
99
100    // Contextual menu positions
101    private static final int POS_METHOD = 0;
102    private static final int POS_SETTINGS = 1;
103
104    private int mSuggestionVisibility;
105    private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
106            = R.string.prefs_suggestion_visibility_show_value;
107    private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
108            = R.string.prefs_suggestion_visibility_show_only_portrait_value;
109    private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
110            = R.string.prefs_suggestion_visibility_hide_value;
111
112    private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
113        SUGGESTION_VISIBILILTY_SHOW_VALUE,
114        SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
115        SUGGESTION_VISIBILILTY_HIDE_VALUE
116    };
117
118    private View mCandidateViewContainer;
119    private CandidateView mCandidateView;
120    private Suggest mSuggest;
121    private CompletionInfo[] mApplicationSpecifiedCompletions;
122
123    private AlertDialog mOptionsDialog;
124
125    private InputMethodManager mImm;
126    private Resources mResources;
127    private SharedPreferences mPrefs;
128    private String mInputMethodId;
129    private KeyboardSwitcher mKeyboardSwitcher;
130    private SubtypeSwitcher mSubtypeSwitcher;
131    private VoiceIMEConnector mVoiceConnector;
132
133    private UserDictionary mUserDictionary;
134    private UserBigramDictionary mUserBigramDictionary;
135    private ContactsDictionary mContactsDictionary;
136    private AutoDictionary mAutoDictionary;
137
138    // These variables are initialized according to the {@link EditorInfo#inputType}.
139    private boolean mAutoSpace;
140    private boolean mInputTypeNoAutoCorrect;
141    private boolean mIsSettingsSuggestionStripOn;
142    private boolean mApplicationSpecifiedCompletionOn;
143
144    private final StringBuilder mComposing = new StringBuilder();
145    private WordComposer mWord = new WordComposer();
146    private CharSequence mBestWord;
147    private boolean mHasValidSuggestions;
148    private boolean mHasDictionary;
149    private boolean mJustAddedAutoSpace;
150    private boolean mAutoCorrectEnabled;
151    private boolean mReCorrectionEnabled;
152    private boolean mBigramSuggestionEnabled;
153    private boolean mAutoCorrectOn;
154    private boolean mVibrateOn;
155    private boolean mSoundOn;
156    private boolean mPopupOn;
157    private boolean mAutoCap;
158    private boolean mQuickFixes;
159    private boolean mConfigEnableShowSubtypeSettings;
160    private boolean mConfigSwipeDownDismissKeyboardEnabled;
161    private int mConfigDelayBeforeFadeoutLanguageOnSpacebar;
162    private int mConfigDurationOfFadeoutLanguageOnSpacebar;
163    private float mConfigFinalFadeoutFactorOfLanguageOnSpacebar;
164
165    private int mCorrectionMode;
166    private int mCommittedLength;
167    private int mOrientation;
168    // Keep track of the last selection range to decide if we need to show word alternatives
169    private int mLastSelectionStart;
170    private int mLastSelectionEnd;
171    private SuggestedWords mSuggestPuncList;
172
173    // Indicates whether the suggestion strip is to be on in landscape
174    private boolean mJustAccepted;
175    private boolean mJustReverted;
176    private int mDeleteCount;
177    private long mLastKeyTime;
178
179    private AudioManager mAudioManager;
180    // Align sound effect volume on music volume
181    private static final float FX_VOLUME = -1.0f;
182    private boolean mSilentMode;
183
184    /* package */ String mWordSeparators;
185    private String mSentenceSeparators;
186    private String mSuggestPuncs;
187    // TODO: Move this flag to VoiceIMEConnector
188    private boolean mConfigurationChanging;
189
190    // Keeps track of most recently inserted text (multi-character key) for reverting
191    private CharSequence mEnteredText;
192    private boolean mRefreshKeyboardRequired;
193
194    private final ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>();
195
196    public abstract static class WordAlternatives {
197        protected CharSequence mChosenWord;
198
199        public WordAlternatives() {
200            // Nothing
201        }
202
203        public WordAlternatives(CharSequence chosenWord) {
204            mChosenWord = chosenWord;
205        }
206
207        @Override
208        public int hashCode() {
209            return mChosenWord.hashCode();
210        }
211
212        public abstract CharSequence getOriginalWord();
213
214        public CharSequence getChosenWord() {
215            return mChosenWord;
216        }
217
218        public abstract SuggestedWords.Builder getAlternatives();
219    }
220
221    public class TypedWordAlternatives extends WordAlternatives {
222        private WordComposer word;
223
224        public TypedWordAlternatives() {
225            // Nothing
226        }
227
228        public TypedWordAlternatives(CharSequence chosenWord, WordComposer wordComposer) {
229            super(chosenWord);
230            word = wordComposer;
231        }
232
233        @Override
234        public CharSequence getOriginalWord() {
235            return word.getTypedWord();
236        }
237
238        @Override
239        public SuggestedWords.Builder getAlternatives() {
240            return getTypedSuggestions(word);
241        }
242    }
243
244    public final UIHandler mHandler = new UIHandler();
245
246    public class UIHandler extends Handler {
247        private static final int MSG_UPDATE_SUGGESTIONS = 0;
248        private static final int MSG_UPDATE_OLD_SUGGESTIONS = 1;
249        private static final int MSG_UPDATE_SHIFT_STATE = 2;
250        private static final int MSG_VOICE_RESULTS = 3;
251        private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 4;
252        private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 5;
253
254        @Override
255        public void handleMessage(Message msg) {
256            final KeyboardSwitcher switcher = mKeyboardSwitcher;
257            final LatinKeyboardView inputView = switcher.getInputView();
258            switch (msg.what) {
259            case MSG_UPDATE_SUGGESTIONS:
260                updateSuggestions();
261                break;
262            case MSG_UPDATE_OLD_SUGGESTIONS:
263                setOldSuggestions();
264                break;
265            case MSG_UPDATE_SHIFT_STATE:
266                switcher.updateShiftState();
267                break;
268            case MSG_VOICE_RESULTS:
269                mVoiceConnector.handleVoiceResults(preferCapitalization()
270                        || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked()));
271                break;
272            case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR:
273                if (inputView != null)
274                    inputView.setSpacebarTextFadeFactor(
275                            (1.0f + mConfigFinalFadeoutFactorOfLanguageOnSpacebar) / 2,
276                            (LatinKeyboard)msg.obj);
277                sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj),
278                        mConfigDurationOfFadeoutLanguageOnSpacebar);
279                break;
280            case MSG_DISMISS_LANGUAGE_ON_SPACEBAR:
281                if (inputView != null)
282                    inputView.setSpacebarTextFadeFactor(
283                            mConfigFinalFadeoutFactorOfLanguageOnSpacebar, (LatinKeyboard)msg.obj);
284                break;
285            }
286        }
287
288        public void postUpdateSuggestions() {
289            removeMessages(MSG_UPDATE_SUGGESTIONS);
290            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), DELAY_UPDATE_SUGGESTIONS);
291        }
292
293        public void cancelUpdateSuggestions() {
294            removeMessages(MSG_UPDATE_SUGGESTIONS);
295        }
296
297        public boolean hasPendingUpdateSuggestions() {
298            return hasMessages(MSG_UPDATE_SUGGESTIONS);
299        }
300
301        public void postUpdateOldSuggestions() {
302            removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
303            sendMessageDelayed(obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS),
304                    DELAY_UPDATE_OLD_SUGGESTIONS);
305        }
306
307        public void cancelUpdateOldSuggestions() {
308            removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
309        }
310
311        public void postUpdateShiftKeyState() {
312            removeMessages(MSG_UPDATE_SHIFT_STATE);
313            sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), DELAY_UPDATE_SHIFT_STATE);
314        }
315
316        public void cancelUpdateShiftState() {
317            removeMessages(MSG_UPDATE_SHIFT_STATE);
318        }
319
320        public void updateVoiceResults() {
321            sendMessage(obtainMessage(MSG_VOICE_RESULTS));
322        }
323
324        public void startDisplayLanguageOnSpacebar(boolean localeChanged) {
325            removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR);
326            removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR);
327            final LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
328            if (inputView != null) {
329                final LatinKeyboard keyboard = inputView.getLatinKeyboard();
330                // The language is never displayed when the delay is zero.
331                if (mConfigDelayBeforeFadeoutLanguageOnSpacebar != 0)
332                    inputView.setSpacebarTextFadeFactor(localeChanged ? 1.0f
333                            : mConfigFinalFadeoutFactorOfLanguageOnSpacebar, keyboard);
334                // The language is always displayed when the delay is negative.
335                if (localeChanged && mConfigDelayBeforeFadeoutLanguageOnSpacebar > 0) {
336                    sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard),
337                            mConfigDelayBeforeFadeoutLanguageOnSpacebar);
338                }
339            }
340        }
341    }
342
343    @Override
344    public void onCreate() {
345        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
346        mPrefs = prefs;
347        LatinImeLogger.init(this, prefs);
348        SubtypeSwitcher.init(this, prefs);
349        KeyboardSwitcher.init(this, prefs);
350
351        super.onCreate();
352
353        mImm = ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE));
354        mInputMethodId = Utils.getInputMethodId(mImm, getApplicationInfo().packageName);
355        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
356        mKeyboardSwitcher = KeyboardSwitcher.getInstance();
357
358        final Resources res = getResources();
359        mResources = res;
360
361        // If the option should not be shown, do not read the recorrection preference
362        // but always use the default setting defined in the resources.
363        if (res.getBoolean(R.bool.config_enable_show_recorrection_option)) {
364            mReCorrectionEnabled = prefs.getBoolean(Settings.PREF_RECORRECTION_ENABLED,
365                    res.getBoolean(R.bool.default_recorrection_enabled));
366        } else {
367            mReCorrectionEnabled = res.getBoolean(R.bool.default_recorrection_enabled);
368        }
369
370        mConfigEnableShowSubtypeSettings = res.getBoolean(
371                R.bool.config_enable_show_subtype_settings);
372        mConfigSwipeDownDismissKeyboardEnabled = res.getBoolean(
373                R.bool.config_swipe_down_dismiss_keyboard_enabled);
374        mConfigDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger(
375                R.integer.config_delay_before_fadeout_language_on_spacebar);
376        mConfigDurationOfFadeoutLanguageOnSpacebar = res.getInteger(
377                R.integer.config_duration_of_fadeout_language_on_spacebar);
378        mConfigFinalFadeoutFactorOfLanguageOnSpacebar = res.getInteger(
379                R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f;
380
381        Utils.GCUtils.getInstance().reset();
382        boolean tryGC = true;
383        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
384            try {
385                initSuggest();
386                tryGC = false;
387            } catch (OutOfMemoryError e) {
388                tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
389            }
390        }
391
392        mOrientation = res.getConfiguration().orientation;
393        initSuggestPuncList();
394
395        // register to receive ringer mode changes for silent mode
396        IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION);
397        registerReceiver(mReceiver, filter);
398        mVoiceConnector = VoiceIMEConnector.init(this, prefs, mHandler);
399        prefs.registerOnSharedPreferenceChangeListener(this);
400    }
401
402    /**
403     * Returns a main dictionary resource id
404     * @return main dictionary resource id
405     */
406    public static int getMainDictionaryResourceId(Resources res) {
407        final String MAIN_DIC_NAME = "main";
408        String packageName = LatinIME.class.getPackage().getName();
409        return res.getIdentifier(MAIN_DIC_NAME, "raw", packageName);
410    }
411
412    private void initSuggest() {
413        updateAutoTextEnabled();
414        String locale = mSubtypeSwitcher.getInputLocaleStr();
415
416        Locale savedLocale = mSubtypeSwitcher.changeSystemLocale(new Locale(locale));
417        if (mSuggest != null) {
418            mSuggest.close();
419        }
420        final SharedPreferences prefs = mPrefs;
421        mQuickFixes = isQuickFixesEnabled(prefs);
422
423        final Resources res = mResources;
424        int mainDicResId = getMainDictionaryResourceId(res);
425        mSuggest = new Suggest(this, mainDicResId);
426        loadAndSetAutoCorrectionThreshold(prefs);
427
428        mUserDictionary = new UserDictionary(this, locale);
429        mSuggest.setUserDictionary(mUserDictionary);
430
431        mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
432        mSuggest.setContactsDictionary(mContactsDictionary);
433
434        mAutoDictionary = new AutoDictionary(this, this, locale, Suggest.DIC_AUTO);
435        mSuggest.setAutoDictionary(mAutoDictionary);
436
437        mUserBigramDictionary = new UserBigramDictionary(this, this, locale, Suggest.DIC_USER);
438        mSuggest.setUserBigramDictionary(mUserBigramDictionary);
439
440        updateCorrectionMode();
441        mWordSeparators = res.getString(R.string.word_separators);
442        mSentenceSeparators = res.getString(R.string.sentence_separators);
443
444        mSubtypeSwitcher.changeSystemLocale(savedLocale);
445    }
446
447    @Override
448    public void onDestroy() {
449        if (mSuggest != null) {
450            mSuggest.close();
451            mSuggest = null;
452        }
453        unregisterReceiver(mReceiver);
454        mVoiceConnector.destroy();
455        LatinImeLogger.commit();
456        LatinImeLogger.onDestroy();
457        super.onDestroy();
458    }
459
460    @Override
461    public void onConfigurationChanged(Configuration conf) {
462        mSubtypeSwitcher.onConfigurationChanged(conf);
463        // If orientation changed while predicting, commit the change
464        if (conf.orientation != mOrientation) {
465            InputConnection ic = getCurrentInputConnection();
466            commitTyped(ic);
467            if (ic != null) ic.finishComposingText(); // For voice input
468            mOrientation = conf.orientation;
469            if (isShowingOptionDialog())
470                mOptionsDialog.dismiss();
471        }
472
473        mConfigurationChanging = true;
474        super.onConfigurationChanged(conf);
475        mVoiceConnector.onConfigurationChanged(conf);
476        mConfigurationChanging = false;
477    }
478
479    @Override
480    public View onCreateInputView() {
481        return mKeyboardSwitcher.onCreateInputView();
482    }
483
484    @Override
485    public View onCreateCandidatesView() {
486        LayoutInflater inflater = getLayoutInflater();
487        LinearLayout container = (LinearLayout)inflater.inflate(R.layout.candidates, null);
488        mCandidateViewContainer = container;
489        if (container.getPaddingRight() != 0) {
490            HorizontalScrollView scrollView =
491                    (HorizontalScrollView) container.findViewById(R.id.candidates_scroll_view);
492            scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
493            container.setGravity(Gravity.CENTER_HORIZONTAL);
494        }
495        mCandidateView = (CandidateView) container.findViewById(R.id.candidates);
496        mCandidateView.setService(this);
497        setCandidatesViewShown(true);
498        return container;
499    }
500
501    private static boolean isPasswordVariation(int variation) {
502        return variation == InputType.TYPE_TEXT_VARIATION_PASSWORD
503                || variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
504                || variation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
505    }
506
507    private static boolean isEmailVariation(int variation) {
508        return variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
509                || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
510    }
511
512    @Override
513    public void onStartInputView(EditorInfo attribute, boolean restarting) {
514        final KeyboardSwitcher switcher = mKeyboardSwitcher;
515        LatinKeyboardView inputView = switcher.getInputView();
516
517        if(DEBUG) {
518            Log.d(TAG, "onStartInputView: " + inputView);
519        }
520        // In landscape mode, this method gets called without the input view being created.
521        if (inputView == null) {
522            return;
523        }
524
525        mSubtypeSwitcher.updateParametersOnStartInputView();
526
527        if (mRefreshKeyboardRequired) {
528            mRefreshKeyboardRequired = false;
529            onRefreshKeyboard();
530        }
531
532        TextEntryState.newSession(this);
533
534        // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to
535        // know now whether this is a password text field, because we need to know now whether we
536        // want to enable the voice button.
537        mVoiceConnector.resetVoiceStates(isPasswordVariation(
538                attribute.inputType & InputType.TYPE_MASK_VARIATION));
539
540        final int mode = initializeInputAttributesAndGetMode(attribute.inputType);
541
542        inputView.closing();
543        mEnteredText = null;
544        mComposing.setLength(0);
545        mHasValidSuggestions = false;
546        mDeleteCount = 0;
547        mJustAddedAutoSpace = false;
548
549        loadSettings(attribute);
550        if (mSubtypeSwitcher.isKeyboardMode()) {
551            switcher.loadKeyboard(mode, attribute.imeOptions,
552                    mVoiceConnector.isVoiceButtonEnabled(),
553                    mVoiceConnector.isVoiceButtonOnPrimary());
554            switcher.updateShiftState();
555        }
556
557        setCandidatesViewShownInternal(isCandidateStripVisible(),
558                false /* needsInputViewShown */ );
559        // Delay updating suggestions because keyboard input view may not be shown at this point.
560        mHandler.postUpdateSuggestions();
561
562        updateCorrectionMode();
563
564        inputView.setPreviewEnabled(mPopupOn);
565        inputView.setProximityCorrectionEnabled(true);
566        // If we just entered a text field, maybe it has some old text that requires correction
567        checkReCorrectionOnStart();
568        inputView.setForeground(true);
569
570        mVoiceConnector.onStartInputView(inputView.getWindowToken());
571
572        if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
573    }
574
575    private int initializeInputAttributesAndGetMode(int inputType) {
576        final int variation = inputType & InputType.TYPE_MASK_VARIATION;
577        mAutoSpace = false;
578        mInputTypeNoAutoCorrect = false;
579        mIsSettingsSuggestionStripOn = false;
580        mApplicationSpecifiedCompletionOn = false;
581        mApplicationSpecifiedCompletions = null;
582
583        final int mode;
584        switch (inputType & InputType.TYPE_MASK_CLASS) {
585            case InputType.TYPE_CLASS_NUMBER:
586            case InputType.TYPE_CLASS_DATETIME:
587                mode = KeyboardId.MODE_NUMBER;
588                break;
589            case InputType.TYPE_CLASS_PHONE:
590                mode = KeyboardId.MODE_PHONE;
591                break;
592            case InputType.TYPE_CLASS_TEXT:
593                mIsSettingsSuggestionStripOn = true;
594                // Make sure that passwords are not displayed in candidate view
595                if (isPasswordVariation(variation)) {
596                    mIsSettingsSuggestionStripOn = false;
597                }
598                if (isEmailVariation(variation)
599                        || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) {
600                    mAutoSpace = false;
601                } else {
602                    mAutoSpace = true;
603                }
604                if (isEmailVariation(variation)) {
605                    mIsSettingsSuggestionStripOn = false;
606                    mode = KeyboardId.MODE_EMAIL;
607                } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
608                    mIsSettingsSuggestionStripOn = false;
609                    mode = KeyboardId.MODE_URL;
610                } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
611                    mode = KeyboardId.MODE_IM;
612                } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
613                    mIsSettingsSuggestionStripOn = false;
614                    mode = KeyboardId.MODE_TEXT;
615                } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) {
616                    mode = KeyboardId.MODE_WEB;
617                    // If it's a browser edit field and auto correct is not ON explicitly, then
618                    // disable auto correction, but keep suggestions on.
619                    if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) {
620                        mInputTypeNoAutoCorrect = true;
621                    }
622                } else {
623                    mode = KeyboardId.MODE_TEXT;
624                }
625
626                // If NO_SUGGESTIONS is set, don't do prediction.
627                if ((inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
628                    mIsSettingsSuggestionStripOn = false;
629                    mInputTypeNoAutoCorrect = true;
630                }
631                // If it's not multiline and the autoCorrect flag is not set, then don't correct
632                if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 &&
633                        (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
634                    mInputTypeNoAutoCorrect = true;
635                }
636                if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
637                    mIsSettingsSuggestionStripOn = false;
638                    mApplicationSpecifiedCompletionOn = isFullscreenMode();
639                }
640                break;
641            default:
642                mode = KeyboardId.MODE_TEXT;
643                break;
644        }
645        return mode;
646    }
647
648    private void checkReCorrectionOnStart() {
649        if (!mReCorrectionEnabled) return;
650
651        final InputConnection ic = getCurrentInputConnection();
652        if (ic == null) return;
653        // There could be a pending composing span.  Clean it up first.
654        ic.finishComposingText();
655
656        if (isShowingSuggestionsStrip() && isSuggestionsRequested()) {
657            // First get the cursor position. This is required by setOldSuggestions(), so that
658            // it can pass the correct range to setComposingRegion(). At this point, we don't
659            // have valid values for mLastSelectionStart/End because onUpdateSelection() has
660            // not been called yet.
661            ExtractedTextRequest etr = new ExtractedTextRequest();
662            etr.token = 0; // anything is fine here
663            ExtractedText et = ic.getExtractedText(etr, 0);
664            if (et == null) return;
665
666            mLastSelectionStart = et.startOffset + et.selectionStart;
667            mLastSelectionEnd = et.startOffset + et.selectionEnd;
668
669            // Then look for possible corrections in a delayed fashion
670            if (!TextUtils.isEmpty(et.text) && isCursorTouchingWord()) {
671                mHandler.postUpdateOldSuggestions();
672            }
673        }
674    }
675
676    @Override
677    public void onFinishInput() {
678        super.onFinishInput();
679
680        LatinImeLogger.commit();
681        mKeyboardSwitcher.onAutoCorrectionStateChanged(false);
682
683        mVoiceConnector.flushVoiceInputLogs(mConfigurationChanging);
684
685        KeyboardView inputView = mKeyboardSwitcher.getInputView();
686        if (inputView != null) inputView.closing();
687        if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites();
688        if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites();
689    }
690
691    @Override
692    public void onFinishInputView(boolean finishingInput) {
693        super.onFinishInputView(finishingInput);
694        KeyboardView inputView = mKeyboardSwitcher.getInputView();
695        if (inputView != null) inputView.setForeground(false);
696        // Remove pending messages related to update suggestions
697        mHandler.cancelUpdateSuggestions();
698        mHandler.cancelUpdateOldSuggestions();
699    }
700
701    @Override
702    public void onUpdateExtractedText(int token, ExtractedText text) {
703        super.onUpdateExtractedText(token, text);
704        mVoiceConnector.showPunctuationHintIfNecessary();
705    }
706
707    @Override
708    public void onUpdateSelection(int oldSelStart, int oldSelEnd,
709            int newSelStart, int newSelEnd,
710            int candidatesStart, int candidatesEnd) {
711        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
712                candidatesStart, candidatesEnd);
713
714        if (DEBUG) {
715            Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
716                    + ", ose=" + oldSelEnd
717                    + ", nss=" + newSelStart
718                    + ", nse=" + newSelEnd
719                    + ", cs=" + candidatesStart
720                    + ", ce=" + candidatesEnd);
721        }
722
723        mVoiceConnector.setCursorAndSelection(newSelEnd, newSelStart);
724
725        // If the current selection in the text view changes, we should
726        // clear whatever candidate text we have.
727        if ((((mComposing.length() > 0 && mHasValidSuggestions)
728                || mVoiceConnector.isVoiceInputHighlighted()) && (newSelStart != candidatesEnd
729                        || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart)) {
730            mComposing.setLength(0);
731            mHasValidSuggestions = false;
732            mHandler.postUpdateSuggestions();
733            TextEntryState.reset();
734            InputConnection ic = getCurrentInputConnection();
735            if (ic != null) {
736                ic.finishComposingText();
737            }
738            mVoiceConnector.setVoiceInputHighlighted(false);
739        } else if (!mHasValidSuggestions && !mJustAccepted) {
740            switch (TextEntryState.getState()) {
741            case ACCEPTED_DEFAULT:
742                TextEntryState.reset();
743                // $FALL-THROUGH$
744            case SPACE_AFTER_PICKED:
745                mJustAddedAutoSpace = false; // The user moved the cursor.
746                break;
747            default:
748                break;
749            }
750        }
751        mJustAccepted = false;
752        mHandler.postUpdateShiftKeyState();
753
754        // Make a note of the cursor position
755        mLastSelectionStart = newSelStart;
756        mLastSelectionEnd = newSelEnd;
757
758        if (mReCorrectionEnabled && isShowingSuggestionsStrip()) {
759            // Don't look for corrections if the keyboard is not visible
760            if (mKeyboardSwitcher.isInputViewShown()) {
761                // Check if we should go in or out of correction mode.
762                if (isSuggestionsRequested() && !mJustReverted
763                        && (candidatesStart == candidatesEnd || newSelStart != oldSelStart
764                                || TextEntryState.isCorrecting())
765                                && (newSelStart < newSelEnd - 1 || !mHasValidSuggestions)) {
766                    if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) {
767                        mHandler.postUpdateOldSuggestions();
768                    } else {
769                        abortCorrection(false);
770                        // Show the punctuation suggestions list if the current one is not
771                        // and if not showing "Touch again to save".
772                        if (mCandidateView != null && !isShowingPunctuationList()
773                                && !mCandidateView.isShowingAddToDictionaryHint()) {
774                            setPunctuationSuggestions();
775                        }
776                    }
777                }
778            }
779        }
780    }
781
782    /**
783     * This is called when the user has clicked on the extracted text view,
784     * when running in fullscreen mode.  The default implementation hides
785     * the candidates view when this happens, but only if the extracted text
786     * editor has a vertical scroll bar because its text doesn't fit.
787     * Here we override the behavior due to the possibility that a re-correction could
788     * cause the candidate strip to disappear and re-appear.
789     */
790    @Override
791    public void onExtractedTextClicked() {
792        if (mReCorrectionEnabled && isSuggestionsRequested()) return;
793
794        super.onExtractedTextClicked();
795    }
796
797    /**
798     * This is called when the user has performed a cursor movement in the
799     * extracted text view, when it is running in fullscreen mode.  The default
800     * implementation hides the candidates view when a vertical movement
801     * happens, but only if the extracted text editor has a vertical scroll bar
802     * because its text doesn't fit.
803     * Here we override the behavior due to the possibility that a re-correction could
804     * cause the candidate strip to disappear and re-appear.
805     */
806    @Override
807    public void onExtractedCursorMovement(int dx, int dy) {
808        if (mReCorrectionEnabled && isSuggestionsRequested()) return;
809
810        super.onExtractedCursorMovement(dx, dy);
811    }
812
813    @Override
814    public void hideWindow() {
815        LatinImeLogger.commit();
816        mKeyboardSwitcher.onAutoCorrectionStateChanged(false);
817
818        if (TRACE) Debug.stopMethodTracing();
819        if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
820            mOptionsDialog.dismiss();
821            mOptionsDialog = null;
822        }
823        mVoiceConnector.hideVoiceWindow(mConfigurationChanging);
824        mWordHistory.clear();
825        super.hideWindow();
826        TextEntryState.endSession();
827    }
828
829    @Override
830    public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
831        if (DEBUG) {
832            Log.i(TAG, "Received completions:");
833            final int count = (applicationSpecifiedCompletions != null)
834                    ? applicationSpecifiedCompletions.length : 0;
835            for (int i = 0; i < count; i++) {
836                Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
837            }
838        }
839        if (mApplicationSpecifiedCompletionOn) {
840            mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
841            if (applicationSpecifiedCompletions == null) {
842                clearSuggestions();
843                return;
844            }
845
846            SuggestedWords.Builder builder = new SuggestedWords.Builder()
847                    .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions)
848                    .setTypedWordValid(true)
849                    .setHasMinimalSuggestion(true);
850            // When in fullscreen mode, show completions generated by the application
851            setSuggestions(builder.build());
852            mBestWord = null;
853            setCandidatesViewShown(true);
854        }
855    }
856
857    private void setCandidatesViewShownInternal(boolean shown, boolean needsInputViewShown) {
858        // TODO: Remove this if we support candidates with hard keyboard
859        if (onEvaluateInputViewShown()) {
860            super.setCandidatesViewShown(shown
861                    && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true));
862        }
863    }
864
865    @Override
866    public void setCandidatesViewShown(boolean shown) {
867        setCandidatesViewShownInternal(shown, true /* needsInputViewShown */ );
868    }
869
870    @Override
871    public void onComputeInsets(InputMethodService.Insets outInsets) {
872        super.onComputeInsets(outInsets);
873        if (!isFullscreenMode()) {
874            outInsets.contentTopInsets = outInsets.visibleTopInsets;
875        }
876        KeyboardView inputView = mKeyboardSwitcher.getInputView();
877        // Need to set touchable region only if input view is being shown
878        if (inputView != null && mKeyboardSwitcher.isInputViewShown()) {
879            final int x = 0;
880            int y = 0;
881            final int width = inputView.getWidth();
882            int height = inputView.getHeight() + EXTENDED_TOUCHABLE_REGION_HEIGHT;
883            if (mCandidateViewContainer != null) {
884                ViewParent candidateParent = mCandidateViewContainer.getParent();
885                if (candidateParent instanceof FrameLayout) {
886                    FrameLayout fl = (FrameLayout) candidateParent;
887                    if (fl != null) {
888                        // Check frame layout's visibility
889                        if (fl.getVisibility() == View.INVISIBLE) {
890                            y = fl.getHeight();
891                            height += y;
892                        } else if (fl.getVisibility() == View.VISIBLE) {
893                            height += fl.getHeight();
894                        }
895                    }
896                }
897            }
898            if (DEBUG) {
899                Log.d(TAG, "Touchable region " + x + ", " + y + ", " + width + ", " + height);
900            }
901            outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
902            outInsets.touchableRegion.set(x, y, width, height);
903        }
904    }
905
906    @Override
907    public boolean onEvaluateFullscreenMode() {
908        final Resources res = mResources;
909        DisplayMetrics dm = res.getDisplayMetrics();
910        float displayHeight = dm.heightPixels;
911        // If the display is more than X inches high, don't go to fullscreen mode
912        float dimen = res.getDimension(R.dimen.max_height_for_fullscreen);
913        if (displayHeight > dimen) {
914            return false;
915        } else {
916            return super.onEvaluateFullscreenMode();
917        }
918    }
919
920    @Override
921    public boolean onKeyDown(int keyCode, KeyEvent event) {
922        switch (keyCode) {
923        case KeyEvent.KEYCODE_BACK:
924            if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) {
925                if (mKeyboardSwitcher.getInputView().handleBack()) {
926                    return true;
927                }
928            }
929            break;
930        }
931        return super.onKeyDown(keyCode, event);
932    }
933
934    @Override
935    public boolean onKeyUp(int keyCode, KeyEvent event) {
936        switch (keyCode) {
937        case KeyEvent.KEYCODE_DPAD_DOWN:
938        case KeyEvent.KEYCODE_DPAD_UP:
939        case KeyEvent.KEYCODE_DPAD_LEFT:
940        case KeyEvent.KEYCODE_DPAD_RIGHT:
941            // Enable shift key and DPAD to do selections
942            if (mKeyboardSwitcher.isInputViewShown()
943                    && mKeyboardSwitcher.isShiftedOrShiftLocked()) {
944                KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
945                        event.getAction(), event.getKeyCode(), event.getRepeatCount(),
946                        event.getDeviceId(), event.getScanCode(),
947                        KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
948                InputConnection ic = getCurrentInputConnection();
949                if (ic != null)
950                    ic.sendKeyEvent(newEvent);
951                return true;
952            }
953            break;
954        }
955        return super.onKeyUp(keyCode, event);
956    }
957
958    public void commitTyped(InputConnection inputConnection) {
959        if (mHasValidSuggestions) {
960            mHasValidSuggestions = false;
961            if (mComposing.length() > 0) {
962                if (inputConnection != null) {
963                    inputConnection.commitText(mComposing, 1);
964                }
965                mCommittedLength = mComposing.length();
966                TextEntryState.acceptedTyped(mComposing);
967                addToDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED);
968            }
969            updateSuggestions();
970        }
971    }
972
973    public boolean getCurrentAutoCapsState() {
974        InputConnection ic = getCurrentInputConnection();
975        EditorInfo ei = getCurrentInputEditorInfo();
976        if (mAutoCap && ic != null && ei != null && ei.inputType != InputType.TYPE_NULL) {
977            return ic.getCursorCapsMode(ei.inputType) != 0;
978        }
979        return false;
980    }
981
982    private void swapPunctuationAndSpace() {
983        final InputConnection ic = getCurrentInputConnection();
984        if (ic == null) return;
985        CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
986        if (lastTwo != null && lastTwo.length() == 2
987                && lastTwo.charAt(0) == Keyboard.CODE_SPACE
988                && isSentenceSeparator(lastTwo.charAt(1))) {
989            ic.beginBatchEdit();
990            ic.deleteSurroundingText(2, 0);
991            ic.commitText(lastTwo.charAt(1) + " ", 1);
992            ic.endBatchEdit();
993            mKeyboardSwitcher.updateShiftState();
994            mJustAddedAutoSpace = true;
995        }
996    }
997
998    private void reswapPeriodAndSpace() {
999        final InputConnection ic = getCurrentInputConnection();
1000        if (ic == null) return;
1001        CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
1002        if (lastThree != null && lastThree.length() == 3
1003                && lastThree.charAt(0) == Keyboard.CODE_PERIOD
1004                && lastThree.charAt(1) == Keyboard.CODE_SPACE
1005                && lastThree.charAt(2) == Keyboard.CODE_PERIOD) {
1006            ic.beginBatchEdit();
1007            ic.deleteSurroundingText(3, 0);
1008            ic.commitText(" ..", 1);
1009            ic.endBatchEdit();
1010            mKeyboardSwitcher.updateShiftState();
1011        }
1012    }
1013
1014    private void doubleSpace() {
1015        //if (!mAutoPunctuate) return;
1016        if (mCorrectionMode == Suggest.CORRECTION_NONE) return;
1017        final InputConnection ic = getCurrentInputConnection();
1018        if (ic == null) return;
1019        CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
1020        if (lastThree != null && lastThree.length() == 3
1021                && Character.isLetterOrDigit(lastThree.charAt(0))
1022                && lastThree.charAt(1) == Keyboard.CODE_SPACE
1023                && lastThree.charAt(2) == Keyboard.CODE_SPACE) {
1024            ic.beginBatchEdit();
1025            ic.deleteSurroundingText(2, 0);
1026            ic.commitText(". ", 1);
1027            ic.endBatchEdit();
1028            mKeyboardSwitcher.updateShiftState();
1029            mJustAddedAutoSpace = true;
1030        }
1031    }
1032
1033    private void maybeRemovePreviousPeriod(CharSequence text) {
1034        final InputConnection ic = getCurrentInputConnection();
1035        if (ic == null) return;
1036
1037        // When the text's first character is '.', remove the previous period
1038        // if there is one.
1039        CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1040        if (lastOne != null && lastOne.length() == 1
1041                && lastOne.charAt(0) == Keyboard.CODE_PERIOD
1042                && text.charAt(0) == Keyboard.CODE_PERIOD) {
1043            ic.deleteSurroundingText(1, 0);
1044        }
1045    }
1046
1047    private void removeTrailingSpace() {
1048        final InputConnection ic = getCurrentInputConnection();
1049        if (ic == null) return;
1050
1051        CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1052        if (lastOne != null && lastOne.length() == 1
1053                && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
1054            ic.deleteSurroundingText(1, 0);
1055        }
1056    }
1057
1058    public boolean addWordToDictionary(String word) {
1059        mUserDictionary.addWord(word, 128);
1060        // Suggestion strip should be updated after the operation of adding word to the
1061        // user dictionary
1062        mHandler.postUpdateSuggestions();
1063        return true;
1064    }
1065
1066    private boolean isAlphabet(int code) {
1067        if (Character.isLetter(code)) {
1068            return true;
1069        } else {
1070            return false;
1071        }
1072    }
1073
1074    private void onSettingsKeyPressed() {
1075        if (!isShowingOptionDialog()) {
1076            if (!mConfigEnableShowSubtypeSettings) {
1077                showSubtypeSelectorAndSettings();
1078            } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) {
1079                showOptionsMenu();
1080            } else {
1081                launchSettings();
1082            }
1083        }
1084    }
1085
1086    private void onSettingsKeyLongPressed() {
1087        if (!isShowingOptionDialog()) {
1088            if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) {
1089                mImm.showInputMethodPicker();
1090            } else {
1091                launchSettings();
1092            }
1093        }
1094    }
1095
1096    private boolean isShowingOptionDialog() {
1097        return mOptionsDialog != null && mOptionsDialog.isShowing();
1098    }
1099
1100    // Implementation of {@link KeyboardActionListener}.
1101    @Override
1102    public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
1103        long when = SystemClock.uptimeMillis();
1104        if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
1105            mDeleteCount = 0;
1106        }
1107        mLastKeyTime = when;
1108        KeyboardSwitcher switcher = mKeyboardSwitcher;
1109        final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
1110        switch (primaryCode) {
1111        case Keyboard.CODE_DELETE:
1112            handleBackspace();
1113            mDeleteCount++;
1114            LatinImeLogger.logOnDelete();
1115            break;
1116        case Keyboard.CODE_SHIFT:
1117            // Shift key is handled in onPress() when device has distinct multi-touch panel.
1118            if (!distinctMultiTouch)
1119                switcher.toggleShift();
1120            break;
1121        case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
1122            // Symbol key is handled in onPress() when device has distinct multi-touch panel.
1123            if (!distinctMultiTouch)
1124                switcher.changeKeyboardMode();
1125            break;
1126        case Keyboard.CODE_CANCEL:
1127            if (!isShowingOptionDialog()) {
1128                handleClose();
1129            }
1130            break;
1131        case Keyboard.CODE_SETTINGS:
1132            onSettingsKeyPressed();
1133            break;
1134        case Keyboard.CODE_SETTINGS_LONGPRESS:
1135            onSettingsKeyLongPressed();
1136            break;
1137        case Keyboard.CODE_NEXT_LANGUAGE:
1138            toggleLanguage(false, true);
1139            break;
1140        case Keyboard.CODE_PREV_LANGUAGE:
1141            toggleLanguage(false, false);
1142            break;
1143        case Keyboard.CODE_CAPSLOCK:
1144            switcher.toggleCapsLock();
1145            break;
1146        case Keyboard.CODE_VOICE:
1147            mSubtypeSwitcher.switchToShortcutIME();
1148            break;
1149        case Keyboard.CODE_TAB:
1150            handleTab();
1151            break;
1152        default:
1153            if (primaryCode != Keyboard.CODE_ENTER) {
1154                mJustAddedAutoSpace = false;
1155            }
1156            RingCharBuffer.getInstance().push((char)primaryCode, x, y);
1157            LatinImeLogger.logOnInputChar();
1158            if (isWordSeparator(primaryCode)) {
1159                handleSeparator(primaryCode);
1160            } else {
1161                handleCharacter(primaryCode, keyCodes);
1162            }
1163            // Cancel the just reverted state
1164            mJustReverted = false;
1165        }
1166        switcher.onKey(primaryCode);
1167        // Reset after any single keystroke
1168        mEnteredText = null;
1169    }
1170
1171    @Override
1172    public void onTextInput(CharSequence text) {
1173        mVoiceConnector.commitVoiceInput();
1174        InputConnection ic = getCurrentInputConnection();
1175        if (ic == null) return;
1176        abortCorrection(false);
1177        ic.beginBatchEdit();
1178        commitTyped(ic);
1179        maybeRemovePreviousPeriod(text);
1180        ic.commitText(text, 1);
1181        ic.endBatchEdit();
1182        mKeyboardSwitcher.updateShiftState();
1183        mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY);
1184        mJustReverted = false;
1185        mJustAddedAutoSpace = false;
1186        mEnteredText = text;
1187    }
1188
1189    @Override
1190    public void onCancelInput() {
1191        // User released a finger outside any key
1192        mKeyboardSwitcher.onCancelInput();
1193    }
1194
1195    private void handleBackspace() {
1196        if (mVoiceConnector.logAndRevertVoiceInput()) return;
1197
1198        final InputConnection ic = getCurrentInputConnection();
1199        if (ic == null) return;
1200        ic.beginBatchEdit();
1201
1202        mVoiceConnector.handleBackspace();
1203
1204        boolean deleteChar = false;
1205        if (mHasValidSuggestions) {
1206            final int length = mComposing.length();
1207            if (length > 0) {
1208                mComposing.delete(length - 1, length);
1209                mWord.deleteLast();
1210                ic.setComposingText(mComposing, 1);
1211                if (mComposing.length() == 0) {
1212                    mHasValidSuggestions = false;
1213                }
1214                mHandler.postUpdateSuggestions();
1215            } else {
1216                ic.deleteSurroundingText(1, 0);
1217            }
1218        } else {
1219            deleteChar = true;
1220        }
1221        mHandler.postUpdateShiftKeyState();
1222
1223        TextEntryState.backspace();
1224        if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) {
1225            revertLastWord(deleteChar);
1226            ic.endBatchEdit();
1227            return;
1228        }
1229
1230        if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
1231            ic.deleteSurroundingText(mEnteredText.length(), 0);
1232        } else if (deleteChar) {
1233            if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
1234                // Go back to the suggestion mode if the user canceled the
1235                // "Touch again to save".
1236                // NOTE: In gerenal, we don't revert the word when backspacing
1237                // from a manual suggestion pick.  We deliberately chose a
1238                // different behavior only in the case of picking the first
1239                // suggestion (typed word).  It's intentional to have made this
1240                // inconsistent with backspacing after selecting other suggestions.
1241                revertLastWord(deleteChar);
1242            } else {
1243                sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1244                if (mDeleteCount > DELETE_ACCELERATE_AT) {
1245                    sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1246                }
1247            }
1248        }
1249        mJustReverted = false;
1250        ic.endBatchEdit();
1251    }
1252
1253    private void handleTab() {
1254        final int imeOptions = getCurrentInputEditorInfo().imeOptions;
1255        final int navigationFlags =
1256                EditorInfo.IME_FLAG_NAVIGATE_NEXT | EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
1257        if ((imeOptions & navigationFlags) == 0) {
1258            sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
1259            return;
1260        }
1261
1262        final InputConnection ic = getCurrentInputConnection();
1263        if (ic == null)
1264            return;
1265
1266        // True if keyboard is in either chording shift or manual temporary upper case mode.
1267        final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase();
1268        if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0
1269                && !isManualTemporaryUpperCase) {
1270            ic.performEditorAction(EditorInfo.IME_ACTION_NEXT);
1271        } else if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0
1272                && isManualTemporaryUpperCase) {
1273            ic.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
1274        }
1275    }
1276
1277    private void abortCorrection(boolean force) {
1278        if (force || TextEntryState.isCorrecting()) {
1279            TextEntryState.onAbortCorrection();
1280            setCandidatesViewShown(isCandidateStripVisible());
1281            getCurrentInputConnection().finishComposingText();
1282            clearSuggestions();
1283        }
1284    }
1285
1286    private void handleCharacter(int primaryCode, int[] keyCodes) {
1287        mVoiceConnector.handleCharacter();
1288
1289        if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) {
1290            abortCorrection(false);
1291        }
1292
1293        int code = primaryCode;
1294        if (isAlphabet(code) && isSuggestionsRequested() && !isCursorTouchingWord()) {
1295            if (!mHasValidSuggestions) {
1296                mHasValidSuggestions = true;
1297                mComposing.setLength(0);
1298                saveWordInHistory(mBestWord);
1299                mWord.reset();
1300            }
1301        }
1302        KeyboardSwitcher switcher = mKeyboardSwitcher;
1303        if (switcher.isShiftedOrShiftLocked()) {
1304            if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
1305                    || keyCodes[0] > Character.MAX_CODE_POINT) {
1306                return;
1307            }
1308            code = keyCodes[0];
1309            if (switcher.isAlphabetMode() && Character.isLowerCase(code)) {
1310                int upperCaseCode = Character.toUpperCase(code);
1311                if (upperCaseCode != code) {
1312                    code = upperCaseCode;
1313                } else {
1314                    // Some keys, such as [eszett], have upper case as multi-characters.
1315                    String upperCase = new String(new int[] {code}, 0, 1).toUpperCase();
1316                    onTextInput(upperCase);
1317                    return;
1318                }
1319            }
1320        }
1321        if (mHasValidSuggestions) {
1322            if (mComposing.length() == 0 && switcher.isAlphabetMode()
1323                    && switcher.isShiftedOrShiftLocked()) {
1324                mWord.setFirstCharCapitalized(true);
1325            }
1326            mComposing.append((char) code);
1327            mWord.add(code, keyCodes);
1328            InputConnection ic = getCurrentInputConnection();
1329            if (ic != null) {
1330                // If it's the first letter, make note of auto-caps state
1331                if (mWord.size() == 1) {
1332                    mWord.setAutoCapitalized(getCurrentAutoCapsState());
1333                }
1334                ic.setComposingText(mComposing, 1);
1335            }
1336            mHandler.postUpdateSuggestions();
1337        } else {
1338            sendKeyChar((char)code);
1339        }
1340        switcher.updateShiftState();
1341        if (LatinIME.PERF_DEBUG) measureCps();
1342        TextEntryState.typedCharacter((char) code, isWordSeparator(code));
1343    }
1344
1345    private void handleSeparator(int primaryCode) {
1346        mVoiceConnector.handleSeparator();
1347
1348        // Should dismiss the "Touch again to save" message when handling separator
1349        if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
1350            mHandler.postUpdateSuggestions();
1351        }
1352
1353        boolean pickedDefault = false;
1354        // Handle separator
1355        final InputConnection ic = getCurrentInputConnection();
1356        if (ic != null) {
1357            ic.beginBatchEdit();
1358            abortCorrection(false);
1359        }
1360        if (mHasValidSuggestions) {
1361            // In certain languages where single quote is a separator, it's better
1362            // not to auto correct, but accept the typed word. For instance,
1363            // in Italian dov' should not be expanded to dove' because the elision
1364            // requires the last vowel to be removed.
1365            if (mAutoCorrectOn && primaryCode != '\'' && !mJustReverted) {
1366                pickedDefault = pickDefaultSuggestion();
1367                // Picked the suggestion by the space key.  We consider this
1368                // as "added an auto space".
1369                if (primaryCode == Keyboard.CODE_SPACE) {
1370                    mJustAddedAutoSpace = true;
1371                }
1372            } else {
1373                commitTyped(ic);
1374            }
1375        }
1376        if (mJustAddedAutoSpace && primaryCode == Keyboard.CODE_ENTER) {
1377            removeTrailingSpace();
1378            mJustAddedAutoSpace = false;
1379        }
1380        sendKeyChar((char)primaryCode);
1381
1382        // Handle the case of ". ." -> " .." with auto-space if necessary
1383        // before changing the TextEntryState.
1384        if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED
1385                && primaryCode == Keyboard.CODE_PERIOD) {
1386            reswapPeriodAndSpace();
1387        }
1388
1389        TextEntryState.typedCharacter((char) primaryCode, true);
1390        if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED
1391                && primaryCode != Keyboard.CODE_ENTER) {
1392            swapPunctuationAndSpace();
1393        } else if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) {
1394            doubleSpace();
1395        }
1396        if (pickedDefault) {
1397            CharSequence typedWord = mWord.getTypedWord();
1398            TextEntryState.backToAcceptedDefault(typedWord);
1399            if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) {
1400                if (ic != null) {
1401                    CorrectionInfo correctionInfo = new CorrectionInfo(
1402                            mLastSelectionEnd - typedWord.length(), typedWord, mBestWord);
1403                    ic.commitCorrection(correctionInfo);
1404                }
1405                if (mCandidateView != null)
1406                    mCandidateView.onAutoCorrectionInverted(mBestWord);
1407            }
1408            setPunctuationSuggestions();
1409        }
1410        mKeyboardSwitcher.updateShiftState();
1411        if (ic != null) {
1412            ic.endBatchEdit();
1413        }
1414    }
1415
1416    private void handleClose() {
1417        commitTyped(getCurrentInputConnection());
1418        mVoiceConnector.handleClose();
1419        requestHideSelf(0);
1420        LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
1421        if (inputView != null)
1422            inputView.closing();
1423        TextEntryState.endSession();
1424    }
1425
1426    private void saveWordInHistory(CharSequence result) {
1427        if (mWord.size() <= 1) {
1428            mWord.reset();
1429            return;
1430        }
1431        // Skip if result is null. It happens in some edge case.
1432        if (TextUtils.isEmpty(result)) {
1433            return;
1434        }
1435
1436        // Make a copy of the CharSequence, since it is/could be a mutable CharSequence
1437        final String resultCopy = result.toString();
1438        TypedWordAlternatives entry = new TypedWordAlternatives(resultCopy,
1439                new WordComposer(mWord));
1440        mWordHistory.add(entry);
1441    }
1442
1443    private boolean isSuggestionsRequested() {
1444        return mIsSettingsSuggestionStripOn
1445                && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
1446    }
1447
1448    private boolean isShowingPunctuationList() {
1449        return mSuggestPuncList == mCandidateView.getSuggestions();
1450    }
1451
1452    private boolean isShowingSuggestionsStrip() {
1453        return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
1454                || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
1455                        && mOrientation == Configuration.ORIENTATION_PORTRAIT);
1456    }
1457
1458    private boolean isCandidateStripVisible() {
1459        if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isCorrecting())
1460            return true;
1461        if (!isShowingSuggestionsStrip())
1462            return false;
1463        if (mApplicationSpecifiedCompletionOn)
1464            return true;
1465        return isSuggestionsRequested();
1466    }
1467
1468    public void switchToKeyboardView() {
1469        mHandler.post(new Runnable() {
1470            @Override
1471            public void run() {
1472                if (DEBUG) {
1473                    Log.d(TAG, "Switch to keyboard view.");
1474                }
1475                View v = mKeyboardSwitcher.getInputView();
1476                if (v != null) {
1477                    // Confirms that the keyboard view doesn't have parent view.
1478                    ViewParent p = v.getParent();
1479                    if (p != null && p instanceof ViewGroup) {
1480                        ((ViewGroup) p).removeView(v);
1481                    }
1482                    setInputView(v);
1483                }
1484                setCandidatesViewShown(isCandidateStripVisible());
1485                updateInputViewShown();
1486                mHandler.postUpdateSuggestions();
1487            }
1488        });
1489    }
1490
1491    public void clearSuggestions() {
1492        setSuggestions(SuggestedWords.EMPTY);
1493    }
1494
1495    public void setSuggestions(SuggestedWords words) {
1496        if (mVoiceConnector.getAndResetIsShowingHint()) {
1497             setCandidatesView(mCandidateViewContainer);
1498        }
1499
1500        if (mCandidateView != null) {
1501            mCandidateView.setSuggestions(words);
1502            if (mCandidateView.isConfigCandidateHighlightFontColorEnabled()) {
1503                mKeyboardSwitcher.onAutoCorrectionStateChanged(
1504                        words.hasWordAboveAutoCorrectionScoreThreshold());
1505            }
1506        }
1507    }
1508
1509    public void updateSuggestions() {
1510        mKeyboardSwitcher.setPreferredLetters(null);
1511
1512        // Check if we have a suggestion engine attached.
1513        if ((mSuggest == null || !isSuggestionsRequested())
1514                && !mVoiceConnector.isVoiceInputHighlighted()) {
1515            return;
1516        }
1517
1518        if (!mHasValidSuggestions) {
1519            setPunctuationSuggestions();
1520            return;
1521        }
1522        showSuggestions(mWord);
1523    }
1524
1525    private SuggestedWords.Builder getTypedSuggestions(WordComposer word) {
1526        return mSuggest.getSuggestedWordBuilder(mKeyboardSwitcher.getInputView(), word, null);
1527    }
1528
1529    private void showCorrections(WordAlternatives alternatives) {
1530        mKeyboardSwitcher.setPreferredLetters(null);
1531        SuggestedWords.Builder builder = alternatives.getAlternatives();
1532        builder.setTypedWordValid(false).setHasMinimalSuggestion(false);
1533        showSuggestions(builder.build(), alternatives.getOriginalWord());
1534    }
1535
1536    private void showSuggestions(WordComposer word) {
1537        // TODO Maybe need better way of retrieving previous word
1538        CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(),
1539                mWordSeparators);
1540        SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
1541                mKeyboardSwitcher.getInputView(), word, prevWord);
1542
1543        int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies();
1544        mKeyboardSwitcher.setPreferredLetters(nextLettersFrequencies);
1545
1546        boolean correctionAvailable = !mInputTypeNoAutoCorrect && !mJustReverted
1547                && mSuggest.hasAutoCorrection();
1548        final CharSequence typedWord = word.getTypedWord();
1549        // If we're in basic correct
1550        final boolean typedWordValid = mSuggest.isValidWord(typedWord) ||
1551                (preferCapitalization()
1552                        && mSuggest.isValidWord(typedWord.toString().toLowerCase()));
1553        if (mCorrectionMode == Suggest.CORRECTION_FULL
1554                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
1555            correctionAvailable |= typedWordValid;
1556        }
1557        // Don't auto-correct words with multiple capital letter
1558        correctionAvailable &= !word.isMostlyCaps();
1559        correctionAvailable &= !TextEntryState.isCorrecting();
1560
1561        // Basically, we update the suggestion strip only when suggestion count > 1.  However,
1562        // there is an exception: We update the suggestion strip whenever typed word's length
1563        // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
1564        // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
1565        // need to clear the previous state when the user starts typing a word (i.e. typed word's
1566        // length == 1).
1567        if (builder.size() > 1 || typedWord.length() == 1 || typedWordValid
1568                || mCandidateView.isShowingAddToDictionaryHint()) {
1569            builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion(correctionAvailable);
1570        } else {
1571            final SuggestedWords previousSuggestions = mCandidateView.getSuggestions();
1572            if (previousSuggestions == mSuggestPuncList)
1573                return;
1574            builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions);
1575        }
1576        showSuggestions(builder.build(), typedWord);
1577    }
1578
1579    private void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) {
1580        setSuggestions(suggestedWords);
1581        if (suggestedWords.size() > 0) {
1582            if (Utils.shouldBlockedBySafetyNetForAutoCorrection(suggestedWords)) {
1583                mBestWord = typedWord;
1584            } else if (suggestedWords.hasAutoCorrectionWord()) {
1585                mBestWord = suggestedWords.getWord(1);
1586            } else {
1587                mBestWord = typedWord;
1588            }
1589        } else {
1590            mBestWord = null;
1591        }
1592        setCandidatesViewShown(isCandidateStripVisible());
1593    }
1594
1595    private boolean pickDefaultSuggestion() {
1596        // Complete any pending candidate query first
1597        if (mHandler.hasPendingUpdateSuggestions()) {
1598            mHandler.cancelUpdateSuggestions();
1599            updateSuggestions();
1600        }
1601        if (mBestWord != null && mBestWord.length() > 0) {
1602            TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord);
1603            mJustAccepted = true;
1604            pickSuggestion(mBestWord);
1605            // Add the word to the auto dictionary if it's not a known word
1606            addToDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED);
1607            return true;
1608
1609        }
1610        return false;
1611    }
1612
1613    public void pickSuggestionManually(int index, CharSequence suggestion) {
1614        SuggestedWords suggestions = mCandidateView.getSuggestions();
1615        mVoiceConnector.flushAndLogAllTextModificationCounters(index, suggestion, mWordSeparators);
1616
1617        final boolean correcting = TextEntryState.isCorrecting();
1618        InputConnection ic = getCurrentInputConnection();
1619        if (ic != null) {
1620            ic.beginBatchEdit();
1621        }
1622        if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null
1623                && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
1624            CompletionInfo ci = mApplicationSpecifiedCompletions[index];
1625            if (ic != null) {
1626                ic.commitCompletion(ci);
1627            }
1628            mCommittedLength = suggestion.length();
1629            if (mCandidateView != null) {
1630                mCandidateView.clear();
1631            }
1632            mKeyboardSwitcher.updateShiftState();
1633            if (ic != null) {
1634                ic.endBatchEdit();
1635            }
1636            return;
1637        }
1638
1639        // If this is a punctuation, apply it through the normal key press
1640        if (suggestion.length() == 1 && (isWordSeparator(suggestion.charAt(0))
1641                || isSuggestedPunctuation(suggestion.charAt(0)))) {
1642            // Word separators are suggested before the user inputs something.
1643            // So, LatinImeLogger logs "" as a user's input.
1644            LatinImeLogger.logOnManualSuggestion(
1645                    "", suggestion.toString(), index, suggestions.mWords);
1646            final char primaryCode = suggestion.charAt(0);
1647            onCodeInput(primaryCode, new int[] { primaryCode },
1648                    KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
1649                    KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
1650            if (ic != null) {
1651                ic.endBatchEdit();
1652            }
1653            return;
1654        }
1655        mJustAccepted = true;
1656        pickSuggestion(suggestion);
1657        // Add the word to the auto dictionary if it's not a known word
1658        if (index == 0) {
1659            addToDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED);
1660        } else {
1661            addToBigramDictionary(suggestion, 1);
1662        }
1663        LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(),
1664                index, suggestions.mWords);
1665        TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion);
1666        // Follow it with a space
1667        if (mAutoSpace && !correcting) {
1668            sendSpace();
1669            mJustAddedAutoSpace = true;
1670        }
1671
1672        final boolean showingAddToDictionaryHint = index == 0 && mCorrectionMode > 0
1673                && !mSuggest.isValidWord(suggestion)
1674                && !mSuggest.isValidWord(suggestion.toString().toLowerCase());
1675
1676        if (!correcting) {
1677            // Fool the state watcher so that a subsequent backspace will not do a revert, unless
1678            // we just did a correction, in which case we need to stay in
1679            // TextEntryState.State.PICKED_SUGGESTION state.
1680            TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true);
1681            setPunctuationSuggestions();
1682        } else if (!showingAddToDictionaryHint) {
1683            // If we're not showing the "Touch again to save", then show corrections again.
1684            // In case the cursor position doesn't change, make sure we show the suggestions again.
1685            clearSuggestions();
1686            mHandler.postUpdateOldSuggestions();
1687        }
1688        if (showingAddToDictionaryHint) {
1689            mCandidateView.showAddToDictionaryHint(suggestion);
1690        }
1691        if (ic != null) {
1692            ic.endBatchEdit();
1693        }
1694    }
1695
1696    /**
1697     * Commits the chosen word to the text field and saves it for later
1698     * retrieval.
1699     * @param suggestion the suggestion picked by the user to be committed to
1700     *            the text field
1701     */
1702    private void pickSuggestion(CharSequence suggestion) {
1703        KeyboardSwitcher switcher = mKeyboardSwitcher;
1704        if (!switcher.isKeyboardAvailable())
1705            return;
1706        InputConnection ic = getCurrentInputConnection();
1707        if (ic != null) {
1708            mVoiceConnector.rememberReplacedWord(suggestion, mWordSeparators);
1709            ic.commitText(suggestion, 1);
1710        }
1711        saveWordInHistory(suggestion);
1712        mHasValidSuggestions = false;
1713        mCommittedLength = suggestion.length();
1714        switcher.setPreferredLetters(null);
1715    }
1716
1717    /**
1718     * Tries to apply any typed alternatives for the word if we have any cached alternatives,
1719     * otherwise tries to find new corrections and completions for the word.
1720     * @param touching The word that the cursor is touching, with position information
1721     * @return true if an alternative was found, false otherwise.
1722     */
1723    private boolean applyTypedAlternatives(EditingUtils.SelectedWord touching) {
1724        // If we didn't find a match, search for result in typed word history
1725        WordComposer foundWord = null;
1726        WordAlternatives alternatives = null;
1727        for (WordAlternatives entry : mWordHistory) {
1728            if (TextUtils.equals(entry.getChosenWord(), touching.mWord)) {
1729                if (entry instanceof TypedWordAlternatives) {
1730                    foundWord = ((TypedWordAlternatives) entry).word;
1731                }
1732                alternatives = entry;
1733                break;
1734            }
1735        }
1736        // If we didn't find a match, at least suggest corrections.
1737        if (foundWord == null
1738                && (mSuggest.isValidWord(touching.mWord)
1739                        || mSuggest.isValidWord(touching.mWord.toString().toLowerCase()))) {
1740            foundWord = new WordComposer();
1741            for (int i = 0; i < touching.mWord.length(); i++) {
1742                foundWord.add(touching.mWord.charAt(i), new int[] {
1743                    touching.mWord.charAt(i)
1744                });
1745            }
1746            foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.mWord.charAt(0)));
1747        }
1748        // Found a match, show suggestions
1749        if (foundWord != null || alternatives != null) {
1750            if (alternatives == null) {
1751                alternatives = new TypedWordAlternatives(touching.mWord, foundWord);
1752            }
1753            showCorrections(alternatives);
1754            if (foundWord != null) {
1755                mWord = new WordComposer(foundWord);
1756            } else {
1757                mWord.reset();
1758            }
1759            return true;
1760        }
1761        return false;
1762    }
1763
1764    private void setOldSuggestions() {
1765        mVoiceConnector.setShowingVoiceSuggestions(false);
1766        if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) {
1767            return;
1768        }
1769        InputConnection ic = getCurrentInputConnection();
1770        if (ic == null) return;
1771        if (!mHasValidSuggestions) {
1772            // Extract the selected or touching text
1773            EditingUtils.SelectedWord touching = EditingUtils.getWordAtCursorOrSelection(ic,
1774                    mLastSelectionStart, mLastSelectionEnd, mWordSeparators);
1775
1776            if (touching != null && touching.mWord.length() > 1) {
1777                ic.beginBatchEdit();
1778
1779                if (!mVoiceConnector.applyVoiceAlternatives(touching)
1780                        && !applyTypedAlternatives(touching)) {
1781                    abortCorrection(true);
1782                } else {
1783                    TextEntryState.selectedForCorrection();
1784                    EditingUtils.underlineWord(ic, touching);
1785                }
1786
1787                ic.endBatchEdit();
1788            } else {
1789                abortCorrection(true);
1790                setPunctuationSuggestions();  // Show the punctuation suggestions list
1791            }
1792        } else {
1793            abortCorrection(true);
1794        }
1795    }
1796
1797    private void setPunctuationSuggestions() {
1798        setSuggestions(mSuggestPuncList);
1799        setCandidatesViewShown(isCandidateStripVisible());
1800    }
1801
1802    private void addToDictionaries(CharSequence suggestion, int frequencyDelta) {
1803        checkAddToDictionary(suggestion, frequencyDelta, false);
1804    }
1805
1806    private void addToBigramDictionary(CharSequence suggestion, int frequencyDelta) {
1807        checkAddToDictionary(suggestion, frequencyDelta, true);
1808    }
1809
1810    /**
1811     * Adds to the UserBigramDictionary and/or AutoDictionary
1812     * @param addToBigramDictionary true if it should be added to bigram dictionary if possible
1813     */
1814    private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta,
1815            boolean addToBigramDictionary) {
1816        if (suggestion == null || suggestion.length() < 1) return;
1817        // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
1818        // adding words in situations where the user or application really didn't
1819        // want corrections enabled or learned.
1820        if (!(mCorrectionMode == Suggest.CORRECTION_FULL
1821                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
1822            return;
1823        }
1824        if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion)
1825                || (!mSuggest.isValidWord(suggestion.toString())
1826                        && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) {
1827            mAutoDictionary.addWord(suggestion.toString(), frequencyDelta);
1828        }
1829
1830        if (mUserBigramDictionary != null) {
1831            CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(),
1832                    mSentenceSeparators);
1833            if (!TextUtils.isEmpty(prevWord)) {
1834                mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString());
1835            }
1836        }
1837    }
1838
1839    private boolean isCursorTouchingWord() {
1840        InputConnection ic = getCurrentInputConnection();
1841        if (ic == null) return false;
1842        CharSequence toLeft = ic.getTextBeforeCursor(1, 0);
1843        CharSequence toRight = ic.getTextAfterCursor(1, 0);
1844        if (!TextUtils.isEmpty(toLeft)
1845                && !isWordSeparator(toLeft.charAt(0))
1846                && !isSuggestedPunctuation(toLeft.charAt(0))) {
1847            return true;
1848        }
1849        if (!TextUtils.isEmpty(toRight)
1850                && !isWordSeparator(toRight.charAt(0))
1851                && !isSuggestedPunctuation(toRight.charAt(0))) {
1852            return true;
1853        }
1854        return false;
1855    }
1856
1857    private boolean sameAsTextBeforeCursor(InputConnection ic, CharSequence text) {
1858        CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
1859        return TextUtils.equals(text, beforeText);
1860    }
1861
1862    public void revertLastWord(boolean deleteChar) {
1863        final int length = mComposing.length();
1864        if (!mHasValidSuggestions && length > 0) {
1865            final InputConnection ic = getCurrentInputConnection();
1866            mJustReverted = true;
1867            final CharSequence punctuation = ic.getTextBeforeCursor(1, 0);
1868            if (deleteChar) ic.deleteSurroundingText(1, 0);
1869            int toDelete = mCommittedLength;
1870            final CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0);
1871            if (!TextUtils.isEmpty(toTheLeft) && isWordSeparator(toTheLeft.charAt(0))) {
1872                toDelete--;
1873            }
1874            ic.deleteSurroundingText(toDelete, 0);
1875            // Re-insert punctuation only when the deleted character was word separator and the
1876            // composing text wasn't equal to the auto-corrected text.
1877            if (deleteChar
1878                    && !TextUtils.isEmpty(punctuation) && isWordSeparator(punctuation.charAt(0))
1879                    && !TextUtils.equals(mComposing, toTheLeft)) {
1880                ic.commitText(mComposing, 1);
1881                TextEntryState.acceptedTyped(mComposing);
1882                ic.commitText(punctuation, 1);
1883                TextEntryState.typedCharacter(punctuation.charAt(0), true);
1884                // Clear composing text
1885                mComposing.setLength(0);
1886            } else {
1887                mHasValidSuggestions = true;
1888                ic.setComposingText(mComposing, 1);
1889                TextEntryState.backspace();
1890            }
1891            mHandler.postUpdateSuggestions();
1892        } else {
1893            sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1894            mJustReverted = false;
1895        }
1896    }
1897
1898    protected String getWordSeparators() {
1899        return mWordSeparators;
1900    }
1901
1902    public boolean isWordSeparator(int code) {
1903        String separators = getWordSeparators();
1904        return separators.contains(String.valueOf((char)code));
1905    }
1906
1907    private boolean isSentenceSeparator(int code) {
1908        return mSentenceSeparators.contains(String.valueOf((char)code));
1909    }
1910
1911    private void sendSpace() {
1912        sendKeyChar((char)Keyboard.CODE_SPACE);
1913        mKeyboardSwitcher.updateShiftState();
1914    }
1915
1916    public boolean preferCapitalization() {
1917        return mWord.isFirstCharCapitalized();
1918    }
1919
1920    // Notify that language or mode have been changed and toggleLanguage will update KeyboaredID
1921    // according to new language or mode.
1922    public void onRefreshKeyboard() {
1923        toggleLanguage(true, true);
1924    }
1925
1926    // "reset" and "next" are used only for USE_SPACEBAR_LANGUAGE_SWITCHER.
1927    private void toggleLanguage(boolean reset, boolean next) {
1928        if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()) {
1929            mSubtypeSwitcher.toggleLanguage(reset, next);
1930        }
1931        // Reload keyboard because the current language has been changed.
1932        KeyboardSwitcher switcher = mKeyboardSwitcher;
1933        final EditorInfo attribute = getCurrentInputEditorInfo();
1934        final int mode = initializeInputAttributesAndGetMode((attribute != null)
1935                ? attribute.inputType : 0);
1936        final int imeOptions = (attribute != null) ? attribute.imeOptions : 0;
1937        switcher.loadKeyboard(mode, imeOptions, mVoiceConnector.isVoiceButtonEnabled(),
1938                mVoiceConnector.isVoiceButtonOnPrimary());
1939        initSuggest();
1940        switcher.updateShiftState();
1941    }
1942
1943    @Override
1944    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
1945            String key) {
1946        mSubtypeSwitcher.onSharedPreferenceChanged(sharedPreferences, key);
1947        if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) {
1948            mRefreshKeyboardRequired = true;
1949        } else if (Settings.PREF_RECORRECTION_ENABLED.equals(key)) {
1950            mReCorrectionEnabled = sharedPreferences.getBoolean(
1951                    Settings.PREF_RECORRECTION_ENABLED,
1952                    mResources.getBoolean(R.bool.default_recorrection_enabled));
1953        }
1954    }
1955
1956    @Override
1957    public void onSwipeDown() {
1958        if (mConfigSwipeDownDismissKeyboardEnabled)
1959            handleClose();
1960    }
1961
1962    @Override
1963    public void onPress(int primaryCode) {
1964        if (mKeyboardSwitcher.isVibrateAndSoundFeedbackRequired()) {
1965            vibrate();
1966            playKeyClick(primaryCode);
1967        }
1968        KeyboardSwitcher switcher = mKeyboardSwitcher;
1969        final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
1970        if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
1971            switcher.onPressShift();
1972        } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
1973            switcher.onPressSymbol();
1974        } else {
1975            switcher.onOtherKeyPressed();
1976        }
1977    }
1978
1979    @Override
1980    public void onRelease(int primaryCode) {
1981        KeyboardSwitcher switcher = mKeyboardSwitcher;
1982        // Reset any drag flags in the keyboard
1983        switcher.keyReleased();
1984        final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
1985        if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
1986            switcher.onReleaseShift();
1987        } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
1988            switcher.onReleaseSymbol();
1989        }
1990    }
1991
1992
1993    // receive ringer mode changes to detect silent mode
1994    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
1995        @Override
1996        public void onReceive(Context context, Intent intent) {
1997            updateRingerMode();
1998        }
1999    };
2000
2001    // update flags for silent mode
2002    private void updateRingerMode() {
2003        if (mAudioManager == null) {
2004            mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
2005        }
2006        if (mAudioManager != null) {
2007            mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
2008        }
2009    }
2010
2011    private void playKeyClick(int primaryCode) {
2012        // if mAudioManager is null, we don't have the ringer state yet
2013        // mAudioManager will be set by updateRingerMode
2014        if (mAudioManager == null) {
2015            if (mKeyboardSwitcher.getInputView() != null) {
2016                updateRingerMode();
2017            }
2018        }
2019        if (mSoundOn && !mSilentMode) {
2020            // FIXME: Volume and enable should come from UI settings
2021            // FIXME: These should be triggered after auto-repeat logic
2022            int sound = AudioManager.FX_KEYPRESS_STANDARD;
2023            switch (primaryCode) {
2024                case Keyboard.CODE_DELETE:
2025                    sound = AudioManager.FX_KEYPRESS_DELETE;
2026                    break;
2027                case Keyboard.CODE_ENTER:
2028                    sound = AudioManager.FX_KEYPRESS_RETURN;
2029                    break;
2030                case Keyboard.CODE_SPACE:
2031                    sound = AudioManager.FX_KEYPRESS_SPACEBAR;
2032                    break;
2033            }
2034            mAudioManager.playSoundEffect(sound, FX_VOLUME);
2035        }
2036    }
2037
2038    public void vibrate() {
2039        if (!mVibrateOn) {
2040            return;
2041        }
2042        LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
2043        if (inputView != null) {
2044            inputView.performHapticFeedback(
2045                    HapticFeedbackConstants.KEYBOARD_TAP,
2046                    HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
2047        }
2048    }
2049
2050    public void promoteToUserDictionary(String word, int frequency) {
2051        if (mUserDictionary.isValidWord(word)) return;
2052        mUserDictionary.addWord(word, frequency);
2053    }
2054
2055    public WordComposer getCurrentWord() {
2056        return mWord;
2057    }
2058
2059    public boolean getPopupOn() {
2060        return mPopupOn;
2061    }
2062
2063    private void updateCorrectionMode() {
2064        mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false;
2065        mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes)
2066                && !mInputTypeNoAutoCorrect && mHasDictionary;
2067        mCorrectionMode = (mAutoCorrectOn && mAutoCorrectEnabled)
2068                ? Suggest.CORRECTION_FULL
2069                : (mAutoCorrectOn ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE);
2070        mCorrectionMode = (mBigramSuggestionEnabled && mAutoCorrectOn && mAutoCorrectEnabled)
2071                ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
2072        if (mSuggest != null) {
2073            mSuggest.setCorrectionMode(mCorrectionMode);
2074        }
2075    }
2076
2077    private void updateAutoTextEnabled() {
2078        if (mSuggest == null) return;
2079        mSuggest.setAutoTextEnabled(mQuickFixes
2080                && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage());
2081    }
2082
2083    private void updateSuggestionVisibility(SharedPreferences prefs) {
2084        final Resources res = mResources;
2085        final String suggestionVisiblityStr = prefs.getString(
2086                Settings.PREF_SHOW_SUGGESTIONS_SETTING,
2087                res.getString(R.string.prefs_suggestion_visibility_default_value));
2088        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
2089            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
2090                mSuggestionVisibility = visibility;
2091                break;
2092            }
2093        }
2094    }
2095
2096    protected void launchSettings() {
2097        launchSettings(Settings.class);
2098    }
2099
2100    public void launchDebugSettings() {
2101        launchSettings(DebugSettings.class);
2102    }
2103
2104    protected void launchSettings(Class<? extends PreferenceActivity> settingsClass) {
2105        handleClose();
2106        Intent intent = new Intent();
2107        intent.setClass(LatinIME.this, settingsClass);
2108        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2109        startActivity(intent);
2110    }
2111
2112    private void loadSettings(EditorInfo attribute) {
2113        // Get the settings preferences
2114        final SharedPreferences prefs = mPrefs;
2115        Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
2116        mVibrateOn = vibrator != null && vibrator.hasVibrator()
2117                && prefs.getBoolean(Settings.PREF_VIBRATE_ON, false);
2118        mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, false);
2119        mPopupOn = prefs.getBoolean(Settings.PREF_POPUP_ON,
2120                mResources.getBoolean(R.bool.config_default_popup_preview));
2121        mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
2122        mQuickFixes = isQuickFixesEnabled(prefs);
2123
2124        mAutoCorrectEnabled = isAutoCorrectEnabled(prefs);
2125        mBigramSuggestionEnabled = mAutoCorrectEnabled && isBigramSuggestionEnabled(prefs);
2126        loadAndSetAutoCorrectionThreshold(prefs);
2127
2128        mVoiceConnector.loadSettings(attribute, prefs);
2129
2130        updateCorrectionMode();
2131        updateAutoTextEnabled();
2132        updateSuggestionVisibility(prefs);
2133        SubtypeSwitcher.getInstance().loadSettings();
2134    }
2135
2136    /**
2137     *  Load Auto correction threshold from SharedPreferences, and modify mSuggest's threshold.
2138     */
2139    private void loadAndSetAutoCorrectionThreshold(SharedPreferences sp) {
2140        // When mSuggest is not initialized, cannnot modify mSuggest's threshold.
2141        if (mSuggest == null) return;
2142        // When auto correction setting is turned off, the threshold is ignored.
2143        if (!isAutoCorrectEnabled(sp)) return;
2144
2145        final String currentAutoCorrectionSetting = sp.getString(
2146                Settings.PREF_AUTO_CORRECTION_THRESHOLD,
2147                mResources.getString(R.string.auto_correction_threshold_mode_index_modest));
2148        final String[] autoCorrectionThresholdValues = mResources.getStringArray(
2149                R.array.auto_correction_threshold_values);
2150        // When autoCrrectionThreshold is greater than 1.0, auto correction is virtually turned off.
2151        double autoCorrectionThreshold = Double.MAX_VALUE;
2152        try {
2153            final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting);
2154            if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) {
2155                autoCorrectionThreshold = Double.parseDouble(
2156                        autoCorrectionThresholdValues[arrayIndex]);
2157            }
2158        } catch (NumberFormatException e) {
2159            // Whenever the threshold settings are correct, never come here.
2160            autoCorrectionThreshold = Double.MAX_VALUE;
2161            Log.w(TAG, "Cannot load auto correction threshold setting."
2162                    + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting
2163                    + ", autoCorrectionThresholdValues: "
2164                    + Arrays.toString(autoCorrectionThresholdValues));
2165        }
2166        // TODO: This should be refactored :
2167        //           setAutoCorrectionThreshold should be called outside of this method.
2168        mSuggest.setAutoCorrectionThreshold(autoCorrectionThreshold);
2169    }
2170
2171    private boolean isQuickFixesEnabled(SharedPreferences sp) {
2172        final boolean showQuickFixesOption = mResources.getBoolean(
2173                R.bool.config_enable_quick_fixes_option);
2174        if (!showQuickFixesOption) {
2175            return isAutoCorrectEnabled(sp);
2176        }
2177        return sp.getBoolean(Settings.PREF_QUICK_FIXES, mResources.getBoolean(
2178                R.bool.config_default_quick_fixes));
2179    }
2180
2181    private boolean isAutoCorrectEnabled(SharedPreferences sp) {
2182        final String currentAutoCorrectionSetting = sp.getString(
2183                Settings.PREF_AUTO_CORRECTION_THRESHOLD,
2184                mResources.getString(R.string.auto_correction_threshold_mode_index_modest));
2185        final String autoCorrectionOff = mResources.getString(
2186                R.string.auto_correction_threshold_mode_index_off);
2187        return !currentAutoCorrectionSetting.equals(autoCorrectionOff);
2188    }
2189
2190    private boolean isBigramSuggestionEnabled(SharedPreferences sp) {
2191        final boolean showBigramSuggestionsOption = mResources.getBoolean(
2192                R.bool.config_enable_bigram_suggestions_option);
2193        if (!showBigramSuggestionsOption) {
2194            return isAutoCorrectEnabled(sp);
2195        }
2196        return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTIONS, mResources.getBoolean(
2197                R.bool.config_default_bigram_suggestions));
2198    }
2199
2200    private void initSuggestPuncList() {
2201        if (mSuggestPuncs != null || mSuggestPuncList != null)
2202            return;
2203        SuggestedWords.Builder builder = new SuggestedWords.Builder();
2204        String puncs = mResources.getString(R.string.suggested_punctuations);
2205        if (puncs != null) {
2206            for (int i = 0; i < puncs.length(); i++) {
2207                builder.addWord(puncs.subSequence(i, i + 1));
2208            }
2209        }
2210        mSuggestPuncList = builder.build();
2211        mSuggestPuncs = puncs;
2212    }
2213
2214    private boolean isSuggestedPunctuation(int code) {
2215        return mSuggestPuncs.contains(String.valueOf((char)code));
2216    }
2217
2218    private void showSubtypeSelectorAndSettings() {
2219        showOptionsMenuInternal(new DialogInterface.OnClickListener() {
2220            @Override
2221            public void onClick(DialogInterface di, int position) {
2222                di.dismiss();
2223                switch (position) {
2224                case POS_SETTINGS:
2225                    launchSettings();
2226                    break;
2227                case POS_METHOD:
2228                    Intent intent = new Intent(
2229                            android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
2230                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
2231                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2232                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2233                    intent.putExtra(android.provider.Settings.EXTRA_INPUT_METHOD_ID,
2234                            mInputMethodId);
2235                    startActivity(intent);
2236                    break;
2237                }
2238            }
2239        });
2240    }
2241
2242    private void showOptionsMenu() {
2243        showOptionsMenuInternal(new DialogInterface.OnClickListener() {
2244            @Override
2245            public void onClick(DialogInterface di, int position) {
2246                di.dismiss();
2247                switch (position) {
2248                case POS_SETTINGS:
2249                    launchSettings();
2250                    break;
2251                case POS_METHOD:
2252                    mImm.showInputMethodPicker();
2253                    break;
2254                }
2255            }
2256        });
2257    }
2258
2259    private void showOptionsMenuInternal(DialogInterface.OnClickListener listener) {
2260        AlertDialog.Builder builder = new AlertDialog.Builder(this);
2261        builder.setCancelable(true);
2262        builder.setIcon(R.drawable.ic_dialog_keyboard);
2263        builder.setNegativeButton(android.R.string.cancel, null);
2264        CharSequence itemSettings = getString(R.string.english_ime_settings);
2265        CharSequence itemInputMethod = getString(R.string.selectInputMethod);
2266        builder.setItems(new CharSequence[] {
2267                itemInputMethod, itemSettings}, listener);
2268        builder.setTitle(mResources.getString(R.string.english_ime_input_options));
2269        mOptionsDialog = builder.create();
2270        mOptionsDialog.setCanceledOnTouchOutside(true);
2271        Window window = mOptionsDialog.getWindow();
2272        WindowManager.LayoutParams lp = window.getAttributes();
2273        lp.token = mKeyboardSwitcher.getInputView().getWindowToken();
2274        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
2275        window.setAttributes(lp);
2276        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
2277        mOptionsDialog.show();
2278    }
2279
2280    @Override
2281    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
2282        super.dump(fd, fout, args);
2283
2284        final Printer p = new PrintWriterPrinter(fout);
2285        p.println("LatinIME state :");
2286        p.println("  Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode());
2287        p.println("  mComposing=" + mComposing.toString());
2288        p.println("  mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn);
2289        p.println("  mCorrectionMode=" + mCorrectionMode);
2290        p.println("  mHasValidSuggestions=" + mHasValidSuggestions);
2291        p.println("  mAutoCorrectOn=" + mAutoCorrectOn);
2292        p.println("  mAutoSpace=" + mAutoSpace);
2293        p.println("  mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn);
2294        p.println("  TextEntryState.state=" + TextEntryState.getState());
2295        p.println("  mSoundOn=" + mSoundOn);
2296        p.println("  mVibrateOn=" + mVibrateOn);
2297        p.println("  mPopupOn=" + mPopupOn);
2298    }
2299
2300    // Characters per second measurement
2301
2302    private long mLastCpsTime;
2303    private static final int CPS_BUFFER_SIZE = 16;
2304    private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE];
2305    private int mCpsIndex;
2306
2307    private void measureCps() {
2308        long now = System.currentTimeMillis();
2309        if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial
2310        mCpsIntervals[mCpsIndex] = now - mLastCpsTime;
2311        mLastCpsTime = now;
2312        mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE;
2313        long total = 0;
2314        for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i];
2315        System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total));
2316    }
2317
2318    @Override
2319    public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) {
2320        SubtypeSwitcher.getInstance().updateSubtype(subtype);
2321    }
2322}
2323