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