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