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