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