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