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