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