LatinIME.java revision 055054eef3ccd32c6dfd69a3f76bfb7383ea93bb
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 = 200;
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 20ms afterward and see if orientation change has
434                // 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())
1156            return;
1157        if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
1158            showSubtypeSelectorAndSettings();
1159        } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) {
1160            showOptionsMenu();
1161        } else {
1162            launchSettings();
1163        }
1164    }
1165
1166    private void onSettingsKeyLongPressed() {
1167        if (!isShowingOptionDialog()) {
1168            if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) {
1169                mImm.showInputMethodPicker();
1170            } else {
1171                launchSettings();
1172            }
1173        }
1174    }
1175
1176    private boolean isShowingOptionDialog() {
1177        return mOptionsDialog != null && mOptionsDialog.isShowing();
1178    }
1179
1180    // Implementation of {@link KeyboardActionListener}.
1181    @Override
1182    public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
1183        long when = SystemClock.uptimeMillis();
1184        if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
1185            mDeleteCount = 0;
1186        }
1187        mLastKeyTime = when;
1188        KeyboardSwitcher switcher = mKeyboardSwitcher;
1189        final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
1190        final boolean lastStateOfJustReplacedDoubleSpace = mJustReplacedDoubleSpace;
1191        mJustReplacedDoubleSpace = false;
1192        switch (primaryCode) {
1193        case Keyboard.CODE_DELETE:
1194            handleBackspace(lastStateOfJustReplacedDoubleSpace);
1195            mDeleteCount++;
1196            mExpectingUpdateSelection = true;
1197            LatinImeLogger.logOnDelete();
1198            break;
1199        case Keyboard.CODE_SHIFT:
1200            // Shift key is handled in onPress() when device has distinct multi-touch panel.
1201            if (!distinctMultiTouch)
1202                switcher.toggleShift();
1203            break;
1204        case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
1205            // Symbol key is handled in onPress() when device has distinct multi-touch panel.
1206            if (!distinctMultiTouch)
1207                switcher.changeKeyboardMode();
1208            break;
1209        case Keyboard.CODE_CANCEL:
1210            if (!isShowingOptionDialog()) {
1211                handleClose();
1212            }
1213            break;
1214        case Keyboard.CODE_SETTINGS:
1215            onSettingsKeyPressed();
1216            break;
1217        case Keyboard.CODE_SETTINGS_LONGPRESS:
1218            onSettingsKeyLongPressed();
1219            break;
1220        case Keyboard.CODE_CAPSLOCK:
1221            switcher.toggleCapsLock();
1222            break;
1223        case Keyboard.CODE_SHORTCUT:
1224            mSubtypeSwitcher.switchToShortcutIME();
1225            break;
1226        case Keyboard.CODE_TAB:
1227            handleTab();
1228            // There are two cases for tab. Either we send a "next" event, that may change the
1229            // focus but will never move the cursor. Or, we send a real tab keycode, which some
1230            // applications may accept or ignore, and we don't know whether this will move the
1231            // cursor or not. So actually, we don't really know.
1232            // So to go with the safer option, we'd rather behave as if the user moved the
1233            // cursor when they didn't than the opposite. We also expect that most applications
1234            // will actually use tab only for focus movement.
1235            // To sum it up: do not update mExpectingUpdateSelection here.
1236            break;
1237        default:
1238            if (mSettingsValues.isWordSeparator(primaryCode)) {
1239                handleSeparator(primaryCode, x, y);
1240            } else {
1241                handleCharacter(primaryCode, keyCodes, x, y);
1242            }
1243            mExpectingUpdateSelection = true;
1244            break;
1245        }
1246        switcher.onKey(primaryCode);
1247        // Reset after any single keystroke
1248        mEnteredText = null;
1249    }
1250
1251    @Override
1252    public void onTextInput(CharSequence text) {
1253        mVoiceProxy.commitVoiceInput();
1254        final InputConnection ic = getCurrentInputConnection();
1255        if (ic == null) return;
1256        mRecorrection.abortRecorrection(false);
1257        ic.beginBatchEdit();
1258        commitTyped(ic);
1259        maybeRemovePreviousPeriod(ic, text);
1260        ic.commitText(text, 1);
1261        ic.endBatchEdit();
1262        mKeyboardSwitcher.updateShiftState();
1263        mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY);
1264        mJustAddedMagicSpace = false;
1265        mEnteredText = text;
1266    }
1267
1268    @Override
1269    public void onCancelInput() {
1270        // User released a finger outside any key
1271        mKeyboardSwitcher.onCancelInput();
1272    }
1273
1274    private void handleBackspace(boolean justReplacedDoubleSpace) {
1275        if (mVoiceProxy.logAndRevertVoiceInput()) return;
1276
1277        final InputConnection ic = getCurrentInputConnection();
1278        if (ic == null) return;
1279        ic.beginBatchEdit();
1280
1281        mVoiceProxy.handleBackspace();
1282
1283        final boolean deleteChar = !mHasUncommittedTypedChars;
1284        if (mHasUncommittedTypedChars) {
1285            final int length = mComposingStringBuilder.length();
1286            if (length > 0) {
1287                mComposingStringBuilder.delete(length - 1, length);
1288                mWordComposer.deleteLast();
1289                ic.setComposingText(mComposingStringBuilder, 1);
1290                if (mComposingStringBuilder.length() == 0) {
1291                    mHasUncommittedTypedChars = false;
1292                }
1293                if (1 == length) {
1294                    // 1 == length means we are about to erase the last character of the word,
1295                    // so we can show bigrams.
1296                    mHandler.postUpdateBigramPredictions();
1297                } else {
1298                    // length > 1, so we still have letters to deduce a suggestion from.
1299                    mHandler.postUpdateSuggestions();
1300                }
1301            } else {
1302                ic.deleteSurroundingText(1, 0);
1303            }
1304        }
1305        mHandler.postUpdateShiftKeyState();
1306
1307        TextEntryState.backspace();
1308        if (TextEntryState.isUndoCommit()) {
1309            revertLastWord(ic);
1310            ic.endBatchEdit();
1311            return;
1312        }
1313        if (justReplacedDoubleSpace) {
1314            if (revertDoubleSpace(ic)) {
1315                ic.endBatchEdit();
1316                return;
1317            }
1318        }
1319
1320        if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
1321            ic.deleteSurroundingText(mEnteredText.length(), 0);
1322        } else if (deleteChar) {
1323            if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
1324                // Go back to the suggestion mode if the user canceled the
1325                // "Touch again to save".
1326                // NOTE: In gerenal, we don't revert the word when backspacing
1327                // from a manual suggestion pick.  We deliberately chose a
1328                // different behavior only in the case of picking the first
1329                // suggestion (typed word).  It's intentional to have made this
1330                // inconsistent with backspacing after selecting other suggestions.
1331                revertLastWord(ic);
1332            } else {
1333                sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1334                if (mDeleteCount > DELETE_ACCELERATE_AT) {
1335                    sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1336                }
1337            }
1338        }
1339        ic.endBatchEdit();
1340    }
1341
1342    private void handleTab() {
1343        final int imeOptions = getCurrentInputEditorInfo().imeOptions;
1344        if (!EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
1345                && !EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)) {
1346            sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
1347            return;
1348        }
1349
1350        final InputConnection ic = getCurrentInputConnection();
1351        if (ic == null)
1352            return;
1353
1354        // True if keyboard is in either chording shift or manual temporary upper case mode.
1355        final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase();
1356        if (EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
1357                && !isManualTemporaryUpperCase) {
1358            EditorInfoCompatUtils.performEditorActionNext(ic);
1359        } else if (EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)
1360                && isManualTemporaryUpperCase) {
1361            EditorInfoCompatUtils.performEditorActionPrevious(ic);
1362        }
1363    }
1364
1365    private void handleCharacter(int primaryCode, int[] keyCodes, int x, int y) {
1366        mVoiceProxy.handleCharacter();
1367
1368        if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceStripper(primaryCode)) {
1369            removeTrailingSpace();
1370        }
1371
1372        if (mLastSelectionStart == mLastSelectionEnd) {
1373            mRecorrection.abortRecorrection(false);
1374        }
1375
1376        int code = primaryCode;
1377        if (isAlphabet(code) && isSuggestionsRequested() && !isCursorTouchingWord()) {
1378            if (!mHasUncommittedTypedChars) {
1379                mHasUncommittedTypedChars = true;
1380                mComposingStringBuilder.setLength(0);
1381                mRecorrection.saveRecorrectionSuggestion(mWordComposer, mBestWord);
1382                mWordComposer.reset();
1383                clearSuggestions();
1384            }
1385        }
1386        final KeyboardSwitcher switcher = mKeyboardSwitcher;
1387        if (switcher.isShiftedOrShiftLocked()) {
1388            if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
1389                    || keyCodes[0] > Character.MAX_CODE_POINT) {
1390                return;
1391            }
1392            code = keyCodes[0];
1393            if (switcher.isAlphabetMode() && Character.isLowerCase(code)) {
1394                // In some locales, such as Turkish, Character.toUpperCase() may return a wrong
1395                // character because it doesn't take care of locale.
1396                final String upperCaseString = new String(new int[] {code}, 0, 1)
1397                        .toUpperCase(mSubtypeSwitcher.getInputLocale());
1398                if (upperCaseString.codePointCount(0, upperCaseString.length()) == 1) {
1399                    code = upperCaseString.codePointAt(0);
1400                } else {
1401                    // Some keys, such as [eszett], have upper case as multi-characters.
1402                    onTextInput(upperCaseString);
1403                    return;
1404                }
1405            }
1406        }
1407        if (mHasUncommittedTypedChars) {
1408            if (mComposingStringBuilder.length() == 0 && switcher.isAlphabetMode()
1409                    && switcher.isShiftedOrShiftLocked()) {
1410                mWordComposer.setFirstCharCapitalized(true);
1411            }
1412            mComposingStringBuilder.append((char) code);
1413            mWordComposer.add(code, keyCodes, x, y);
1414            final InputConnection ic = getCurrentInputConnection();
1415            if (ic != null) {
1416                // If it's the first letter, make note of auto-caps state
1417                if (mWordComposer.size() == 1) {
1418                    mWordComposer.setAutoCapitalized(getCurrentAutoCapsState());
1419                }
1420                ic.setComposingText(mComposingStringBuilder, 1);
1421            }
1422            mHandler.postUpdateSuggestions();
1423        } else {
1424            sendKeyChar((char)code);
1425        }
1426        if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
1427            swapSwapperAndSpace();
1428        } else {
1429            mJustAddedMagicSpace = false;
1430        }
1431
1432        switcher.updateShiftState();
1433        if (LatinIME.PERF_DEBUG) measureCps();
1434        TextEntryState.typedCharacter((char) code, mSettingsValues.isWordSeparator(code), x, y);
1435    }
1436
1437    private void handleSeparator(int primaryCode, int x, int y) {
1438        mVoiceProxy.handleSeparator();
1439
1440        // Should dismiss the "Touch again to save" message when handling separator
1441        if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
1442            mHandler.cancelUpdateBigramPredictions();
1443            mHandler.postUpdateSuggestions();
1444        }
1445
1446        boolean pickedDefault = false;
1447        // Handle separator
1448        final InputConnection ic = getCurrentInputConnection();
1449        if (ic != null) {
1450            ic.beginBatchEdit();
1451            mRecorrection.abortRecorrection(false);
1452        }
1453        if (mHasUncommittedTypedChars) {
1454            // In certain languages where single quote is a separator, it's better
1455            // not to auto correct, but accept the typed word. For instance,
1456            // in Italian dov' should not be expanded to dove' because the elision
1457            // requires the last vowel to be removed.
1458            final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
1459                    && !mInputTypeNoAutoCorrect && mHasDictionary;
1460            if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
1461                pickedDefault = pickDefaultSuggestion(primaryCode);
1462            } else {
1463                commitTyped(ic);
1464            }
1465        }
1466
1467        if (mJustAddedMagicSpace) {
1468            if (mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
1469                sendKeyChar((char)primaryCode);
1470                swapSwapperAndSpace();
1471            } else {
1472                if (mSettingsValues.isMagicSpaceStripper(primaryCode)) removeTrailingSpace();
1473                sendKeyChar((char)primaryCode);
1474                mJustAddedMagicSpace = false;
1475            }
1476        } else {
1477            sendKeyChar((char)primaryCode);
1478        }
1479
1480        if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) {
1481            maybeDoubleSpace();
1482        }
1483
1484        TextEntryState.typedCharacter((char) primaryCode, true, x, y);
1485
1486        if (pickedDefault) {
1487            CharSequence typedWord = mWordComposer.getTypedWord();
1488            TextEntryState.backToAcceptedDefault(typedWord);
1489            if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) {
1490                InputConnectionCompatUtils.commitCorrection(
1491                        ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord);
1492                if (mCandidateView != null)
1493                    mCandidateView.onAutoCorrectionInverted(mBestWord);
1494            }
1495        }
1496        if (Keyboard.CODE_SPACE == primaryCode) {
1497            if (!isCursorTouchingWord()) {
1498                mHandler.cancelUpdateSuggestions();
1499                mHandler.cancelUpdateOldSuggestions();
1500                mHandler.postUpdateBigramPredictions();
1501            }
1502        } else {
1503            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
1504            // already displayed or not, so it's okay.
1505            setPunctuationSuggestions();
1506        }
1507        mKeyboardSwitcher.updateShiftState();
1508        if (ic != null) {
1509            ic.endBatchEdit();
1510        }
1511    }
1512
1513    private void handleClose() {
1514        commitTyped(getCurrentInputConnection());
1515        mVoiceProxy.handleClose();
1516        requestHideSelf(0);
1517        LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1518        if (inputView != null)
1519            inputView.closing();
1520    }
1521
1522    public boolean isSuggestionsRequested() {
1523        return mIsSettingsSuggestionStripOn
1524                && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
1525    }
1526
1527    public boolean isShowingPunctuationList() {
1528        return mSettingsValues.mSuggestPuncList == mCandidateView.getSuggestions();
1529    }
1530
1531    public boolean isShowingSuggestionsStrip() {
1532        return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
1533                || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
1534                        && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
1535    }
1536
1537    public boolean isCandidateStripVisible() {
1538        if (mCandidateView == null)
1539            return false;
1540        if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isRecorrecting())
1541            return true;
1542        if (!isShowingSuggestionsStrip())
1543            return false;
1544        if (mApplicationSpecifiedCompletionOn)
1545            return true;
1546        return isSuggestionsRequested();
1547    }
1548
1549    public void switchToKeyboardView() {
1550        if (DEBUG) {
1551            Log.d(TAG, "Switch to keyboard view.");
1552        }
1553        View v = mKeyboardSwitcher.getKeyboardView();
1554        if (v != null) {
1555            // Confirms that the keyboard view doesn't have parent view.
1556            ViewParent p = v.getParent();
1557            if (p != null && p instanceof ViewGroup) {
1558                ((ViewGroup) p).removeView(v);
1559            }
1560            setInputView(v);
1561        }
1562        setSuggestionStripShown(isCandidateStripVisible());
1563        updateInputViewShown();
1564        mHandler.postUpdateSuggestions();
1565    }
1566
1567    public void clearSuggestions() {
1568        setSuggestions(SuggestedWords.EMPTY);
1569    }
1570
1571    public void setSuggestions(SuggestedWords words) {
1572        if (mCandidateView != null) {
1573            mCandidateView.setSuggestions(words);
1574            mKeyboardSwitcher.onAutoCorrectionStateChanged(
1575                    words.hasWordAboveAutoCorrectionScoreThreshold());
1576        }
1577    }
1578
1579    public void updateSuggestions() {
1580        // Check if we have a suggestion engine attached.
1581        if ((mSuggest == null || !isSuggestionsRequested())
1582                && !mVoiceProxy.isVoiceInputHighlighted()) {
1583            return;
1584        }
1585
1586        if (!mHasUncommittedTypedChars) {
1587            setPunctuationSuggestions();
1588            return;
1589        }
1590
1591        final WordComposer wordComposer = mWordComposer;
1592        // TODO: May need a better way of retrieving previous word
1593        final InputConnection ic = getCurrentInputConnection();
1594        final CharSequence prevWord;
1595        if (null == ic) {
1596            prevWord = null;
1597        } else {
1598            prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
1599        }
1600        // getSuggestedWordBuilder handles gracefully a null value of prevWord
1601        final SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
1602                mKeyboardSwitcher.getKeyboardView(), wordComposer, prevWord);
1603
1604        boolean autoCorrectionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection();
1605        final CharSequence typedWord = wordComposer.getTypedWord();
1606        // Here, we want to promote a whitelisted word if exists.
1607        final boolean typedWordValid = AutoCorrection.isValidWordForAutoCorrection(
1608                mSuggest.getUnigramDictionaries(), typedWord, preferCapitalization());
1609        if (mCorrectionMode == Suggest.CORRECTION_FULL
1610                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
1611            autoCorrectionAvailable |= typedWordValid;
1612        }
1613        // Don't auto-correct words with multiple capital letter
1614        autoCorrectionAvailable &= !wordComposer.isMostlyCaps();
1615        autoCorrectionAvailable &= !TextEntryState.isRecorrecting();
1616
1617        // Basically, we update the suggestion strip only when suggestion count > 1.  However,
1618        // there is an exception: We update the suggestion strip whenever typed word's length
1619        // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
1620        // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
1621        // need to clear the previous state when the user starts typing a word (i.e. typed word's
1622        // length == 1).
1623        if (typedWord != null) {
1624            if (builder.size() > 1 || typedWord.length() == 1 || typedWordValid
1625                    || mCandidateView.isShowingAddToDictionaryHint()) {
1626                builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion(
1627                        autoCorrectionAvailable);
1628            } else {
1629                final SuggestedWords previousSuggestions = mCandidateView.getSuggestions();
1630                if (previousSuggestions == mSettingsValues.mSuggestPuncList)
1631                    return;
1632                builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions);
1633            }
1634        }
1635        showSuggestions(builder.build(), typedWord);
1636    }
1637
1638    public void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) {
1639        setSuggestions(suggestedWords);
1640        if (suggestedWords.size() > 0) {
1641            if (Utils.shouldBlockedBySafetyNetForAutoCorrection(suggestedWords, mSuggest)) {
1642                mBestWord = typedWord;
1643            } else if (suggestedWords.hasAutoCorrectionWord()) {
1644                mBestWord = suggestedWords.getWord(1);
1645            } else {
1646                mBestWord = typedWord;
1647            }
1648        } else {
1649            mBestWord = null;
1650        }
1651        setSuggestionStripShown(isCandidateStripVisible());
1652    }
1653
1654    private boolean pickDefaultSuggestion(int separatorCode) {
1655        // Complete any pending candidate query first
1656        if (mHandler.hasPendingUpdateSuggestions()) {
1657            mHandler.cancelUpdateSuggestions();
1658            updateSuggestions();
1659        }
1660        if (mBestWord != null && mBestWord.length() > 0) {
1661            TextEntryState.acceptedDefault(mWordComposer.getTypedWord(), mBestWord, separatorCode);
1662            mExpectingUpdateSelection = true;
1663            commitBestWord(mBestWord);
1664            // Add the word to the user unigram dictionary if it's not a known word
1665            addToUserUnigramAndBigramDictionaries(mBestWord,
1666                    UserUnigramDictionary.FREQUENCY_FOR_TYPED);
1667            return true;
1668        }
1669        return false;
1670    }
1671
1672    @Override
1673    public void pickSuggestionManually(int index, CharSequence suggestion) {
1674        SuggestedWords suggestions = mCandidateView.getSuggestions();
1675        mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion,
1676                mSettingsValues.mWordSeparators);
1677
1678        final boolean recorrecting = TextEntryState.isRecorrecting();
1679        final InputConnection ic = getCurrentInputConnection();
1680        if (ic != null) {
1681            ic.beginBatchEdit();
1682        }
1683        if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null
1684                && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
1685            if (ic != null) {
1686                final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
1687                ic.commitCompletion(completionInfo);
1688            }
1689            mCommittedLength = suggestion.length();
1690            if (mCandidateView != null) {
1691                mCandidateView.clear();
1692            }
1693            mKeyboardSwitcher.updateShiftState();
1694            if (ic != null) {
1695                ic.endBatchEdit();
1696            }
1697            return;
1698        }
1699
1700        // If this is a punctuation, apply it through the normal key press
1701        if (suggestion.length() == 1 && (mSettingsValues.isWordSeparator(suggestion.charAt(0))
1702                || mSettingsValues.isSuggestedPunctuation(suggestion.charAt(0)))) {
1703            // Word separators are suggested before the user inputs something.
1704            // So, LatinImeLogger logs "" as a user's input.
1705            LatinImeLogger.logOnManualSuggestion(
1706                    "", suggestion.toString(), index, suggestions.mWords);
1707            // Find out whether the previous character is a space. If it is, as a special case
1708            // for punctuation entered through the suggestion strip, it should be considered
1709            // a magic space even if it was a normal space. This is meant to help in case the user
1710            // pressed space on purpose of displaying the suggestion strip punctuation.
1711            final int rawPrimaryCode = suggestion.charAt(0);
1712            // Maybe apply the "bidi mirrored" conversions for parentheses
1713            final LatinKeyboard keyboard = mKeyboardSwitcher.getLatinKeyboard();
1714            final int primaryCode = keyboard.mIsRtlKeyboard
1715                    ? Key.getRtlParenthesisCode(rawPrimaryCode) : rawPrimaryCode;
1716
1717            final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : "";
1718            final int toLeft = (ic == null || TextUtils.isEmpty(beforeText))
1719                    ? 0 : beforeText.charAt(0);
1720            final boolean oldMagicSpace = mJustAddedMagicSpace;
1721            if (Keyboard.CODE_SPACE == toLeft) mJustAddedMagicSpace = true;
1722            onCodeInput(primaryCode, new int[] { primaryCode },
1723                    KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
1724                    KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
1725            mJustAddedMagicSpace = oldMagicSpace;
1726            if (ic != null) {
1727                ic.endBatchEdit();
1728            }
1729            return;
1730        }
1731        if (!mHasUncommittedTypedChars) {
1732            // If we are not composing a word, then it was a suggestion inferred from
1733            // context - no user input. We should reset the word composer.
1734            mWordComposer.reset();
1735        }
1736        mExpectingUpdateSelection = true;
1737        commitBestWord(suggestion);
1738        // Add the word to the auto dictionary if it's not a known word
1739        if (index == 0) {
1740            addToUserUnigramAndBigramDictionaries(suggestion,
1741                    UserUnigramDictionary.FREQUENCY_FOR_PICKED);
1742        } else {
1743            addToOnlyBigramDictionary(suggestion, 1);
1744        }
1745        LatinImeLogger.logOnManualSuggestion(mComposingStringBuilder.toString(),
1746                suggestion.toString(), index, suggestions.mWords);
1747        TextEntryState.acceptedSuggestion(mComposingStringBuilder.toString(), suggestion);
1748        // Follow it with a space
1749        if (mShouldInsertMagicSpace && !recorrecting) {
1750            sendMagicSpace();
1751        }
1752
1753        // We should show the hint if the user pressed the first entry AND either:
1754        // - There is no dictionary (we know that because we tried to load it => null != mSuggest
1755        //   AND mHasDictionary is false)
1756        // - There is a dictionary and the word is not in it
1757        // Please note that if mSuggest is null, it means that everything is off: suggestion
1758        // and correction, so we shouldn't try to show the hint
1759        // We used to look at mCorrectionMode here, but showing the hint should have nothing
1760        // to do with the autocorrection setting.
1761        final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
1762                // If there is no dictionary the hint should be shown.
1763                && (!mHasDictionary
1764                        // If "suggestion" is not in the dictionary, the hint should be shown.
1765                        || !AutoCorrection.isValidWord(
1766                                mSuggest.getUnigramDictionaries(), suggestion, true));
1767
1768        if (!recorrecting) {
1769            // Fool the state watcher so that a subsequent backspace will not do a revert, unless
1770            // we just did a correction, in which case we need to stay in
1771            // TextEntryState.State.PICKED_SUGGESTION state.
1772            TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true,
1773                    WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
1774        }
1775        if (!showingAddToDictionaryHint) {
1776            // If we're not showing the "Touch again to save", then show corrections again.
1777            // In case the cursor position doesn't change, make sure we show the suggestions again.
1778            updateBigramPredictions();
1779            // Updating the predictions right away may be slow and feel unresponsive on slower
1780            // terminals. On the other hand if we just postUpdateBigramPredictions() it will
1781            // take a noticeable delay to update them which may feel uneasy.
1782        }
1783        if (showingAddToDictionaryHint) {
1784            if (mIsUserDictionaryAvaliable) {
1785                mCandidateView.showAddToDictionaryHint(suggestion);
1786            } else {
1787                mHandler.postUpdateSuggestions();
1788            }
1789        }
1790        if (ic != null) {
1791            ic.endBatchEdit();
1792        }
1793    }
1794
1795    /**
1796     * Commits the chosen word to the text field and saves it for later retrieval.
1797     */
1798    private void commitBestWord(CharSequence bestWord) {
1799        final KeyboardSwitcher switcher = mKeyboardSwitcher;
1800        if (!switcher.isKeyboardAvailable())
1801            return;
1802        final InputConnection ic = getCurrentInputConnection();
1803        if (ic != null) {
1804            mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators);
1805            SuggestedWords suggestedWords = mCandidateView.getSuggestions();
1806            ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
1807                    this, bestWord, suggestedWords), 1);
1808        }
1809        mRecorrection.saveRecorrectionSuggestion(mWordComposer, bestWord);
1810        mHasUncommittedTypedChars = false;
1811        mCommittedLength = bestWord.length();
1812    }
1813
1814    private static final WordComposer sEmptyWordComposer = new WordComposer();
1815    public void updateBigramPredictions() {
1816        if (mSuggest == null || !isSuggestionsRequested())
1817            return;
1818
1819        if (!mSettingsValues.mBigramPredictionEnabled) {
1820            setPunctuationSuggestions();
1821            return;
1822        }
1823
1824        final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
1825                mSettingsValues.mWordSeparators);
1826        SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
1827                mKeyboardSwitcher.getKeyboardView(), sEmptyWordComposer, prevWord);
1828
1829        if (builder.size() > 0) {
1830            // Explicitly supply an empty typed word (the no-second-arg version of
1831            // showSuggestions will retrieve the word near the cursor, we don't want that here)
1832            showSuggestions(builder.build(), "");
1833        } else {
1834            if (!isShowingPunctuationList()) setPunctuationSuggestions();
1835        }
1836    }
1837
1838    public void setPunctuationSuggestions() {
1839        setSuggestions(mSettingsValues.mSuggestPuncList);
1840        setSuggestionStripShown(isCandidateStripVisible());
1841    }
1842
1843    private void addToUserUnigramAndBigramDictionaries(CharSequence suggestion,
1844            int frequencyDelta) {
1845        checkAddToDictionary(suggestion, frequencyDelta, false);
1846    }
1847
1848    private void addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta) {
1849        checkAddToDictionary(suggestion, frequencyDelta, true);
1850    }
1851
1852    /**
1853     * Adds to the UserBigramDictionary and/or UserUnigramDictionary
1854     * @param selectedANotTypedWord true if it should be added to bigram dictionary if possible
1855     */
1856    private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta,
1857            boolean selectedANotTypedWord) {
1858        if (suggestion == null || suggestion.length() < 1) return;
1859
1860        // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
1861        // adding words in situations where the user or application really didn't
1862        // want corrections enabled or learned.
1863        if (!(mCorrectionMode == Suggest.CORRECTION_FULL
1864                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
1865            return;
1866        }
1867
1868        final boolean selectedATypedWordAndItsInUserUnigramDic =
1869                !selectedANotTypedWord && mUserUnigramDictionary.isValidWord(suggestion);
1870        final boolean isValidWord = AutoCorrection.isValidWord(
1871                mSuggest.getUnigramDictionaries(), suggestion, true);
1872        final boolean needsToAddToUserUnigramDictionary = selectedATypedWordAndItsInUserUnigramDic
1873                || !isValidWord;
1874        if (needsToAddToUserUnigramDictionary) {
1875            mUserUnigramDictionary.addWord(suggestion.toString(), frequencyDelta);
1876        }
1877
1878        if (mUserBigramDictionary != null) {
1879            // We don't want to register as bigrams words separated by a separator.
1880            // For example "I will, and you too" : we don't want the pair ("will" "and") to be
1881            // a bigram.
1882            final InputConnection ic = getCurrentInputConnection();
1883            if (null != ic) {
1884                final CharSequence prevWord =
1885                        EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
1886                if (!TextUtils.isEmpty(prevWord)) {
1887                    mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString());
1888                }
1889            }
1890        }
1891    }
1892
1893    public boolean isCursorTouchingWord() {
1894        final InputConnection ic = getCurrentInputConnection();
1895        if (ic == null) return false;
1896        CharSequence toLeft = ic.getTextBeforeCursor(1, 0);
1897        CharSequence toRight = ic.getTextAfterCursor(1, 0);
1898        if (!TextUtils.isEmpty(toLeft)
1899                && !mSettingsValues.isWordSeparator(toLeft.charAt(0))
1900                && !mSettingsValues.isSuggestedPunctuation(toLeft.charAt(0))) {
1901            return true;
1902        }
1903        if (!TextUtils.isEmpty(toRight)
1904                && !mSettingsValues.isWordSeparator(toRight.charAt(0))
1905                && !mSettingsValues.isSuggestedPunctuation(toRight.charAt(0))) {
1906            return true;
1907        }
1908        return false;
1909    }
1910
1911    // "ic" must not null
1912    private boolean sameAsTextBeforeCursor(final InputConnection ic, CharSequence text) {
1913        CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
1914        return TextUtils.equals(text, beforeText);
1915    }
1916
1917    // "ic" must not null
1918    private void revertLastWord(final InputConnection ic) {
1919        if (mHasUncommittedTypedChars || mComposingStringBuilder.length() <= 0) {
1920            sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1921            return;
1922        }
1923
1924        final CharSequence separator = ic.getTextBeforeCursor(1, 0);
1925        ic.deleteSurroundingText(mCommittedLength + 1 /* separator */, 0);
1926
1927        // Re-insert "separator" only when the deleted character was word separator and the
1928        // composing text wasn't equal to the auto-corrected text which can be found before
1929        // the cursor.
1930        if (!TextUtils.isEmpty(separator)
1931                && mSettingsValues.isWordSeparator(separator.charAt(0))
1932                && !TextUtils.equals(mComposingStringBuilder,
1933                        ic.getTextBeforeCursor(mCommittedLength, 0))) {
1934            ic.commitText(mComposingStringBuilder, 1);
1935            TextEntryState.acceptedTyped(mComposingStringBuilder);
1936            ic.commitText(separator, 1);
1937            TextEntryState.typedCharacter(separator.charAt(0), true,
1938                    WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
1939            // Clear composing text
1940            mComposingStringBuilder.setLength(0);
1941        } else {
1942            mHasUncommittedTypedChars = true;
1943            ic.setComposingText(mComposingStringBuilder, 1);
1944            TextEntryState.backspace();
1945        }
1946        mHandler.cancelUpdateBigramPredictions();
1947        mHandler.postUpdateSuggestions();
1948    }
1949
1950    // "ic" must not null
1951    private boolean revertDoubleSpace(final InputConnection ic) {
1952        mHandler.cancelDoubleSpacesTimer();
1953        // Here we test whether we indeed have a period and a space before us. This should not
1954        // be needed, but it's there just in case something went wrong.
1955        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
1956        if (!". ".equals(textBeforeCursor))
1957            return false;
1958        ic.beginBatchEdit();
1959        ic.deleteSurroundingText(2, 0);
1960        ic.commitText("  ", 1);
1961        ic.endBatchEdit();
1962        return true;
1963    }
1964
1965    public boolean isWordSeparator(int code) {
1966        return mSettingsValues.isWordSeparator(code);
1967    }
1968
1969    private void sendMagicSpace() {
1970        sendKeyChar((char)Keyboard.CODE_SPACE);
1971        mJustAddedMagicSpace = true;
1972        mKeyboardSwitcher.updateShiftState();
1973    }
1974
1975    public boolean preferCapitalization() {
1976        return mWordComposer.isFirstCharCapitalized();
1977    }
1978
1979    // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
1980    // according to new language or mode.
1981    public void onRefreshKeyboard() {
1982        if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
1983            // Before Honeycomb, Voice IME is in LatinIME and it changes the current input view,
1984            // so that we need to re-create the keyboard input view here.
1985            setInputView(mKeyboardSwitcher.onCreateInputView());
1986        }
1987        // Reload keyboard because the current language has been changed.
1988        mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
1989        initSuggest();
1990        loadSettings();
1991    }
1992
1993    @Override
1994    public void onPress(int primaryCode, boolean withSliding) {
1995        final KeyboardSwitcher switcher = mKeyboardSwitcher;
1996        switcher.registerWindowWidth();
1997        if (switcher.isVibrateAndSoundFeedbackRequired()) {
1998            vibrate();
1999            playKeyClick(primaryCode);
2000        }
2001        final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
2002        if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
2003            switcher.onPressShift(withSliding);
2004        } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
2005            switcher.onPressSymbol();
2006        } else {
2007            switcher.onOtherKeyPressed();
2008        }
2009    }
2010
2011    @Override
2012    public void onRelease(int primaryCode, boolean withSliding) {
2013        KeyboardSwitcher switcher = mKeyboardSwitcher;
2014        // Reset any drag flags in the keyboard
2015        final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
2016        if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
2017            switcher.onReleaseShift(withSliding);
2018        } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
2019            switcher.onReleaseSymbol();
2020        }
2021    }
2022
2023
2024    // receive ringer mode change and network state change.
2025    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2026        @Override
2027        public void onReceive(Context context, Intent intent) {
2028            final String action = intent.getAction();
2029            if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2030                updateRingerMode();
2031            } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2032                mSubtypeSwitcher.onNetworkStateChanged(intent);
2033            }
2034        }
2035    };
2036
2037    // update flags for silent mode
2038    private void updateRingerMode() {
2039        if (mAudioManager == null) {
2040            mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
2041        }
2042        if (mAudioManager != null) {
2043            mSilentModeOn = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
2044        }
2045    }
2046
2047    private void playKeyClick(int primaryCode) {
2048        // if mAudioManager is null, we don't have the ringer state yet
2049        // mAudioManager will be set by updateRingerMode
2050        if (mAudioManager == null) {
2051            if (mKeyboardSwitcher.getKeyboardView() != null) {
2052                updateRingerMode();
2053            }
2054        }
2055        if (isSoundOn()) {
2056            // FIXME: Volume and enable should come from UI settings
2057            // FIXME: These should be triggered after auto-repeat logic
2058            int sound = AudioManager.FX_KEYPRESS_STANDARD;
2059            switch (primaryCode) {
2060                case Keyboard.CODE_DELETE:
2061                    sound = AudioManager.FX_KEYPRESS_DELETE;
2062                    break;
2063                case Keyboard.CODE_ENTER:
2064                    sound = AudioManager.FX_KEYPRESS_RETURN;
2065                    break;
2066                case Keyboard.CODE_SPACE:
2067                    sound = AudioManager.FX_KEYPRESS_SPACEBAR;
2068                    break;
2069            }
2070            mAudioManager.playSoundEffect(sound, FX_VOLUME);
2071        }
2072    }
2073
2074    public void vibrate() {
2075        if (!mSettingsValues.mVibrateOn) {
2076            return;
2077        }
2078        LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
2079        if (inputView != null) {
2080            inputView.performHapticFeedback(
2081                    HapticFeedbackConstants.KEYBOARD_TAP,
2082                    HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
2083        }
2084    }
2085
2086    public WordComposer getCurrentWord() {
2087        return mWordComposer;
2088    }
2089
2090    boolean isSoundOn() {
2091        return mSettingsValues.mSoundOn && !mSilentModeOn;
2092    }
2093
2094    private void updateCorrectionMode() {
2095        // TODO: cleanup messy flags
2096        mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false;
2097        final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
2098                && !mInputTypeNoAutoCorrect && mHasDictionary;
2099        mCorrectionMode = (shouldAutoCorrect && mSettingsValues.mAutoCorrectEnabled)
2100                ? Suggest.CORRECTION_FULL
2101                : (shouldAutoCorrect ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE);
2102        mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect
2103                && mSettingsValues.mAutoCorrectEnabled)
2104                ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
2105        if (mSuggest != null) {
2106            mSuggest.setCorrectionMode(mCorrectionMode);
2107        }
2108    }
2109
2110    private void updateAutoTextEnabled() {
2111        if (mSuggest == null) return;
2112        // We want to use autotext if the settings are asking for auto corrections, and if
2113        // the input language is the same as the system language (because autotext will only
2114        // work in the system language so if we are entering text in a different language we
2115        // do not want it on).
2116        // We used to look at the "quick fixes" option instead of mAutoCorrectEnabled, but
2117        // this option was redundant and confusing and therefore removed.
2118        mSuggest.setQuickFixesEnabled(mSettingsValues.mAutoCorrectEnabled
2119                && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage());
2120    }
2121
2122    private void updateSuggestionVisibility(final SharedPreferences prefs, final Resources res) {
2123        final String suggestionVisiblityStr = prefs.getString(
2124                Settings.PREF_SHOW_SUGGESTIONS_SETTING,
2125                res.getString(R.string.prefs_suggestion_visibility_default_value));
2126        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
2127            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
2128                mSuggestionVisibility = visibility;
2129                break;
2130            }
2131        }
2132    }
2133
2134    protected void launchSettings() {
2135        launchSettings(Settings.class);
2136    }
2137
2138    public void launchDebugSettings() {
2139        launchSettings(DebugSettings.class);
2140    }
2141
2142    protected void launchSettings(Class<? extends PreferenceActivity> settingsClass) {
2143        handleClose();
2144        Intent intent = new Intent();
2145        intent.setClass(LatinIME.this, settingsClass);
2146        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2147        startActivity(intent);
2148    }
2149
2150    private void showSubtypeSelectorAndSettings() {
2151        final CharSequence title = getString(R.string.english_ime_input_options);
2152        final CharSequence[] items = new CharSequence[] {
2153                // TODO: Should use new string "Select active input modes".
2154                getString(R.string.language_selection_title),
2155                getString(R.string.english_ime_settings),
2156        };
2157        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2158            @Override
2159            public void onClick(DialogInterface di, int position) {
2160                di.dismiss();
2161                switch (position) {
2162                case 0:
2163                    Intent intent = CompatUtils.getInputLanguageSelectionIntent(
2164                            mInputMethodId, Intent.FLAG_ACTIVITY_NEW_TASK
2165                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2166                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2167                    startActivity(intent);
2168                    break;
2169                case 1:
2170                    launchSettings();
2171                    break;
2172                }
2173            }
2174        };
2175        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2176                .setItems(items, listener)
2177                .setTitle(title);
2178        showOptionDialogInternal(builder.create());
2179    }
2180
2181    private void showOptionsMenu() {
2182        final CharSequence title = getString(R.string.english_ime_input_options);
2183        final CharSequence[] items = new CharSequence[] {
2184                getString(R.string.selectInputMethod),
2185                getString(R.string.english_ime_settings),
2186        };
2187        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2188            @Override
2189            public void onClick(DialogInterface di, int position) {
2190                di.dismiss();
2191                switch (position) {
2192                case 0:
2193                    mImm.showInputMethodPicker();
2194                    break;
2195                case 1:
2196                    launchSettings();
2197                    break;
2198                }
2199            }
2200        };
2201        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2202                .setItems(items, listener)
2203                .setTitle(title);
2204        showOptionDialogInternal(builder.create());
2205    }
2206
2207    @Override
2208    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
2209        super.dump(fd, fout, args);
2210
2211        final Printer p = new PrintWriterPrinter(fout);
2212        p.println("LatinIME state :");
2213        p.println("  Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode());
2214        p.println("  mComposingStringBuilder=" + mComposingStringBuilder.toString());
2215        p.println("  mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn);
2216        p.println("  mCorrectionMode=" + mCorrectionMode);
2217        p.println("  mHasUncommittedTypedChars=" + mHasUncommittedTypedChars);
2218        p.println("  mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
2219        p.println("  mShouldInsertMagicSpace=" + mShouldInsertMagicSpace);
2220        p.println("  mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn);
2221        p.println("  TextEntryState.state=" + TextEntryState.getState());
2222        p.println("  mSoundOn=" + mSettingsValues.mSoundOn);
2223        p.println("  mVibrateOn=" + mSettingsValues.mVibrateOn);
2224        p.println("  mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
2225    }
2226
2227    // Characters per second measurement
2228
2229    private long mLastCpsTime;
2230    private static final int CPS_BUFFER_SIZE = 16;
2231    private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE];
2232    private int mCpsIndex;
2233
2234    private void measureCps() {
2235        long now = System.currentTimeMillis();
2236        if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial
2237        mCpsIntervals[mCpsIndex] = now - mLastCpsTime;
2238        mLastCpsTime = now;
2239        mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE;
2240        long total = 0;
2241        for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i];
2242        System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total));
2243    }
2244}
2245