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