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