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