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