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