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