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