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