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