LatinIME.java revision 11d9ee742f8ff3fb31b0e3beb32ee4870c63d8e3
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 (StringUtils.inPrivateImeOptions(null, IME_OPTION_NO_MICROPHONE_COMPAT, editorInfo)) {
669            Log.w(TAG, "Deprecated private IME option specified: "
670                    + editorInfo.privateImeOptions);
671            Log.w(TAG, "Use " + getPackageName() + "." + IME_OPTION_NO_MICROPHONE + " instead");
672        }
673        if (StringUtils.inPrivateImeOptions(getPackageName(), IME_OPTION_FORCE_ASCII, editorInfo)) {
674            Log.w(TAG, "Deprecated private IME option specified: "
675                    + editorInfo.privateImeOptions);
676            Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
677        }
678
679        LatinImeLogger.onStartInputView(editorInfo);
680        // In landscape mode, this method gets called without the input view being created.
681        if (inputView == null) {
682            return;
683        }
684
685        // Forward this event to the accessibility utilities, if enabled.
686        final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
687        if (accessUtils.isTouchExplorationEnabled()) {
688            accessUtils.onStartInputViewInternal(editorInfo, restarting);
689        }
690
691        mSubtypeSwitcher.updateParametersOnStartInputView();
692
693        // The EditorInfo might have a flag that affects fullscreen mode.
694        // Note: This call should be done by InputMethodService?
695        updateFullscreenMode();
696        mLastSelectionStart = editorInfo.initialSelStart;
697        mLastSelectionEnd = editorInfo.initialSelEnd;
698        mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode());
699        mApplicationSpecifiedCompletions = null;
700
701        inputView.closing();
702        mEnteredText = null;
703        resetComposingState(true /* alsoResetLastComposedWord */);
704        mDeleteCount = 0;
705        mSpaceState = SPACE_STATE_NONE;
706
707        loadSettings();
708        updateCorrectionMode();
709        updateSuggestionVisibility(mResources);
710
711        if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
712            mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
713        }
714
715        switcher.loadKeyboard(editorInfo, mSettingsValues);
716
717        if (mSuggestionsView != null)
718            mSuggestionsView.clear();
719        setSuggestionStripShownInternal(
720                isSuggestionsStripVisible(), /* needsInputViewShown */ false);
721        // Delay updating suggestions because keyboard input view may not be shown at this point.
722        mHandler.postUpdateSuggestions();
723        mHandler.cancelDoubleSpacesTimer();
724
725        inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
726                mSettingsValues.mKeyPreviewPopupDismissDelay);
727        inputView.setProximityCorrectionEnabled(true);
728
729        if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
730    }
731
732    @Override
733    public void onWindowHidden() {
734        super.onWindowHidden();
735        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
736        if (inputView != null) inputView.closing();
737    }
738
739    private void onFinishInputInternal() {
740        super.onFinishInput();
741
742        LatinImeLogger.commit();
743
744        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
745        if (inputView != null) inputView.closing();
746        if (mUserHistoryDictionary != null) mUserHistoryDictionary.flushPendingWrites();
747    }
748
749    private void onFinishInputViewInternal(boolean finishingInput) {
750        super.onFinishInputView(finishingInput);
751        mKeyboardSwitcher.onFinishInputView();
752        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
753        if (inputView != null) inputView.cancelAllMessages();
754        // Remove pending messages related to update suggestions
755        mHandler.cancelUpdateSuggestions();
756    }
757
758    @Override
759    public void onUpdateSelection(int oldSelStart, int oldSelEnd,
760            int newSelStart, int newSelEnd,
761            int composingSpanStart, int composingSpanEnd) {
762        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
763                composingSpanStart, composingSpanEnd);
764
765        if (ProductionFlag.IS_EXPERIMENTAL) {
766            if (ResearchLogger.UnsLogGroup.ON_UPDATE_SELECTION.isEnabled) {
767                final String s = "onUpdateSelection: oss=" + oldSelStart
768                    + ", ose=" + oldSelEnd
769                    + ", lss=" + mLastSelectionStart
770                    + ", lse=" + mLastSelectionEnd
771                    + ", nss=" + newSelStart
772                    + ", nse=" + newSelEnd
773                    + ", cs=" + composingSpanStart
774                    + ", ce=" + composingSpanEnd;
775                ResearchLogger.logUnstructured(ResearchLogger.UnsLogGroup.ON_UPDATE_SELECTION, s);
776            }
777        }
778        if (DEBUG) {
779            Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
780                    + ", ose=" + oldSelEnd
781                    + ", lss=" + mLastSelectionStart
782                    + ", lse=" + mLastSelectionEnd
783                    + ", nss=" + newSelStart
784                    + ", nse=" + newSelEnd
785                    + ", cs=" + composingSpanStart
786                    + ", ce=" + composingSpanEnd);
787        }
788
789        // TODO: refactor the following code to be less contrived.
790        // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means
791        // that the cursor is not at the end of the composing span, or there is a selection.
792        // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place
793        // as last time we were called (if there is a selection, it means the start hasn't
794        // changed, so it's the end that did).
795        final boolean selectionChanged = (newSelStart != composingSpanEnd
796                || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart;
797        // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
798        // span in the view - we can use that to narrow down whether the cursor was moved
799        // by us or not. If we are composing a word but there is no composing span, then
800        // we know for sure the cursor moved while we were composing and we should reset
801        // the state.
802        final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
803        if (!mExpectingUpdateSelection) {
804            // TAKE CARE: there is a race condition when we enter this test even when the user
805            // did not explicitly move the cursor. This happens when typing fast, where two keys
806            // turn this flag on in succession and both onUpdateSelection() calls arrive after
807            // the second one - the first call successfully avoids this test, but the second one
808            // enters. For the moment we rely on noComposingSpan to further reduce the impact.
809
810            // TODO: the following is probably better done in resetEntireInputState().
811            // it should only happen when the cursor moved, and the very purpose of the
812            // test below is to narrow down whether this happened or not. Likewise with
813            // the call to postUpdateShiftState.
814            // We set this to NONE because after a cursor move, we don't want the space
815            // state-related special processing to kick in.
816            mSpaceState = SPACE_STATE_NONE;
817
818            if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
819                resetEntireInputState();
820            }
821
822            mHandler.postUpdateShiftState();
823        }
824        mExpectingUpdateSelection = false;
825        // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not
826        // here. It would probably be too expensive to call directly here but we may want to post a
827        // message to delay it. The point would be to unify behavior between backspace to the
828        // end of a word and manually put the pointer at the end of the word.
829
830        // Make a note of the cursor position
831        mLastSelectionStart = newSelStart;
832        mLastSelectionEnd = newSelEnd;
833    }
834
835    /**
836     * This is called when the user has clicked on the extracted text view,
837     * when running in fullscreen mode.  The default implementation hides
838     * the suggestions view when this happens, but only if the extracted text
839     * editor has a vertical scroll bar because its text doesn't fit.
840     * Here we override the behavior due to the possibility that a re-correction could
841     * cause the suggestions strip to disappear and re-appear.
842     */
843    @Override
844    public void onExtractedTextClicked() {
845        if (isSuggestionsRequested()) return;
846
847        super.onExtractedTextClicked();
848    }
849
850    /**
851     * This is called when the user has performed a cursor movement in the
852     * extracted text view, when it is running in fullscreen mode.  The default
853     * implementation hides the suggestions view when a vertical movement
854     * happens, but only if the extracted text editor has a vertical scroll bar
855     * because its text doesn't fit.
856     * Here we override the behavior due to the possibility that a re-correction could
857     * cause the suggestions strip to disappear and re-appear.
858     */
859    @Override
860    public void onExtractedCursorMovement(int dx, int dy) {
861        if (isSuggestionsRequested()) return;
862
863        super.onExtractedCursorMovement(dx, dy);
864    }
865
866    @Override
867    public void hideWindow() {
868        LatinImeLogger.commit();
869        mKeyboardSwitcher.onHideWindow();
870
871        if (TRACE) Debug.stopMethodTracing();
872        if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
873            mOptionsDialog.dismiss();
874            mOptionsDialog = null;
875        }
876        super.hideWindow();
877    }
878
879    @Override
880    public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
881        if (DEBUG) {
882            Log.i(TAG, "Received completions:");
883            if (applicationSpecifiedCompletions != null) {
884                for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
885                    Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
886                }
887            }
888        }
889        if (mInputAttributes.mApplicationSpecifiedCompletionOn) {
890            mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
891            if (applicationSpecifiedCompletions == null) {
892                clearSuggestions();
893                return;
894            }
895
896            final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
897                    SuggestedWords.getFromApplicationSpecifiedCompletions(
898                            applicationSpecifiedCompletions);
899            final SuggestedWords suggestedWords = new SuggestedWords(
900                    applicationSuggestedWords,
901                    false /* typedWordValid */,
902                    false /* hasAutoCorrectionCandidate */,
903                    false /* allowsToBeAutoCorrected */,
904                    false /* isPunctuationSuggestions */,
905                    false /* isObsoleteSuggestions */);
906            // When in fullscreen mode, show completions generated by the application
907            final boolean isAutoCorrection = false;
908            setSuggestions(suggestedWords, isAutoCorrection);
909            setAutoCorrectionIndicator(isAutoCorrection);
910            // TODO: is this the right thing to do? What should we auto-correct to in
911            // this case? This says to keep whatever the user typed.
912            mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
913            setSuggestionStripShown(true);
914        }
915    }
916
917    private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
918        // TODO: Modify this if we support suggestions with hard keyboard
919        if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
920            final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
921            final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false;
922            final boolean shouldShowSuggestions = shown
923                    && (needsInputViewShown ? inputViewShown : true);
924            if (isFullscreenMode()) {
925                mSuggestionsContainer.setVisibility(
926                        shouldShowSuggestions ? View.VISIBLE : View.GONE);
927            } else {
928                mSuggestionsContainer.setVisibility(
929                        shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
930            }
931        }
932    }
933
934    private void setSuggestionStripShown(boolean shown) {
935        setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
936    }
937
938    private void adjustInputViewHeight() {
939        if (mKeyPreviewBackingView.getHeight() > 0) {
940            return;
941        }
942
943        final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
944        if (keyboardView == null) return;
945        final int keyboardHeight = keyboardView.getHeight();
946        final int suggestionsHeight = mSuggestionsContainer.getHeight();
947        final int displayHeight = mResources.getDisplayMetrics().heightPixels;
948        final Rect rect = new Rect();
949        mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
950        final int notificationBarHeight = rect.top;
951        final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
952                - keyboardHeight;
953
954        final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
955        params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight);
956        mKeyPreviewBackingView.setLayoutParams(params);
957    }
958
959    @Override
960    public void onComputeInsets(InputMethodService.Insets outInsets) {
961        super.onComputeInsets(outInsets);
962        final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
963        if (inputView == null || mSuggestionsContainer == null)
964            return;
965        adjustInputViewHeight();
966        // In fullscreen mode, the height of the extract area managed by InputMethodService should
967        // be considered.
968        // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
969        final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
970        final int backingHeight = (mKeyPreviewBackingView.getVisibility() == View.GONE) ? 0
971                : mKeyPreviewBackingView.getHeight();
972        final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0
973                : mSuggestionsContainer.getHeight();
974        final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
975        int touchY = extraHeight;
976        // Need to set touchable region only if input view is being shown
977        final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
978        if (keyboardView != null && keyboardView.isShown()) {
979            if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
980                touchY -= suggestionsHeight;
981            }
982            final int touchWidth = inputView.getWidth();
983            final int touchHeight = inputView.getHeight() + extraHeight
984                    // Extend touchable region below the keyboard.
985                    + EXTENDED_TOUCHABLE_REGION_HEIGHT;
986            outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
987            outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight);
988        }
989        outInsets.contentTopInsets = touchY;
990        outInsets.visibleTopInsets = touchY;
991    }
992
993    @Override
994    public boolean onEvaluateFullscreenMode() {
995        // Reread resource value here, because this method is called by framework anytime as needed.
996        final boolean isFullscreenModeAllowed =
997                mSettingsValues.isFullscreenModeAllowed(getResources());
998        return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed;
999    }
1000
1001    @Override
1002    public void updateFullscreenMode() {
1003        super.updateFullscreenMode();
1004
1005        if (mKeyPreviewBackingView == null) return;
1006        // In fullscreen mode, no need to have extra space to show the key preview.
1007        // If not, we should have extra space above the keyboard to show the key preview.
1008        mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
1009    }
1010
1011    @Override
1012    public boolean onKeyDown(int keyCode, KeyEvent event) {
1013        switch (keyCode) {
1014        case KeyEvent.KEYCODE_BACK:
1015            if (event.getRepeatCount() == 0) {
1016                if (mSuggestionsView != null && mSuggestionsView.handleBack()) {
1017                    return true;
1018                }
1019                final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
1020                if (keyboardView != null && keyboardView.handleBack()) {
1021                    return true;
1022                }
1023            }
1024            break;
1025        }
1026        return super.onKeyDown(keyCode, event);
1027    }
1028
1029    @Override
1030    public boolean onKeyUp(int keyCode, KeyEvent event) {
1031        switch (keyCode) {
1032        case KeyEvent.KEYCODE_DPAD_DOWN:
1033        case KeyEvent.KEYCODE_DPAD_UP:
1034        case KeyEvent.KEYCODE_DPAD_LEFT:
1035        case KeyEvent.KEYCODE_DPAD_RIGHT:
1036            final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
1037            final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1038            // Enable shift key and DPAD to do selections
1039            if ((keyboardView != null && keyboardView.isShown())
1040                    && (keyboard != null && keyboard.isShiftedOrShiftLocked())) {
1041                KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
1042                        event.getAction(), event.getKeyCode(), event.getRepeatCount(),
1043                        event.getDeviceId(), event.getScanCode(),
1044                        KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
1045                final InputConnection ic = getCurrentInputConnection();
1046                if (ic != null)
1047                    ic.sendKeyEvent(newEvent);
1048                return true;
1049            }
1050            break;
1051        }
1052        return super.onKeyUp(keyCode, event);
1053    }
1054
1055    // This will reset the whole input state to the starting state. It will clear
1056    // the composing word, reset the last composed word, tell the inputconnection
1057    // and the composingStateManager about it.
1058    private void resetEntireInputState() {
1059        resetComposingState(true /* alsoResetLastComposedWord */);
1060        updateSuggestions();
1061        final InputConnection ic = getCurrentInputConnection();
1062        if (ic != null) {
1063            ic.finishComposingText();
1064        }
1065    }
1066
1067    private void resetComposingState(final boolean alsoResetLastComposedWord) {
1068        mWordComposer.reset();
1069        if (alsoResetLastComposedWord)
1070            mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
1071    }
1072
1073    public void commitTyped(final InputConnection ic, final int separatorCode) {
1074        if (!mWordComposer.isComposingWord()) return;
1075        final CharSequence typedWord = mWordComposer.getTypedWord();
1076        if (typedWord.length() > 0) {
1077            mLastComposedWord = mWordComposer.commitWord(
1078                    LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
1079                    separatorCode);
1080            if (ic != null) {
1081                ic.commitText(typedWord, 1);
1082            }
1083            addToUserHistoryDictionary(typedWord);
1084        }
1085        updateSuggestions();
1086    }
1087
1088    public boolean getCurrentAutoCapsState() {
1089        final InputConnection ic = getCurrentInputConnection();
1090        EditorInfo ei = getCurrentInputEditorInfo();
1091        if (mSettingsValues.mAutoCap && ic != null && ei != null
1092                && ei.inputType != InputType.TYPE_NULL) {
1093            return ic.getCursorCapsMode(ei.inputType) != 0;
1094        }
1095        return false;
1096    }
1097
1098    // "ic" may be null
1099    private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) {
1100        if (null == ic) return;
1101        CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
1102        // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
1103        if (lastTwo != null && lastTwo.length() == 2
1104                && lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
1105            ic.deleteSurroundingText(2, 0);
1106            ic.commitText(lastTwo.charAt(1) + " ", 1);
1107            mKeyboardSwitcher.updateShiftState();
1108        }
1109    }
1110
1111    private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
1112        if (mCorrectionMode == Suggest.CORRECTION_NONE) return false;
1113        if (ic == null) return false;
1114        final CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
1115        if (lastThree != null && lastThree.length() == 3
1116                && StringUtils.canBeFollowedByPeriod(lastThree.charAt(0))
1117                && lastThree.charAt(1) == Keyboard.CODE_SPACE
1118                && lastThree.charAt(2) == Keyboard.CODE_SPACE
1119                && mHandler.isAcceptingDoubleSpaces()) {
1120            mHandler.cancelDoubleSpacesTimer();
1121            ic.deleteSurroundingText(2, 0);
1122            ic.commitText(". ", 1);
1123            mKeyboardSwitcher.updateShiftState();
1124            return true;
1125        }
1126        return false;
1127    }
1128
1129    // "ic" may be null
1130    private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) {
1131        if (ic == null) return;
1132        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1133        if (lastOne != null && lastOne.length() == 1
1134                && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
1135            ic.deleteSurroundingText(1, 0);
1136        }
1137    }
1138
1139    @Override
1140    public boolean addWordToDictionary(String word) {
1141        mUserDictionary.addWord(word, 128);
1142        // Suggestion strip should be updated after the operation of adding word to the
1143        // user dictionary
1144        mHandler.postUpdateSuggestions();
1145        return true;
1146    }
1147
1148    private static boolean isAlphabet(int code) {
1149        return Character.isLetter(code);
1150    }
1151
1152    private void onSettingsKeyPressed() {
1153        if (isShowingOptionDialog()) return;
1154        showSubtypeSelectorAndSettings();
1155    }
1156
1157    // Virtual codes representing custom requests.  These are used in onCustomRequest() below.
1158    public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
1159    public static final int CODE_HAPTIC_AND_AUDIO_FEEDBACK = 2;
1160
1161    @Override
1162    public boolean onCustomRequest(int requestCode) {
1163        if (isShowingOptionDialog()) return false;
1164        switch (requestCode) {
1165        case CODE_SHOW_INPUT_METHOD_PICKER:
1166            if (SubtypeUtils.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
1167                mImm.showInputMethodPicker();
1168                return true;
1169            }
1170            return false;
1171        case CODE_HAPTIC_AND_AUDIO_FEEDBACK:
1172            hapticAndAudioFeedback(Keyboard.CODE_UNSPECIFIED);
1173            return true;
1174        }
1175        return false;
1176    }
1177
1178    private boolean isShowingOptionDialog() {
1179        return mOptionsDialog != null && mOptionsDialog.isShowing();
1180    }
1181
1182    private static int getActionId(Keyboard keyboard) {
1183        return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE;
1184    }
1185
1186    private void performEditorAction(int actionId) {
1187        final InputConnection ic = getCurrentInputConnection();
1188        if (ic != null) {
1189            ic.performEditorAction(actionId);
1190        }
1191    }
1192
1193    private void handleLanguageSwitchKey() {
1194        final boolean includesOtherImes = mSettingsValues.mIncludesOtherImesInLanguageSwitchList;
1195        final IBinder token = getWindow().getWindow().getAttributes().token;
1196        if (mShouldSwitchToLastSubtype) {
1197            final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype();
1198            final boolean lastSubtypeBelongsToThisIme = SubtypeUtils.checkIfSubtypeBelongsToThisIme(
1199                    this, lastSubtype);
1200            if ((includesOtherImes || lastSubtypeBelongsToThisIme)
1201                    && mImm.switchToLastInputMethod(token)) {
1202                mShouldSwitchToLastSubtype = false;
1203            } else {
1204                mImm.switchToNextInputMethod(token, !includesOtherImes);
1205                mShouldSwitchToLastSubtype = true;
1206            }
1207        } else {
1208            mImm.switchToNextInputMethod(token, !includesOtherImes);
1209        }
1210    }
1211
1212    private void sendKeyCodePoint(int code) {
1213        // TODO: Remove this special handling of digit letters.
1214        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
1215        if (code >= '0' && code <= '9') {
1216            super.sendKeyChar((char)code);
1217            return;
1218        }
1219
1220        final InputConnection ic = getCurrentInputConnection();
1221        if (ic != null) {
1222            final String text = new String(new int[] { code }, 0, 1);
1223            ic.commitText(text, text.length());
1224        }
1225    }
1226
1227    // Implementation of {@link KeyboardActionListener}.
1228    @Override
1229    public void onCodeInput(int primaryCode, int x, int y) {
1230        final long when = SystemClock.uptimeMillis();
1231        if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
1232            mDeleteCount = 0;
1233        }
1234        mLastKeyTime = when;
1235
1236        if (ProductionFlag.IS_EXPERIMENTAL) {
1237            if (ResearchLogger.sIsLogging) {
1238                ResearchLogger.getInstance().logKeyEvent(primaryCode, x, y);
1239            }
1240        }
1241
1242        final KeyboardSwitcher switcher = mKeyboardSwitcher;
1243        // The space state depends only on the last character pressed and its own previous
1244        // state. Here, we revert the space state to neutral if the key is actually modifying
1245        // the input contents (any non-shift key), which is what we should do for
1246        // all inputs that do not result in a special state. Each character handling is then
1247        // free to override the state as they see fit.
1248        final int spaceState = mSpaceState;
1249        if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
1250
1251        // TODO: Consolidate the double space timer, mLastKeyTime, and the space state.
1252        if (primaryCode != Keyboard.CODE_SPACE) {
1253            mHandler.cancelDoubleSpacesTimer();
1254        }
1255
1256        boolean didAutoCorrect = false;
1257        switch (primaryCode) {
1258        case Keyboard.CODE_DELETE:
1259            mSpaceState = SPACE_STATE_NONE;
1260            handleBackspace(spaceState);
1261            mDeleteCount++;
1262            mExpectingUpdateSelection = true;
1263            mShouldSwitchToLastSubtype = true;
1264            LatinImeLogger.logOnDelete(x, y);
1265            break;
1266        case Keyboard.CODE_SHIFT:
1267        case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
1268            // Shift and symbol key is handled in onPressKey() and onReleaseKey().
1269            break;
1270        case Keyboard.CODE_SETTINGS:
1271            onSettingsKeyPressed();
1272            break;
1273        case Keyboard.CODE_SHORTCUT:
1274            mSubtypeSwitcher.switchToShortcutIME();
1275            break;
1276        case Keyboard.CODE_ACTION_ENTER:
1277            performEditorAction(getActionId(switcher.getKeyboard()));
1278            break;
1279        case Keyboard.CODE_ACTION_NEXT:
1280            performEditorAction(EditorInfo.IME_ACTION_NEXT);
1281            break;
1282        case Keyboard.CODE_ACTION_PREVIOUS:
1283            performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
1284            break;
1285        case Keyboard.CODE_LANGUAGE_SWITCH:
1286            handleLanguageSwitchKey();
1287            break;
1288        default:
1289            mSpaceState = SPACE_STATE_NONE;
1290            if (mSettingsValues.isWordSeparator(primaryCode)) {
1291                didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
1292            } else {
1293                final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1294                if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
1295                    handleCharacter(primaryCode, x, y, spaceState);
1296                } else {
1297                    handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE,
1298                            spaceState);
1299                }
1300            }
1301            mExpectingUpdateSelection = true;
1302            mShouldSwitchToLastSubtype = true;
1303            break;
1304        }
1305        switcher.onCodeInput(primaryCode);
1306        // Reset after any single keystroke
1307        if (!didAutoCorrect)
1308            mLastComposedWord.deactivate();
1309        mEnteredText = null;
1310    }
1311
1312    @Override
1313    public void onTextInput(CharSequence text) {
1314        final InputConnection ic = getCurrentInputConnection();
1315        if (ic == null) return;
1316        ic.beginBatchEdit();
1317        commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
1318        text = specificTldProcessingOnTextInput(ic, text);
1319        if (SPACE_STATE_PHANTOM == mSpaceState) {
1320            sendKeyCodePoint(Keyboard.CODE_SPACE);
1321        }
1322        ic.commitText(text, 1);
1323        ic.endBatchEdit();
1324        mKeyboardSwitcher.updateShiftState();
1325        mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
1326        mSpaceState = SPACE_STATE_NONE;
1327        mEnteredText = text;
1328        resetComposingState(true /* alsoResetLastComposedWord */);
1329    }
1330
1331    // ic may not be null
1332    private CharSequence specificTldProcessingOnTextInput(final InputConnection ic,
1333            final CharSequence text) {
1334        if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
1335                || !Character.isLetter(text.charAt(1))) {
1336            // Not a tld: do nothing.
1337            return text;
1338        }
1339        // We have a TLD (or something that looks like this): make sure we don't add
1340        // a space even if currently in phantom mode.
1341        mSpaceState = SPACE_STATE_NONE;
1342        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1343        if (lastOne != null && lastOne.length() == 1
1344                && lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
1345            return text.subSequence(1, text.length());
1346        } else {
1347            return text;
1348        }
1349    }
1350
1351    @Override
1352    public void onCancelInput() {
1353        // User released a finger outside any key
1354        mKeyboardSwitcher.onCancelInput();
1355    }
1356
1357    private void handleBackspace(final int spaceState) {
1358        final InputConnection ic = getCurrentInputConnection();
1359        if (ic == null) return;
1360        ic.beginBatchEdit();
1361        handleBackspaceWhileInBatchEdit(spaceState, ic);
1362        ic.endBatchEdit();
1363    }
1364
1365    // "ic" may not be null.
1366    private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) {
1367        // In many cases, we may have to put the keyboard in auto-shift state again.
1368        mHandler.postUpdateShiftState();
1369
1370        if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
1371            // Cancel multi-character input: remove the text we just entered.
1372            // This is triggered on backspace after a key that inputs multiple characters,
1373            // like the smiley key or the .com key.
1374            ic.deleteSurroundingText(mEnteredText.length(), 0);
1375            // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
1376            // In addition we know that spaceState is false, and that we should not be
1377            // reverting any autocorrect at this point. So we can safely return.
1378            return;
1379        }
1380
1381        if (mWordComposer.isComposingWord()) {
1382            final int length = mWordComposer.size();
1383            if (length > 0) {
1384                mWordComposer.deleteLast();
1385                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1386                // If we have deleted the last remaining character of a word, then we are not
1387                // isComposingWord() any more.
1388                if (!mWordComposer.isComposingWord()) {
1389                    // Not composing word any more, so we can show bigrams.
1390                    mHandler.postUpdateBigramPredictions();
1391                } else {
1392                    // Still composing a word, so we still have letters to deduce a suggestion from.
1393                    mHandler.postUpdateSuggestions();
1394                }
1395            } else {
1396                ic.deleteSurroundingText(1, 0);
1397            }
1398        } else {
1399            if (mLastComposedWord.canRevertCommit()) {
1400                Utils.Stats.onAutoCorrectionCancellation();
1401                revertCommit(ic);
1402                return;
1403            }
1404            if (SPACE_STATE_DOUBLE == spaceState) {
1405                if (revertDoubleSpaceWhileInBatchEdit(ic)) {
1406                    // No need to reset mSpaceState, it has already be done (that's why we
1407                    // receive it as a parameter)
1408                    return;
1409                }
1410            } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1411                if (revertSwapPunctuation(ic)) {
1412                    // Likewise
1413                    return;
1414                }
1415            }
1416
1417            // No cancelling of commit/double space/swap: we have a regular backspace.
1418            // We should backspace one char and restart suggestion if at the end of a word.
1419            if (mLastSelectionStart != mLastSelectionEnd) {
1420                // If there is a selection, remove it.
1421                final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
1422                ic.setSelection(mLastSelectionEnd, mLastSelectionEnd);
1423                ic.deleteSurroundingText(lengthToDelete, 0);
1424            } else {
1425                // There is no selection, just delete one character.
1426                if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
1427                    // This should never happen.
1428                    Log.e(TAG, "Backspace when we don't know the selection position");
1429                }
1430                ic.deleteSurroundingText(1, 0);
1431                if (mDeleteCount > DELETE_ACCELERATE_AT) {
1432                    ic.deleteSurroundingText(1, 0);
1433                }
1434            }
1435            if (isSuggestionsRequested()) {
1436                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic);
1437            }
1438        }
1439    }
1440
1441    // ic may be null
1442    private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code,
1443            final int spaceState, final boolean isFromSuggestionStrip) {
1444        if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1445            removeTrailingSpaceWhileInBatchEdit(ic);
1446            return false;
1447        } else if ((SPACE_STATE_WEAK == spaceState
1448                || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
1449                && isFromSuggestionStrip) {
1450            if (mSettingsValues.isWeakSpaceSwapper(code)) {
1451                return true;
1452            } else {
1453                if (mSettingsValues.isWeakSpaceStripper(code)) {
1454                    removeTrailingSpaceWhileInBatchEdit(ic);
1455                }
1456                return false;
1457            }
1458        } else {
1459            return false;
1460        }
1461    }
1462
1463    private void handleCharacter(final int primaryCode, final int x,
1464            final int y, final int spaceState) {
1465        final InputConnection ic = getCurrentInputConnection();
1466        if (null != ic) ic.beginBatchEdit();
1467        // TODO: if ic is null, does it make any sense to call this?
1468        handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic);
1469        if (null != ic) ic.endBatchEdit();
1470    }
1471
1472    // "ic" may be null without this crashing, but the behavior will be really strange
1473    private void handleCharacterWhileInBatchEdit(final int primaryCode,
1474            final int x, final int y, final int spaceState, final InputConnection ic) {
1475        boolean isComposingWord = mWordComposer.isComposingWord();
1476
1477        if (SPACE_STATE_PHANTOM == spaceState &&
1478                !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) {
1479            if (isComposingWord) {
1480                // Sanity check
1481                throw new RuntimeException("Should not be composing here");
1482            }
1483            sendKeyCodePoint(Keyboard.CODE_SPACE);
1484        }
1485
1486        if ((isAlphabet(primaryCode)
1487                || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode))
1488                && isSuggestionsRequested() && !isCursorTouchingWord()) {
1489            if (!isComposingWord) {
1490                // Reset entirely the composing state anyway, then start composing a new word unless
1491                // the character is a single quote. The idea here is, single quote is not a
1492                // separator and it should be treated as a normal character, except in the first
1493                // position where it should not start composing a word.
1494                isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode);
1495                // Here we don't need to reset the last composed word. It will be reset
1496                // when we commit this one, if we ever do; if on the other hand we backspace
1497                // it entirely and resume suggestions on the previous word, we'd like to still
1498                // have touch coordinates for it.
1499                resetComposingState(false /* alsoResetLastComposedWord */);
1500                clearSuggestions();
1501            }
1502        }
1503        if (isComposingWord) {
1504            mWordComposer.add(
1505                    primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
1506            if (ic != null) {
1507                // If it's the first letter, make note of auto-caps state
1508                if (mWordComposer.size() == 1) {
1509                    mWordComposer.setAutoCapitalized(getCurrentAutoCapsState());
1510                }
1511                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1512            }
1513            mHandler.postUpdateSuggestions();
1514        } else {
1515            final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode,
1516                    spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
1517
1518            sendKeyCodePoint(primaryCode);
1519
1520            if (swapWeakSpace) {
1521                swapSwapperAndSpaceWhileInBatchEdit(ic);
1522                mSpaceState = SPACE_STATE_WEAK;
1523            }
1524            // Some characters are not word separators, yet they don't start a new
1525            // composing span. For these, we haven't changed the suggestion strip, and
1526            // if the "add to dictionary" hint is shown, we should do so now. Examples of
1527            // such characters include single quote, dollar, and others; the exact list is
1528            // the list of characters for which we enter handleCharacterWhileInBatchEdit
1529            // that don't match the test if ((isAlphabet...)) at the top of this method.
1530            if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) {
1531                mHandler.postUpdateBigramPredictions();
1532            }
1533        }
1534        Utils.Stats.onNonSeparator((char)primaryCode, x, y);
1535    }
1536
1537    // Returns true if we did an autocorrection, false otherwise.
1538    private boolean handleSeparator(final int primaryCode, final int x, final int y,
1539            final int spaceState) {
1540        // Should dismiss the "Touch again to save" message when handling separator
1541        if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
1542            mHandler.cancelUpdateBigramPredictions();
1543            mHandler.postUpdateSuggestions();
1544        }
1545
1546        boolean didAutoCorrect = false;
1547        // Handle separator
1548        final InputConnection ic = getCurrentInputConnection();
1549        if (ic != null) {
1550            ic.beginBatchEdit();
1551        }
1552        if (mWordComposer.isComposingWord()) {
1553            // In certain languages where single quote is a separator, it's better
1554            // not to auto correct, but accept the typed word. For instance,
1555            // in Italian dov' should not be expanded to dove' because the elision
1556            // requires the last vowel to be removed.
1557            final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
1558                    && !mInputAttributes.mInputTypeNoAutoCorrect;
1559            if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
1560                commitCurrentAutoCorrection(primaryCode, ic);
1561                didAutoCorrect = true;
1562            } else {
1563                commitTyped(ic, primaryCode);
1564            }
1565        }
1566
1567        final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState,
1568                KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
1569
1570        if (SPACE_STATE_PHANTOM == spaceState &&
1571                mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) {
1572            sendKeyCodePoint(Keyboard.CODE_SPACE);
1573        }
1574        sendKeyCodePoint(primaryCode);
1575
1576        if (Keyboard.CODE_SPACE == primaryCode) {
1577            if (isSuggestionsRequested()) {
1578                if (maybeDoubleSpaceWhileInBatchEdit(ic)) {
1579                    mSpaceState = SPACE_STATE_DOUBLE;
1580                } else if (!isShowingPunctuationList()) {
1581                    mSpaceState = SPACE_STATE_WEAK;
1582                }
1583            }
1584
1585            mHandler.startDoubleSpacesTimer();
1586            if (!isCursorTouchingWord()) {
1587                mHandler.cancelUpdateSuggestions();
1588                mHandler.postUpdateBigramPredictions();
1589            }
1590        } else {
1591            if (swapWeakSpace) {
1592                swapSwapperAndSpaceWhileInBatchEdit(ic);
1593                mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
1594            } else if (SPACE_STATE_PHANTOM == spaceState) {
1595                // If we are in phantom space state, and the user presses a separator, we want to
1596                // stay in phantom space state so that the next keypress has a chance to add the
1597                // space. For example, if I type "Good dat", pick "day" from the suggestion strip
1598                // then insert a comma and go on to typing the next word, I want the space to be
1599                // inserted automatically before the next word, the same way it is when I don't
1600                // input the comma.
1601                mSpaceState = SPACE_STATE_PHANTOM;
1602            }
1603
1604            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
1605            // already displayed or not, so it's okay.
1606            setPunctuationSuggestions();
1607        }
1608
1609        Utils.Stats.onSeparator((char)primaryCode, x, y);
1610
1611        if (ic != null) {
1612            ic.endBatchEdit();
1613        }
1614        return didAutoCorrect;
1615    }
1616
1617    private CharSequence getTextWithUnderline(final CharSequence text) {
1618        return mIsAutoCorrectionIndicatorOn
1619                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
1620                : text;
1621    }
1622
1623    private void handleClose() {
1624        commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR);
1625        requestHideSelf(0);
1626        LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1627        if (inputView != null)
1628            inputView.closing();
1629    }
1630
1631    public boolean isSuggestionsRequested() {
1632        return mInputAttributes.mIsSettingsSuggestionStripOn
1633                && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
1634    }
1635
1636    public boolean isShowingPunctuationList() {
1637        if (mSuggestionsView == null) return false;
1638        return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
1639    }
1640
1641    public boolean isShowingSuggestionsStrip() {
1642        return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
1643                || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
1644                        && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
1645    }
1646
1647    public boolean isSuggestionsStripVisible() {
1648        if (mSuggestionsView == null)
1649            return false;
1650        if (mSuggestionsView.isShowingAddToDictionaryHint())
1651            return true;
1652        if (!isShowingSuggestionsStrip())
1653            return false;
1654        if (mInputAttributes.mApplicationSpecifiedCompletionOn)
1655            return true;
1656        return isSuggestionsRequested();
1657    }
1658
1659    public void switchToKeyboardView() {
1660        if (DEBUG) {
1661            Log.d(TAG, "Switch to keyboard view.");
1662        }
1663        View v = mKeyboardSwitcher.getKeyboardView();
1664        if (v != null) {
1665            // Confirms that the keyboard view doesn't have parent view.
1666            ViewParent p = v.getParent();
1667            if (p != null && p instanceof ViewGroup) {
1668                ((ViewGroup) p).removeView(v);
1669            }
1670            setInputView(v);
1671        }
1672        setSuggestionStripShown(isSuggestionsStripVisible());
1673        updateInputViewShown();
1674        mHandler.postUpdateSuggestions();
1675    }
1676
1677    public void clearSuggestions() {
1678        setSuggestions(SuggestedWords.EMPTY, false);
1679        setAutoCorrectionIndicator(false);
1680    }
1681
1682    private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) {
1683        if (mSuggestionsView != null) {
1684            mSuggestionsView.setSuggestions(words);
1685            mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
1686        }
1687    }
1688
1689    private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
1690        // Put a blue underline to a word in TextView which will be auto-corrected.
1691        final InputConnection ic = getCurrentInputConnection();
1692        if (ic == null) return;
1693        if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
1694                && mWordComposer.isComposingWord()) {
1695            mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
1696            final CharSequence textWithUnderline =
1697                    getTextWithUnderline(mWordComposer.getTypedWord());
1698            ic.setComposingText(textWithUnderline, 1);
1699        }
1700    }
1701
1702    public void updateSuggestions() {
1703        // Check if we have a suggestion engine attached.
1704        if ((mSuggest == null || !isSuggestionsRequested())) {
1705            if (mWordComposer.isComposingWord()) {
1706                Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
1707                mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
1708            }
1709            return;
1710        }
1711
1712        mHandler.cancelUpdateSuggestions();
1713        mHandler.cancelUpdateBigramPredictions();
1714
1715        if (!mWordComposer.isComposingWord()) {
1716            setPunctuationSuggestions();
1717            return;
1718        }
1719
1720        // TODO: May need a better way of retrieving previous word
1721        final InputConnection ic = getCurrentInputConnection();
1722        final CharSequence prevWord;
1723        if (null == ic) {
1724            prevWord = null;
1725        } else {
1726            prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
1727        }
1728
1729        final CharSequence typedWord = mWordComposer.getTypedWord();
1730        // getSuggestedWords handles gracefully a null value of prevWord
1731        final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
1732                prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode);
1733
1734        // Basically, we update the suggestion strip only when suggestion count > 1.  However,
1735        // there is an exception: We update the suggestion strip whenever typed word's length
1736        // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
1737        // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
1738        // need to clear the previous state when the user starts typing a word (i.e. typed word's
1739        // length == 1).
1740        if (suggestedWords.size() > 1 || typedWord.length() == 1
1741                || !suggestedWords.mAllowsToBeAutoCorrected
1742                || mSuggestionsView.isShowingAddToDictionaryHint()) {
1743            showSuggestions(suggestedWords, typedWord);
1744        } else {
1745            SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
1746            if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
1747                previousSuggestions = SuggestedWords.EMPTY;
1748            }
1749            final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
1750                    SuggestedWords.getTypedWordAndPreviousSuggestions(
1751                            typedWord, previousSuggestions);
1752            final SuggestedWords obsoleteSuggestedWords =
1753                    new SuggestedWords(typedWordAndPreviousSuggestions,
1754                            false /* typedWordValid */,
1755                            false /* hasAutoCorrectionCandidate */,
1756                            false /* allowsToBeAutoCorrected */,
1757                            false /* isPunctuationSuggestions */,
1758                            true /* isObsoleteSuggestions */);
1759            showSuggestions(obsoleteSuggestedWords, typedWord);
1760        }
1761    }
1762
1763    public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) {
1764        final CharSequence autoCorrection;
1765        if (suggestedWords.size() > 0) {
1766            if (suggestedWords.hasAutoCorrectionWord()) {
1767                autoCorrection = suggestedWords.getWord(1);
1768            } else {
1769                autoCorrection = typedWord;
1770            }
1771        } else {
1772            autoCorrection = null;
1773        }
1774        mWordComposer.setAutoCorrection(autoCorrection);
1775        final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
1776        setSuggestions(suggestedWords, isAutoCorrection);
1777        setAutoCorrectionIndicator(isAutoCorrection);
1778        setSuggestionStripShown(isSuggestionsStripVisible());
1779    }
1780
1781    private void commitCurrentAutoCorrection(final int separatorCodePoint,
1782            final InputConnection ic) {
1783        // Complete any pending suggestions query first
1784        if (mHandler.hasPendingUpdateSuggestions()) {
1785            mHandler.cancelUpdateSuggestions();
1786            updateSuggestions();
1787        }
1788        final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
1789        if (autoCorrection != null) {
1790            final String typedWord = mWordComposer.getTypedWord();
1791            if (TextUtils.isEmpty(typedWord)) {
1792                throw new RuntimeException("We have an auto-correction but the typed word "
1793                        + "is empty? Impossible! I must commit suicide.");
1794            }
1795            Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
1796            mExpectingUpdateSelection = true;
1797            commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
1798                    separatorCodePoint);
1799            // Add the word to the user history dictionary
1800            addToUserHistoryDictionary(autoCorrection);
1801            if (!typedWord.equals(autoCorrection) && null != ic) {
1802                // This will make the correction flash for a short while as a visual clue
1803                // to the user that auto-correction happened.
1804                ic.commitCorrection(new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
1805                        typedWord, autoCorrection));
1806            }
1807        }
1808    }
1809
1810    @Override
1811    public void pickSuggestionManually(final int index, final CharSequence suggestion) {
1812        final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
1813
1814        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
1815            int firstChar = Character.codePointAt(suggestion, 0);
1816            if ((!mSettingsValues.isWeakSpaceStripper(firstChar))
1817                    && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) {
1818                sendKeyCodePoint(Keyboard.CODE_SPACE);
1819            }
1820        }
1821
1822        if (mInputAttributes.mApplicationSpecifiedCompletionOn
1823                && mApplicationSpecifiedCompletions != null
1824                && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
1825            if (mSuggestionsView != null) {
1826                mSuggestionsView.clear();
1827            }
1828            mKeyboardSwitcher.updateShiftState();
1829            resetComposingState(true /* alsoResetLastComposedWord */);
1830            final InputConnection ic = getCurrentInputConnection();
1831            if (ic != null) {
1832                final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
1833                ic.commitCompletion(completionInfo);
1834            }
1835            return;
1836        }
1837
1838        // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
1839        if (suggestion.length() == 1 && isShowingPunctuationList()) {
1840            // Word separators are suggested before the user inputs something.
1841            // So, LatinImeLogger logs "" as a user's input.
1842            LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
1843            // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
1844            final int primaryCode = suggestion.charAt(0);
1845            onCodeInput(primaryCode,
1846                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
1847                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
1848            return;
1849        }
1850        // We need to log before we commit, because the word composer will store away the user
1851        // typed word.
1852        LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(),
1853                suggestion.toString(), index, suggestedWords);
1854        mExpectingUpdateSelection = true;
1855        commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
1856                LastComposedWord.NOT_A_SEPARATOR);
1857        // Add the word to the user history dictionary
1858        addToUserHistoryDictionary(suggestion);
1859        mSpaceState = SPACE_STATE_PHANTOM;
1860        // TODO: is this necessary?
1861        mKeyboardSwitcher.updateShiftState();
1862
1863        // We should show the "Touch again to save" hint if the user pressed the first entry
1864        // AND either:
1865        // - There is no dictionary (we know that because we tried to load it => null != mSuggest
1866        //   AND mSuggest.hasMainDictionary() is false)
1867        // - There is a dictionary and the word is not in it
1868        // Please note that if mSuggest is null, it means that everything is off: suggestion
1869        // and correction, so we shouldn't try to show the hint
1870        // We used to look at mCorrectionMode here, but showing the hint should have nothing
1871        // to do with the autocorrection setting.
1872        final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
1873                // If there is no dictionary the hint should be shown.
1874                && (!mSuggest.hasMainDictionary()
1875                        // If "suggestion" is not in the dictionary, the hint should be shown.
1876                        || !AutoCorrection.isValidWord(
1877                                mSuggest.getUnigramDictionaries(), suggestion, true));
1878
1879        Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE,
1880                WordComposer.NOT_A_COORDINATE);
1881        if (!showingAddToDictionaryHint) {
1882            // If we're not showing the "Touch again to save", then show corrections again.
1883            // In case the cursor position doesn't change, make sure we show the suggestions again.
1884            updateBigramPredictions();
1885            // Updating the predictions right away may be slow and feel unresponsive on slower
1886            // terminals. On the other hand if we just postUpdateBigramPredictions() it will
1887            // take a noticeable delay to update them which may feel uneasy.
1888        } else {
1889            if (mIsUserDictionaryAvailable) {
1890                mSuggestionsView.showAddToDictionaryHint(
1891                        suggestion, mSettingsValues.mHintToSaveText);
1892            } else {
1893                mHandler.postUpdateSuggestions();
1894            }
1895        }
1896    }
1897
1898    /**
1899     * Commits the chosen word to the text field and saves it for later retrieval.
1900     */
1901    private void commitChosenWord(final CharSequence bestWord, final int commitType,
1902            final int separatorCode) {
1903        final InputConnection ic = getCurrentInputConnection();
1904        if (ic != null) {
1905            if (mSettingsValues.mEnableSuggestionSpanInsertion) {
1906                final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
1907                ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
1908                        this, bestWord, suggestedWords), 1);
1909            } else {
1910                ic.commitText(bestWord, 1);
1911            }
1912        }
1913        // TODO: figure out here if this is an auto-correct or if the best word is actually
1914        // what user typed. Note: currently this is done much later in
1915        // LastComposedWord#didCommitTypedWord by string equality of the remembered
1916        // strings.
1917        mLastComposedWord = mWordComposer.commitWord(commitType, bestWord.toString(),
1918                separatorCode);
1919    }
1920
1921    public void updateBigramPredictions() {
1922        if (mSuggest == null || !isSuggestionsRequested())
1923            return;
1924
1925        if (!mSettingsValues.mBigramPredictionEnabled) {
1926            setPunctuationSuggestions();
1927            return;
1928        }
1929
1930        final SuggestedWords suggestedWords;
1931        if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
1932            final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
1933                    mSettingsValues.mWordSeparators);
1934            if (!TextUtils.isEmpty(prevWord)) {
1935                suggestedWords = mSuggest.getBigramPredictions(prevWord);
1936            } else {
1937                suggestedWords = null;
1938            }
1939        } else {
1940            suggestedWords = null;
1941        }
1942
1943        if (null != suggestedWords && suggestedWords.size() > 0) {
1944            // Explicitly supply an empty typed word (the no-second-arg version of
1945            // showSuggestions will retrieve the word near the cursor, we don't want that here)
1946            showSuggestions(suggestedWords, "");
1947        } else {
1948            if (!isShowingPunctuationList()) setPunctuationSuggestions();
1949        }
1950    }
1951
1952    public void setPunctuationSuggestions() {
1953        setSuggestions(mSettingsValues.mSuggestPuncList, false);
1954        setAutoCorrectionIndicator(false);
1955        setSuggestionStripShown(isSuggestionsStripVisible());
1956    }
1957
1958    private void addToUserHistoryDictionary(final CharSequence suggestion) {
1959        if (TextUtils.isEmpty(suggestion)) return;
1960
1961        // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
1962        // adding words in situations where the user or application really didn't
1963        // want corrections enabled or learned.
1964        if (!(mCorrectionMode == Suggest.CORRECTION_FULL
1965                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
1966            return;
1967        }
1968
1969        if (mUserHistoryDictionary != null) {
1970            final InputConnection ic = getCurrentInputConnection();
1971            final CharSequence prevWord;
1972            if (null != ic) {
1973                prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
1974            } else {
1975                prevWord = null;
1976            }
1977            final String secondWord;
1978            if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
1979                secondWord = suggestion.toString().toLowerCase(mSubtypeSwitcher.getInputLocale());
1980            } else {
1981                secondWord = suggestion.toString();
1982            }
1983            mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
1984                    secondWord);
1985        }
1986    }
1987
1988    public boolean isCursorTouchingWord() {
1989        final InputConnection ic = getCurrentInputConnection();
1990        if (ic == null) return false;
1991        CharSequence before = ic.getTextBeforeCursor(1, 0);
1992        CharSequence after = ic.getTextAfterCursor(1, 0);
1993        if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0))
1994                && !mSettingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
1995            return true;
1996        }
1997        if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0))
1998                && !mSettingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
1999            return true;
2000        }
2001        return false;
2002    }
2003
2004    // "ic" must not be null
2005    private static boolean sameAsTextBeforeCursor(final InputConnection ic,
2006            final CharSequence text) {
2007        final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
2008        return TextUtils.equals(text, beforeText);
2009    }
2010
2011    // "ic" must not be null
2012    /**
2013     * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
2014     * word, else do nothing.
2015     */
2016    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
2017            final InputConnection ic) {
2018        // Bail out if the cursor is not at the end of a word (cursor must be preceded by
2019        // non-whitespace, non-separator, non-start-of-text)
2020        // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
2021        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0);
2022        if (TextUtils.isEmpty(textBeforeCursor)
2023                || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return;
2024
2025        // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
2026        // separator or end of line/text)
2027        // Example: "test|"<EOL> "te|st" get rejected here
2028        final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0);
2029        if (!TextUtils.isEmpty(textAfterCursor)
2030                && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return;
2031
2032        // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
2033        // Example: " -|" gets rejected here but "e-|" and "e|" are okay
2034        CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators);
2035        // We don't suggest on leading single quotes, so we have to remove them from the word if
2036        // it starts with single quotes.
2037        while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
2038            word = word.subSequence(1, word.length());
2039        }
2040        if (TextUtils.isEmpty(word)) return;
2041        final char firstChar = word.charAt(0); // we just tested that word is not empty
2042        if (word.length() == 1 && !Character.isLetter(firstChar)) return;
2043
2044        // We only suggest on words that start with a letter or a symbol that is excluded from
2045        // word separators (see #handleCharacterWhileInBatchEdit).
2046        if (!(isAlphabet(firstChar)
2047                || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) {
2048            return;
2049        }
2050
2051        // Okay, we are at the end of a word. Restart suggestions.
2052        restartSuggestionsOnWordBeforeCursor(ic, word);
2053    }
2054
2055    // "ic" must not be null
2056    private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic,
2057            final CharSequence word) {
2058        mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
2059        ic.deleteSurroundingText(word.length(), 0);
2060        ic.setComposingText(word, 1);
2061        mHandler.postUpdateSuggestions();
2062    }
2063
2064    // "ic" must not be null
2065    private void revertCommit(final InputConnection ic) {
2066        final String originallyTypedWord = mLastComposedWord.mTypedWord;
2067        final CharSequence committedWord = mLastComposedWord.mCommittedWord;
2068        final int cancelLength = committedWord.length();
2069        final int separatorLength = LastComposedWord.getSeparatorLength(
2070                mLastComposedWord.mSeparatorCode);
2071        // TODO: should we check our saved separator against the actual contents of the text view?
2072        if (DEBUG) {
2073            if (mWordComposer.isComposingWord()) {
2074                throw new RuntimeException("revertCommit, but we are composing a word");
2075            }
2076            final String wordBeforeCursor =
2077                    ic.getTextBeforeCursor(cancelLength + separatorLength, 0)
2078                            .subSequence(0, cancelLength).toString();
2079            if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
2080                throw new RuntimeException("revertCommit check failed: we thought we were "
2081                        + "reverting \"" + committedWord
2082                        + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
2083            }
2084        }
2085        ic.deleteSurroundingText(cancelLength + separatorLength, 0);
2086        if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) {
2087            // This is the case when we cancel a manual pick.
2088            // We should restart suggestion on the word right away.
2089            mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
2090            ic.setComposingText(originallyTypedWord, 1);
2091        } else {
2092            ic.commitText(originallyTypedWord, 1);
2093            // Re-insert the separator
2094            sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
2095            Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
2096                    WordComposer.NOT_A_COORDINATE);
2097            // Don't restart suggestion yet. We'll restart if the user deletes the
2098            // separator.
2099        }
2100        mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
2101        mHandler.cancelUpdateBigramPredictions();
2102        mHandler.postUpdateSuggestions();
2103    }
2104
2105    // "ic" must not be null
2106    private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
2107        mHandler.cancelDoubleSpacesTimer();
2108        // Here we test whether we indeed have a period and a space before us. This should not
2109        // be needed, but it's there just in case something went wrong.
2110        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
2111        if (!". ".equals(textBeforeCursor)) {
2112            // Theoretically we should not be coming here if there isn't ". " before the
2113            // cursor, but the application may be changing the text while we are typing, so
2114            // anything goes. We should not crash.
2115            Log.d(TAG, "Tried to revert double-space combo but we didn't find "
2116                    + "\". \" just before the cursor.");
2117            return false;
2118        }
2119        ic.deleteSurroundingText(2, 0);
2120        ic.commitText("  ", 1);
2121        return true;
2122    }
2123
2124    private static boolean revertSwapPunctuation(final InputConnection ic) {
2125        // Here we test whether we indeed have a space and something else before us. This should not
2126        // be needed, but it's there just in case something went wrong.
2127        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
2128        // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
2129        // enter surrogate pairs this code will have been removed.
2130        if (TextUtils.isEmpty(textBeforeCursor)
2131                || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
2132            // We may only come here if the application is changing the text while we are typing.
2133            // This is quite a broken case, but not logically impossible, so we shouldn't crash,
2134            // but some debugging log may be in order.
2135            Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
2136                    + "find a space just before the cursor.");
2137            return false;
2138        }
2139        ic.beginBatchEdit();
2140        ic.deleteSurroundingText(2, 0);
2141        ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
2142        ic.endBatchEdit();
2143        return true;
2144    }
2145
2146    public boolean isWordSeparator(int code) {
2147        return mSettingsValues.isWordSeparator(code);
2148    }
2149
2150    public boolean preferCapitalization() {
2151        return mWordComposer.isFirstCharCapitalized();
2152    }
2153
2154    // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
2155    // according to new language or mode.
2156    public void onRefreshKeyboard() {
2157        // When the device locale is changed in SetupWizard etc., this method may get called via
2158        // onConfigurationChanged before SoftInputWindow is shown.
2159        if (mKeyboardSwitcher.getKeyboardView() != null) {
2160            // Reload keyboard because the current language has been changed.
2161            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
2162        }
2163        initSuggest();
2164        updateCorrectionMode();
2165        loadSettings();
2166        // Since we just changed languages, we should re-evaluate suggestions with whatever word
2167        // we are currently composing. If we are not composing anything, we may want to display
2168        // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
2169        if (isCursorTouchingWord()) {
2170            mHandler.postUpdateSuggestions();
2171        } else {
2172            mHandler.postUpdateBigramPredictions();
2173        }
2174    }
2175
2176    public void hapticAndAudioFeedback(final int primaryCode) {
2177        mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView());
2178    }
2179
2180    @Override
2181    public void onPressKey(int primaryCode) {
2182        mKeyboardSwitcher.onPressKey(primaryCode);
2183    }
2184
2185    @Override
2186    public void onReleaseKey(int primaryCode, boolean withSliding) {
2187        mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
2188
2189        // If accessibility is on, ensure the user receives keyboard state updates.
2190        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
2191            switch (primaryCode) {
2192            case Keyboard.CODE_SHIFT:
2193                AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
2194                break;
2195            case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
2196                AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
2197                break;
2198            }
2199        }
2200    }
2201
2202    // receive ringer mode change and network state change.
2203    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2204        @Override
2205        public void onReceive(Context context, Intent intent) {
2206            final String action = intent.getAction();
2207            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2208                mSubtypeSwitcher.onNetworkStateChanged(intent);
2209            } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2210                mFeedbackManager.onRingerModeChanged();
2211            }
2212        }
2213    };
2214
2215    private void updateCorrectionMode() {
2216        // TODO: cleanup messy flags
2217        final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
2218                && !mInputAttributes.mInputTypeNoAutoCorrect;
2219        mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE;
2220        mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect)
2221                ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
2222    }
2223
2224    private void updateSuggestionVisibility(final Resources res) {
2225        final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting;
2226        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
2227            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
2228                mSuggestionVisibility = visibility;
2229                break;
2230            }
2231        }
2232    }
2233
2234    protected void launchSettings() {
2235        launchSettingsClass(Settings.class);
2236    }
2237
2238    public void launchDebugSettings() {
2239        launchSettingsClass(DebugSettings.class);
2240    }
2241
2242    protected void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
2243        handleClose();
2244        Intent intent = new Intent();
2245        intent.setClass(LatinIME.this, settingsClass);
2246        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2247        startActivity(intent);
2248    }
2249
2250    private void showSubtypeSelectorAndSettings() {
2251        final CharSequence title = getString(R.string.english_ime_input_options);
2252        final CharSequence[] items = new CharSequence[] {
2253                // TODO: Should use new string "Select active input modes".
2254                getString(R.string.language_selection_title),
2255                getString(R.string.english_ime_settings),
2256        };
2257        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2258            @Override
2259            public void onClick(DialogInterface di, int position) {
2260                di.dismiss();
2261                switch (position) {
2262                case 0:
2263                    Intent intent = CompatUtils.getInputLanguageSelectionIntent(
2264                            SubtypeUtils.getInputMethodId(getPackageName()),
2265                            Intent.FLAG_ACTIVITY_NEW_TASK
2266                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2267                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2268                    startActivity(intent);
2269                    break;
2270                case 1:
2271                    launchSettings();
2272                    break;
2273                }
2274            }
2275        };
2276        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2277                .setItems(items, listener)
2278                .setTitle(title);
2279        showOptionDialogInternal(builder.create());
2280    }
2281
2282    private void showOptionDialogInternal(AlertDialog dialog) {
2283        final IBinder windowToken = KeyboardSwitcher.getInstance().getKeyboardView()
2284                .getWindowToken();
2285        if (windowToken == null) return;
2286
2287        dialog.setCancelable(true);
2288        dialog.setCanceledOnTouchOutside(true);
2289
2290        final Window window = dialog.getWindow();
2291        final WindowManager.LayoutParams lp = window.getAttributes();
2292        lp.token = windowToken;
2293        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
2294        window.setAttributes(lp);
2295        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
2296
2297        mOptionsDialog = dialog;
2298        dialog.show();
2299    }
2300
2301    @Override
2302    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
2303        super.dump(fd, fout, args);
2304
2305        final Printer p = new PrintWriterPrinter(fout);
2306        p.println("LatinIME state :");
2307        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
2308        final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
2309        p.println("  Keyboard mode = " + keyboardMode);
2310        p.println("  mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn);
2311        p.println("  mCorrectionMode=" + mCorrectionMode);
2312        p.println("  isComposingWord=" + mWordComposer.isComposingWord());
2313        p.println("  mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
2314        p.println("  mSoundOn=" + mSettingsValues.mSoundOn);
2315        p.println("  mVibrateOn=" + mSettingsValues.mVibrateOn);
2316        p.println("  mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
2317        p.println("  mInputAttributes=" + mInputAttributes.toString());
2318    }
2319}
2320