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