LatinIME.java revision 68823ae08e820f0951447ed12c1bd32a24333d2e
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.graphics.Rect;
29import android.inputmethodservice.InputMethodService;
30import android.media.AudioManager;
31import android.net.ConnectivityManager;
32import android.os.Debug;
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.Log;
41import android.util.PrintWriterPrinter;
42import android.util.Printer;
43import android.view.KeyEvent;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.ViewGroup.LayoutParams;
47import android.view.ViewParent;
48import android.view.inputmethod.CompletionInfo;
49import android.view.inputmethod.EditorInfo;
50import android.view.inputmethod.ExtractedText;
51import android.view.inputmethod.InputConnection;
52
53import com.android.inputmethod.accessibility.AccessibilityUtils;
54import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
55import com.android.inputmethod.compat.CompatUtils;
56import com.android.inputmethod.compat.EditorInfoCompatUtils;
57import com.android.inputmethod.compat.InputConnectionCompatUtils;
58import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
59import com.android.inputmethod.compat.InputMethodServiceCompatWrapper;
60import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper;
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.keyboard.Keyboard;
66import com.android.inputmethod.keyboard.KeyboardActionListener;
67import com.android.inputmethod.keyboard.KeyboardId;
68import com.android.inputmethod.keyboard.KeyboardSwitcher;
69import com.android.inputmethod.keyboard.KeyboardView;
70import com.android.inputmethod.keyboard.LatinKeyboardView;
71import com.android.inputmethod.latin.suggestions.SuggestionsView;
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        SuggestionsView.Listener {
82    private static final String TAG = LatinIME.class.getSimpleName();
83    private static final boolean TRACE = false;
84    private static boolean DEBUG;
85
86    /**
87     * The private IME option used to indicate that no microphone should be
88     * shown for a given text field. For instance, this is specified by the
89     * search dialog when the dialog is already showing a voice search button.
90     *
91     * @deprecated Use {@link LatinIME#IME_OPTION_NO_MICROPHONE} with package name prefixed.
92     */
93    @SuppressWarnings("dep-ann")
94    public static final String IME_OPTION_NO_MICROPHONE_COMPAT = "nm";
95
96    /**
97     * The private IME option used to indicate that no microphone should be
98     * shown for a given text field. For instance, this is specified by the
99     * search dialog when the dialog is already showing a voice search button.
100     */
101    public static final String IME_OPTION_NO_MICROPHONE = "noMicrophoneKey";
102
103    /**
104     * The private IME option used to indicate that no settings key should be
105     * shown for a given text field.
106     */
107    public static final String IME_OPTION_NO_SETTINGS_KEY = "noSettingsKey";
108
109    /**
110     * The private IME option used to indicate that the given text field needs
111     * ASCII code points input.
112     *
113     * @deprecated Use {@link EditorInfo#IME_FLAG_FORCE_ASCII}.
114     */
115    @SuppressWarnings("dep-ann")
116    public static final String IME_OPTION_FORCE_ASCII = "forceAscii";
117
118    /**
119     * The subtype extra value used to indicate that the subtype keyboard layout is capable for
120     * typing ASCII characters.
121     */
122    public static final String SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE = "AsciiCapable";
123
124    /**
125     * The subtype extra value used to indicate that the subtype keyboard layout supports touch
126     * position correction.
127     */
128    public static final String SUBTYPE_EXTRA_VALUE_SUPPORT_TOUCH_POSITION_CORRECTION =
129            "SupportTouchPositionCorrection";
130    /**
131     * The subtype extra value used to indicate that the subtype keyboard layout should be loaded
132     * from the specified locale.
133     */
134    public static final String SUBTYPE_EXTRA_VALUE_KEYBOARD_LOCALE = "KeyboardLocale";
135
136    private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
137
138    // How many continuous deletes at which to start deleting at a higher speed.
139    private static final int DELETE_ACCELERATE_AT = 20;
140    // Key events coming any faster than this are long-presses.
141    private static final int QUICK_PRESS = 200;
142
143    private static final int PENDING_IMS_CALLBACK_DURATION = 800;
144
145    /**
146     * The name of the scheme used by the Package Manager to warn of a new package installation,
147     * replacement or removal.
148     */
149    private static final String SCHEME_PACKAGE = "package";
150
151    // TODO: migrate this to SettingsValues
152    private int mSuggestionVisibility;
153    private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
154            = R.string.prefs_suggestion_visibility_show_value;
155    private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
156            = R.string.prefs_suggestion_visibility_show_only_portrait_value;
157    private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
158            = R.string.prefs_suggestion_visibility_hide_value;
159
160    private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
161        SUGGESTION_VISIBILILTY_SHOW_VALUE,
162        SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
163        SUGGESTION_VISIBILILTY_HIDE_VALUE
164    };
165
166    private static final int SPACE_STATE_NONE = 0;
167    // Double space: the state where the user pressed space twice quickly, which LatinIME
168    // resolved as period-space. Undoing this converts the period to a space.
169    private static final int SPACE_STATE_DOUBLE = 1;
170    // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
171    // have just been swapped. Undoing this swaps them back; the space is still considered weak.
172    private static final int SPACE_STATE_SWAP_PUNCTUATION = 2;
173    // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
174    // spaces happen when the user presses space, accepting the current suggestion (whether
175    // it's an auto-correction or not).
176    private static final int SPACE_STATE_WEAK = 3;
177    // Phantom space: a not-yet-inserted space that should get inserted on the next input,
178    // character provided it's not a separator. If it's a separator, the phantom space is dropped.
179    // Phantom spaces happen when a user chooses a word from the suggestion strip.
180    private static final int SPACE_STATE_PHANTOM = 4;
181
182    // Current space state of the input method. This can be any of the above constants.
183    private int mSpaceState;
184
185    private SettingsValues mSettingsValues;
186    private InputAttributes mInputAttributes;
187
188    private View mExtractArea;
189    private View mKeyPreviewBackingView;
190    private View mSuggestionsContainer;
191    private SuggestionsView mSuggestionsView;
192    /* package for tests */ Suggest mSuggest;
193    private CompletionInfo[] mApplicationSpecifiedCompletions;
194
195    private InputMethodManagerCompatWrapper mImm;
196    private Resources mResources;
197    private SharedPreferences mPrefs;
198    private final KeyboardSwitcher mKeyboardSwitcher;
199    private final SubtypeSwitcher mSubtypeSwitcher;
200    private VoiceProxy mVoiceProxy;
201    private boolean mShouldSwitchToLastSubtype = true;
202
203    private UserDictionary mUserDictionary;
204    private UserBigramDictionary mUserBigramDictionary;
205    private UserUnigramDictionary mUserUnigramDictionary;
206    private boolean mIsUserDictionaryAvailable;
207
208    private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
209    private WordComposer mWordComposer = new WordComposer();
210
211    private int mCorrectionMode;
212
213    // Keep track of the last selection range to decide if we need to show word alternatives
214    private static final int NOT_A_CURSOR_POSITION = -1;
215    private int mLastSelectionStart = NOT_A_CURSOR_POSITION;
216    private int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
217
218    // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
219    // "expect" it, it means the user actually moved the cursor.
220    private boolean mExpectingUpdateSelection;
221    private int mDeleteCount;
222    private long mLastKeyTime;
223
224    private AudioAndHapticFeedbackManager mFeedbackManager;
225
226    // Member variables for remembering the current device orientation.
227    private int mDisplayOrientation;
228
229    // Object for reacting to adding/removing a dictionary pack.
230    private BroadcastReceiver mDictionaryPackInstallReceiver =
231            new DictionaryPackInstallBroadcastReceiver(this);
232
233    // Keeps track of most recently inserted text (multi-character key) for reverting
234    private CharSequence mEnteredText;
235
236    private boolean mIsAutoCorrectionIndicatorOn;
237
238    public final UIHandler mHandler = new UIHandler(this);
239
240    public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
241        private static final int MSG_UPDATE_SHIFT_STATE = 1;
242        private static final int MSG_VOICE_RESULTS = 2;
243        private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 3;
244        private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 4;
245        private static final int MSG_SPACE_TYPED = 5;
246        private static final int MSG_SET_BIGRAM_PREDICTIONS = 6;
247        private static final int MSG_PENDING_IMS_CALLBACK = 7;
248        private static final int MSG_UPDATE_SUGGESTIONS = 8;
249
250        private int mDelayBeforeFadeoutLanguageOnSpacebar;
251        private int mDelayUpdateSuggestions;
252        private int mDelayUpdateShiftState;
253        private int mDurationOfFadeoutLanguageOnSpacebar;
254        private float mFinalFadeoutFactorOfLanguageOnSpacebar;
255        private long mDoubleSpacesTurnIntoPeriodTimeout;
256
257        public UIHandler(LatinIME outerInstance) {
258            super(outerInstance);
259        }
260
261        public void onCreate() {
262            final Resources res = getOuterInstance().getResources();
263            mDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger(
264                    R.integer.config_delay_before_fadeout_language_on_spacebar);
265            mDelayUpdateSuggestions =
266                    res.getInteger(R.integer.config_delay_update_suggestions);
267            mDelayUpdateShiftState =
268                    res.getInteger(R.integer.config_delay_update_shift_state);
269            mDurationOfFadeoutLanguageOnSpacebar = res.getInteger(
270                    R.integer.config_duration_of_fadeout_language_on_spacebar);
271            mFinalFadeoutFactorOfLanguageOnSpacebar = res.getInteger(
272                    R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f;
273            mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger(
274                    R.integer.config_double_spaces_turn_into_period_timeout);
275        }
276
277        @Override
278        public void handleMessage(Message msg) {
279            final LatinIME latinIme = getOuterInstance();
280            final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
281            final LatinKeyboardView inputView = switcher.getKeyboardView();
282            switch (msg.what) {
283            case MSG_UPDATE_SUGGESTIONS:
284                latinIme.updateSuggestions();
285                break;
286            case MSG_UPDATE_SHIFT_STATE:
287                switcher.updateShiftState();
288                break;
289            case MSG_SET_BIGRAM_PREDICTIONS:
290                latinIme.updateBigramPredictions();
291                break;
292            case MSG_VOICE_RESULTS:
293                final Keyboard keyboard = switcher.getKeyboard();
294                latinIme.mVoiceProxy.handleVoiceResults(latinIme.preferCapitalization()
295                        || (keyboard != null && keyboard.isShiftedOrShiftLocked()));
296                break;
297            case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR:
298                setSpacebarTextFadeFactor(inputView,
299                        (1.0f + mFinalFadeoutFactorOfLanguageOnSpacebar) / 2,
300                        (Keyboard)msg.obj);
301                sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj),
302                        mDurationOfFadeoutLanguageOnSpacebar);
303                break;
304            case MSG_DISMISS_LANGUAGE_ON_SPACEBAR:
305                setSpacebarTextFadeFactor(inputView, mFinalFadeoutFactorOfLanguageOnSpacebar,
306                        (Keyboard)msg.obj);
307                break;
308            }
309        }
310
311        public void postUpdateSuggestions() {
312            removeMessages(MSG_UPDATE_SUGGESTIONS);
313            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions);
314        }
315
316        public void cancelUpdateSuggestions() {
317            removeMessages(MSG_UPDATE_SUGGESTIONS);
318        }
319
320        public boolean hasPendingUpdateSuggestions() {
321            return hasMessages(MSG_UPDATE_SUGGESTIONS);
322        }
323
324        public void postUpdateShiftState() {
325            removeMessages(MSG_UPDATE_SHIFT_STATE);
326            sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
327        }
328
329        public void cancelUpdateShiftState() {
330            removeMessages(MSG_UPDATE_SHIFT_STATE);
331        }
332
333        public void postUpdateBigramPredictions() {
334            removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
335            sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions);
336        }
337
338        public void cancelUpdateBigramPredictions() {
339            removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
340        }
341
342        public void updateVoiceResults() {
343            sendMessage(obtainMessage(MSG_VOICE_RESULTS));
344        }
345
346        private static void setSpacebarTextFadeFactor(LatinKeyboardView inputView,
347                float fadeFactor, Keyboard oldKeyboard) {
348            if (inputView == null) return;
349            final Keyboard keyboard = inputView.getKeyboard();
350            if (keyboard == oldKeyboard) {
351                inputView.updateSpacebar(fadeFactor,
352                        SubtypeSwitcher.getInstance().needsToDisplayLanguage(
353                                keyboard.mId.mLocale));
354            }
355        }
356
357        public void startDisplayLanguageOnSpacebar(boolean localeChanged) {
358            final LatinIME latinIme = getOuterInstance();
359            removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR);
360            removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR);
361            final LatinKeyboardView inputView = latinIme.mKeyboardSwitcher.getKeyboardView();
362            if (inputView != null) {
363                final Keyboard keyboard = latinIme.mKeyboardSwitcher.getKeyboard();
364                // The language is always displayed when the delay is negative.
365                final boolean needsToDisplayLanguage = localeChanged
366                        || mDelayBeforeFadeoutLanguageOnSpacebar < 0;
367                // The language is never displayed when the delay is zero.
368                if (mDelayBeforeFadeoutLanguageOnSpacebar != 0) {
369                    setSpacebarTextFadeFactor(inputView,
370                            needsToDisplayLanguage ? 1.0f : mFinalFadeoutFactorOfLanguageOnSpacebar,
371                                    keyboard);
372                }
373                // The fadeout animation will start when the delay is positive.
374                if (localeChanged && mDelayBeforeFadeoutLanguageOnSpacebar > 0) {
375                    sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard),
376                            mDelayBeforeFadeoutLanguageOnSpacebar);
377                }
378            }
379        }
380
381        public void startDoubleSpacesTimer() {
382            removeMessages(MSG_SPACE_TYPED);
383            sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), mDoubleSpacesTurnIntoPeriodTimeout);
384        }
385
386        public void cancelDoubleSpacesTimer() {
387            removeMessages(MSG_SPACE_TYPED);
388        }
389
390        public boolean isAcceptingDoubleSpaces() {
391            return hasMessages(MSG_SPACE_TYPED);
392        }
393
394        // Working variables for the following methods.
395        private boolean mIsOrientationChanging;
396        private boolean mPendingSuccessiveImsCallback;
397        private boolean mHasPendingStartInput;
398        private boolean mHasPendingFinishInputView;
399        private boolean mHasPendingFinishInput;
400        private EditorInfo mAppliedEditorInfo;
401
402        public void startOrientationChanging() {
403            removeMessages(MSG_PENDING_IMS_CALLBACK);
404            resetPendingImsCallback();
405            mIsOrientationChanging = true;
406            final LatinIME latinIme = getOuterInstance();
407            if (latinIme.isInputViewShown()) {
408                latinIme.mKeyboardSwitcher.saveKeyboardState();
409            }
410        }
411
412        private void resetPendingImsCallback() {
413            mHasPendingFinishInputView = false;
414            mHasPendingFinishInput = false;
415            mHasPendingStartInput = false;
416        }
417
418        private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo,
419                boolean restarting) {
420            if (mHasPendingFinishInputView)
421                latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
422            if (mHasPendingFinishInput)
423                latinIme.onFinishInputInternal();
424            if (mHasPendingStartInput)
425                latinIme.onStartInputInternal(editorInfo, restarting);
426            resetPendingImsCallback();
427        }
428
429        public void onStartInput(EditorInfo editorInfo, boolean restarting) {
430            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
431                // Typically this is the second onStartInput after orientation changed.
432                mHasPendingStartInput = true;
433            } else {
434                if (mIsOrientationChanging && restarting) {
435                    // This is the first onStartInput after orientation changed.
436                    mIsOrientationChanging = false;
437                    mPendingSuccessiveImsCallback = true;
438                }
439                final LatinIME latinIme = getOuterInstance();
440                executePendingImsCallback(latinIme, editorInfo, restarting);
441                latinIme.onStartInputInternal(editorInfo, restarting);
442            }
443        }
444
445        public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
446            if (hasMessages(MSG_PENDING_IMS_CALLBACK)
447                    && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
448                // Typically this is the second onStartInputView after orientation changed.
449                resetPendingImsCallback();
450            } else {
451                if (mPendingSuccessiveImsCallback) {
452                    // This is the first onStartInputView after orientation changed.
453                    mPendingSuccessiveImsCallback = false;
454                    resetPendingImsCallback();
455                    sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
456                            PENDING_IMS_CALLBACK_DURATION);
457                }
458                final LatinIME latinIme = getOuterInstance();
459                executePendingImsCallback(latinIme, editorInfo, restarting);
460                latinIme.onStartInputViewInternal(editorInfo, restarting);
461                mAppliedEditorInfo = editorInfo;
462            }
463        }
464
465        public void onFinishInputView(boolean finishingInput) {
466            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
467                // Typically this is the first onFinishInputView after orientation changed.
468                mHasPendingFinishInputView = true;
469            } else {
470                final LatinIME latinIme = getOuterInstance();
471                latinIme.onFinishInputViewInternal(finishingInput);
472                mAppliedEditorInfo = null;
473            }
474        }
475
476        public void onFinishInput() {
477            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
478                // Typically this is the first onFinishInput after orientation changed.
479                mHasPendingFinishInput = true;
480            } else {
481                final LatinIME latinIme = getOuterInstance();
482                executePendingImsCallback(latinIme, null, false);
483                latinIme.onFinishInputInternal();
484            }
485        }
486    }
487
488    public LatinIME() {
489        super();
490        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
491        mKeyboardSwitcher = KeyboardSwitcher.getInstance();
492    }
493
494    @Override
495    public void onCreate() {
496        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
497        mPrefs = prefs;
498        LatinImeLogger.init(this, prefs);
499        LanguageSwitcherProxy.init(this, prefs);
500        InputMethodManagerCompatWrapper.init(this);
501        SubtypeSwitcher.init(this);
502        KeyboardSwitcher.init(this, prefs);
503        AccessibilityUtils.init(this);
504
505        super.onCreate();
506
507        mImm = InputMethodManagerCompatWrapper.getInstance();
508        mHandler.onCreate();
509        DEBUG = LatinImeLogger.sDBG;
510
511        final Resources res = getResources();
512        mResources = res;
513
514        loadSettings();
515
516        // TODO: remove the following when it's not needed by updateCorrectionMode() any more
517        mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
518        Utils.GCUtils.getInstance().reset();
519        boolean tryGC = true;
520        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
521            try {
522                initSuggest();
523                tryGC = false;
524            } catch (OutOfMemoryError e) {
525                tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
526            }
527        }
528
529        mDisplayOrientation = res.getConfiguration().orientation;
530
531        // Register to receive ringer mode change and network state change.
532        // Also receive installation and removal of a dictionary pack.
533        final IntentFilter filter = new IntentFilter();
534        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
535        filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
536        registerReceiver(mReceiver, filter);
537        mVoiceProxy = VoiceProxy.init(this, prefs, mHandler);
538
539        final IntentFilter packageFilter = new IntentFilter();
540        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
541        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
542        packageFilter.addDataScheme(SCHEME_PACKAGE);
543        registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
544
545        final IntentFilter newDictFilter = new IntentFilter();
546        newDictFilter.addAction(
547                DictionaryPackInstallBroadcastReceiver.NEW_DICTIONARY_INTENT_ACTION);
548        registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
549    }
550
551    // Has to be package-visible for unit tests
552    /* package */ void loadSettings() {
553        if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
554        mSettingsValues = new SettingsValues(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr());
555        mFeedbackManager = new AudioAndHapticFeedbackManager(this, mSettingsValues);
556        resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
557    }
558
559    private void initSuggest() {
560        final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
561        final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr);
562
563        final Resources res = mResources;
564        final Locale savedLocale = LocaleUtils.setSystemLocale(res, keyboardLocale);
565        final ContactsDictionary oldContactsDictionary;
566        if (mSuggest != null) {
567            oldContactsDictionary = mSuggest.getContactsDictionary();
568            mSuggest.close();
569        } else {
570            oldContactsDictionary = null;
571        }
572
573        int mainDicResId = DictionaryFactory.getMainDictionaryResourceId(res);
574        mSuggest = new Suggest(this, mainDicResId, keyboardLocale);
575        if (mSettingsValues.mAutoCorrectEnabled) {
576            mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
577        }
578
579        mUserDictionary = new UserDictionary(this, localeStr);
580        mSuggest.setUserDictionary(mUserDictionary);
581        mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
582
583        resetContactsDictionary(oldContactsDictionary);
584
585        mUserUnigramDictionary
586                = new UserUnigramDictionary(this, this, localeStr, Suggest.DIC_USER_UNIGRAM);
587        mSuggest.setUserUnigramDictionary(mUserUnigramDictionary);
588
589        mUserBigramDictionary
590                = new UserBigramDictionary(this, this, localeStr, Suggest.DIC_USER_BIGRAM);
591        mSuggest.setUserBigramDictionary(mUserBigramDictionary);
592
593        updateCorrectionMode();
594
595        LocaleUtils.setSystemLocale(res, savedLocale);
596    }
597
598    /**
599     * Resets the contacts dictionary in mSuggest according to the user settings.
600     *
601     * This method takes an optional contacts dictionary to use. Since the contacts dictionary
602     * does not depend on the locale, it can be reused across different instances of Suggest.
603     * The dictionary will also be opened or closed as necessary depending on the settings.
604     *
605     * @param oldContactsDictionary an optional dictionary to use, or null
606     */
607    private void resetContactsDictionary(final ContactsDictionary oldContactsDictionary) {
608        final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict);
609
610        final ContactsDictionary dictionaryToUse;
611        if (!shouldSetDictionary) {
612            // Make sure the dictionary is closed. If it is already closed, this is a no-op,
613            // so it's safe to call it anyways.
614            if (null != oldContactsDictionary) oldContactsDictionary.close();
615            dictionaryToUse = null;
616        } else if (null != oldContactsDictionary) {
617            // Make sure the old contacts dictionary is opened. If it is already open, this is a
618            // no-op, so it's safe to call it anyways.
619            oldContactsDictionary.reopen(this);
620            dictionaryToUse = oldContactsDictionary;
621        } else {
622            dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
623        }
624
625        if (null != mSuggest) {
626            mSuggest.setContactsDictionary(dictionaryToUse);
627        }
628    }
629
630    /* package private */ void resetSuggestMainDict() {
631        final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
632        final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr);
633        int mainDicResId = DictionaryFactory.getMainDictionaryResourceId(mResources);
634        mSuggest.resetMainDict(this, mainDicResId, keyboardLocale);
635    }
636
637    @Override
638    public void onDestroy() {
639        if (mSuggest != null) {
640            mSuggest.close();
641            mSuggest = null;
642        }
643        unregisterReceiver(mReceiver);
644        unregisterReceiver(mDictionaryPackInstallReceiver);
645        mVoiceProxy.destroy();
646        LatinImeLogger.commit();
647        LatinImeLogger.onDestroy();
648        super.onDestroy();
649    }
650
651    @Override
652    public void onConfigurationChanged(Configuration conf) {
653        mSubtypeSwitcher.onConfigurationChanged(conf);
654        // If orientation changed while predicting, commit the change
655        if (mDisplayOrientation != conf.orientation) {
656            mDisplayOrientation = conf.orientation;
657            mHandler.startOrientationChanging();
658            final InputConnection ic = getCurrentInputConnection();
659            commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
660            if (ic != null) ic.finishComposingText(); // For voice input
661            if (isShowingOptionDialog())
662                mOptionsDialog.dismiss();
663        }
664
665        mVoiceProxy.startChangingConfiguration();
666        super.onConfigurationChanged(conf);
667        mVoiceProxy.onConfigurationChanged(conf);
668        mVoiceProxy.finishChangingConfiguration();
669
670        // This will work only when the subtype is not supported.
671        LanguageSwitcherProxy.onConfigurationChanged(conf);
672    }
673
674    @Override
675    public View onCreateInputView() {
676        return mKeyboardSwitcher.onCreateInputView();
677    }
678
679    @Override
680    public void setInputView(View view) {
681        super.setInputView(view);
682        mExtractArea = getWindow().getWindow().getDecorView()
683                .findViewById(android.R.id.extractArea);
684        mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
685        mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
686        mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view);
687        if (mSuggestionsView != null)
688            mSuggestionsView.setListener(this, view);
689        if (LatinImeLogger.sVISUALDEBUG) {
690            mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
691        }
692    }
693
694    @Override
695    public void setCandidatesView(View view) {
696        // To ensure that CandidatesView will never be set.
697        return;
698    }
699
700    @Override
701    public void onStartInput(EditorInfo editorInfo, boolean restarting) {
702        mHandler.onStartInput(editorInfo, restarting);
703    }
704
705    @Override
706    public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
707        mHandler.onStartInputView(editorInfo, restarting);
708    }
709
710    @Override
711    public void onFinishInputView(boolean finishingInput) {
712        mHandler.onFinishInputView(finishingInput);
713    }
714
715    @Override
716    public void onFinishInput() {
717        mHandler.onFinishInput();
718    }
719
720    private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) {
721        super.onStartInput(editorInfo, restarting);
722    }
723
724    private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
725        super.onStartInputView(editorInfo, restarting);
726        final KeyboardSwitcher switcher = mKeyboardSwitcher;
727        LatinKeyboardView inputView = switcher.getKeyboardView();
728
729        if (editorInfo == null) {
730            Log.e(TAG, "Null EditorInfo in onStartInputView()");
731            if (LatinImeLogger.sDBG) {
732                throw new NullPointerException("Null EditorInfo in onStartInputView()");
733            }
734            return;
735        }
736        if (DEBUG) {
737            Log.d(TAG, "onStartInputView: editorInfo:"
738                    + String.format("inputType=0x%08x imeOptions=0x%08x",
739                            editorInfo.inputType, editorInfo.imeOptions));
740        }
741        if (StringUtils.inPrivateImeOptions(null, IME_OPTION_NO_MICROPHONE_COMPAT, editorInfo)) {
742            Log.w(TAG, "Deprecated private IME option specified: "
743                    + editorInfo.privateImeOptions);
744            Log.w(TAG, "Use " + getPackageName() + "." + IME_OPTION_NO_MICROPHONE + " instead");
745        }
746        if (StringUtils.inPrivateImeOptions(getPackageName(), IME_OPTION_FORCE_ASCII, editorInfo)) {
747            Log.w(TAG, "Deprecated private IME option specified: "
748                    + editorInfo.privateImeOptions);
749            Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
750        }
751
752        LatinImeLogger.onStartInputView(editorInfo);
753        // In landscape mode, this method gets called without the input view being created.
754        if (inputView == null) {
755            return;
756        }
757
758        // Forward this event to the accessibility utilities, if enabled.
759        final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
760        if (accessUtils.isTouchExplorationEnabled()) {
761            accessUtils.onStartInputViewInternal(editorInfo, restarting);
762        }
763
764        mSubtypeSwitcher.updateParametersOnStartInputView();
765
766        // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to
767        // know now whether this is a password text field, because we need to know now whether we
768        // want to enable the voice button.
769        final int inputType = editorInfo.inputType;
770        mVoiceProxy.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(inputType)
771                || InputTypeCompatUtils.isVisiblePasswordInputType(inputType));
772
773        // The EditorInfo might have a flag that affects fullscreen mode.
774        // Note: This call should be done by InputMethodService?
775        updateFullscreenMode();
776        mLastSelectionStart = editorInfo.initialSelStart;
777        mLastSelectionEnd = editorInfo.initialSelEnd;
778        mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode());
779        mApplicationSpecifiedCompletions = null;
780
781        inputView.closing();
782        mEnteredText = null;
783        resetComposingState(true /* alsoResetLastComposedWord */);
784        mDeleteCount = 0;
785        mSpaceState = SPACE_STATE_NONE;
786
787        loadSettings();
788        updateCorrectionMode();
789        updateSuggestionVisibility(mResources);
790
791        if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
792            mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
793        }
794        mVoiceProxy.loadSettings(editorInfo, mPrefs);
795        // This will work only when the subtype is not supported.
796        LanguageSwitcherProxy.loadSettings();
797
798        if (mSubtypeSwitcher.isKeyboardMode()) {
799            switcher.loadKeyboard(editorInfo, mSettingsValues);
800        }
801
802        if (mSuggestionsView != null)
803            mSuggestionsView.clear();
804        setSuggestionStripShownInternal(
805                isSuggestionsStripVisible(), /* needsInputViewShown */ false);
806        // Delay updating suggestions because keyboard input view may not be shown at this point.
807        mHandler.postUpdateSuggestions();
808        mHandler.cancelDoubleSpacesTimer();
809
810        inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
811                mSettingsValues.mKeyPreviewPopupDismissDelay);
812        inputView.setProximityCorrectionEnabled(true);
813
814        mVoiceProxy.onStartInputView(inputView.getWindowToken());
815
816        if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
817    }
818
819    @Override
820    public void onWindowHidden() {
821        super.onWindowHidden();
822        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
823        if (inputView != null) inputView.closing();
824    }
825
826    private void onFinishInputInternal() {
827        super.onFinishInput();
828
829        LatinImeLogger.commit();
830
831        mVoiceProxy.flushVoiceInputLogs();
832
833        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
834        if (inputView != null) inputView.closing();
835        if (mUserUnigramDictionary != null) mUserUnigramDictionary.flushPendingWrites();
836        if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites();
837    }
838
839    private void onFinishInputViewInternal(boolean finishingInput) {
840        super.onFinishInputView(finishingInput);
841        mKeyboardSwitcher.onFinishInputView();
842        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
843        if (inputView != null) inputView.cancelAllMessages();
844        // Remove pending messages related to update suggestions
845        mHandler.cancelUpdateSuggestions();
846    }
847
848    @Override
849    public void onUpdateExtractedText(int token, ExtractedText text) {
850        super.onUpdateExtractedText(token, text);
851        mVoiceProxy.showPunctuationHintIfNecessary();
852    }
853
854    @Override
855    public void onUpdateSelection(int oldSelStart, int oldSelEnd,
856            int newSelStart, int newSelEnd,
857            int composingSpanStart, int composingSpanEnd) {
858        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
859                composingSpanStart, composingSpanEnd);
860
861        if (DEBUG) {
862            Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
863                    + ", ose=" + oldSelEnd
864                    + ", lss=" + mLastSelectionStart
865                    + ", lse=" + mLastSelectionEnd
866                    + ", nss=" + newSelStart
867                    + ", nse=" + newSelEnd
868                    + ", cs=" + composingSpanStart
869                    + ", ce=" + composingSpanEnd);
870        }
871
872        mVoiceProxy.setCursorAndSelection(newSelEnd, newSelStart);
873
874        // TODO: refactor the following code to be less contrived.
875        // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means
876        // that the cursor is not at the end of the composing span, or there is a selection.
877        // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place
878        // as last time we were called (if there is a selection, it means the start hasn't
879        // changed, so it's the end that did).
880        final boolean selectionChanged = (newSelStart != composingSpanEnd
881                || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart;
882        // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
883        // span in the view - we can use that to narrow down whether the cursor was moved
884        // by us or not. If we are composing a word but there is no composing span, then
885        // we know for sure the cursor moved while we were composing and we should reset
886        // the state.
887        final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
888        if (!mExpectingUpdateSelection) {
889            // TAKE CARE: there is a race condition when we enter this test even when the user
890            // did not explicitly move the cursor. This happens when typing fast, where two keys
891            // turn this flag on in succession and both onUpdateSelection() calls arrive after
892            // the second one - the first call successfully avoids this test, but the second one
893            // enters. For the moment we rely on noComposingSpan to further reduce the impact.
894
895            // TODO: the following is probably better done in resetEntireInputState().
896            // it should only happen when the cursor moved, and the very purpose of the
897            // test below is to narrow down whether this happened or not. Likewise with
898            // the call to postUpdateShiftState.
899            // We set this to NONE because after a cursor move, we don't want the space
900            // state-related special processing to kick in.
901            mSpaceState = SPACE_STATE_NONE;
902
903            if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
904                resetEntireInputState();
905            }
906
907            mHandler.postUpdateShiftState();
908        }
909        mExpectingUpdateSelection = false;
910        // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not
911        // here. It would probably be too expensive to call directly here but we may want to post a
912        // message to delay it. The point would be to unify behavior between backspace to the
913        // end of a word and manually put the pointer at the end of the word.
914
915        // Make a note of the cursor position
916        mLastSelectionStart = newSelStart;
917        mLastSelectionEnd = newSelEnd;
918    }
919
920    /**
921     * This is called when the user has clicked on the extracted text view,
922     * when running in fullscreen mode.  The default implementation hides
923     * the suggestions view when this happens, but only if the extracted text
924     * editor has a vertical scroll bar because its text doesn't fit.
925     * Here we override the behavior due to the possibility that a re-correction could
926     * cause the suggestions strip to disappear and re-appear.
927     */
928    @Override
929    public void onExtractedTextClicked() {
930        if (isSuggestionsRequested()) return;
931
932        super.onExtractedTextClicked();
933    }
934
935    /**
936     * This is called when the user has performed a cursor movement in the
937     * extracted text view, when it is running in fullscreen mode.  The default
938     * implementation hides the suggestions view when a vertical movement
939     * happens, but only if the extracted text editor has a vertical scroll bar
940     * because its text doesn't fit.
941     * Here we override the behavior due to the possibility that a re-correction could
942     * cause the suggestions strip to disappear and re-appear.
943     */
944    @Override
945    public void onExtractedCursorMovement(int dx, int dy) {
946        if (isSuggestionsRequested()) return;
947
948        super.onExtractedCursorMovement(dx, dy);
949    }
950
951    @Override
952    public void hideWindow() {
953        LatinImeLogger.commit();
954        mKeyboardSwitcher.onHideWindow();
955
956        if (TRACE) Debug.stopMethodTracing();
957        if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
958            mOptionsDialog.dismiss();
959            mOptionsDialog = null;
960        }
961        mVoiceProxy.hideVoiceWindow();
962        super.hideWindow();
963    }
964
965    @Override
966    public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
967        if (DEBUG) {
968            Log.i(TAG, "Received completions:");
969            if (applicationSpecifiedCompletions != null) {
970                for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
971                    Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
972                }
973            }
974        }
975        if (mInputAttributes.mApplicationSpecifiedCompletionOn) {
976            mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
977            if (applicationSpecifiedCompletions == null) {
978                clearSuggestions();
979                return;
980            }
981
982            SuggestedWords.Builder builder = new SuggestedWords.Builder()
983                    .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions)
984                    .setTypedWordValid(false)
985                    .setHasMinimalSuggestion(false);
986            // When in fullscreen mode, show completions generated by the application
987            final SuggestedWords words = builder.build();
988            final boolean isAutoCorrection = false;
989            setSuggestions(words, isAutoCorrection);
990            setAutoCorrectionIndicator(isAutoCorrection);
991            // TODO: is this the right thing to do? What should we auto-correct to in
992            // this case? This says to keep whatever the user typed.
993            mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
994            setSuggestionStripShown(true);
995        }
996    }
997
998    private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
999        // TODO: Modify this if we support suggestions with hard keyboard
1000        if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
1001            final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
1002            final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false;
1003            final boolean shouldShowSuggestions = shown
1004                    && (needsInputViewShown ? inputViewShown : true);
1005            if (isFullscreenMode()) {
1006                mSuggestionsContainer.setVisibility(
1007                        shouldShowSuggestions ? View.VISIBLE : View.GONE);
1008            } else {
1009                mSuggestionsContainer.setVisibility(
1010                        shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
1011            }
1012        }
1013    }
1014
1015    private void setSuggestionStripShown(boolean shown) {
1016        setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
1017    }
1018
1019    private void adjustInputViewHeight() {
1020        if (mKeyPreviewBackingView.getHeight() > 0) {
1021            return;
1022        }
1023
1024        final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
1025        if (keyboardView == null) return;
1026        final int keyboardHeight = keyboardView.getHeight();
1027        final int suggestionsHeight = mSuggestionsContainer.getHeight();
1028        final int displayHeight = mResources.getDisplayMetrics().heightPixels;
1029        final Rect rect = new Rect();
1030        mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
1031        final int notificationBarHeight = rect.top;
1032        final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
1033                - keyboardHeight;
1034
1035        final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
1036        params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight);
1037        mKeyPreviewBackingView.setLayoutParams(params);
1038    }
1039
1040    @Override
1041    public void onComputeInsets(InputMethodService.Insets outInsets) {
1042        super.onComputeInsets(outInsets);
1043        final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1044        if (inputView == null || mSuggestionsContainer == null)
1045            return;
1046        adjustInputViewHeight();
1047        // In fullscreen mode, the height of the extract area managed by InputMethodService should
1048        // be considered.
1049        // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
1050        final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
1051        final int backingHeight = (mKeyPreviewBackingView.getVisibility() == View.GONE) ? 0
1052                : mKeyPreviewBackingView.getHeight();
1053        final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0
1054                : mSuggestionsContainer.getHeight();
1055        final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
1056        int touchY = extraHeight;
1057        // Need to set touchable region only if input view is being shown
1058        final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
1059        if (keyboardView != null && keyboardView.isShown()) {
1060            if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
1061                touchY -= suggestionsHeight;
1062            }
1063            final int touchWidth = inputView.getWidth();
1064            final int touchHeight = inputView.getHeight() + extraHeight
1065                    // Extend touchable region below the keyboard.
1066                    + EXTENDED_TOUCHABLE_REGION_HEIGHT;
1067            if (DEBUG) {
1068                Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth
1069                        + " height=" + touchHeight);
1070            }
1071            setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight);
1072        }
1073        outInsets.contentTopInsets = touchY;
1074        outInsets.visibleTopInsets = touchY;
1075    }
1076
1077    @Override
1078    public boolean onEvaluateFullscreenMode() {
1079        // Reread resource value here, because this method is called by framework anytime as needed.
1080        final boolean isFullscreenModeAllowed =
1081                mSettingsValues.isFullscreenModeAllowed(getResources());
1082        return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed;
1083    }
1084
1085    @Override
1086    public void updateFullscreenMode() {
1087        super.updateFullscreenMode();
1088
1089        if (mKeyPreviewBackingView == null) return;
1090        // In fullscreen mode, no need to have extra space to show the key preview.
1091        // If not, we should have extra space above the keyboard to show the key preview.
1092        mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
1093    }
1094
1095    @Override
1096    public boolean onKeyDown(int keyCode, KeyEvent event) {
1097        switch (keyCode) {
1098        case KeyEvent.KEYCODE_BACK:
1099            if (event.getRepeatCount() == 0) {
1100                if (mSuggestionsView != null && mSuggestionsView.handleBack()) {
1101                    return true;
1102                }
1103                final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
1104                if (keyboardView != null && keyboardView.handleBack()) {
1105                    return true;
1106                }
1107            }
1108            break;
1109        }
1110        return super.onKeyDown(keyCode, event);
1111    }
1112
1113    @Override
1114    public boolean onKeyUp(int keyCode, KeyEvent event) {
1115        switch (keyCode) {
1116        case KeyEvent.KEYCODE_DPAD_DOWN:
1117        case KeyEvent.KEYCODE_DPAD_UP:
1118        case KeyEvent.KEYCODE_DPAD_LEFT:
1119        case KeyEvent.KEYCODE_DPAD_RIGHT:
1120            final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
1121            final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1122            // Enable shift key and DPAD to do selections
1123            if ((keyboardView != null && keyboardView.isShown())
1124                    && (keyboard != null && keyboard.isShiftedOrShiftLocked())) {
1125                KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
1126                        event.getAction(), event.getKeyCode(), event.getRepeatCount(),
1127                        event.getDeviceId(), event.getScanCode(),
1128                        KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
1129                final InputConnection ic = getCurrentInputConnection();
1130                if (ic != null)
1131                    ic.sendKeyEvent(newEvent);
1132                return true;
1133            }
1134            break;
1135        }
1136        return super.onKeyUp(keyCode, event);
1137    }
1138
1139    // This will reset the whole input state to the starting state. It will clear
1140    // the composing word, reset the last composed word, tell the inputconnection
1141    // and the composingStateManager about it.
1142    private void resetEntireInputState() {
1143        resetComposingState(true /* alsoResetLastComposedWord */);
1144        updateSuggestions();
1145        final InputConnection ic = getCurrentInputConnection();
1146        if (ic != null) {
1147            ic.finishComposingText();
1148        }
1149        mVoiceProxy.setVoiceInputHighlighted(false);
1150    }
1151
1152    private void resetComposingState(final boolean alsoResetLastComposedWord) {
1153        mWordComposer.reset();
1154        if (alsoResetLastComposedWord)
1155            mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
1156    }
1157
1158    public void commitTyped(final InputConnection ic, final int separatorCode) {
1159        if (!mWordComposer.isComposingWord()) return;
1160        final CharSequence typedWord = mWordComposer.getTypedWord();
1161        if (typedWord.length() > 0) {
1162            mLastComposedWord = mWordComposer.commitWord(
1163                    LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
1164                    separatorCode);
1165            if (ic != null) {
1166                ic.commitText(typedWord, 1);
1167            }
1168            addToUserUnigramAndBigramDictionaries(typedWord,
1169                    UserUnigramDictionary.FREQUENCY_FOR_TYPED);
1170        }
1171        updateSuggestions();
1172    }
1173
1174    public boolean getCurrentAutoCapsState() {
1175        final InputConnection ic = getCurrentInputConnection();
1176        EditorInfo ei = getCurrentInputEditorInfo();
1177        if (mSettingsValues.mAutoCap && ic != null && ei != null
1178                && ei.inputType != InputType.TYPE_NULL) {
1179            return ic.getCursorCapsMode(ei.inputType) != 0;
1180        }
1181        return false;
1182    }
1183
1184    // "ic" may be null
1185    private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) {
1186        if (null == ic) return;
1187        CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
1188        // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
1189        if (lastTwo != null && lastTwo.length() == 2
1190                && lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
1191            ic.deleteSurroundingText(2, 0);
1192            ic.commitText(lastTwo.charAt(1) + " ", 1);
1193            mKeyboardSwitcher.updateShiftState();
1194        }
1195    }
1196
1197    private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
1198        if (mCorrectionMode == Suggest.CORRECTION_NONE) return false;
1199        if (ic == null) return false;
1200        final CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
1201        if (lastThree != null && lastThree.length() == 3
1202                && StringUtils.canBeFollowedByPeriod(lastThree.charAt(0))
1203                && lastThree.charAt(1) == Keyboard.CODE_SPACE
1204                && lastThree.charAt(2) == Keyboard.CODE_SPACE
1205                && mHandler.isAcceptingDoubleSpaces()) {
1206            mHandler.cancelDoubleSpacesTimer();
1207            ic.deleteSurroundingText(2, 0);
1208            ic.commitText(". ", 1);
1209            mKeyboardSwitcher.updateShiftState();
1210            return true;
1211        }
1212        return false;
1213    }
1214
1215    // "ic" may be null
1216    private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) {
1217        if (ic == null) return;
1218        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1219        if (lastOne != null && lastOne.length() == 1
1220                && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
1221            ic.deleteSurroundingText(1, 0);
1222        }
1223    }
1224
1225    @Override
1226    public boolean addWordToDictionary(String word) {
1227        mUserDictionary.addWord(word, 128);
1228        // Suggestion strip should be updated after the operation of adding word to the
1229        // user dictionary
1230        mHandler.postUpdateSuggestions();
1231        return true;
1232    }
1233
1234    private static boolean isAlphabet(int code) {
1235        return Character.isLetter(code);
1236    }
1237
1238    private void onSettingsKeyPressed() {
1239        if (isShowingOptionDialog()) return;
1240        if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
1241            showSubtypeSelectorAndSettings();
1242        } else if (SubtypeUtils.hasMultipleEnabledIMEsOrSubtypes(
1243                false /* exclude aux subtypes */)) {
1244            showOptionsMenu();
1245        } else {
1246            launchSettings();
1247        }
1248    }
1249
1250    // Virtual codes representing custom requests.  These are used in onCustomRequest() below.
1251    public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
1252    public static final int CODE_HAPTIC_AND_AUDIO_FEEDBACK = 2;
1253
1254    @Override
1255    public boolean onCustomRequest(int requestCode) {
1256        if (isShowingOptionDialog()) return false;
1257        switch (requestCode) {
1258        case CODE_SHOW_INPUT_METHOD_PICKER:
1259            if (SubtypeUtils.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
1260                mImm.showInputMethodPicker();
1261                return true;
1262            }
1263            return false;
1264        case CODE_HAPTIC_AND_AUDIO_FEEDBACK:
1265            hapticAndAudioFeedback(Keyboard.CODE_UNSPECIFIED);
1266            return true;
1267        }
1268        return false;
1269    }
1270
1271    private boolean isShowingOptionDialog() {
1272        return mOptionsDialog != null && mOptionsDialog.isShowing();
1273    }
1274
1275    private static int getActionId(Keyboard keyboard) {
1276        return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE;
1277    }
1278
1279    private void performeEditorAction(int actionId) {
1280        final InputConnection ic = getCurrentInputConnection();
1281        if (ic != null) {
1282            ic.performEditorAction(actionId);
1283        }
1284    }
1285
1286    private void handleLanguageSwitchKey() {
1287        final boolean includesOtherImes = !mSettingsValues.mIncludesOtherImesInLanguageSwitchList;
1288        final IBinder token = getWindow().getWindow().getAttributes().token;
1289        if (mShouldSwitchToLastSubtype) {
1290            final InputMethodSubtypeCompatWrapper lastSubtype = mImm.getLastInputMethodSubtype();
1291            final boolean lastSubtypeBelongsToThisIme = SubtypeUtils.checkIfSubtypeBelongsToThisIme(
1292                    this, lastSubtype);
1293            if ((includesOtherImes || lastSubtypeBelongsToThisIme)
1294                    && mImm.switchToLastInputMethod(token)) {
1295                mShouldSwitchToLastSubtype = false;
1296            } else {
1297                mImm.switchToNextInputMethod(token, !includesOtherImes);
1298                mShouldSwitchToLastSubtype = true;
1299            }
1300        } else {
1301            mImm.switchToNextInputMethod(token, !includesOtherImes);
1302        }
1303    }
1304
1305    private void sendKeyCodePoint(int code) {
1306        // TODO: Remove this special handling of digit letters.
1307        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
1308        if (code >= '0' && code <= '9') {
1309            super.sendKeyChar((char)code);
1310            return;
1311        }
1312
1313        final InputConnection ic = getCurrentInputConnection();
1314        if (ic != null) {
1315            final String text = new String(new int[] { code }, 0, 1);
1316            ic.commitText(text, text.length());
1317        }
1318    }
1319
1320    // Implementation of {@link KeyboardActionListener}.
1321    @Override
1322    public void onCodeInput(int primaryCode, int x, int y) {
1323        final long when = SystemClock.uptimeMillis();
1324        if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
1325            mDeleteCount = 0;
1326        }
1327        mLastKeyTime = when;
1328        final KeyboardSwitcher switcher = mKeyboardSwitcher;
1329        // The space state depends only on the last character pressed and its own previous
1330        // state. Here, we revert the space state to neutral if the key is actually modifying
1331        // the input contents (any non-shift key), which is what we should do for
1332        // all inputs that do not result in a special state. Each character handling is then
1333        // free to override the state as they see fit.
1334        final int spaceState = mSpaceState;
1335        if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
1336
1337        // TODO: Consolidate the double space timer, mLastKeyTime, and the space state.
1338        if (primaryCode != Keyboard.CODE_SPACE) {
1339            mHandler.cancelDoubleSpacesTimer();
1340        }
1341
1342        boolean didAutoCorrect = false;
1343        switch (primaryCode) {
1344        case Keyboard.CODE_DELETE:
1345            mSpaceState = SPACE_STATE_NONE;
1346            handleBackspace(spaceState);
1347            mDeleteCount++;
1348            mExpectingUpdateSelection = true;
1349            mShouldSwitchToLastSubtype = true;
1350            LatinImeLogger.logOnDelete();
1351            break;
1352        case Keyboard.CODE_SHIFT:
1353        case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
1354            // Shift and symbol key is handled in onPressKey() and onReleaseKey().
1355            break;
1356        case Keyboard.CODE_SETTINGS:
1357            onSettingsKeyPressed();
1358            break;
1359        case Keyboard.CODE_SHORTCUT:
1360            mSubtypeSwitcher.switchToShortcutIME();
1361            break;
1362        case Keyboard.CODE_ACTION_ENTER:
1363            performeEditorAction(getActionId(switcher.getKeyboard()));
1364            break;
1365        case Keyboard.CODE_ACTION_NEXT:
1366            performeEditorAction(EditorInfo.IME_ACTION_NEXT);
1367            break;
1368        case Keyboard.CODE_ACTION_PREVIOUS:
1369            EditorInfoCompatUtils.performEditorActionPrevious(getCurrentInputConnection());
1370            break;
1371        case Keyboard.CODE_LANGUAGE_SWITCH:
1372            handleLanguageSwitchKey();
1373            break;
1374        default:
1375            mSpaceState = SPACE_STATE_NONE;
1376            if (mSettingsValues.isWordSeparator(primaryCode)) {
1377                didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
1378            } else {
1379                handleCharacter(primaryCode, x, y, spaceState);
1380            }
1381            mExpectingUpdateSelection = true;
1382            mShouldSwitchToLastSubtype = true;
1383            break;
1384        }
1385        switcher.onCodeInput(primaryCode);
1386        // Reset after any single keystroke
1387        if (!didAutoCorrect)
1388            mLastComposedWord.deactivate();
1389        mEnteredText = null;
1390    }
1391
1392    @Override
1393    public void onTextInput(CharSequence text) {
1394        mVoiceProxy.commitVoiceInput();
1395        final InputConnection ic = getCurrentInputConnection();
1396        if (ic == null) return;
1397        ic.beginBatchEdit();
1398        commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
1399        text = specificTldProcessingOnTextInput(ic, text);
1400        if (SPACE_STATE_PHANTOM == mSpaceState) {
1401            sendKeyCodePoint(Keyboard.CODE_SPACE);
1402        }
1403        ic.commitText(text, 1);
1404        ic.endBatchEdit();
1405        mKeyboardSwitcher.updateShiftState();
1406        mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
1407        mSpaceState = SPACE_STATE_NONE;
1408        mEnteredText = text;
1409        resetComposingState(true /* alsoResetLastComposedWord */);
1410    }
1411
1412    // ic may not be null
1413    private CharSequence specificTldProcessingOnTextInput(final InputConnection ic,
1414            final CharSequence text) {
1415        if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
1416                || !Character.isLetter(text.charAt(1))) {
1417            // Not a tld: do nothing.
1418            return text;
1419        }
1420        // We have a TLD (or something that looks like this): make sure we don't add
1421        // a space even if currently in phantom mode.
1422        mSpaceState = SPACE_STATE_NONE;
1423        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1424        if (lastOne != null && lastOne.length() == 1
1425                && lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
1426            return text.subSequence(1, text.length());
1427        } else {
1428            return text;
1429        }
1430    }
1431
1432    @Override
1433    public void onCancelInput() {
1434        // User released a finger outside any key
1435        mKeyboardSwitcher.onCancelInput();
1436    }
1437
1438    private void handleBackspace(final int spaceState) {
1439        if (mVoiceProxy.logAndRevertVoiceInput()) return;
1440        final InputConnection ic = getCurrentInputConnection();
1441        if (ic == null) return;
1442        ic.beginBatchEdit();
1443        handleBackspaceWhileInBatchEdit(spaceState, ic);
1444        ic.endBatchEdit();
1445    }
1446
1447    // "ic" may not be null.
1448    private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) {
1449        mVoiceProxy.handleBackspace();
1450
1451        // In many cases, we may have to put the keyboard in auto-shift state again.
1452        mHandler.postUpdateShiftState();
1453
1454        if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
1455            // Cancel multi-character input: remove the text we just entered.
1456            // This is triggered on backspace after a key that inputs multiple characters,
1457            // like the smiley key or the .com key.
1458            ic.deleteSurroundingText(mEnteredText.length(), 0);
1459            // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
1460            // In addition we know that spaceState is false, and that we should not be
1461            // reverting any autocorrect at this point. So we can safely return.
1462            return;
1463        }
1464
1465        if (mWordComposer.isComposingWord()) {
1466            final int length = mWordComposer.size();
1467            if (length > 0) {
1468                mWordComposer.deleteLast();
1469                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1470                // If we have deleted the last remaining character of a word, then we are not
1471                // isComposingWord() any more.
1472                if (!mWordComposer.isComposingWord()) {
1473                    // Not composing word any more, so we can show bigrams.
1474                    mHandler.postUpdateBigramPredictions();
1475                } else {
1476                    // Still composing a word, so we still have letters to deduce a suggestion from.
1477                    mHandler.postUpdateSuggestions();
1478                }
1479            } else {
1480                ic.deleteSurroundingText(1, 0);
1481            }
1482        } else {
1483            if (mLastComposedWord.canRevertCommit()) {
1484                Utils.Stats.onAutoCorrectionCancellation();
1485                revertCommit(ic);
1486                return;
1487            }
1488            if (SPACE_STATE_DOUBLE == spaceState) {
1489                if (revertDoubleSpaceWhileInBatchEdit(ic)) {
1490                    // No need to reset mSpaceState, it has already be done (that's why we
1491                    // receive it as a parameter)
1492                    return;
1493                }
1494            } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1495                if (revertSwapPunctuation(ic)) {
1496                    // Likewise
1497                    return;
1498                }
1499            }
1500
1501            // No cancelling of commit/double space/swap: we have a regular backspace.
1502            // We should backspace one char and restart suggestion if at the end of a word.
1503            if (mLastSelectionStart != mLastSelectionEnd) {
1504                // If there is a selection, remove it.
1505                final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
1506                ic.setSelection(mLastSelectionEnd, mLastSelectionEnd);
1507                ic.deleteSurroundingText(lengthToDelete, 0);
1508            } else {
1509                // There is no selection, just delete one character.
1510                if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
1511                    // This should never happen.
1512                    Log.e(TAG, "Backspace when we don't know the selection position");
1513                }
1514                ic.deleteSurroundingText(1, 0);
1515                if (mDeleteCount > DELETE_ACCELERATE_AT) {
1516                    ic.deleteSurroundingText(1, 0);
1517                }
1518            }
1519            if (isSuggestionsRequested()) {
1520                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic);
1521            }
1522        }
1523    }
1524
1525    // ic may be null
1526    private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code,
1527            final int spaceState, final boolean isFromSuggestionStrip) {
1528        if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1529            removeTrailingSpaceWhileInBatchEdit(ic);
1530            return false;
1531        } else if ((SPACE_STATE_WEAK == spaceState
1532                || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
1533                && isFromSuggestionStrip) {
1534            if (mSettingsValues.isWeakSpaceSwapper(code)) {
1535                return true;
1536            } else {
1537                if (mSettingsValues.isWeakSpaceStripper(code)) {
1538                    removeTrailingSpaceWhileInBatchEdit(ic);
1539                }
1540                return false;
1541            }
1542        } else {
1543            return false;
1544        }
1545    }
1546
1547    private void handleCharacter(final int primaryCode, final int x,
1548            final int y, final int spaceState) {
1549        mVoiceProxy.handleCharacter();
1550        final InputConnection ic = getCurrentInputConnection();
1551        if (null != ic) ic.beginBatchEdit();
1552        // TODO: if ic is null, does it make any sense to call this?
1553        handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic);
1554        if (null != ic) ic.endBatchEdit();
1555    }
1556
1557    // "ic" may be null without this crashing, but the behavior will be really strange
1558    private void handleCharacterWhileInBatchEdit(final int primaryCode,
1559            final int x, final int y, final int spaceState, final InputConnection ic) {
1560        boolean isComposingWord = mWordComposer.isComposingWord();
1561
1562        if (SPACE_STATE_PHANTOM == spaceState &&
1563                !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) {
1564            if (isComposingWord) {
1565                // Sanity check
1566                throw new RuntimeException("Should not be composing here");
1567            }
1568            sendKeyCodePoint(Keyboard.CODE_SPACE);
1569        }
1570
1571        if ((isAlphabet(primaryCode)
1572                || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode))
1573                && isSuggestionsRequested() && !isCursorTouchingWord()) {
1574            if (!isComposingWord) {
1575                // Reset entirely the composing state anyway, then start composing a new word unless
1576                // the character is a single quote. The idea here is, single quote is not a
1577                // separator and it should be treated as a normal character, except in the first
1578                // position where it should not start composing a word.
1579                isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode);
1580                // Here we don't need to reset the last composed word. It will be reset
1581                // when we commit this one, if we ever do; if on the other hand we backspace
1582                // it entirely and resume suggestions on the previous word, we'd like to still
1583                // have touch coordinates for it.
1584                resetComposingState(false /* alsoResetLastComposedWord */);
1585                clearSuggestions();
1586            }
1587        }
1588        if (isComposingWord) {
1589            mWordComposer.add(
1590                    primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
1591            if (ic != null) {
1592                // If it's the first letter, make note of auto-caps state
1593                if (mWordComposer.size() == 1) {
1594                    mWordComposer.setAutoCapitalized(getCurrentAutoCapsState());
1595                }
1596                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1597            }
1598            mHandler.postUpdateSuggestions();
1599        } else {
1600            final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode,
1601                    spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
1602
1603            sendKeyCodePoint(primaryCode);
1604
1605            if (swapWeakSpace) {
1606                swapSwapperAndSpaceWhileInBatchEdit(ic);
1607                mSpaceState = SPACE_STATE_WEAK;
1608            }
1609            // Some characters are not word separators, yet they don't start a new
1610            // composing span. For these, we haven't changed the suggestion strip, and
1611            // if the "add to dictionary" hint is shown, we should do so now. Examples of
1612            // such characters include single quote, dollar, and others; the exact list is
1613            // the list of characters for which we enter handleCharacterWhileInBatchEdit
1614            // that don't match the test if ((isAlphabet...)) at the top of this method.
1615            if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) {
1616                mHandler.postUpdateBigramPredictions();
1617            }
1618        }
1619        Utils.Stats.onNonSeparator((char)primaryCode, x, y);
1620    }
1621
1622    // Returns true if we did an autocorrection, false otherwise.
1623    private boolean handleSeparator(final int primaryCode, final int x, final int y,
1624            final int spaceState) {
1625        mVoiceProxy.handleSeparator();
1626
1627        // Should dismiss the "Touch again to save" message when handling separator
1628        if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
1629            mHandler.cancelUpdateBigramPredictions();
1630            mHandler.postUpdateSuggestions();
1631        }
1632
1633        boolean didAutoCorrect = false;
1634        // Handle separator
1635        final InputConnection ic = getCurrentInputConnection();
1636        if (ic != null) {
1637            ic.beginBatchEdit();
1638        }
1639        if (mWordComposer.isComposingWord()) {
1640            // In certain languages where single quote is a separator, it's better
1641            // not to auto correct, but accept the typed word. For instance,
1642            // in Italian dov' should not be expanded to dove' because the elision
1643            // requires the last vowel to be removed.
1644            final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
1645                    && !mInputAttributes.mInputTypeNoAutoCorrect;
1646            if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
1647                commitCurrentAutoCorrection(primaryCode, ic);
1648                didAutoCorrect = true;
1649            } else {
1650                commitTyped(ic, primaryCode);
1651            }
1652        }
1653
1654        final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState,
1655                KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
1656
1657        if (SPACE_STATE_PHANTOM == spaceState &&
1658                mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) {
1659            sendKeyCodePoint(Keyboard.CODE_SPACE);
1660        }
1661        sendKeyCodePoint(primaryCode);
1662
1663        if (Keyboard.CODE_SPACE == primaryCode) {
1664            if (isSuggestionsRequested()) {
1665                if (maybeDoubleSpaceWhileInBatchEdit(ic)) {
1666                    mSpaceState = SPACE_STATE_DOUBLE;
1667                } else if (!isShowingPunctuationList()) {
1668                    mSpaceState = SPACE_STATE_WEAK;
1669                }
1670            }
1671
1672            mHandler.startDoubleSpacesTimer();
1673            if (!isCursorTouchingWord()) {
1674                mHandler.cancelUpdateSuggestions();
1675                mHandler.postUpdateBigramPredictions();
1676            }
1677        } else {
1678            if (swapWeakSpace) {
1679                swapSwapperAndSpaceWhileInBatchEdit(ic);
1680                mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
1681            } else if (SPACE_STATE_PHANTOM == spaceState) {
1682                // If we are in phantom space state, and the user presses a separator, we want to
1683                // stay in phantom space state so that the next keypress has a chance to add the
1684                // space. For example, if I type "Good dat", pick "day" from the suggestion strip
1685                // then insert a comma and go on to typing the next word, I want the space to be
1686                // inserted automatically before the next word, the same way it is when I don't
1687                // input the comma.
1688                mSpaceState = SPACE_STATE_PHANTOM;
1689            }
1690
1691            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
1692            // already displayed or not, so it's okay.
1693            setPunctuationSuggestions();
1694        }
1695
1696        Utils.Stats.onSeparator((char)primaryCode, x, y);
1697
1698        if (ic != null) {
1699            ic.endBatchEdit();
1700        }
1701        return didAutoCorrect;
1702    }
1703
1704    private CharSequence getTextWithUnderline(final CharSequence text) {
1705        return mIsAutoCorrectionIndicatorOn
1706                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
1707                : text;
1708    }
1709
1710    private void handleClose() {
1711        commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR);
1712        mVoiceProxy.handleClose();
1713        requestHideSelf(0);
1714        LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1715        if (inputView != null)
1716            inputView.closing();
1717    }
1718
1719    public boolean isSuggestionsRequested() {
1720        return mInputAttributes.mIsSettingsSuggestionStripOn
1721                && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
1722    }
1723
1724    public boolean isShowingPunctuationList() {
1725        if (mSuggestionsView == null) return false;
1726        return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
1727    }
1728
1729    public boolean isShowingSuggestionsStrip() {
1730        return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
1731                || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
1732                        && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
1733    }
1734
1735    public boolean isSuggestionsStripVisible() {
1736        if (mSuggestionsView == null)
1737            return false;
1738        if (mSuggestionsView.isShowingAddToDictionaryHint())
1739            return true;
1740        if (!isShowingSuggestionsStrip())
1741            return false;
1742        if (mInputAttributes.mApplicationSpecifiedCompletionOn)
1743            return true;
1744        return isSuggestionsRequested();
1745    }
1746
1747    public void switchToKeyboardView() {
1748        if (DEBUG) {
1749            Log.d(TAG, "Switch to keyboard view.");
1750        }
1751        View v = mKeyboardSwitcher.getKeyboardView();
1752        if (v != null) {
1753            // Confirms that the keyboard view doesn't have parent view.
1754            ViewParent p = v.getParent();
1755            if (p != null && p instanceof ViewGroup) {
1756                ((ViewGroup) p).removeView(v);
1757            }
1758            setInputView(v);
1759        }
1760        setSuggestionStripShown(isSuggestionsStripVisible());
1761        updateInputViewShown();
1762        mHandler.postUpdateSuggestions();
1763    }
1764
1765    public void clearSuggestions() {
1766        setSuggestions(SuggestedWords.EMPTY, false);
1767        setAutoCorrectionIndicator(false);
1768    }
1769
1770    public void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) {
1771        if (mSuggestionsView != null) {
1772            mSuggestionsView.setSuggestions(words);
1773            mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
1774        }
1775    }
1776
1777    private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
1778        // Put a blue underline to a word in TextView which will be auto-corrected.
1779        final InputConnection ic = getCurrentInputConnection();
1780        if (ic != null) {
1781            if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator) {
1782                if (mWordComposer.isComposingWord()) {
1783                    mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
1784                    final CharSequence textWithUnderline =
1785                            getTextWithUnderline(mWordComposer.getTypedWord());
1786                    ic.setComposingText(textWithUnderline, 1);
1787                }
1788            }
1789        }
1790    }
1791
1792    public void updateSuggestions() {
1793        // Check if we have a suggestion engine attached.
1794        if ((mSuggest == null || !isSuggestionsRequested())
1795                && !mVoiceProxy.isVoiceInputHighlighted()) {
1796            if (mWordComposer.isComposingWord()) {
1797                Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
1798                mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
1799            }
1800            return;
1801        }
1802
1803        mHandler.cancelUpdateSuggestions();
1804        mHandler.cancelUpdateBigramPredictions();
1805
1806        if (!mWordComposer.isComposingWord()) {
1807            setPunctuationSuggestions();
1808            return;
1809        }
1810
1811        // TODO: May need a better way of retrieving previous word
1812        final InputConnection ic = getCurrentInputConnection();
1813        final CharSequence prevWord;
1814        if (null == ic) {
1815            prevWord = null;
1816        } else {
1817            prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
1818        }
1819
1820        final CharSequence typedWord = mWordComposer.getTypedWord();
1821        // getSuggestedWordBuilder handles gracefully a null value of prevWord
1822        final SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(mWordComposer,
1823                prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode);
1824
1825        // Basically, we update the suggestion strip only when suggestion count > 1.  However,
1826        // there is an exception: We update the suggestion strip whenever typed word's length
1827        // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
1828        // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
1829        // need to clear the previous state when the user starts typing a word (i.e. typed word's
1830        // length == 1).
1831        if (builder.size() > 1 || typedWord.length() == 1 || !builder.allowsToBeAutoCorrected()
1832                || mSuggestionsView.isShowingAddToDictionaryHint()) {
1833            showSuggestions(builder.build(), typedWord);
1834        } else {
1835            SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
1836            if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
1837                previousSuggestions = SuggestedWords.EMPTY;
1838            }
1839            final SuggestedWords.Builder obsoleteSuggestionsBuilder = new SuggestedWords.Builder()
1840                    .addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions);
1841            showSuggestions(obsoleteSuggestionsBuilder.build(), typedWord);
1842        }
1843    }
1844
1845    public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) {
1846        final CharSequence autoCorrection;
1847        if (suggestedWords.size() > 0) {
1848            if (suggestedWords.hasAutoCorrectionWord()) {
1849                autoCorrection = suggestedWords.getWord(1);
1850            } else {
1851                autoCorrection = typedWord;
1852            }
1853        } else {
1854            autoCorrection = null;
1855        }
1856        mWordComposer.setAutoCorrection(autoCorrection);
1857        final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
1858        setSuggestions(suggestedWords, isAutoCorrection);
1859        setAutoCorrectionIndicator(isAutoCorrection);
1860        setSuggestionStripShown(isSuggestionsStripVisible());
1861    }
1862
1863    private void commitCurrentAutoCorrection(final int separatorCodePoint,
1864            final InputConnection ic) {
1865        // Complete any pending suggestions query first
1866        if (mHandler.hasPendingUpdateSuggestions()) {
1867            mHandler.cancelUpdateSuggestions();
1868            updateSuggestions();
1869        }
1870        final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
1871        if (autoCorrection != null) {
1872            final String typedWord = mWordComposer.getTypedWord();
1873            if (TextUtils.isEmpty(typedWord)) {
1874                throw new RuntimeException("We have an auto-correction but the typed word "
1875                        + "is empty? Impossible! I must commit suicide.");
1876            }
1877            Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
1878            mExpectingUpdateSelection = true;
1879            commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
1880                    separatorCodePoint);
1881            // Add the word to the user unigram dictionary if it's not a known word
1882            addToUserUnigramAndBigramDictionaries(autoCorrection,
1883                    UserUnigramDictionary.FREQUENCY_FOR_TYPED);
1884            if (!typedWord.equals(autoCorrection) && null != ic) {
1885                // This will make the correction flash for a short while as a visual clue
1886                // to the user that auto-correction happened.
1887                InputConnectionCompatUtils.commitCorrection(ic,
1888                        mLastSelectionEnd - typedWord.length(), typedWord, autoCorrection);
1889            }
1890        }
1891    }
1892
1893    @Override
1894    public void pickSuggestionManually(final int index, final CharSequence suggestion) {
1895        final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
1896        mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion,
1897                mSettingsValues.mWordSeparators);
1898
1899        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
1900            int firstChar = Character.codePointAt(suggestion, 0);
1901            if ((!mSettingsValues.isWeakSpaceStripper(firstChar))
1902                    && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) {
1903                sendKeyCodePoint(Keyboard.CODE_SPACE);
1904            }
1905        }
1906
1907        if (mInputAttributes.mApplicationSpecifiedCompletionOn
1908                && mApplicationSpecifiedCompletions != null
1909                && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
1910            if (mSuggestionsView != null) {
1911                mSuggestionsView.clear();
1912            }
1913            mKeyboardSwitcher.updateShiftState();
1914            resetComposingState(true /* alsoResetLastComposedWord */);
1915            final InputConnection ic = getCurrentInputConnection();
1916            if (ic != null) {
1917                final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
1918                ic.commitCompletion(completionInfo);
1919            }
1920            return;
1921        }
1922
1923        // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
1924        if (suggestion.length() == 1 && isShowingPunctuationList()) {
1925            // Word separators are suggested before the user inputs something.
1926            // So, LatinImeLogger logs "" as a user's input.
1927            LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
1928            // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
1929            final int primaryCode = suggestion.charAt(0);
1930            onCodeInput(primaryCode,
1931                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
1932                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
1933            return;
1934        }
1935        // We need to log before we commit, because the word composer will store away the user
1936        // typed word.
1937        LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(),
1938                suggestion.toString(), index, suggestedWords);
1939        mExpectingUpdateSelection = true;
1940        commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
1941                LastComposedWord.NOT_A_SEPARATOR);
1942        // Add the word to the auto dictionary if it's not a known word
1943        if (index == 0) {
1944            addToUserUnigramAndBigramDictionaries(suggestion,
1945                    UserUnigramDictionary.FREQUENCY_FOR_PICKED);
1946        } else {
1947            addToOnlyBigramDictionary(suggestion, 1);
1948        }
1949        mSpaceState = SPACE_STATE_PHANTOM;
1950        // TODO: is this necessary?
1951        mKeyboardSwitcher.updateShiftState();
1952
1953        // We should show the "Touch again to save" hint if the user pressed the first entry
1954        // AND either:
1955        // - There is no dictionary (we know that because we tried to load it => null != mSuggest
1956        //   AND mSuggest.hasMainDictionary() is false)
1957        // - There is a dictionary and the word is not in it
1958        // Please note that if mSuggest is null, it means that everything is off: suggestion
1959        // and correction, so we shouldn't try to show the hint
1960        // We used to look at mCorrectionMode here, but showing the hint should have nothing
1961        // to do with the autocorrection setting.
1962        final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
1963                // If there is no dictionary the hint should be shown.
1964                && (!mSuggest.hasMainDictionary()
1965                        // If "suggestion" is not in the dictionary, the hint should be shown.
1966                        || !AutoCorrection.isValidWord(
1967                                mSuggest.getUnigramDictionaries(), suggestion, true));
1968
1969        Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE,
1970                WordComposer.NOT_A_COORDINATE);
1971        if (!showingAddToDictionaryHint) {
1972            // If we're not showing the "Touch again to save", then show corrections again.
1973            // In case the cursor position doesn't change, make sure we show the suggestions again.
1974            updateBigramPredictions();
1975            // Updating the predictions right away may be slow and feel unresponsive on slower
1976            // terminals. On the other hand if we just postUpdateBigramPredictions() it will
1977            // take a noticeable delay to update them which may feel uneasy.
1978        } else {
1979            if (mIsUserDictionaryAvailable) {
1980                mSuggestionsView.showAddToDictionaryHint(
1981                        suggestion, mSettingsValues.mHintToSaveText);
1982            } else {
1983                mHandler.postUpdateSuggestions();
1984            }
1985        }
1986    }
1987
1988    /**
1989     * Commits the chosen word to the text field and saves it for later retrieval.
1990     */
1991    private void commitChosenWord(final CharSequence bestWord, final int commitType,
1992            final int separatorCode) {
1993        final InputConnection ic = getCurrentInputConnection();
1994        if (ic != null) {
1995            mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators);
1996            if (mSettingsValues.mEnableSuggestionSpanInsertion) {
1997                final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
1998                ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
1999                        this, bestWord, suggestedWords), 1);
2000            } else {
2001                ic.commitText(bestWord, 1);
2002            }
2003        }
2004        // TODO: figure out here if this is an auto-correct or if the best word is actually
2005        // what user typed. Note: currently this is done much later in
2006        // LastComposedWord#didCommitTypedWord by string equality of the remembered
2007        // strings.
2008        mLastComposedWord = mWordComposer.commitWord(commitType, bestWord.toString(),
2009                separatorCode);
2010    }
2011
2012    public void updateBigramPredictions() {
2013        if (mSuggest == null || !isSuggestionsRequested())
2014            return;
2015
2016        if (!mSettingsValues.mBigramPredictionEnabled) {
2017            setPunctuationSuggestions();
2018            return;
2019        }
2020
2021        final SuggestedWords.Builder builder;
2022        if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
2023            final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
2024                    mSettingsValues.mWordSeparators);
2025            if (!TextUtils.isEmpty(prevWord)) {
2026                builder = mSuggest.getBigramPredictionWordBuilder(prevWord);
2027            } else {
2028                builder = null;
2029            }
2030        } else {
2031            builder = null;
2032        }
2033
2034        if (null != builder && builder.size() > 0) {
2035            // Explicitly supply an empty typed word (the no-second-arg version of
2036            // showSuggestions will retrieve the word near the cursor, we don't want that here)
2037            showSuggestions(builder.build(), "");
2038        } else {
2039            if (!isShowingPunctuationList()) setPunctuationSuggestions();
2040        }
2041    }
2042
2043    public void setPunctuationSuggestions() {
2044        setSuggestions(mSettingsValues.mSuggestPuncList, false);
2045        setAutoCorrectionIndicator(false);
2046        setSuggestionStripShown(isSuggestionsStripVisible());
2047    }
2048
2049    private void addToUserUnigramAndBigramDictionaries(CharSequence suggestion,
2050            int frequencyDelta) {
2051        checkAddToDictionary(suggestion, frequencyDelta, false);
2052    }
2053
2054    private void addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta) {
2055        checkAddToDictionary(suggestion, frequencyDelta, true);
2056    }
2057
2058    /**
2059     * Adds to the UserBigramDictionary and/or UserUnigramDictionary
2060     * @param selectedANotTypedWord true if it should be added to bigram dictionary if possible
2061     */
2062    private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta,
2063            boolean selectedANotTypedWord) {
2064        if (suggestion == null || suggestion.length() < 1) return;
2065
2066        // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
2067        // adding words in situations where the user or application really didn't
2068        // want corrections enabled or learned.
2069        if (!(mCorrectionMode == Suggest.CORRECTION_FULL
2070                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
2071            return;
2072        }
2073
2074        if (null != mSuggest && null != mUserUnigramDictionary) {
2075            final boolean selectedATypedWordAndItsInUserUnigramDic =
2076                    !selectedANotTypedWord && mUserUnigramDictionary.isValidWord(suggestion);
2077            final boolean isValidWord = AutoCorrection.isValidWord(
2078                    mSuggest.getUnigramDictionaries(), suggestion, true);
2079            final boolean needsToAddToUserUnigramDictionary =
2080                    selectedATypedWordAndItsInUserUnigramDic || !isValidWord;
2081            if (needsToAddToUserUnigramDictionary) {
2082                mUserUnigramDictionary.addWord(suggestion.toString(), frequencyDelta);
2083            }
2084        }
2085
2086        if (mUserBigramDictionary != null) {
2087            // We don't want to register as bigrams words separated by a separator.
2088            // For example "I will, and you too" : we don't want the pair ("will" "and") to be
2089            // a bigram.
2090            final InputConnection ic = getCurrentInputConnection();
2091            if (null != ic) {
2092                final CharSequence prevWord =
2093                        EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
2094                if (!TextUtils.isEmpty(prevWord)) {
2095                    mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString());
2096                }
2097            }
2098        }
2099    }
2100
2101    public boolean isCursorTouchingWord() {
2102        final InputConnection ic = getCurrentInputConnection();
2103        if (ic == null) return false;
2104        CharSequence before = ic.getTextBeforeCursor(1, 0);
2105        CharSequence after = ic.getTextAfterCursor(1, 0);
2106        if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0))) {
2107            return true;
2108        }
2109        if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0))) {
2110            return true;
2111        }
2112        return false;
2113    }
2114
2115    // "ic" must not be null
2116    private static boolean sameAsTextBeforeCursor(final InputConnection ic,
2117            final CharSequence text) {
2118        final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
2119        return TextUtils.equals(text, beforeText);
2120    }
2121
2122    // "ic" must not be null
2123    /**
2124     * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
2125     * word, else do nothing.
2126     */
2127    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
2128            final InputConnection ic) {
2129        // Bail out if the cursor is not at the end of a word (cursor must be preceded by
2130        // non-whitespace, non-separator, non-start-of-text)
2131        // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
2132        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0);
2133        if (TextUtils.isEmpty(textBeforeCursor)
2134                || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return;
2135
2136        // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
2137        // separator or end of line/text)
2138        // Example: "test|"<EOL> "te|st" get rejected here
2139        final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0);
2140        if (!TextUtils.isEmpty(textAfterCursor)
2141                && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return;
2142
2143        // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
2144        // Example: " -|" gets rejected here but "e-|" and "e|" are okay
2145        CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators);
2146        // We don't suggest on leading single quotes, so we have to remove them from the word if
2147        // it starts with single quotes.
2148        while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
2149            word = word.subSequence(1, word.length());
2150        }
2151        if (TextUtils.isEmpty(word)) return;
2152        final char firstChar = word.charAt(0); // we just tested that word is not empty
2153        if (word.length() == 1 && !Character.isLetter(firstChar)) return;
2154
2155        // We only suggest on words that start with a letter or a symbol that is excluded from
2156        // word separators (see #handleCharacterWhileInBatchEdit).
2157        if (!(isAlphabet(firstChar)
2158                || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) {
2159            return;
2160        }
2161
2162        // Okay, we are at the end of a word. Restart suggestions.
2163        restartSuggestionsOnWordBeforeCursor(ic, word);
2164    }
2165
2166    // "ic" must not be null
2167    private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic,
2168            final CharSequence word) {
2169        mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
2170        ic.deleteSurroundingText(word.length(), 0);
2171        ic.setComposingText(word, 1);
2172        mHandler.postUpdateSuggestions();
2173    }
2174
2175    // "ic" must not be null
2176    private void revertCommit(final InputConnection ic) {
2177        final String originallyTypedWord = mLastComposedWord.mTypedWord;
2178        final CharSequence committedWord = mLastComposedWord.mCommittedWord;
2179        final int cancelLength = committedWord.length();
2180        final int separatorLength = LastComposedWord.getSeparatorLength(
2181                mLastComposedWord.mSeparatorCode);
2182        // TODO: should we check our saved separator against the actual contents of the text view?
2183        if (DEBUG) {
2184            if (mWordComposer.isComposingWord()) {
2185                throw new RuntimeException("revertCommit, but we are composing a word");
2186            }
2187            final String wordBeforeCursor =
2188                    ic.getTextBeforeCursor(cancelLength + separatorLength, 0)
2189                            .subSequence(0, cancelLength).toString();
2190            if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
2191                throw new RuntimeException("revertCommit check failed: we thought we were "
2192                        + "reverting \"" + committedWord
2193                        + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
2194            }
2195        }
2196        ic.deleteSurroundingText(cancelLength + separatorLength, 0);
2197        if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) {
2198            // This is the case when we cancel a manual pick.
2199            // We should restart suggestion on the word right away.
2200            mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
2201            ic.setComposingText(originallyTypedWord, 1);
2202        } else {
2203            ic.commitText(originallyTypedWord, 1);
2204            // Re-insert the separator
2205            sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
2206            Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
2207                    WordComposer.NOT_A_COORDINATE);
2208            // Don't restart suggestion yet. We'll restart if the user deletes the
2209            // separator.
2210        }
2211        mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
2212        mHandler.cancelUpdateBigramPredictions();
2213        mHandler.postUpdateSuggestions();
2214    }
2215
2216    // "ic" must not be null
2217    private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
2218        mHandler.cancelDoubleSpacesTimer();
2219        // Here we test whether we indeed have a period and a space before us. This should not
2220        // be needed, but it's there just in case something went wrong.
2221        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
2222        if (!". ".equals(textBeforeCursor)) {
2223            // Theoretically we should not be coming here if there isn't ". " before the
2224            // cursor, but the application may be changing the text while we are typing, so
2225            // anything goes. We should not crash.
2226            Log.d(TAG, "Tried to revert double-space combo but we didn't find "
2227                    + "\". \" just before the cursor.");
2228            return false;
2229        }
2230        ic.deleteSurroundingText(2, 0);
2231        ic.commitText("  ", 1);
2232        return true;
2233    }
2234
2235    private static boolean revertSwapPunctuation(final InputConnection ic) {
2236        // Here we test whether we indeed have a space and something else before us. This should not
2237        // be needed, but it's there just in case something went wrong.
2238        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
2239        // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
2240        // enter surrogate pairs this code will have been removed.
2241        if (TextUtils.isEmpty(textBeforeCursor)
2242                || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
2243            // We may only come here if the application is changing the text while we are typing.
2244            // This is quite a broken case, but not logically impossible, so we shouldn't crash,
2245            // but some debugging log may be in order.
2246            Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
2247                    + "find a space just before the cursor.");
2248            return false;
2249        }
2250        ic.beginBatchEdit();
2251        ic.deleteSurroundingText(2, 0);
2252        ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
2253        ic.endBatchEdit();
2254        return true;
2255    }
2256
2257    public boolean isWordSeparator(int code) {
2258        return mSettingsValues.isWordSeparator(code);
2259    }
2260
2261    public boolean preferCapitalization() {
2262        return mWordComposer.isFirstCharCapitalized();
2263    }
2264
2265    // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
2266    // according to new language or mode.
2267    public void onRefreshKeyboard() {
2268        if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
2269            // Before Honeycomb, Voice IME is in LatinIME and it changes the current input view,
2270            // so that we need to re-create the keyboard input view here.
2271            setInputView(mKeyboardSwitcher.onCreateInputView());
2272        }
2273        // When the device locale is changed in SetupWizard etc., this method may get called via
2274        // onConfigurationChanged before SoftInputWindow is shown.
2275        if (mKeyboardSwitcher.getKeyboardView() != null) {
2276            // Reload keyboard because the current language has been changed.
2277            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
2278        }
2279        initSuggest();
2280        loadSettings();
2281        // Since we just changed languages, we should re-evaluate suggestions with whatever word
2282        // we are currently composing. If we are not composing anything, we may want to display
2283        // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
2284        if (isCursorTouchingWord()) {
2285            mHandler.postUpdateSuggestions();
2286        } else {
2287            mHandler.postUpdateBigramPredictions();
2288        }
2289    }
2290
2291    public void hapticAndAudioFeedback(final int primaryCode) {
2292        mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView());
2293    }
2294
2295    @Override
2296    public void onPressKey(int primaryCode) {
2297        mKeyboardSwitcher.onPressKey(primaryCode);
2298    }
2299
2300    @Override
2301    public void onReleaseKey(int primaryCode, boolean withSliding) {
2302        mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
2303
2304        // If accessibility is on, ensure the user receives keyboard state updates.
2305        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
2306            switch (primaryCode) {
2307            case Keyboard.CODE_SHIFT:
2308                AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
2309                break;
2310            case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
2311                AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
2312                break;
2313            }
2314        }
2315    }
2316
2317    // receive ringer mode change and network state change.
2318    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2319        @Override
2320        public void onReceive(Context context, Intent intent) {
2321            final String action = intent.getAction();
2322            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2323                mSubtypeSwitcher.onNetworkStateChanged(intent);
2324            } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2325                mFeedbackManager.onRingerModeChanged();
2326            }
2327        }
2328    };
2329
2330    // TODO: remove this method when VoiceProxy has been removed
2331    public void vibrate() {
2332        mFeedbackManager.vibrate(mKeyboardSwitcher.getKeyboardView());
2333    }
2334
2335    public boolean isAutoCapitalized() {
2336        return mWordComposer.isAutoCapitalized();
2337    }
2338
2339    private void updateCorrectionMode() {
2340        // TODO: cleanup messy flags
2341        final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
2342                && !mInputAttributes.mInputTypeNoAutoCorrect;
2343        mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE;
2344        mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect)
2345                ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
2346    }
2347
2348    private void updateSuggestionVisibility(final Resources res) {
2349        final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting;
2350        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
2351            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
2352                mSuggestionVisibility = visibility;
2353                break;
2354            }
2355        }
2356    }
2357
2358    protected void launchSettings() {
2359        launchSettingsClass(Settings.class);
2360    }
2361
2362    public void launchDebugSettings() {
2363        launchSettingsClass(DebugSettings.class);
2364    }
2365
2366    protected void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
2367        handleClose();
2368        Intent intent = new Intent();
2369        intent.setClass(LatinIME.this, settingsClass);
2370        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2371        startActivity(intent);
2372    }
2373
2374    private void showSubtypeSelectorAndSettings() {
2375        final CharSequence title = getString(R.string.english_ime_input_options);
2376        final CharSequence[] items = new CharSequence[] {
2377                // TODO: Should use new string "Select active input modes".
2378                getString(R.string.language_selection_title),
2379                getString(R.string.english_ime_settings),
2380        };
2381        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2382            @Override
2383            public void onClick(DialogInterface di, int position) {
2384                di.dismiss();
2385                switch (position) {
2386                case 0:
2387                    Intent intent = CompatUtils.getInputLanguageSelectionIntent(
2388                            SubtypeUtils.getInputMethodId(getPackageName()),
2389                            Intent.FLAG_ACTIVITY_NEW_TASK
2390                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2391                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2392                    startActivity(intent);
2393                    break;
2394                case 1:
2395                    launchSettings();
2396                    break;
2397                }
2398            }
2399        };
2400        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2401                .setItems(items, listener)
2402                .setTitle(title);
2403        showOptionDialogInternal(builder.create());
2404    }
2405
2406    private void showOptionsMenu() {
2407        final CharSequence title = getString(R.string.english_ime_input_options);
2408        final CharSequence[] items = new CharSequence[] {
2409                getString(R.string.selectInputMethod),
2410                getString(R.string.english_ime_settings),
2411        };
2412        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2413            @Override
2414            public void onClick(DialogInterface di, int position) {
2415                di.dismiss();
2416                switch (position) {
2417                case 0:
2418                    mImm.showInputMethodPicker();
2419                    break;
2420                case 1:
2421                    launchSettings();
2422                    break;
2423                }
2424            }
2425        };
2426        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2427                .setItems(items, listener)
2428                .setTitle(title);
2429        showOptionDialogInternal(builder.create());
2430    }
2431
2432    @Override
2433    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
2434        super.dump(fd, fout, args);
2435
2436        final Printer p = new PrintWriterPrinter(fout);
2437        p.println("LatinIME state :");
2438        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
2439        final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
2440        p.println("  Keyboard mode = " + keyboardMode);
2441        p.println("  mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn);
2442        p.println("  mCorrectionMode=" + mCorrectionMode);
2443        p.println("  isComposingWord=" + mWordComposer.isComposingWord());
2444        p.println("  mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
2445        p.println("  mSoundOn=" + mSettingsValues.mSoundOn);
2446        p.println("  mVibrateOn=" + mSettingsValues.mVibrateOn);
2447        p.println("  mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
2448        p.println("  mInputAttributes=" + mInputAttributes.toString());
2449    }
2450}
2451