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