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