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