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