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