LatinIME.java revision b1e98c2f4fbfc637db302e699a7a331691cf90a3
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
20import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
21import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
22
23import android.app.Activity;
24import android.app.AlertDialog;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.SharedPreferences;
31import android.content.pm.PackageInfo;
32import android.content.res.Configuration;
33import android.content.res.Resources;
34import android.graphics.Rect;
35import android.inputmethodservice.InputMethodService;
36import android.media.AudioManager;
37import android.net.ConnectivityManager;
38import android.os.Debug;
39import android.os.Handler;
40import android.os.HandlerThread;
41import android.os.IBinder;
42import android.os.Message;
43import android.os.SystemClock;
44import android.preference.PreferenceManager;
45import android.text.InputType;
46import android.text.TextUtils;
47import android.text.style.SuggestionSpan;
48import android.util.Log;
49import android.util.PrintWriterPrinter;
50import android.util.Printer;
51import android.view.KeyCharacterMap;
52import android.view.KeyEvent;
53import android.view.View;
54import android.view.ViewGroup.LayoutParams;
55import android.view.Window;
56import android.view.WindowManager;
57import android.view.inputmethod.CompletionInfo;
58import android.view.inputmethod.CorrectionInfo;
59import android.view.inputmethod.EditorInfo;
60import android.view.inputmethod.InputMethodSubtype;
61
62import com.android.inputmethod.accessibility.AccessibilityUtils;
63import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
64import com.android.inputmethod.annotations.UsedForTesting;
65import com.android.inputmethod.compat.AppWorkaroundsUtils;
66import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
67import com.android.inputmethod.compat.SuggestionSpanUtils;
68import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
69import com.android.inputmethod.event.EventInterpreter;
70import com.android.inputmethod.keyboard.KeyDetector;
71import com.android.inputmethod.keyboard.Keyboard;
72import com.android.inputmethod.keyboard.KeyboardActionListener;
73import com.android.inputmethod.keyboard.KeyboardId;
74import com.android.inputmethod.keyboard.KeyboardSwitcher;
75import com.android.inputmethod.keyboard.MainKeyboardView;
76import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
77import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
78import com.android.inputmethod.latin.define.ProductionFlag;
79import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
80import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister;
81import com.android.inputmethod.latin.personalization.PersonalizationHelper;
82import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary;
83import com.android.inputmethod.latin.personalization.UserHistoryPredictionDictionary;
84import com.android.inputmethod.latin.settings.Settings;
85import com.android.inputmethod.latin.settings.SettingsActivity;
86import com.android.inputmethod.latin.settings.SettingsValues;
87import com.android.inputmethod.latin.suggestions.SuggestionStripView;
88import com.android.inputmethod.latin.utils.ApplicationUtils;
89import com.android.inputmethod.latin.utils.AsyncResultHolder;
90import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
91import com.android.inputmethod.latin.utils.CapsModeUtils;
92import com.android.inputmethod.latin.utils.CollectionUtils;
93import com.android.inputmethod.latin.utils.CompletionInfoUtils;
94import com.android.inputmethod.latin.utils.InputTypeUtils;
95import com.android.inputmethod.latin.utils.IntentUtils;
96import com.android.inputmethod.latin.utils.JniUtils;
97import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
98import com.android.inputmethod.latin.utils.RecapitalizeStatus;
99import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
100import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
101import com.android.inputmethod.latin.utils.TextRange;
102import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils;
103import com.android.inputmethod.research.ResearchLogger;
104
105import java.io.FileDescriptor;
106import java.io.PrintWriter;
107import java.util.ArrayList;
108import java.util.Locale;
109import java.util.TreeSet;
110
111/**
112 * Input method implementation for Qwerty'ish keyboard.
113 */
114public class LatinIME extends InputMethodService implements KeyboardActionListener,
115        SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener,
116        Suggest.SuggestInitializationListener {
117    private static final String TAG = LatinIME.class.getSimpleName();
118    private static final boolean TRACE = false;
119    private static boolean DEBUG;
120
121    private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
122
123    // How many continuous deletes at which to start deleting at a higher speed.
124    private static final int DELETE_ACCELERATE_AT = 20;
125    // Key events coming any faster than this are long-presses.
126    private static final int QUICK_PRESS = 200;
127
128    private static final int PENDING_IMS_CALLBACK_DURATION = 800;
129
130    private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2;
131
132    // TODO: Set this value appropriately.
133    private static final int GET_SUGGESTED_WORDS_TIMEOUT = 200;
134
135    /**
136     * The name of the scheme used by the Package Manager to warn of a new package installation,
137     * replacement or removal.
138     */
139    private static final String SCHEME_PACKAGE = "package";
140
141    private static final int SPACE_STATE_NONE = 0;
142    // Double space: the state where the user pressed space twice quickly, which LatinIME
143    // resolved as period-space. Undoing this converts the period to a space.
144    private static final int SPACE_STATE_DOUBLE = 1;
145    // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
146    // have just been swapped. Undoing this swaps them back; the space is still considered weak.
147    private static final int SPACE_STATE_SWAP_PUNCTUATION = 2;
148    // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
149    // spaces happen when the user presses space, accepting the current suggestion (whether
150    // it's an auto-correction or not).
151    private static final int SPACE_STATE_WEAK = 3;
152    // Phantom space: a not-yet-inserted space that should get inserted on the next input,
153    // character provided it's not a separator. If it's a separator, the phantom space is dropped.
154    // Phantom spaces happen when a user chooses a word from the suggestion strip.
155    private static final int SPACE_STATE_PHANTOM = 4;
156
157    // Current space state of the input method. This can be any of the above constants.
158    private int mSpaceState;
159
160    private final Settings mSettings;
161
162    private View mExtractArea;
163    private View mKeyPreviewBackingView;
164    private SuggestionStripView mSuggestionStripView;
165    // Never null
166    private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
167    private Suggest mSuggest;
168    private CompletionInfo[] mApplicationSpecifiedCompletions;
169    private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils();
170
171    private RichInputMethodManager mRichImm;
172    @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
173    private final SubtypeSwitcher mSubtypeSwitcher;
174    private final SubtypeState mSubtypeState = new SubtypeState();
175    // At start, create a default event interpreter that does nothing by passing it no decoder spec.
176    // The event interpreter should never be null.
177    private EventInterpreter mEventInterpreter = new EventInterpreter(this);
178
179    private boolean mIsMainDictionaryAvailable;
180    private UserBinaryDictionary mUserDictionary;
181    private UserHistoryPredictionDictionary mUserHistoryPredictionDictionary;
182    private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary;
183    private PersonalizationDictionary mPersonalizationDictionary;
184    private boolean mIsUserDictionaryAvailable;
185
186    private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
187    private final WordComposer mWordComposer = new WordComposer();
188    private final RichInputConnection mConnection = new RichInputConnection(this);
189    private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
190
191    // Keep track of the last selection range to decide if we need to show word alternatives
192    private static final int NOT_A_CURSOR_POSITION = -1;
193    private int mLastSelectionStart = NOT_A_CURSOR_POSITION;
194    private int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
195
196    // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
197    // "expect" it, it means the user actually moved the cursor.
198    private boolean mExpectingUpdateSelection;
199    private int mDeleteCount;
200    private long mLastKeyTime;
201    private final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
202    // Personalization debugging params
203    private boolean mUseOnlyPersonalizationDictionaryForDebug = false;
204    private boolean mBoostPersonalizationDictionaryForDebug = false;
205
206    // Member variables for remembering the current device orientation.
207    private int mDisplayOrientation;
208
209    // Object for reacting to adding/removing a dictionary pack.
210    private BroadcastReceiver mDictionaryPackInstallReceiver =
211            new DictionaryPackInstallBroadcastReceiver(this);
212
213    // Keeps track of most recently inserted text (multi-character key) for reverting
214    private String mEnteredText;
215
216    // TODO: This boolean is persistent state and causes large side effects at unexpected times.
217    // Find a way to remove it for readability.
218    private boolean mIsAutoCorrectionIndicatorOn;
219
220    private AlertDialog mOptionsDialog;
221
222    private final boolean mIsHardwareAcceleratedDrawingEnabled;
223
224    public final UIHandler mHandler = new UIHandler(this);
225    private InputUpdater mInputUpdater;
226
227    public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
228        private static final int MSG_UPDATE_SHIFT_STATE = 0;
229        private static final int MSG_PENDING_IMS_CALLBACK = 1;
230        private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
231        private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
232        private static final int MSG_RESUME_SUGGESTIONS = 4;
233        private static final int MSG_REOPEN_DICTIONARIES = 5;
234        private static final int MSG_ON_END_BATCH_INPUT = 6;
235
236        private static final int ARG1_NOT_GESTURE_INPUT = 0;
237        private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
238        private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2;
239
240        private int mDelayUpdateSuggestions;
241        private int mDelayUpdateShiftState;
242        private long mDoubleSpacePeriodTimeout;
243        private long mDoubleSpacePeriodTimerStart;
244
245        public UIHandler(final LatinIME outerInstance) {
246            super(outerInstance);
247        }
248
249        public void onCreate() {
250            final Resources res = getOuterInstance().getResources();
251            mDelayUpdateSuggestions =
252                    res.getInteger(R.integer.config_delay_update_suggestions);
253            mDelayUpdateShiftState =
254                    res.getInteger(R.integer.config_delay_update_shift_state);
255            mDoubleSpacePeriodTimeout =
256                    res.getInteger(R.integer.config_double_space_period_timeout);
257        }
258
259        @Override
260        public void handleMessage(final Message msg) {
261            final LatinIME latinIme = getOuterInstance();
262            final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
263            switch (msg.what) {
264            case MSG_UPDATE_SUGGESTION_STRIP:
265                latinIme.updateSuggestionStrip();
266                break;
267            case MSG_UPDATE_SHIFT_STATE:
268                switcher.updateShiftState();
269                break;
270            case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
271                if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) {
272                    latinIme.showSuggestionStrip((SuggestedWords) msg.obj);
273                } else {
274                    latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj,
275                            msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
276                }
277                break;
278            case MSG_RESUME_SUGGESTIONS:
279                latinIme.restartSuggestionsOnWordTouchedByCursor();
280                break;
281            case MSG_REOPEN_DICTIONARIES:
282                latinIme.initSuggest();
283                // In theory we could call latinIme.updateSuggestionStrip() right away, but
284                // in the practice, the dictionary is not finished opening yet so we wouldn't
285                // get any suggestions. Wait one frame.
286                postUpdateSuggestionStrip();
287                break;
288            case MSG_ON_END_BATCH_INPUT:
289                latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj);
290                break;
291            }
292        }
293
294        public void postUpdateSuggestionStrip() {
295            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
296        }
297
298        public void postReopenDictionaries() {
299            sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES));
300        }
301
302        public void postResumeSuggestions() {
303            removeMessages(MSG_RESUME_SUGGESTIONS);
304            sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
305        }
306
307        public void cancelUpdateSuggestionStrip() {
308            removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
309        }
310
311        public boolean hasPendingUpdateSuggestions() {
312            return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
313        }
314
315        public boolean hasPendingReopenDictionaries() {
316            return hasMessages(MSG_REOPEN_DICTIONARIES);
317        }
318
319        public void postUpdateShiftState() {
320            removeMessages(MSG_UPDATE_SHIFT_STATE);
321            sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
322        }
323
324        public void cancelUpdateShiftState() {
325            removeMessages(MSG_UPDATE_SHIFT_STATE);
326        }
327
328        public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
329                final boolean dismissGestureFloatingPreviewText) {
330            removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
331            final int arg1 = dismissGestureFloatingPreviewText
332                    ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT
333                    : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT;
334            obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords)
335                    .sendToTarget();
336        }
337
338        public void showSuggestionStrip(final SuggestedWords suggestedWords) {
339            removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
340            obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP,
341                    ARG1_NOT_GESTURE_INPUT, 0, suggestedWords).sendToTarget();
342        }
343
344        public void onEndBatchInput(final SuggestedWords suggestedWords) {
345            obtainMessage(MSG_ON_END_BATCH_INPUT, suggestedWords).sendToTarget();
346        }
347
348        public void startDoubleSpacePeriodTimer() {
349            mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis();
350        }
351
352        public void cancelDoubleSpacePeriodTimer() {
353            mDoubleSpacePeriodTimerStart = 0;
354        }
355
356        public boolean isAcceptingDoubleSpacePeriod() {
357            return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart
358                    < mDoubleSpacePeriodTimeout;
359        }
360
361        // Working variables for the following methods.
362        private boolean mIsOrientationChanging;
363        private boolean mPendingSuccessiveImsCallback;
364        private boolean mHasPendingStartInput;
365        private boolean mHasPendingFinishInputView;
366        private boolean mHasPendingFinishInput;
367        private EditorInfo mAppliedEditorInfo;
368
369        public void startOrientationChanging() {
370            removeMessages(MSG_PENDING_IMS_CALLBACK);
371            resetPendingImsCallback();
372            mIsOrientationChanging = true;
373            final LatinIME latinIme = getOuterInstance();
374            if (latinIme.isInputViewShown()) {
375                latinIme.mKeyboardSwitcher.saveKeyboardState();
376            }
377        }
378
379        private void resetPendingImsCallback() {
380            mHasPendingFinishInputView = false;
381            mHasPendingFinishInput = false;
382            mHasPendingStartInput = false;
383        }
384
385        private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo,
386                boolean restarting) {
387            if (mHasPendingFinishInputView)
388                latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
389            if (mHasPendingFinishInput)
390                latinIme.onFinishInputInternal();
391            if (mHasPendingStartInput)
392                latinIme.onStartInputInternal(editorInfo, restarting);
393            resetPendingImsCallback();
394        }
395
396        public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
397            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
398                // Typically this is the second onStartInput after orientation changed.
399                mHasPendingStartInput = true;
400            } else {
401                if (mIsOrientationChanging && restarting) {
402                    // This is the first onStartInput after orientation changed.
403                    mIsOrientationChanging = false;
404                    mPendingSuccessiveImsCallback = true;
405                }
406                final LatinIME latinIme = getOuterInstance();
407                executePendingImsCallback(latinIme, editorInfo, restarting);
408                latinIme.onStartInputInternal(editorInfo, restarting);
409            }
410        }
411
412        public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
413            if (hasMessages(MSG_PENDING_IMS_CALLBACK)
414                    && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
415                // Typically this is the second onStartInputView after orientation changed.
416                resetPendingImsCallback();
417            } else {
418                if (mPendingSuccessiveImsCallback) {
419                    // This is the first onStartInputView after orientation changed.
420                    mPendingSuccessiveImsCallback = false;
421                    resetPendingImsCallback();
422                    sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
423                            PENDING_IMS_CALLBACK_DURATION);
424                }
425                final LatinIME latinIme = getOuterInstance();
426                executePendingImsCallback(latinIme, editorInfo, restarting);
427                latinIme.onStartInputViewInternal(editorInfo, restarting);
428                mAppliedEditorInfo = editorInfo;
429            }
430        }
431
432        public void onFinishInputView(final boolean finishingInput) {
433            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
434                // Typically this is the first onFinishInputView after orientation changed.
435                mHasPendingFinishInputView = true;
436            } else {
437                final LatinIME latinIme = getOuterInstance();
438                latinIme.onFinishInputViewInternal(finishingInput);
439                mAppliedEditorInfo = null;
440            }
441        }
442
443        public void onFinishInput() {
444            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
445                // Typically this is the first onFinishInput after orientation changed.
446                mHasPendingFinishInput = true;
447            } else {
448                final LatinIME latinIme = getOuterInstance();
449                executePendingImsCallback(latinIme, null, false);
450                latinIme.onFinishInputInternal();
451            }
452        }
453    }
454
455    static final class SubtypeState {
456        private InputMethodSubtype mLastActiveSubtype;
457        private boolean mCurrentSubtypeUsed;
458
459        public void currentSubtypeUsed() {
460            mCurrentSubtypeUsed = true;
461        }
462
463        public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) {
464            final InputMethodSubtype currentSubtype = richImm.getInputMethodManager()
465                    .getCurrentInputMethodSubtype();
466            final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype;
467            final boolean currentSubtypeUsed = mCurrentSubtypeUsed;
468            if (currentSubtypeUsed) {
469                mLastActiveSubtype = currentSubtype;
470                mCurrentSubtypeUsed = false;
471            }
472            if (currentSubtypeUsed
473                    && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
474                    && !currentSubtype.equals(lastActiveSubtype)) {
475                richImm.setInputMethodAndSubtype(token, lastActiveSubtype);
476                return;
477            }
478            richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */);
479        }
480    }
481
482    // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial
483    // JNI call as much as possible.
484    static {
485        JniUtils.loadNativeLibrary();
486    }
487
488    public LatinIME() {
489        super();
490        mSettings = Settings.getInstance();
491        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
492        mKeyboardSwitcher = KeyboardSwitcher.getInstance();
493        mIsHardwareAcceleratedDrawingEnabled =
494                InputMethodServiceCompatUtils.enableHardwareAcceleration(this);
495        Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled);
496    }
497
498    @Override
499    public void onCreate() {
500        Settings.init(this);
501        LatinImeLogger.init(this);
502        RichInputMethodManager.init(this);
503        mRichImm = RichInputMethodManager.getInstance();
504        SubtypeSwitcher.init(this);
505        KeyboardSwitcher.init(this);
506        AudioAndHapticFeedbackManager.init(this);
507        AccessibilityUtils.init(this);
508        PersonalizationDictionarySessionRegister.init(this);
509
510        super.onCreate();
511
512        mHandler.onCreate();
513        DEBUG = LatinImeLogger.sDBG;
514
515        // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}.
516        loadSettings();
517        initSuggest();
518
519        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
520            ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest);
521        }
522        mDisplayOrientation = getResources().getConfiguration().orientation;
523
524        // Register to receive ringer mode change and network state change.
525        // Also receive installation and removal of a dictionary pack.
526        final IntentFilter filter = new IntentFilter();
527        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
528        filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
529        registerReceiver(mReceiver, filter);
530
531        final IntentFilter packageFilter = new IntentFilter();
532        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
533        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
534        packageFilter.addDataScheme(SCHEME_PACKAGE);
535        registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
536
537        final IntentFilter newDictFilter = new IntentFilter();
538        newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
539        registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
540
541        mInputUpdater = new InputUpdater(this);
542    }
543
544    // Has to be package-visible for unit tests
545    @UsedForTesting
546    void loadSettings() {
547        final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
548        final InputAttributes inputAttributes =
549                new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
550        mSettings.loadSettings(locale, inputAttributes);
551        AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent());
552        // To load the keyboard we need to load all the settings once, but resetting the
553        // contacts dictionary should be deferred until after the new layout has been displayed
554        // to improve responsivity. In the language switching process, we post a reopenDictionaries
555        // message, then come here to read the settings for the new language before we change
556        // the layout; at this time, we need to skip resetting the contacts dictionary. It will
557        // be done later inside {@see #initSuggest()} when the reopenDictionaries message is
558        // processed.
559        if (!mHandler.hasPendingReopenDictionaries()) {
560            // May need to reset the contacts dictionary depending on the user settings.
561            resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
562        }
563    }
564
565    // Note that this method is called from a non-UI thread.
566    @Override
567    public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) {
568        mIsMainDictionaryAvailable = isMainDictionaryAvailable;
569        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
570        if (mainKeyboardView != null) {
571            mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
572        }
573    }
574
575    private void initSuggest() {
576        final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
577        final String localeStr = subtypeLocale.toString();
578
579        final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale,
580                this /* SuggestInitializationListener */);
581        final SettingsValues settingsValues = mSettings.getCurrent();
582        if (settingsValues.mCorrectionEnabled) {
583            newSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
584        }
585
586        mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
587        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
588            ResearchLogger.getInstance().initSuggest(newSuggest);
589        }
590
591        mUserDictionary = new UserBinaryDictionary(this, localeStr);
592        mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
593        newSuggest.setUserDictionary(mUserDictionary);
594
595        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
596
597        mUserHistoryPredictionDictionary = PersonalizationHelper
598                .getUserHistoryPredictionDictionary(this, localeStr, prefs);
599        newSuggest.setUserHistoryPredictionDictionary(mUserHistoryPredictionDictionary);
600        mPersonalizationDictionary = PersonalizationHelper
601                .getPersonalizationDictionary(this, localeStr, prefs);
602        newSuggest.setPersonalizationDictionary(mPersonalizationDictionary);
603        mPersonalizationPredictionDictionary = PersonalizationHelper
604                .getPersonalizationPredictionDictionary(this, localeStr, prefs);
605        newSuggest.setPersonalizationPredictionDictionary(mPersonalizationPredictionDictionary);
606
607        final Suggest oldSuggest = mSuggest;
608        resetContactsDictionary(null != oldSuggest ? oldSuggest.getContactsDictionary() : null);
609        mSuggest = newSuggest;
610        if (oldSuggest != null) oldSuggest.close();
611    }
612
613    /**
614     * Resets the contacts dictionary in mSuggest according to the user settings.
615     *
616     * This method takes an optional contacts dictionary to use when the locale hasn't changed
617     * since the contacts dictionary can be opened or closed as necessary depending on the settings.
618     *
619     * @param oldContactsDictionary an optional dictionary to use, or null
620     */
621    private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
622        final Suggest suggest = mSuggest;
623        final boolean shouldSetDictionary =
624                (null != suggest && mSettings.getCurrent().mUseContactsDict);
625
626        final ContactsBinaryDictionary dictionaryToUse;
627        if (!shouldSetDictionary) {
628            // Make sure the dictionary is closed. If it is already closed, this is a no-op,
629            // so it's safe to call it anyways.
630            if (null != oldContactsDictionary) oldContactsDictionary.close();
631            dictionaryToUse = null;
632        } else {
633            final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
634            if (null != oldContactsDictionary) {
635                if (!oldContactsDictionary.mLocale.equals(locale)) {
636                    // If the locale has changed then recreate the contacts dictionary. This
637                    // allows locale dependent rules for handling bigram name predictions.
638                    oldContactsDictionary.close();
639                    dictionaryToUse = new ContactsBinaryDictionary(this, locale);
640                } else {
641                    // Make sure the old contacts dictionary is opened. If it is already open,
642                    // this is a no-op, so it's safe to call it anyways.
643                    oldContactsDictionary.reopen(this);
644                    dictionaryToUse = oldContactsDictionary;
645                }
646            } else {
647                dictionaryToUse = new ContactsBinaryDictionary(this, locale);
648            }
649        }
650
651        if (null != suggest) {
652            suggest.setContactsDictionary(dictionaryToUse);
653        }
654    }
655
656    /* package private */ void resetSuggestMainDict() {
657        final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
658        mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
659        mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
660    }
661
662    @Override
663    public void onDestroy() {
664        final Suggest suggest = mSuggest;
665        if (suggest != null) {
666            suggest.close();
667            mSuggest = null;
668        }
669        mSettings.onDestroy();
670        unregisterReceiver(mReceiver);
671        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
672            ResearchLogger.getInstance().onDestroy();
673        }
674        unregisterReceiver(mDictionaryPackInstallReceiver);
675        PersonalizationDictionarySessionRegister.onDestroy(this);
676        LatinImeLogger.commit();
677        LatinImeLogger.onDestroy();
678        if (mInputUpdater != null) {
679            mInputUpdater.onDestroy();
680            mInputUpdater = null;
681        }
682        super.onDestroy();
683    }
684
685    @Override
686    public void onConfigurationChanged(final Configuration conf) {
687        // If orientation changed while predicting, commit the change
688        if (mDisplayOrientation != conf.orientation) {
689            mDisplayOrientation = conf.orientation;
690            mHandler.startOrientationChanging();
691            mConnection.beginBatchEdit();
692            commitTyped(LastComposedWord.NOT_A_SEPARATOR);
693            mConnection.finishComposingText();
694            mConnection.endBatchEdit();
695            if (isShowingOptionDialog()) {
696                mOptionsDialog.dismiss();
697            }
698        }
699        PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf);
700        super.onConfigurationChanged(conf);
701    }
702
703    @Override
704    public View onCreateInputView() {
705        return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
706    }
707
708    @Override
709    public void setInputView(final View view) {
710        super.setInputView(view);
711        mExtractArea = getWindow().getWindow().getDecorView()
712                .findViewById(android.R.id.extractArea);
713        mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
714        mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
715        if (mSuggestionStripView != null)
716            mSuggestionStripView.setListener(this, view);
717        if (LatinImeLogger.sVISUALDEBUG) {
718            mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
719        }
720    }
721
722    @Override
723    public void setCandidatesView(final View view) {
724        // To ensure that CandidatesView will never be set.
725        return;
726    }
727
728    @Override
729    public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
730        mHandler.onStartInput(editorInfo, restarting);
731    }
732
733    @Override
734    public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
735        mHandler.onStartInputView(editorInfo, restarting);
736    }
737
738    @Override
739    public void onFinishInputView(final boolean finishingInput) {
740        mHandler.onFinishInputView(finishingInput);
741    }
742
743    @Override
744    public void onFinishInput() {
745        mHandler.onFinishInput();
746    }
747
748    @Override
749    public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) {
750        // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
751        // is not guaranteed. It may even be called at the same time on a different thread.
752        mSubtypeSwitcher.onSubtypeChanged(subtype);
753        loadKeyboard();
754    }
755
756    private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
757        super.onStartInput(editorInfo, restarting);
758    }
759
760    @SuppressWarnings("deprecation")
761    private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
762        super.onStartInputView(editorInfo, restarting);
763        final KeyboardSwitcher switcher = mKeyboardSwitcher;
764        final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
765        // If we are starting input in a different text field from before, we'll have to reload
766        // settings, so currentSettingsValues can't be final.
767        SettingsValues currentSettingsValues = mSettings.getCurrent();
768
769        if (editorInfo == null) {
770            Log.e(TAG, "Null EditorInfo in onStartInputView()");
771            if (LatinImeLogger.sDBG) {
772                throw new NullPointerException("Null EditorInfo in onStartInputView()");
773            }
774            return;
775        }
776        if (DEBUG) {
777            Log.d(TAG, "onStartInputView: editorInfo:"
778                    + String.format("inputType=0x%08x imeOptions=0x%08x",
779                            editorInfo.inputType, editorInfo.imeOptions));
780            Log.d(TAG, "All caps = "
781                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0)
782                    + ", sentence caps = "
783                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0)
784                    + ", word caps = "
785                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
786        }
787        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
788            final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
789            ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs);
790        }
791        if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
792            Log.w(TAG, "Deprecated private IME option specified: "
793                    + editorInfo.privateImeOptions);
794            Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead");
795        }
796        if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) {
797            Log.w(TAG, "Deprecated private IME option specified: "
798                    + editorInfo.privateImeOptions);
799            Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
800        }
801
802        final PackageInfo packageInfo =
803                TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName);
804        mAppWorkAroundsUtils.setPackageInfo(packageInfo);
805        if (null == packageInfo) {
806            new TargetPackageInfoGetterTask(this /* context */, this /* listener */)
807                    .execute(editorInfo.packageName);
808        }
809
810        LatinImeLogger.onStartInputView(editorInfo);
811        // In landscape mode, this method gets called without the input view being created.
812        if (mainKeyboardView == null) {
813            return;
814        }
815
816        // Forward this event to the accessibility utilities, if enabled.
817        final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
818        if (accessUtils.isTouchExplorationEnabled()) {
819            accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
820        }
821
822        final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo);
823        final boolean isDifferentTextField = !restarting || inputTypeChanged;
824        if (isDifferentTextField) {
825            mSubtypeSwitcher.updateParametersOnStartInputView();
826        }
827
828        // The EditorInfo might have a flag that affects fullscreen mode.
829        // Note: This call should be done by InputMethodService?
830        updateFullscreenMode();
831        mApplicationSpecifiedCompletions = null;
832
833        // The app calling setText() has the effect of clearing the composing
834        // span, so we should reset our state unconditionally, even if restarting is true.
835        mEnteredText = null;
836        resetComposingState(true /* alsoResetLastComposedWord */);
837        if (isDifferentTextField) mHandler.postResumeSuggestions();
838        mDeleteCount = 0;
839        mSpaceState = SPACE_STATE_NONE;
840        mRecapitalizeStatus.deactivate();
841        mCurrentlyPressedHardwareKeys.clear();
842
843        // Note: the following does a round-trip IPC on the main thread: be careful
844        final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
845        final Suggest suggest = mSuggest;
846        if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) {
847            initSuggest();
848        }
849        if (mSuggestionStripView != null) {
850            // This will set the punctuation suggestions if next word suggestion is off;
851            // otherwise it will clear the suggestion strip.
852            setPunctuationSuggestions();
853        }
854        mSuggestedWords = SuggestedWords.EMPTY;
855
856        mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart,
857                false /* shouldFinishComposition */);
858
859        if (isDifferentTextField) {
860            mainKeyboardView.closing();
861            loadSettings();
862            currentSettingsValues = mSettings.getCurrent();
863
864            if (suggest != null && currentSettingsValues.mCorrectionEnabled) {
865                suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
866            }
867
868            switcher.loadKeyboard(editorInfo, currentSettingsValues);
869        } else if (restarting) {
870            // TODO: Come up with a more comprehensive way to reset the keyboard layout when
871            // a keyboard layout set doesn't get reloaded in this method.
872            switcher.resetKeyboardStateToAlphabet();
873            // In apps like Talk, we come here when the text is sent and the field gets emptied and
874            // we need to re-evaluate the shift state, but not the whole layout which would be
875            // disruptive.
876            // Space state must be updated before calling updateShiftState
877            switcher.updateShiftState();
878        }
879        setSuggestionStripShownInternal(
880                isSuggestionsStripVisible(), /* needsInputViewShown */ false);
881
882        mLastSelectionStart = editorInfo.initialSelStart;
883        mLastSelectionEnd = editorInfo.initialSelEnd;
884
885        mHandler.cancelUpdateSuggestionStrip();
886        mHandler.cancelDoubleSpacePeriodTimer();
887
888        mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
889        mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
890                currentSettingsValues.mKeyPreviewPopupDismissDelay);
891        mainKeyboardView.setSlidingKeyInputPreviewEnabled(
892                currentSettingsValues.mSlidingKeyInputPreviewEnabled);
893        mainKeyboardView.setGestureHandlingEnabledByUser(
894                currentSettingsValues.mGestureInputEnabled,
895                currentSettingsValues.mGestureTrailEnabled,
896                currentSettingsValues.mGestureFloatingPreviewTextEnabled);
897
898        initPersonalizationDebugSettings(currentSettingsValues);
899
900        if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
901    }
902
903    // Initialization of personalization debug settings. This must be called inside
904    // onStartInputView.
905    private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) {
906        if (mUseOnlyPersonalizationDictionaryForDebug
907                != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
908            // Only for debug
909            initSuggest();
910            mUseOnlyPersonalizationDictionaryForDebug =
911                    currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug;
912        }
913
914        if (mBoostPersonalizationDictionaryForDebug !=
915                currentSettingsValues.mBoostPersonalizationDictionaryForDebug) {
916            // Only for debug
917            mBoostPersonalizationDictionaryForDebug =
918                    currentSettingsValues.mBoostPersonalizationDictionaryForDebug;
919            if (mBoostPersonalizationDictionaryForDebug) {
920                UserHistoryForgettingCurveUtils.boostMaxFreqForDebug();
921            } else {
922                UserHistoryForgettingCurveUtils.resetMaxFreqForDebug();
923            }
924        }
925    }
926
927    // Callback for the TargetPackageInfoGetterTask
928    @Override
929    public void onTargetPackageInfoKnown(final PackageInfo info) {
930        mAppWorkAroundsUtils.setPackageInfo(info);
931    }
932
933    @Override
934    public void onWindowHidden() {
935        super.onWindowHidden();
936        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
937        if (mainKeyboardView != null) {
938            mainKeyboardView.closing();
939        }
940    }
941
942    private void onFinishInputInternal() {
943        super.onFinishInput();
944
945        LatinImeLogger.commit();
946        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
947        if (mainKeyboardView != null) {
948            mainKeyboardView.closing();
949        }
950    }
951
952    private void onFinishInputViewInternal(final boolean finishingInput) {
953        super.onFinishInputView(finishingInput);
954        mKeyboardSwitcher.onFinishInputView();
955        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
956        if (mainKeyboardView != null) {
957            mainKeyboardView.cancelAllOngoingEvents();
958            mainKeyboardView.deallocateMemory();
959        }
960        // Remove pending messages related to update suggestions
961        mHandler.cancelUpdateSuggestionStrip();
962        // Should do the following in onFinishInputInternal but until JB MR2 it's not called :(
963        if (mWordComposer.isComposingWord()) mConnection.finishComposingText();
964        resetComposingState(true /* alsoResetLastComposedWord */);
965        mRichImm.clearSubtypeCaches();
966        // Notify ResearchLogger
967        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
968            ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart,
969                    mLastSelectionEnd, getCurrentInputConnection());
970        }
971    }
972
973    @Override
974    public void onUpdateSelection(final int oldSelStart, final int oldSelEnd,
975            final int newSelStart, final int newSelEnd,
976            final int composingSpanStart, final int composingSpanEnd) {
977        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
978                composingSpanStart, composingSpanEnd);
979        if (DEBUG) {
980            Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
981                    + ", ose=" + oldSelEnd
982                    + ", lss=" + mLastSelectionStart
983                    + ", lse=" + mLastSelectionEnd
984                    + ", nss=" + newSelStart
985                    + ", nse=" + newSelEnd
986                    + ", cs=" + composingSpanStart
987                    + ", ce=" + composingSpanEnd);
988        }
989        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
990            final boolean expectingUpdateSelectionFromLogger =
991                    ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
992            ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
993                    oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
994                    composingSpanEnd, mExpectingUpdateSelection,
995                    expectingUpdateSelectionFromLogger, mConnection);
996            if (expectingUpdateSelectionFromLogger) {
997                // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
998                return;
999            }
1000        }
1001
1002        final boolean selectionChanged = mLastSelectionStart != newSelStart
1003                || mLastSelectionEnd != newSelEnd;
1004
1005        // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
1006        // span in the view - we can use that to narrow down whether the cursor was moved
1007        // by us or not. If we are composing a word but there is no composing span, then
1008        // we know for sure the cursor moved while we were composing and we should reset
1009        // the state. TODO: rescind this policy: the framework never removes the composing
1010        // span on its own accord while editing. This test is useless.
1011        final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
1012
1013        // If the keyboard is not visible, we don't need to do all the housekeeping work, as it
1014        // will be reset when the keyboard shows up anyway.
1015        // TODO: revisit this when LatinIME supports hardware keyboards.
1016        // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
1017        // TODO: find a better way to simulate actual execution.
1018        if (isInputViewShown() && !mExpectingUpdateSelection
1019                && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) {
1020            // TAKE CARE: there is a race condition when we enter this test even when the user
1021            // did not explicitly move the cursor. This happens when typing fast, where two keys
1022            // turn this flag on in succession and both onUpdateSelection() calls arrive after
1023            // the second one - the first call successfully avoids this test, but the second one
1024            // enters. For the moment we rely on noComposingSpan to further reduce the impact.
1025
1026            // TODO: the following is probably better done in resetEntireInputState().
1027            // it should only happen when the cursor moved, and the very purpose of the
1028            // test below is to narrow down whether this happened or not. Likewise with
1029            // the call to updateShiftState.
1030            // We set this to NONE because after a cursor move, we don't want the space
1031            // state-related special processing to kick in.
1032            mSpaceState = SPACE_STATE_NONE;
1033
1034            // TODO: is it still necessary to test for composingSpan related stuff?
1035            final boolean selectionChangedOrSafeToReset = selectionChanged
1036                    || (!mWordComposer.isComposingWord()) || noComposingSpan;
1037            final boolean hasOrHadSelection = (oldSelStart != oldSelEnd
1038                    || newSelStart != newSelEnd);
1039            final int moveAmount = newSelStart - oldSelStart;
1040            if (selectionChangedOrSafeToReset && (hasOrHadSelection
1041                    || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) {
1042                // If we are composing a word and moving the cursor, we would want to set a
1043                // suggestion span for recorrection to work correctly. Unfortunately, that
1044                // would involve the keyboard committing some new text, which would move the
1045                // cursor back to where it was. Latin IME could then fix the position of the cursor
1046                // again, but the asynchronous nature of the calls results in this wreaking havoc
1047                // with selection on double tap and the like.
1048                // Another option would be to send suggestions each time we set the composing
1049                // text, but that is probably too expensive to do, so we decided to leave things
1050                // as is.
1051                resetEntireInputState(newSelStart);
1052            } else {
1053                // resetEntireInputState calls resetCachesUponCursorMove, but with the second
1054                // argument as true. But in all cases where we don't reset the entire input state,
1055                // we still want to tell the rich input connection about the new cursor position so
1056                // that it can update its caches.
1057                mConnection.resetCachesUponCursorMove(newSelStart,
1058                        false /* shouldFinishComposition */);
1059            }
1060
1061            // We moved the cursor. If we are touching a word, we need to resume suggestion,
1062            // unless suggestions are off.
1063            if (isSuggestionsStripVisible()) {
1064                mHandler.postResumeSuggestions();
1065            }
1066            // Reset the last recapitalization.
1067            mRecapitalizeStatus.deactivate();
1068            mKeyboardSwitcher.updateShiftState();
1069        }
1070        mExpectingUpdateSelection = false;
1071
1072        // Make a note of the cursor position
1073        mLastSelectionStart = newSelStart;
1074        mLastSelectionEnd = newSelEnd;
1075        mSubtypeState.currentSubtypeUsed();
1076    }
1077
1078    /**
1079     * This is called when the user has clicked on the extracted text view,
1080     * when running in fullscreen mode.  The default implementation hides
1081     * the suggestions view when this happens, but only if the extracted text
1082     * editor has a vertical scroll bar because its text doesn't fit.
1083     * Here we override the behavior due to the possibility that a re-correction could
1084     * cause the suggestions strip to disappear and re-appear.
1085     */
1086    @Override
1087    public void onExtractedTextClicked() {
1088        if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
1089
1090        super.onExtractedTextClicked();
1091    }
1092
1093    /**
1094     * This is called when the user has performed a cursor movement in the
1095     * extracted text view, when it is running in fullscreen mode.  The default
1096     * implementation hides the suggestions view when a vertical movement
1097     * happens, but only if the extracted text editor has a vertical scroll bar
1098     * because its text doesn't fit.
1099     * Here we override the behavior due to the possibility that a re-correction could
1100     * cause the suggestions strip to disappear and re-appear.
1101     */
1102    @Override
1103    public void onExtractedCursorMovement(final int dx, final int dy) {
1104        if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
1105
1106        super.onExtractedCursorMovement(dx, dy);
1107    }
1108
1109    @Override
1110    public void hideWindow() {
1111        LatinImeLogger.commit();
1112        mKeyboardSwitcher.onHideWindow();
1113
1114        if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
1115            AccessibleKeyboardViewProxy.getInstance().onHideWindow();
1116        }
1117
1118        if (TRACE) Debug.stopMethodTracing();
1119        if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
1120            mOptionsDialog.dismiss();
1121            mOptionsDialog = null;
1122        }
1123        super.hideWindow();
1124    }
1125
1126    @Override
1127    public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
1128        if (DEBUG) {
1129            Log.i(TAG, "Received completions:");
1130            if (applicationSpecifiedCompletions != null) {
1131                for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
1132                    Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
1133                }
1134            }
1135        }
1136        if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return;
1137        if (applicationSpecifiedCompletions == null) {
1138            clearSuggestionStrip();
1139            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1140                ResearchLogger.latinIME_onDisplayCompletions(null);
1141            }
1142            return;
1143        }
1144        mApplicationSpecifiedCompletions =
1145                CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions);
1146
1147        final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
1148                SuggestedWords.getFromApplicationSpecifiedCompletions(
1149                        applicationSpecifiedCompletions);
1150        final SuggestedWords suggestedWords = new SuggestedWords(
1151                applicationSuggestedWords,
1152                false /* typedWordValid */,
1153                false /* hasAutoCorrectionCandidate */,
1154                false /* isPunctuationSuggestions */,
1155                false /* isObsoleteSuggestions */,
1156                false /* isPrediction */);
1157        // When in fullscreen mode, show completions generated by the application
1158        final boolean isAutoCorrection = false;
1159        setSuggestedWords(suggestedWords, isAutoCorrection);
1160        setAutoCorrectionIndicator(isAutoCorrection);
1161        setSuggestionStripShown(true);
1162        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1163            ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
1164        }
1165    }
1166
1167    private void setSuggestionStripShownInternal(final boolean shown,
1168            final boolean needsInputViewShown) {
1169        // TODO: Modify this if we support suggestions with hard keyboard
1170        if (onEvaluateInputViewShown() && mSuggestionStripView != null) {
1171            final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1172            final boolean inputViewShown = (mainKeyboardView != null)
1173                    ? mainKeyboardView.isShown() : false;
1174            final boolean shouldShowSuggestions = shown
1175                    && (needsInputViewShown ? inputViewShown : true);
1176            if (isFullscreenMode()) {
1177                mSuggestionStripView.setVisibility(
1178                        shouldShowSuggestions ? View.VISIBLE : View.GONE);
1179            } else {
1180                mSuggestionStripView.setVisibility(
1181                        shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
1182            }
1183        }
1184    }
1185
1186    private void setSuggestionStripShown(final boolean shown) {
1187        setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
1188    }
1189
1190    private int getAdjustedBackingViewHeight() {
1191        final int currentHeight = mKeyPreviewBackingView.getHeight();
1192        if (currentHeight > 0) {
1193            return currentHeight;
1194        }
1195
1196        final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
1197        if (visibleKeyboardView == null) {
1198            return 0;
1199        }
1200        // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main   !!!
1201        // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1202        final int keyboardHeight = visibleKeyboardView.getHeight();
1203        final int suggestionsHeight = mSuggestionStripView.getHeight();
1204        final int displayHeight = getResources().getDisplayMetrics().heightPixels;
1205        final Rect rect = new Rect();
1206        mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
1207        final int notificationBarHeight = rect.top;
1208        final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
1209                - keyboardHeight;
1210
1211        final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
1212        params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight);
1213        mKeyPreviewBackingView.setLayoutParams(params);
1214        return params.height;
1215    }
1216
1217    @Override
1218    public void onComputeInsets(final InputMethodService.Insets outInsets) {
1219        super.onComputeInsets(outInsets);
1220        final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
1221        if (visibleKeyboardView == null || mSuggestionStripView == null) {
1222            return;
1223        }
1224        final int adjustedBackingHeight = getAdjustedBackingViewHeight();
1225        final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
1226        final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
1227        // In fullscreen mode, the height of the extract area managed by InputMethodService should
1228        // be considered.
1229        // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
1230        final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
1231        final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0
1232                : mSuggestionStripView.getHeight();
1233        final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
1234        int visibleTopY = extraHeight;
1235        // Need to set touchable region only if input view is being shown
1236        if (visibleKeyboardView.isShown()) {
1237            // Note that the height of Emoji layout is the same as the height of the main keyboard
1238            // and the suggestion strip
1239            if (mKeyboardSwitcher.isShowingEmojiKeyboard()
1240                    || mSuggestionStripView.getVisibility() == View.VISIBLE) {
1241                visibleTopY -= suggestionsHeight;
1242            }
1243            final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
1244            final int touchWidth = visibleKeyboardView.getWidth();
1245            final int touchHeight = visibleKeyboardView.getHeight() + extraHeight
1246                    // Extend touchable region below the keyboard.
1247                    + EXTENDED_TOUCHABLE_REGION_HEIGHT;
1248            outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
1249            outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight);
1250        }
1251        outInsets.contentTopInsets = visibleTopY;
1252        outInsets.visibleTopInsets = visibleTopY;
1253    }
1254
1255    @Override
1256    public boolean onEvaluateFullscreenMode() {
1257        // Reread resource value here, because this method is called by framework anytime as needed.
1258        final boolean isFullscreenModeAllowed =
1259                Settings.readUseFullscreenMode(getResources());
1260        if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) {
1261            // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI
1262            // implies NO_FULLSCREEN. However, the framework mistakenly does.  i.e. NO_EXTRACT_UI
1263            // without NO_FULLSCREEN doesn't work as expected. Because of this we need this
1264            // hack for now.  Let's get rid of this once the framework gets fixed.
1265            final EditorInfo ei = getCurrentInputEditorInfo();
1266            return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0));
1267        } else {
1268            return false;
1269        }
1270    }
1271
1272    @Override
1273    public void updateFullscreenMode() {
1274        super.updateFullscreenMode();
1275
1276        if (mKeyPreviewBackingView == null) return;
1277        // In fullscreen mode, no need to have extra space to show the key preview.
1278        // If not, we should have extra space above the keyboard to show the key preview.
1279        mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
1280    }
1281
1282    // This will reset the whole input state to the starting state. It will clear
1283    // the composing word, reset the last composed word, tell the inputconnection about it.
1284    private void resetEntireInputState(final int newCursorPosition) {
1285        final boolean shouldFinishComposition = mWordComposer.isComposingWord();
1286        resetComposingState(true /* alsoResetLastComposedWord */);
1287        final SettingsValues settingsValues = mSettings.getCurrent();
1288        if (settingsValues.mBigramPredictionEnabled) {
1289            clearSuggestionStrip();
1290        } else {
1291            setSuggestedWords(settingsValues.mSuggestPuncList, false);
1292        }
1293        mConnection.resetCachesUponCursorMove(newCursorPosition, shouldFinishComposition);
1294    }
1295
1296    private void resetComposingState(final boolean alsoResetLastComposedWord) {
1297        mWordComposer.reset();
1298        if (alsoResetLastComposedWord)
1299            mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
1300    }
1301
1302    private void commitTyped(final String separatorString) {
1303        if (!mWordComposer.isComposingWord()) return;
1304        final String typedWord = mWordComposer.getTypedWord();
1305        if (typedWord.length() > 0) {
1306            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1307                ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
1308            }
1309            commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
1310                    separatorString);
1311        }
1312    }
1313
1314    // Called from the KeyboardSwitcher which needs to know auto caps state to display
1315    // the right layout.
1316    public int getCurrentAutoCapsState() {
1317        if (!mSettings.getCurrent().mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
1318
1319        final EditorInfo ei = getCurrentInputEditorInfo();
1320        if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
1321        final int inputType = ei.inputType;
1322        // Warning: this depends on mSpaceState, which may not be the most current value. If
1323        // mSpaceState gets updated later, whoever called this may need to be told about it.
1324        return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(),
1325                SPACE_STATE_PHANTOM == mSpaceState);
1326    }
1327
1328    public int getCurrentRecapitalizeState() {
1329        if (!mRecapitalizeStatus.isActive()
1330                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
1331            // Not recapitalizing at the moment
1332            return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
1333        }
1334        return mRecapitalizeStatus.getCurrentMode();
1335    }
1336
1337    // Factor in auto-caps and manual caps and compute the current caps mode.
1338    private int getActualCapsMode() {
1339        final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
1340        if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
1341        final int auto = getCurrentAutoCapsState();
1342        if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
1343            return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
1344        }
1345        if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED;
1346        return WordComposer.CAPS_MODE_OFF;
1347    }
1348
1349    private void swapSwapperAndSpace() {
1350        final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
1351        // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
1352        if (lastTwo != null && lastTwo.length() == 2
1353                && lastTwo.charAt(0) == Constants.CODE_SPACE) {
1354            mConnection.deleteSurroundingText(2, 0);
1355            final String text = lastTwo.charAt(1) + " ";
1356            mConnection.commitText(text, 1);
1357            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1358                ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
1359            }
1360            mKeyboardSwitcher.updateShiftState();
1361        }
1362    }
1363
1364    private boolean maybeDoubleSpacePeriod() {
1365        final SettingsValues settingsValues = mSettings.getCurrent();
1366        if (!settingsValues.mCorrectionEnabled) return false;
1367        if (!settingsValues.mUseDoubleSpacePeriod) return false;
1368        if (!mHandler.isAcceptingDoubleSpacePeriod()) return false;
1369        final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
1370        if (lastThree != null && lastThree.length() == 3
1371                && canBeFollowedByDoubleSpacePeriod(lastThree.charAt(0))
1372                && lastThree.charAt(1) == Constants.CODE_SPACE
1373                && lastThree.charAt(2) == Constants.CODE_SPACE) {
1374            mHandler.cancelDoubleSpacePeriodTimer();
1375            mConnection.deleteSurroundingText(2, 0);
1376            final String textToInsert = ". ";
1377            mConnection.commitText(textToInsert, 1);
1378            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1379                ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
1380                        false /* isBatchMode */);
1381            }
1382            mKeyboardSwitcher.updateShiftState();
1383            return true;
1384        }
1385        return false;
1386    }
1387
1388    private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
1389        // TODO: Check again whether there really ain't a better way to check this.
1390        // TODO: This should probably be language-dependant...
1391        return Character.isLetterOrDigit(codePoint)
1392                || codePoint == Constants.CODE_SINGLE_QUOTE
1393                || codePoint == Constants.CODE_DOUBLE_QUOTE
1394                || codePoint == Constants.CODE_CLOSING_PARENTHESIS
1395                || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
1396                || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
1397                || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET;
1398    }
1399
1400    // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
1401    // pressed.
1402    @Override
1403    public void addWordToUserDictionary(final String word) {
1404        if (TextUtils.isEmpty(word)) {
1405            // Probably never supposed to happen, but just in case.
1406            return;
1407        }
1408        final String wordToEdit;
1409        if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) {
1410            wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
1411        } else {
1412            wordToEdit = word;
1413        }
1414        mUserDictionary.addWordToUserDictionary(wordToEdit);
1415    }
1416
1417    private void onSettingsKeyPressed() {
1418        if (isShowingOptionDialog()) return;
1419        showSubtypeSelectorAndSettings();
1420    }
1421
1422    @Override
1423    public boolean onCustomRequest(final int requestCode) {
1424        if (isShowingOptionDialog()) return false;
1425        switch (requestCode) {
1426        case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
1427            if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
1428                mRichImm.getInputMethodManager().showInputMethodPicker();
1429                return true;
1430            }
1431            return false;
1432        }
1433        return false;
1434    }
1435
1436    private boolean isShowingOptionDialog() {
1437        return mOptionsDialog != null && mOptionsDialog.isShowing();
1438    }
1439
1440    private void performEditorAction(final int actionId) {
1441        mConnection.performEditorAction(actionId);
1442    }
1443
1444    // TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
1445    private void handleLanguageSwitchKey() {
1446        final IBinder token = getWindow().getWindow().getAttributes().token;
1447        if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) {
1448            mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
1449            return;
1450        }
1451        mSubtypeState.switchSubtype(token, mRichImm);
1452    }
1453
1454    private void sendDownUpKeyEventForBackwardCompatibility(final int code) {
1455        final long eventTime = SystemClock.uptimeMillis();
1456        mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
1457                KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1458                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1459        mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
1460                KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1461                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1462    }
1463
1464    private void sendKeyCodePoint(final int code) {
1465        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1466            ResearchLogger.latinIME_sendKeyCodePoint(code);
1467        }
1468        // TODO: Remove this special handling of digit letters.
1469        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
1470        if (code >= '0' && code <= '9') {
1471            sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0);
1472            return;
1473        }
1474
1475        if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) {
1476            // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
1477            // a hardware keyboard event on pressing enter or delete. This is bad for many
1478            // reasons (there are race conditions with commits) but some applications are
1479            // relying on this behavior so we continue to support it for older apps.
1480            sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER);
1481        } else {
1482            final String text = new String(new int[] { code }, 0, 1);
1483            mConnection.commitText(text, text.length());
1484        }
1485    }
1486
1487    // Implementation of {@link KeyboardActionListener}.
1488    @Override
1489    public void onCodeInput(final int primaryCode, final int x, final int y) {
1490        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1491            ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
1492        }
1493        final long when = SystemClock.uptimeMillis();
1494        if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
1495            mDeleteCount = 0;
1496        }
1497        mLastKeyTime = when;
1498        mConnection.beginBatchEdit();
1499        final KeyboardSwitcher switcher = mKeyboardSwitcher;
1500        // The space state depends only on the last character pressed and its own previous
1501        // state. Here, we revert the space state to neutral if the key is actually modifying
1502        // the input contents (any non-shift key), which is what we should do for
1503        // all inputs that do not result in a special state. Each character handling is then
1504        // free to override the state as they see fit.
1505        final int spaceState = mSpaceState;
1506        if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
1507
1508        // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
1509        if (primaryCode != Constants.CODE_SPACE) {
1510            mHandler.cancelDoubleSpacePeriodTimer();
1511        }
1512
1513        boolean didAutoCorrect = false;
1514        switch (primaryCode) {
1515        case Constants.CODE_DELETE:
1516            mSpaceState = SPACE_STATE_NONE;
1517            handleBackspace(spaceState);
1518            mDeleteCount++;
1519            mExpectingUpdateSelection = true;
1520            LatinImeLogger.logOnDelete(x, y);
1521            break;
1522        case Constants.CODE_SHIFT:
1523            // Note: Calling back to the keyboard on Shift key is handled in
1524            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
1525            final Keyboard currentKeyboard = switcher.getKeyboard();
1526            if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
1527                // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
1528                // alphabetic shift and shift while in symbol layout.
1529                handleRecapitalize();
1530            }
1531            break;
1532        case Constants.CODE_CAPSLOCK:
1533            // Note: Changing keyboard to shift lock state is handled in
1534            // {@link KeyboardSwitcher#onCodeInput(int)}.
1535            break;
1536        case Constants.CODE_SWITCH_ALPHA_SYMBOL:
1537            // Note: Calling back to the keyboard on symbol key is handled in
1538            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
1539            break;
1540        case Constants.CODE_SETTINGS:
1541            onSettingsKeyPressed();
1542            break;
1543        case Constants.CODE_SHORTCUT:
1544            mSubtypeSwitcher.switchToShortcutIME(this);
1545            break;
1546        case Constants.CODE_ACTION_NEXT:
1547            performEditorAction(EditorInfo.IME_ACTION_NEXT);
1548            break;
1549        case Constants.CODE_ACTION_PREVIOUS:
1550            performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
1551            break;
1552        case Constants.CODE_LANGUAGE_SWITCH:
1553            handleLanguageSwitchKey();
1554            break;
1555        case Constants.CODE_EMOJI:
1556            // Note: Switching emoji keyboard is being handled in
1557            // {@link KeyboardState#onCodeInput(int,int)}.
1558            break;
1559        case Constants.CODE_ENTER:
1560            final EditorInfo editorInfo = getCurrentInputEditorInfo();
1561            final int imeOptionsActionId =
1562                    InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
1563            if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
1564                // Either we have an actionLabel and we should performEditorAction with actionId
1565                // regardless of its value.
1566                performEditorAction(editorInfo.actionId);
1567            } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
1568                // We didn't have an actionLabel, but we had another action to execute.
1569                // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
1570                // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
1571                // means there should be an action and the app didn't bother to set a specific
1572                // code for it - presumably it only handles one. It does not have to be treated
1573                // in any specific way: anything that is not IME_ACTION_NONE should be sent to
1574                // performEditorAction.
1575                performEditorAction(imeOptionsActionId);
1576            } else {
1577                // No action label, and the action from imeOptions is NONE: this is a regular
1578                // enter key that should input a carriage return.
1579                didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
1580            }
1581            break;
1582        case Constants.CODE_SHIFT_ENTER:
1583            didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
1584            break;
1585        default:
1586            didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState);
1587            break;
1588        }
1589        switcher.onCodeInput(primaryCode);
1590        // Reset after any single keystroke, except shift, capslock, and symbol-shift
1591        if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT
1592                && primaryCode != Constants.CODE_CAPSLOCK
1593                && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
1594            mLastComposedWord.deactivate();
1595        if (Constants.CODE_DELETE != primaryCode) {
1596            mEnteredText = null;
1597        }
1598        mConnection.endBatchEdit();
1599    }
1600
1601    private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y,
1602            final int spaceState) {
1603        mSpaceState = SPACE_STATE_NONE;
1604        final boolean didAutoCorrect;
1605        final SettingsValues settingsValues = mSettings.getCurrent();
1606        if (settingsValues.isWordSeparator(primaryCode)) {
1607            didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
1608        } else {
1609            didAutoCorrect = false;
1610            if (SPACE_STATE_PHANTOM == spaceState) {
1611                if (settingsValues.mIsInternal) {
1612                    if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
1613                        LatinImeLoggerUtils.onAutoCorrection(
1614                                "", mWordComposer.getTypedWord(), " ", mWordComposer);
1615                    }
1616                }
1617                if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
1618                    // If we are in the middle of a recorrection, we need to commit the recorrection
1619                    // first so that we can insert the character at the current cursor position.
1620                    resetEntireInputState(mLastSelectionStart);
1621                } else {
1622                    commitTyped(LastComposedWord.NOT_A_SEPARATOR);
1623                }
1624            }
1625            final int keyX, keyY;
1626            final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1627            if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
1628                keyX = x;
1629                keyY = y;
1630            } else {
1631                keyX = Constants.NOT_A_COORDINATE;
1632                keyY = Constants.NOT_A_COORDINATE;
1633            }
1634            handleCharacter(primaryCode, keyX, keyY, spaceState);
1635        }
1636        mExpectingUpdateSelection = true;
1637        return didAutoCorrect;
1638    }
1639
1640    // Called from PointerTracker through the KeyboardActionListener interface
1641    @Override
1642    public void onTextInput(final String rawText) {
1643        mConnection.beginBatchEdit();
1644        if (mWordComposer.isComposingWord()) {
1645            commitCurrentAutoCorrection(rawText);
1646        } else {
1647            resetComposingState(true /* alsoResetLastComposedWord */);
1648        }
1649        mHandler.postUpdateSuggestionStrip();
1650        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
1651                && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) {
1652            ResearchLogger.getInstance().onResearchKeySelected(this);
1653            return;
1654        }
1655        final String text = specificTldProcessingOnTextInput(rawText);
1656        if (SPACE_STATE_PHANTOM == mSpaceState) {
1657            promotePhantomSpace();
1658        }
1659        mConnection.commitText(text, 1);
1660        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1661            ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */);
1662        }
1663        mConnection.endBatchEdit();
1664        // Space state must be updated before calling updateShiftState
1665        mSpaceState = SPACE_STATE_NONE;
1666        mKeyboardSwitcher.updateShiftState();
1667        mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT);
1668        mEnteredText = text;
1669    }
1670
1671    @Override
1672    public void onStartBatchInput() {
1673        mInputUpdater.onStartBatchInput();
1674        mHandler.cancelUpdateSuggestionStrip();
1675        mConnection.beginBatchEdit();
1676        final SettingsValues settingsValues = mSettings.getCurrent();
1677        if (mWordComposer.isComposingWord()) {
1678            if (settingsValues.mIsInternal) {
1679                if (mWordComposer.isBatchMode()) {
1680                    LatinImeLoggerUtils.onAutoCorrection(
1681                            "", mWordComposer.getTypedWord(), " ", mWordComposer);
1682                }
1683            }
1684            final int wordComposerSize = mWordComposer.size();
1685            // Since isComposingWord() is true, the size is at least 1.
1686            if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
1687                // If we are in the middle of a recorrection, we need to commit the recorrection
1688                // first so that we can insert the batch input at the current cursor position.
1689                resetEntireInputState(mLastSelectionStart);
1690            } else if (wordComposerSize <= 1) {
1691                // We auto-correct the previous (typed, not gestured) string iff it's one character
1692                // long. The reason for this is, even in the middle of gesture typing, you'll still
1693                // tap one-letter words and you want them auto-corrected (typically, "i" in English
1694                // should become "I"). However for any longer word, we assume that the reason for
1695                // tapping probably is that the word you intend to type is not in the dictionary,
1696                // so we do not attempt to correct, on the assumption that if that was a dictionary
1697                // word, the user would probably have gestured instead.
1698                commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
1699            } else {
1700                commitTyped(LastComposedWord.NOT_A_SEPARATOR);
1701            }
1702            mExpectingUpdateSelection = true;
1703        }
1704        final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
1705        if (Character.isLetterOrDigit(codePointBeforeCursor)
1706                || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
1707            mSpaceState = SPACE_STATE_PHANTOM;
1708        }
1709        mConnection.endBatchEdit();
1710        mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
1711    }
1712
1713    private static final class InputUpdater implements Handler.Callback {
1714        private final Handler mHandler;
1715        private final LatinIME mLatinIme;
1716        private final Object mLock = new Object();
1717        private boolean mInBatchInput; // synchronized using {@link #mLock}.
1718
1719        private InputUpdater(final LatinIME latinIme) {
1720            final HandlerThread handlerThread = new HandlerThread(
1721                    InputUpdater.class.getSimpleName());
1722            handlerThread.start();
1723            mHandler = new Handler(handlerThread.getLooper(), this);
1724            mLatinIme = latinIme;
1725        }
1726
1727        private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1;
1728        private static final int MSG_GET_SUGGESTED_WORDS = 2;
1729
1730        @Override
1731        public boolean handleMessage(final Message msg) {
1732            switch (msg.what) {
1733                case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
1734                    updateBatchInput((InputPointers)msg.obj);
1735                    break;
1736                case MSG_GET_SUGGESTED_WORDS:
1737                    mLatinIme.getSuggestedWords(msg.arg1, (OnGetSuggestedWordsCallback) msg.obj);
1738                    break;
1739            }
1740            return true;
1741        }
1742
1743        // Run in the UI thread.
1744        public void onStartBatchInput() {
1745            synchronized (mLock) {
1746                mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
1747                mInBatchInput = true;
1748                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
1749                        SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
1750            }
1751        }
1752
1753        // Run in the Handler thread.
1754        private void updateBatchInput(final InputPointers batchPointers) {
1755            synchronized (mLock) {
1756                if (!mInBatchInput) {
1757                    // Batch input has ended or canceled while the message was being delivered.
1758                    return;
1759                }
1760
1761                getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() {
1762                    @Override
1763                    public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
1764                        mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
1765                                suggestedWords, false /* dismissGestureFloatingPreviewText */);
1766                    }
1767                });
1768            }
1769        }
1770
1771        // Run in the UI thread.
1772        public void onUpdateBatchInput(final InputPointers batchPointers) {
1773            if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
1774                return;
1775            }
1776            mHandler.obtainMessage(
1777                    MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers)
1778                    .sendToTarget();
1779        }
1780
1781        public void onCancelBatchInput() {
1782            synchronized (mLock) {
1783                mInBatchInput = false;
1784                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
1785                        SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
1786            }
1787        }
1788
1789        // Run in the UI thread.
1790        public void onEndBatchInput(final InputPointers batchPointers) {
1791            synchronized(mLock) {
1792                getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() {
1793                    @Override
1794                    public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
1795                        mInBatchInput = false;
1796                        mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords,
1797                                true /* dismissGestureFloatingPreviewText */);
1798                        mLatinIme.mHandler.onEndBatchInput(suggestedWords);
1799                    }
1800                });
1801            }
1802        }
1803
1804        // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
1805        // be synchronized.
1806        private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
1807                final OnGetSuggestedWordsCallback callback) {
1808            mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
1809            mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
1810                    new OnGetSuggestedWordsCallback() {
1811                @Override
1812                public void onGetSuggestedWords(SuggestedWords suggestedWords) {
1813                    final int suggestionCount = suggestedWords.size();
1814                    if (suggestionCount <= 1) {
1815                        final String mostProbableSuggestion = (suggestionCount == 0) ? null
1816                                : suggestedWords.getWord(0);
1817                        callback.onGetSuggestedWords(
1818                                mLatinIme.getOlderSuggestions(mostProbableSuggestion));
1819                    }
1820                    callback.onGetSuggestedWords(suggestedWords);
1821                }
1822            });
1823        }
1824
1825        public void getSuggestedWords(final int sessionId,
1826                final OnGetSuggestedWordsCallback callback) {
1827            mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, 0, callback).sendToTarget();
1828        }
1829
1830        private void onDestroy() {
1831            mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS);
1832            mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
1833            mHandler.getLooper().quit();
1834        }
1835    }
1836
1837    // This method must run in UI Thread.
1838    private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
1839            final boolean dismissGestureFloatingPreviewText) {
1840        showSuggestionStrip(suggestedWords);
1841        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1842        mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
1843        if (dismissGestureFloatingPreviewText) {
1844            mainKeyboardView.dismissGestureFloatingPreviewText();
1845        }
1846    }
1847
1848    @Override
1849    public void onUpdateBatchInput(final InputPointers batchPointers) {
1850        if (mSettings.getCurrent().mPhraseGestureEnabled) {
1851            final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
1852            if (null != candidate) {
1853                if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
1854                    final String[] commitParts = candidate.mWord.split(" ", 2);
1855                    batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
1856                    promotePhantomSpace();
1857                    mConnection.commitText(commitParts[0], 0);
1858                    mSpaceState = SPACE_STATE_PHANTOM;
1859                    mKeyboardSwitcher.updateShiftState();
1860                    mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
1861                }
1862            }
1863        }
1864        mInputUpdater.onUpdateBatchInput(batchPointers);
1865    }
1866
1867    // This method must run in UI Thread.
1868    public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) {
1869        final String batchInputText = suggestedWords.isEmpty()
1870                ? null : suggestedWords.getWord(0);
1871        if (TextUtils.isEmpty(batchInputText)) {
1872            return;
1873        }
1874        mConnection.beginBatchEdit();
1875        if (SPACE_STATE_PHANTOM == mSpaceState) {
1876            promotePhantomSpace();
1877        }
1878        if (mSettings.getCurrent().mPhraseGestureEnabled) {
1879            // Find the last space
1880            final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
1881            if (0 != indexOfLastSpace) {
1882                mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
1883                showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture());
1884            }
1885            final String lastWord = batchInputText.substring(indexOfLastSpace);
1886            mWordComposer.setBatchInputWord(lastWord);
1887            mConnection.setComposingText(lastWord, 1);
1888        } else {
1889            mWordComposer.setBatchInputWord(batchInputText);
1890            mConnection.setComposingText(batchInputText, 1);
1891        }
1892        mExpectingUpdateSelection = true;
1893        mConnection.endBatchEdit();
1894        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1895            ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords);
1896        }
1897        // Space state must be updated before calling updateShiftState
1898        mSpaceState = SPACE_STATE_PHANTOM;
1899        mKeyboardSwitcher.updateShiftState();
1900    }
1901
1902    @Override
1903    public void onEndBatchInput(final InputPointers batchPointers) {
1904        mInputUpdater.onEndBatchInput(batchPointers);
1905    }
1906
1907    private String specificTldProcessingOnTextInput(final String text) {
1908        if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
1909                || !Character.isLetter(text.charAt(1))) {
1910            // Not a tld: do nothing.
1911            return text;
1912        }
1913        // We have a TLD (or something that looks like this): make sure we don't add
1914        // a space even if currently in phantom mode.
1915        mSpaceState = SPACE_STATE_NONE;
1916        // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code
1917        final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
1918        if (lastOne != null && lastOne.length() == 1
1919                && lastOne.charAt(0) == Constants.CODE_PERIOD) {
1920            return text.substring(1);
1921        } else {
1922            return text;
1923        }
1924    }
1925
1926    // Called from PointerTracker through the KeyboardActionListener interface
1927    @Override
1928    public void onFinishSlidingInput() {
1929        // User finished sliding input.
1930        mKeyboardSwitcher.onFinishSlidingInput();
1931    }
1932
1933    // Called from PointerTracker through the KeyboardActionListener interface
1934    @Override
1935    public void onCancelInput() {
1936        // User released a finger outside any key
1937        // Nothing to do so far.
1938    }
1939
1940    @Override
1941    public void onCancelBatchInput() {
1942        mInputUpdater.onCancelBatchInput();
1943    }
1944
1945    private void handleBackspace(final int spaceState) {
1946        // In many cases, we may have to put the keyboard in auto-shift state again. However
1947        // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
1948        // during key repeat.
1949        mHandler.postUpdateShiftState();
1950
1951        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
1952            // If we are in the middle of a recorrection, we need to commit the recorrection
1953            // first so that we can remove the character at the current cursor position.
1954            resetEntireInputState(mLastSelectionStart);
1955            // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
1956        }
1957        if (mWordComposer.isComposingWord()) {
1958            if (mWordComposer.isBatchMode()) {
1959                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1960                    final String word = mWordComposer.getTypedWord();
1961                    ResearchLogger.latinIME_handleBackspace_batch(word, 1);
1962                }
1963                final String rejectedSuggestion = mWordComposer.getTypedWord();
1964                mWordComposer.reset();
1965                mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
1966            } else {
1967                mWordComposer.deleteLast();
1968            }
1969            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1970            mHandler.postUpdateSuggestionStrip();
1971            if (!mWordComposer.isComposingWord()) {
1972                // If we just removed the last character, auto-caps mode may have changed so we
1973                // need to re-evaluate.
1974                mKeyboardSwitcher.updateShiftState();
1975            }
1976        } else {
1977            final SettingsValues currentSettings = mSettings.getCurrent();
1978            if (mLastComposedWord.canRevertCommit()) {
1979                if (currentSettings.mIsInternal) {
1980                    LatinImeLoggerUtils.onAutoCorrectionCancellation();
1981                }
1982                revertCommit();
1983                return;
1984            }
1985            if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
1986                // Cancel multi-character input: remove the text we just entered.
1987                // This is triggered on backspace after a key that inputs multiple characters,
1988                // like the smiley key or the .com key.
1989                mConnection.deleteSurroundingText(mEnteredText.length(), 0);
1990                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1991                    ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
1992                }
1993                mEnteredText = null;
1994                // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
1995                // In addition we know that spaceState is false, and that we should not be
1996                // reverting any autocorrect at this point. So we can safely return.
1997                return;
1998            }
1999            if (SPACE_STATE_DOUBLE == spaceState) {
2000                mHandler.cancelDoubleSpacePeriodTimer();
2001                if (mConnection.revertDoubleSpacePeriod()) {
2002                    // No need to reset mSpaceState, it has already be done (that's why we
2003                    // receive it as a parameter)
2004                    return;
2005                }
2006            } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
2007                if (mConnection.revertSwapPunctuation()) {
2008                    // Likewise
2009                    return;
2010                }
2011            }
2012
2013            // No cancelling of commit/double space/swap: we have a regular backspace.
2014            // We should backspace one char and restart suggestion if at the end of a word.
2015            if (mLastSelectionStart != mLastSelectionEnd) {
2016                // If there is a selection, remove it.
2017                final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
2018                mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
2019                // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
2020                // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
2021                // but we want to set it right away to avoid it being used with the wrong values
2022                // later (typically, in a subsequent press on backspace).
2023                mLastSelectionEnd = mLastSelectionStart;
2024                mConnection.deleteSurroundingText(numCharsDeleted, 0);
2025                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2026                    ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
2027                            false /* shouldUncommitLogUnit */);
2028                }
2029            } else {
2030                // There is no selection, just delete one character.
2031                if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
2032                    // This should never happen.
2033                    Log.e(TAG, "Backspace when we don't know the selection position");
2034                }
2035                final int lengthToDelete = Character.isSupplementaryCodePoint(
2036                        mConnection.getCodePointBeforeCursor()) ? 2 : 1;
2037                if (mAppWorkAroundsUtils.isBeforeJellyBean()) {
2038                    // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
2039                    // a hardware keyboard event on pressing enter or delete. This is bad for many
2040                    // reasons (there are race conditions with commits) but some applications are
2041                    // relying on this behavior so we continue to support it for older apps.
2042                    sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL);
2043                } else {
2044                    mConnection.deleteSurroundingText(lengthToDelete, 0);
2045                }
2046                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2047                    ResearchLogger.latinIME_handleBackspace(lengthToDelete,
2048                            true /* shouldUncommitLogUnit */);
2049                }
2050                if (mDeleteCount > DELETE_ACCELERATE_AT) {
2051                    final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
2052                            mConnection.getCodePointBeforeCursor()) ? 2 : 1;
2053                    mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
2054                    if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2055                        ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
2056                                true /* shouldUncommitLogUnit */);
2057                    }
2058                }
2059            }
2060            if (currentSettings.isSuggestionsRequested(mDisplayOrientation)
2061                    && currentSettings.mCurrentLanguageHasSpaces) {
2062                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
2063            }
2064            // We just removed a character. We need to update the auto-caps state.
2065            mKeyboardSwitcher.updateShiftState();
2066        }
2067    }
2068
2069    /*
2070     * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
2071     */
2072    private boolean maybeStripSpace(final int code,
2073            final int spaceState, final boolean isFromSuggestionStrip) {
2074        if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
2075            mConnection.removeTrailingSpace();
2076            return false;
2077        }
2078        if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
2079                && isFromSuggestionStrip) {
2080            final SettingsValues currentSettings = mSettings.getCurrent();
2081            if (currentSettings.isUsuallyPrecededBySpace(code)) return false;
2082            if (currentSettings.isUsuallyFollowedBySpace(code)) return true;
2083            mConnection.removeTrailingSpace();
2084        }
2085        return false;
2086    }
2087
2088    private void handleCharacter(final int primaryCode, final int x,
2089            final int y, final int spaceState) {
2090        // TODO: refactor this method to stop flipping isComposingWord around all the time, and
2091        // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
2092        // which has the same name as other handle* methods but is not the same.
2093        boolean isComposingWord = mWordComposer.isComposingWord();
2094
2095        // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
2096        // See onStartBatchInput() to see how to do it.
2097        final SettingsValues currentSettings = mSettings.getCurrent();
2098        if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) {
2099            if (isComposingWord) {
2100                // Sanity check
2101                throw new RuntimeException("Should not be composing here");
2102            }
2103            promotePhantomSpace();
2104        }
2105
2106        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
2107            // If we are in the middle of a recorrection, we need to commit the recorrection
2108            // first so that we can insert the character at the current cursor position.
2109            resetEntireInputState(mLastSelectionStart);
2110            isComposingWord = false;
2111        }
2112        // We want to find out whether to start composing a new word with this character. If so,
2113        // we need to reset the composing state and switch isComposingWord. The order of the
2114        // tests is important for good performance.
2115        // We only start composing if we're not already composing.
2116        if (!isComposingWord
2117        // We only start composing if this is a word code point. Essentially that means it's a
2118        // a letter or a word connector.
2119                && currentSettings.isWordCodePoint(primaryCode)
2120        // We never go into composing state if suggestions are not requested.
2121                && currentSettings.isSuggestionsRequested(mDisplayOrientation) &&
2122        // In languages with spaces, we only start composing a word when we are not already
2123        // touching a word. In languages without spaces, the above conditions are sufficient.
2124                (!mConnection.isCursorTouchingWord(currentSettings)
2125                        || !currentSettings.mCurrentLanguageHasSpaces)) {
2126            // Reset entirely the composing state anyway, then start composing a new word unless
2127            // the character is a single quote or a dash. The idea here is, single quote and dash
2128            // are not separators and they should be treated as normal characters, except in the
2129            // first position where they should not start composing a word.
2130            isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode
2131                    && Constants.CODE_DASH != primaryCode);
2132            // Here we don't need to reset the last composed word. It will be reset
2133            // when we commit this one, if we ever do; if on the other hand we backspace
2134            // it entirely and resume suggestions on the previous word, we'd like to still
2135            // have touch coordinates for it.
2136            resetComposingState(false /* alsoResetLastComposedWord */);
2137        }
2138        if (isComposingWord) {
2139            final int keyX, keyY;
2140            if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) {
2141                final KeyDetector keyDetector =
2142                        mKeyboardSwitcher.getMainKeyboardView().getKeyDetector();
2143                keyX = keyDetector.getTouchX(x);
2144                keyY = keyDetector.getTouchY(y);
2145            } else {
2146                keyX = x;
2147                keyY = y;
2148            }
2149            mWordComposer.add(primaryCode, keyX, keyY);
2150            // If it's the first letter, make note of auto-caps state
2151            if (mWordComposer.size() == 1) {
2152                mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
2153            }
2154            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
2155        } else {
2156            final boolean swapWeakSpace = maybeStripSpace(primaryCode,
2157                    spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
2158
2159            sendKeyCodePoint(primaryCode);
2160
2161            if (swapWeakSpace) {
2162                swapSwapperAndSpace();
2163                mSpaceState = SPACE_STATE_WEAK;
2164            }
2165            // In case the "add to dictionary" hint was still displayed.
2166            if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
2167        }
2168        mHandler.postUpdateSuggestionStrip();
2169        if (currentSettings.mIsInternal) {
2170            LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y);
2171        }
2172    }
2173
2174    private void handleRecapitalize() {
2175        if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
2176        // If we have a recapitalize in progress, use it; otherwise, create a new one.
2177        if (!mRecapitalizeStatus.isActive()
2178                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
2179            final CharSequence selectedText =
2180                    mConnection.getSelectedText(0 /* flags, 0 for no styles */);
2181            if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
2182            final SettingsValues currentSettings = mSettings.getCurrent();
2183            mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
2184                    selectedText.toString(), currentSettings.mLocale,
2185                    currentSettings.mWordSeparators);
2186            // We trim leading and trailing whitespace.
2187            mRecapitalizeStatus.trim();
2188            // Trimming the object may have changed the length of the string, and we need to
2189            // reposition the selection handles accordingly. As this result in an IPC call,
2190            // only do it if it's actually necessary, in other words if the recapitalize status
2191            // is not set at the same place as before.
2192            if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
2193                mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
2194                mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
2195                mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
2196            }
2197        }
2198        mRecapitalizeStatus.rotate();
2199        final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
2200        mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
2201        mConnection.deleteSurroundingText(numCharsDeleted, 0);
2202        mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
2203        mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
2204        mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
2205        mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
2206        // Match the keyboard to the new state.
2207        mKeyboardSwitcher.updateShiftState();
2208    }
2209
2210    // Returns true if we do an autocorrection, false otherwise.
2211    private boolean handleSeparator(final int primaryCode, final int x, final int y,
2212            final int spaceState) {
2213        boolean didAutoCorrect = false;
2214        final SettingsValues currentSettings = mSettings.getCurrent();
2215        // We avoid sending spaces in languages without spaces if we were composing.
2216        final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode
2217                && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord();
2218        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
2219            // If we are in the middle of a recorrection, we need to commit the recorrection
2220            // first so that we can insert the separator at the current cursor position.
2221            resetEntireInputState(mLastSelectionStart);
2222        }
2223        if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
2224            if (currentSettings.mCorrectionEnabled) {
2225                final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
2226                        : new String(new int[] { primaryCode }, 0, 1);
2227                commitCurrentAutoCorrection(separator);
2228                didAutoCorrect = true;
2229            } else {
2230                commitTyped(new String(new int[]{primaryCode}, 0, 1));
2231            }
2232        }
2233
2234        final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
2235                Constants.SUGGESTION_STRIP_COORDINATE == x);
2236
2237        if (SPACE_STATE_PHANTOM == spaceState &&
2238                currentSettings.isUsuallyPrecededBySpace(primaryCode)) {
2239            promotePhantomSpace();
2240        }
2241        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2242            ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
2243        }
2244
2245        if (!shouldAvoidSendingCode) {
2246            sendKeyCodePoint(primaryCode);
2247        }
2248
2249        if (Constants.CODE_SPACE == primaryCode) {
2250            if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
2251                if (maybeDoubleSpacePeriod()) {
2252                    mSpaceState = SPACE_STATE_DOUBLE;
2253                } else if (!isShowingPunctuationList()) {
2254                    mSpaceState = SPACE_STATE_WEAK;
2255                }
2256            }
2257
2258            mHandler.startDoubleSpacePeriodTimer();
2259            mHandler.postUpdateSuggestionStrip();
2260        } else {
2261            if (swapWeakSpace) {
2262                swapSwapperAndSpace();
2263                mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
2264            } else if (SPACE_STATE_PHANTOM == spaceState
2265                    && currentSettings.isUsuallyFollowedBySpace(primaryCode)) {
2266                // If we are in phantom space state, and the user presses a separator, we want to
2267                // stay in phantom space state so that the next keypress has a chance to add the
2268                // space. For example, if I type "Good dat", pick "day" from the suggestion strip
2269                // then insert a comma and go on to typing the next word, I want the space to be
2270                // inserted automatically before the next word, the same way it is when I don't
2271                // input the comma.
2272                // The case is a little different if the separator is a space stripper. Such a
2273                // separator does not normally need a space on the right (that's the difference
2274                // between swappers and strippers), so we should not stay in phantom space state if
2275                // the separator is a stripper. Hence the additional test above.
2276                mSpaceState = SPACE_STATE_PHANTOM;
2277            }
2278
2279            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
2280            // already displayed or not, so it's okay.
2281            setPunctuationSuggestions();
2282        }
2283        if (currentSettings.mIsInternal) {
2284            LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y);
2285        }
2286
2287        mKeyboardSwitcher.updateShiftState();
2288        return didAutoCorrect;
2289    }
2290
2291    private CharSequence getTextWithUnderline(final String text) {
2292        return mIsAutoCorrectionIndicatorOn
2293                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
2294                : text;
2295    }
2296
2297    private void handleClose() {
2298        // TODO: Verify that words are logged properly when IME is closed.
2299        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
2300        requestHideSelf(0);
2301        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
2302        if (mainKeyboardView != null) {
2303            mainKeyboardView.closing();
2304        }
2305    }
2306
2307    // TODO: make this private
2308    // Outside LatinIME, only used by the test suite.
2309    @UsedForTesting
2310    boolean isShowingPunctuationList() {
2311        if (mSuggestedWords == null) return false;
2312        return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords;
2313    }
2314
2315    private boolean isSuggestionsStripVisible() {
2316        final SettingsValues currentSettings = mSettings.getCurrent();
2317        if (mSuggestionStripView == null)
2318            return false;
2319        if (mSuggestionStripView.isShowingAddToDictionaryHint())
2320            return true;
2321        if (null == currentSettings)
2322            return false;
2323        if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
2324            return false;
2325        if (currentSettings.isApplicationSpecifiedCompletionsOn())
2326            return true;
2327        return currentSettings.isSuggestionsRequested(mDisplayOrientation);
2328    }
2329
2330    private void clearSuggestionStrip() {
2331        setSuggestedWords(SuggestedWords.EMPTY, false);
2332        setAutoCorrectionIndicator(false);
2333    }
2334
2335    private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) {
2336        mSuggestedWords = words;
2337        if (mSuggestionStripView != null) {
2338            mSuggestionStripView.setSuggestions(words);
2339            mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
2340        }
2341    }
2342
2343    private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
2344        // Put a blue underline to a word in TextView which will be auto-corrected.
2345        if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
2346                && mWordComposer.isComposingWord()) {
2347            mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
2348            final CharSequence textWithUnderline =
2349                    getTextWithUnderline(mWordComposer.getTypedWord());
2350            // TODO: when called from an updateSuggestionStrip() call that results from a posted
2351            // message, this is called outside any batch edit. Potentially, this may result in some
2352            // janky flickering of the screen, although the display speed makes it unlikely in
2353            // the practice.
2354            mConnection.setComposingText(textWithUnderline, 1);
2355        }
2356    }
2357
2358    private void updateSuggestionStrip() {
2359        mHandler.cancelUpdateSuggestionStrip();
2360        final SettingsValues currentSettings = mSettings.getCurrent();
2361
2362        // Check if we have a suggestion engine attached.
2363        if (mSuggest == null
2364                || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
2365            if (mWordComposer.isComposingWord()) {
2366                Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
2367                        + "requested!");
2368            }
2369            return;
2370        }
2371
2372        if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) {
2373            setPunctuationSuggestions();
2374            return;
2375        }
2376
2377        final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
2378        getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
2379                new OnGetSuggestedWordsCallback() {
2380                    @Override
2381                    public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
2382                        holder.set(suggestedWords);
2383                    }
2384                }
2385        );
2386
2387        // This line may cause the current thread to wait.
2388        final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT);
2389        if (suggestedWords != null) {
2390            showSuggestionStrip(suggestedWords);
2391        }
2392    }
2393
2394    private void getSuggestedWords(final int sessionId,
2395            final OnGetSuggestedWordsCallback callback) {
2396        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
2397        final Suggest suggest = mSuggest;
2398        if (keyboard == null || suggest == null) {
2399            callback.onGetSuggestedWords(SuggestedWords.EMPTY);
2400            return;
2401        }
2402        // Get the word on which we should search the bigrams. If we are composing a word, it's
2403        // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
2404        // should just skip whitespace if any, so 1.
2405        final SettingsValues currentSettings = mSettings.getCurrent();
2406        final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
2407        final String prevWord;
2408        if (currentSettings.mCurrentLanguageHasSpaces) {
2409            // If we are typing in a language with spaces we can just look up the previous
2410            // word from textview.
2411            prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators,
2412                    mWordComposer.isComposingWord() ? 2 : 1);
2413        } else {
2414            prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
2415                    : mLastComposedWord.mCommittedWord;
2416        }
2417        suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
2418                currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
2419                additionalFeaturesOptions, sessionId, callback);
2420    }
2421
2422    private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId,
2423            final OnGetSuggestedWordsCallback callback) {
2424        mInputUpdater.getSuggestedWords(sessionId, new OnGetSuggestedWordsCallback() {
2425            @Override
2426            public void onGetSuggestedWords(SuggestedWords suggestedWords) {
2427                callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
2428                        mWordComposer.getTypedWord(), suggestedWords));
2429            }
2430        });
2431    }
2432
2433    private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
2434            final SuggestedWords suggestedWords) {
2435        // TODO: consolidate this into getSuggestedWords
2436        // We update the suggestion strip only when we have some suggestions to show, i.e. when
2437        // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
2438        // replaced with the new one. However, when the word is a dictionary word, or when the
2439        // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
2440        // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
2441        // revert to suggestions - although it is unclear how we can come here if it's displayed.
2442        if (suggestedWords.size() > 1 || typedWord.length() <= 1
2443                || suggestedWords.mTypedWordValid || null == mSuggestionStripView
2444                || mSuggestionStripView.isShowingAddToDictionaryHint()) {
2445            return suggestedWords;
2446        } else {
2447            return getOlderSuggestions(typedWord);
2448        }
2449    }
2450
2451    private SuggestedWords getOlderSuggestions(final String typedWord) {
2452        SuggestedWords previousSuggestedWords = mSuggestedWords;
2453        if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) {
2454            previousSuggestedWords = SuggestedWords.EMPTY;
2455        }
2456        if (typedWord == null) {
2457            return previousSuggestedWords;
2458        }
2459        final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
2460                SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord,
2461                        previousSuggestedWords);
2462        return new SuggestedWords(typedWordAndPreviousSuggestions,
2463                false /* typedWordValid */,
2464                false /* hasAutoCorrectionCandidate */,
2465                false /* isPunctuationSuggestions */,
2466                true /* isObsoleteSuggestions */,
2467                false /* isPrediction */);
2468    }
2469
2470    private void setAutoCorrection(final SuggestedWords suggestedWords) {
2471        if (suggestedWords.isEmpty()) return;
2472        final String autoCorrection;
2473        if (suggestedWords.mWillAutoCorrect) {
2474            autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
2475        } else {
2476            autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD);
2477        }
2478        mWordComposer.setAutoCorrection(autoCorrection);
2479    }
2480
2481    private void showSuggestionStrip(final SuggestedWords suggestedWords) {
2482        if (suggestedWords.isEmpty()) {
2483            clearSuggestionStrip();
2484            return;
2485        }
2486        setAutoCorrection(suggestedWords);
2487        final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
2488        setSuggestedWords(suggestedWords, isAutoCorrection);
2489        setAutoCorrectionIndicator(isAutoCorrection);
2490        setSuggestionStripShown(isSuggestionsStripVisible());
2491    }
2492
2493    private void commitCurrentAutoCorrection(final String separator) {
2494        // Complete any pending suggestions query first
2495        if (mHandler.hasPendingUpdateSuggestions()) {
2496            updateSuggestionStrip();
2497        }
2498        final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
2499        final String typedWord = mWordComposer.getTypedWord();
2500        final String autoCorrection = (typedAutoCorrection != null)
2501                ? typedAutoCorrection : typedWord;
2502        if (autoCorrection != null) {
2503            if (TextUtils.isEmpty(typedWord)) {
2504                throw new RuntimeException("We have an auto-correction but the typed word "
2505                        + "is empty? Impossible! I must commit suicide.");
2506            }
2507            if (mSettings.isInternal()) {
2508                LatinImeLoggerUtils.onAutoCorrection(
2509                        typedWord, autoCorrection, separator, mWordComposer);
2510            }
2511            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2512                final SuggestedWords suggestedWords = mSuggestedWords;
2513                ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
2514                        separator, mWordComposer.isBatchMode(), suggestedWords);
2515            }
2516            mExpectingUpdateSelection = true;
2517            commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
2518                    separator);
2519            if (!typedWord.equals(autoCorrection)) {
2520                // This will make the correction flash for a short while as a visual clue
2521                // to the user that auto-correction happened. It has no other effect; in particular
2522                // note that this won't affect the text inside the text field AT ALL: it only makes
2523                // the segment of text starting at the supplied index and running for the length
2524                // of the auto-correction flash. At this moment, the "typedWord" argument is
2525                // ignored by TextView.
2526                mConnection.commitCorrection(
2527                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
2528                        typedWord, autoCorrection));
2529            }
2530        }
2531    }
2532
2533    // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
2534    // interface
2535    @Override
2536    public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) {
2537        final SuggestedWords suggestedWords = mSuggestedWords;
2538        final String suggestion = suggestionInfo.mWord;
2539        // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
2540        if (suggestion.length() == 1 && isShowingPunctuationList()) {
2541            // Word separators are suggested before the user inputs something.
2542            // So, LatinImeLogger logs "" as a user's input.
2543            LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords);
2544            // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
2545            final int primaryCode = suggestion.charAt(0);
2546            onCodeInput(primaryCode,
2547                    Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
2548            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2549                ResearchLogger.latinIME_punctuationSuggestion(index, suggestion,
2550                        false /* isBatchMode */, suggestedWords.mIsPrediction);
2551            }
2552            return;
2553        }
2554
2555        mConnection.beginBatchEdit();
2556        final SettingsValues currentSettings = mSettings.getCurrent();
2557        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
2558                // In the batch input mode, a manually picked suggested word should just replace
2559                // the current batch input text and there is no need for a phantom space.
2560                && !mWordComposer.isBatchMode()) {
2561            final int firstChar = Character.codePointAt(suggestion, 0);
2562            if (!currentSettings.isWordSeparator(firstChar)
2563                    || currentSettings.isUsuallyPrecededBySpace(firstChar)) {
2564                promotePhantomSpace();
2565            }
2566        }
2567
2568        if (currentSettings.isApplicationSpecifiedCompletionsOn()
2569                && mApplicationSpecifiedCompletions != null
2570                && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
2571            mSuggestedWords = SuggestedWords.EMPTY;
2572            if (mSuggestionStripView != null) {
2573                mSuggestionStripView.clear();
2574            }
2575            mKeyboardSwitcher.updateShiftState();
2576            resetComposingState(true /* alsoResetLastComposedWord */);
2577            final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
2578            mConnection.commitCompletion(completionInfo);
2579            mConnection.endBatchEdit();
2580            return;
2581        }
2582
2583        // We need to log before we commit, because the word composer will store away the user
2584        // typed word.
2585        final String replacedWord = mWordComposer.getTypedWord();
2586        LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords);
2587        mExpectingUpdateSelection = true;
2588        commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
2589                LastComposedWord.NOT_A_SEPARATOR);
2590        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2591            ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
2592                    mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind,
2593                    suggestionInfo.mSourceDict.mDictType);
2594        }
2595        mConnection.endBatchEdit();
2596        // Don't allow cancellation of manual pick
2597        mLastComposedWord.deactivate();
2598        // Space state must be updated before calling updateShiftState
2599        mSpaceState = SPACE_STATE_PHANTOM;
2600        mKeyboardSwitcher.updateShiftState();
2601
2602        // We should show the "Touch again to save" hint if the user pressed the first entry
2603        // AND it's in none of our current dictionaries (main, user or otherwise).
2604        // Please note that if mSuggest is null, it means that everything is off: suggestion
2605        // and correction, so we shouldn't try to show the hint
2606        final Suggest suggest = mSuggest;
2607        final boolean showingAddToDictionaryHint =
2608                (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
2609                        || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
2610                        && suggest != null
2611                        // If the suggestion is not in the dictionary, the hint should be shown.
2612                        && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true);
2613
2614        if (currentSettings.mIsInternal) {
2615            LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
2616                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
2617        }
2618        if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
2619            mSuggestionStripView.showAddToDictionaryHint(
2620                    suggestion, currentSettings.mHintToSaveText);
2621        } else {
2622            // If we're not showing the "Touch again to save", then update the suggestion strip.
2623            mHandler.postUpdateSuggestionStrip();
2624        }
2625    }
2626
2627    /**
2628     * Commits the chosen word to the text field and saves it for later retrieval.
2629     */
2630    private void commitChosenWord(final String chosenWord, final int commitType,
2631            final String separatorString) {
2632        final SuggestedWords suggestedWords = mSuggestedWords;
2633        mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
2634                this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
2635        // Add the word to the user history dictionary
2636        final String prevWord = addToUserHistoryDictionary(chosenWord);
2637        // TODO: figure out here if this is an auto-correct or if the best word is actually
2638        // what user typed. Note: currently this is done much later in
2639        // LastComposedWord#didCommitTypedWord by string equality of the remembered
2640        // strings.
2641        mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString,
2642                prevWord);
2643    }
2644
2645    private void setPunctuationSuggestions() {
2646        final SettingsValues currentSettings = mSettings.getCurrent();
2647        if (currentSettings.mBigramPredictionEnabled) {
2648            clearSuggestionStrip();
2649        } else {
2650            setSuggestedWords(currentSettings.mSuggestPuncList, false);
2651        }
2652        setAutoCorrectionIndicator(false);
2653        setSuggestionStripShown(isSuggestionsStripVisible());
2654    }
2655
2656    private String addToUserHistoryDictionary(final String suggestion) {
2657        if (TextUtils.isEmpty(suggestion)) return null;
2658        final Suggest suggest = mSuggest;
2659        if (suggest == null) return null;
2660
2661        // If correction is not enabled, we don't add words to the user history dictionary.
2662        // That's to avoid unintended additions in some sensitive fields, or fields that
2663        // expect to receive non-words.
2664        final SettingsValues currentSettings = mSettings.getCurrent();
2665        if (!currentSettings.mCorrectionEnabled) return null;
2666
2667        final UserHistoryPredictionDictionary userHistoryPredictionDictionary =
2668                mUserHistoryPredictionDictionary;
2669        if (userHistoryPredictionDictionary == null) return null;
2670
2671        final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2);
2672        final String secondWord;
2673        if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
2674            secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
2675        } else {
2676            secondWord = suggestion;
2677        }
2678        // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
2679        // We don't add words with 0-frequency (assuming they would be profanity etc.).
2680        final int maxFreq = AutoCorrectionUtils.getMaxFrequency(
2681                suggest.getUnigramDictionaries(), suggestion);
2682        if (maxFreq == 0) return null;
2683        userHistoryPredictionDictionary
2684                .addToPersonalizationPredictionDictionary(prevWord, secondWord, maxFreq > 0);
2685        return prevWord;
2686    }
2687
2688    /**
2689     * Check if the cursor is touching a word. If so, restart suggestions on this word, else
2690     * do nothing.
2691     */
2692    private void restartSuggestionsOnWordTouchedByCursor() {
2693        // HACK: We may want to special-case some apps that exhibit bad behavior in case of
2694        // recorrection. This is a temporary, stopgap measure that will be removed later.
2695        // TODO: remove this.
2696        if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
2697        // A simple way to test for support from the TextView.
2698        if (!isSuggestionsStripVisible()) return;
2699        // Recorrection is not supported in languages without spaces because we don't know
2700        // how to segment them yet.
2701        if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
2702        // If the cursor is not touching a word, or if there is a selection, return right away.
2703        if (mLastSelectionStart != mLastSelectionEnd) return;
2704        // If we don't know the cursor location, return.
2705        if (mLastSelectionStart < 0) return;
2706        final SettingsValues currentSettings = mSettings.getCurrent();
2707        if (!mConnection.isCursorTouchingWord(currentSettings)) return;
2708        final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators,
2709                0 /* additionalPrecedingWordsCount */);
2710        if (null == range) return; // Happens if we don't have an input connection at all
2711        // If for some strange reason (editor bug or so) we measure the text before the cursor as
2712        // longer than what the entire text is supposed to be, the safe thing to do is bail out.
2713        final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
2714        if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return;
2715        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
2716        final String typedWord = range.mWord.toString();
2717        int i = 0;
2718        for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) {
2719            for (final String s : span.getSuggestions()) {
2720                ++i;
2721                if (!TextUtils.equals(s, typedWord)) {
2722                    suggestions.add(new SuggestedWordInfo(s,
2723                            SuggestionStripView.MAX_SUGGESTIONS - i,
2724                            SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
2725                            SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
2726                            SuggestedWordInfo.NOT_A_CONFIDENCE
2727                                    /* autoCommitFirstWordConfidence */));
2728                }
2729            }
2730        }
2731        mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
2732        mWordComposer.setCursorPositionWithinWord(
2733                typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
2734        mConnection.setComposingRegion(
2735                mLastSelectionStart - numberOfCharsInWordBeforeCursor,
2736                mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
2737        if (suggestions.isEmpty()) {
2738            // We come here if there weren't any suggestion spans on this word. We will try to
2739            // compute suggestions for it instead.
2740            mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
2741                    new OnGetSuggestedWordsCallback() {
2742                @Override
2743                public void onGetSuggestedWords(
2744                        final SuggestedWords suggestedWordsIncludingTypedWord) {
2745                    final SuggestedWords suggestedWords;
2746                    if (suggestedWordsIncludingTypedWord.size() > 1) {
2747                        // We were able to compute new suggestions for this word.
2748                        // Remove the typed word, since we don't want to display it in this case.
2749                        // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to
2750                        // false.
2751                        suggestedWords = suggestedWordsIncludingTypedWord
2752                                .getSuggestedWordsExcludingTypedWord();
2753                    } else {
2754                        // No saved suggestions, and we were unable to compute any good one either.
2755                        // Rather than displaying an empty suggestion strip, we'll display the
2756                        // original word alone in the middle.
2757                        // Since there is only one word, willAutoCorrect is false.
2758                        suggestedWords = suggestedWordsIncludingTypedWord;
2759                    }
2760                    unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords);
2761                }});
2762        } else {
2763            // We found suggestion spans in the word. We'll create the SuggestedWords out of
2764            // them, and make willAutoCorrect false.
2765            final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
2766                    true /* typedWordValid */, false /* willAutoCorrect */,
2767                    false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
2768                    false /* isPrediction */);
2769            unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords);
2770        }
2771    }
2772
2773    public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
2774            final SuggestedWords suggestedWords) {
2775        // Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
2776        // We never want to auto-correct on a resumed suggestion. Please refer to the three places
2777        // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected.
2778        // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching
2779        // the text to adapt it.
2780        // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition)
2781        mIsAutoCorrectionIndicatorOn = false;
2782        mHandler.showSuggestionStrip(suggestedWords);
2783    }
2784
2785    /**
2786     * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
2787     * word, else do nothing.
2788     */
2789    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
2790        final CharSequence word =
2791                mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
2792        if (null != word) {
2793            final String wordString = word.toString();
2794            restartSuggestionsOnWordBeforeCursor(wordString);
2795            // TODO: Handle the case where the user manually moves the cursor and then backs up over
2796            // a separator.  In that case, the current log unit should not be uncommitted.
2797            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2798                ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
2799                        true /* dumpCurrentLogUnit */);
2800            }
2801        }
2802    }
2803
2804    private void restartSuggestionsOnWordBeforeCursor(final String word) {
2805        mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
2806        final int length = word.length();
2807        mConnection.deleteSurroundingText(length, 0);
2808        mConnection.setComposingText(word, 1);
2809        mHandler.postUpdateSuggestionStrip();
2810    }
2811
2812    private void revertCommit() {
2813        final String previousWord = mLastComposedWord.mPrevWord;
2814        final String originallyTypedWord = mLastComposedWord.mTypedWord;
2815        final String committedWord = mLastComposedWord.mCommittedWord;
2816        final int cancelLength = committedWord.length();
2817        final int separatorLength = LastComposedWord.getSeparatorLength(
2818                mLastComposedWord.mSeparatorString);
2819        // TODO: should we check our saved separator against the actual contents of the text view?
2820        final int deleteLength = cancelLength + separatorLength;
2821        if (DEBUG) {
2822            if (mWordComposer.isComposingWord()) {
2823                throw new RuntimeException("revertCommit, but we are composing a word");
2824            }
2825            final CharSequence wordBeforeCursor =
2826                    mConnection.getTextBeforeCursor(deleteLength, 0)
2827                            .subSequence(0, cancelLength);
2828            if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
2829                throw new RuntimeException("revertCommit check failed: we thought we were "
2830                        + "reverting \"" + committedWord
2831                        + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
2832            }
2833        }
2834        mConnection.deleteSurroundingText(deleteLength, 0);
2835        if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
2836            mUserHistoryPredictionDictionary.cancelAddingUserHistory(previousWord, committedWord);
2837        }
2838        final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
2839        if (mSettings.getCurrent().mCurrentLanguageHasSpaces) {
2840            // For languages with spaces, we revert to the typed string, but the cursor is still
2841            // after the separator so we don't resume suggestions. If the user wants to correct
2842            // the word, they have to press backspace again.
2843            mConnection.commitText(stringToCommit, 1);
2844        } else {
2845            // For languages without spaces, we revert the typed string but the cursor is flush
2846            // with the typed word, so we need to resume suggestions right away.
2847            mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard());
2848            mConnection.setComposingText(stringToCommit, 1);
2849        }
2850        if (mSettings.isInternal()) {
2851            LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
2852                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
2853        }
2854        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2855            ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
2856                    mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
2857        }
2858        // Don't restart suggestion yet. We'll restart if the user deletes the
2859        // separator.
2860        mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
2861        // We have a separator between the word and the cursor: we should show predictions.
2862        mHandler.postUpdateSuggestionStrip();
2863    }
2864
2865    // This essentially inserts a space, and that's it.
2866    public void promotePhantomSpace() {
2867        final SettingsValues currentSettings = mSettings.getCurrent();
2868        if (currentSettings.shouldInsertSpacesAutomatically()
2869                && currentSettings.mCurrentLanguageHasSpaces
2870                && !mConnection.textBeforeCursorLooksLikeURL()) {
2871            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2872                ResearchLogger.latinIME_promotePhantomSpace();
2873            }
2874            sendKeyCodePoint(Constants.CODE_SPACE);
2875        }
2876    }
2877
2878    // TODO: Make this private
2879    // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
2880    @UsedForTesting
2881    void loadKeyboard() {
2882        // Since we are switching languages, the most urgent thing is to let the keyboard graphics
2883        // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on
2884        // the screen. Anything we do right now will delay this, so wait until the next frame
2885        // before we do the rest, like reopening dictionaries and updating suggestions. So we
2886        // post a message.
2887        mHandler.postReopenDictionaries();
2888        loadSettings();
2889        if (mKeyboardSwitcher.getMainKeyboardView() != null) {
2890            // Reload keyboard because the current language has been changed.
2891            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
2892        }
2893    }
2894
2895    private void hapticAndAudioFeedback(final int code, final int repeatCount) {
2896        final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView();
2897        if (keyboardView != null && keyboardView.isInSlidingKeyInput()) {
2898            // No need to feedback while sliding input.
2899            return;
2900        }
2901        if (repeatCount > 0) {
2902            if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
2903                // No need to feedback when repeat delete key will have no effect.
2904                return;
2905            }
2906            // TODO: Use event time that the last feedback has been generated instead of relying on
2907            // a repeat count to thin out feedback.
2908            if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) {
2909                return;
2910            }
2911        }
2912        final AudioAndHapticFeedbackManager feedbackManager =
2913                AudioAndHapticFeedbackManager.getInstance();
2914        if (repeatCount == 0) {
2915            // TODO: Reconsider how to perform haptic feedback when repeating key.
2916            feedbackManager.performHapticFeedback(keyboardView);
2917        }
2918        feedbackManager.performAudioFeedback(code);
2919    }
2920
2921    // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
2922    // release matching call is {@link #onReleaseKey(int,boolean)} below.
2923    @Override
2924    public void onPressKey(final int primaryCode, final int repeatCount,
2925            final boolean isSinglePointer) {
2926        mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
2927        hapticAndAudioFeedback(primaryCode, repeatCount);
2928    }
2929
2930    // Callback of the {@link KeyboardActionListener}. This is called when a key is released;
2931    // press matching call is {@link #onPressKey(int,int,boolean)} above.
2932    @Override
2933    public void onReleaseKey(final int primaryCode, final boolean withSliding) {
2934        mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
2935
2936        // If accessibility is on, ensure the user receives keyboard state updates.
2937        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
2938            switch (primaryCode) {
2939            case Constants.CODE_SHIFT:
2940                AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
2941                break;
2942            case Constants.CODE_SWITCH_ALPHA_SYMBOL:
2943                AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
2944                break;
2945            }
2946        }
2947    }
2948
2949    // Hooks for hardware keyboard
2950    @Override
2951    public boolean onKeyDown(final int keyCode, final KeyEvent event) {
2952        if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event);
2953        // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if
2954        // it doesn't know what to do with it and leave it to the application. For example,
2955        // hardware key events for adjusting the screen's brightness are passed as is.
2956        if (mEventInterpreter.onHardwareKeyEvent(event)) {
2957            final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
2958            mCurrentlyPressedHardwareKeys.add(keyIdentifier);
2959            return true;
2960        }
2961        return super.onKeyDown(keyCode, event);
2962    }
2963
2964    @Override
2965    public boolean onKeyUp(final int keyCode, final KeyEvent event) {
2966        final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
2967        if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
2968            return true;
2969        }
2970        return super.onKeyUp(keyCode, event);
2971    }
2972
2973    // onKeyDown and onKeyUp are the main events we are interested in. There are two more events
2974    // related to handling of hardware key events that we may want to implement in the future:
2975    // boolean onKeyLongPress(final int keyCode, final KeyEvent event);
2976    // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event);
2977
2978    // receive ringer mode change and network state change.
2979    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2980        @Override
2981        public void onReceive(final Context context, final Intent intent) {
2982            final String action = intent.getAction();
2983            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2984                mSubtypeSwitcher.onNetworkStateChanged(intent);
2985            } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2986                AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
2987            }
2988        }
2989    };
2990
2991    private void launchSettings() {
2992        handleClose();
2993        launchSubActivity(SettingsActivity.class);
2994    }
2995
2996    public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
2997        // Put the text in the attached EditText into a safe, saved state before switching to a
2998        // new activity that will also use the soft keyboard.
2999        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
3000        launchSubActivity(activityClass);
3001    }
3002
3003    private void launchSubActivity(final Class<? extends Activity> activityClass) {
3004        Intent intent = new Intent();
3005        intent.setClass(LatinIME.this, activityClass);
3006        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
3007                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
3008                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
3009        startActivity(intent);
3010    }
3011
3012    private void showSubtypeSelectorAndSettings() {
3013        final CharSequence title = getString(R.string.english_ime_input_options);
3014        final CharSequence[] items = new CharSequence[] {
3015                // TODO: Should use new string "Select active input modes".
3016                getString(R.string.language_selection_title),
3017                getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)),
3018        };
3019        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
3020            @Override
3021            public void onClick(DialogInterface di, int position) {
3022                di.dismiss();
3023                switch (position) {
3024                case 0:
3025                    final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
3026                            mRichImm.getInputMethodIdOfThisIme(),
3027                            Intent.FLAG_ACTIVITY_NEW_TASK
3028                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
3029                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
3030                    startActivity(intent);
3031                    break;
3032                case 1:
3033                    launchSettings();
3034                    break;
3035                }
3036            }
3037        };
3038        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
3039                .setItems(items, listener)
3040                .setTitle(title);
3041        showOptionDialog(builder.create());
3042    }
3043
3044    public void showOptionDialog(final AlertDialog dialog) {
3045        final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
3046        if (windowToken == null) {
3047            return;
3048        }
3049
3050        dialog.setCancelable(true);
3051        dialog.setCanceledOnTouchOutside(true);
3052
3053        final Window window = dialog.getWindow();
3054        final WindowManager.LayoutParams lp = window.getAttributes();
3055        lp.token = windowToken;
3056        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
3057        window.setAttributes(lp);
3058        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
3059
3060        mOptionsDialog = dialog;
3061        dialog.show();
3062    }
3063
3064    // TODO: can this be removed somehow without breaking the tests?
3065    @UsedForTesting
3066    /* package for test */ String getFirstSuggestedWord() {
3067        return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null;
3068    }
3069
3070    // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
3071    @UsedForTesting
3072    /* package for test */ boolean isCurrentlyWaitingForMainDictionary() {
3073        return mSuggest.isCurrentlyWaitingForMainDictionary();
3074    }
3075
3076    // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
3077    @UsedForTesting
3078    /* package for test */ boolean hasMainDictionary() {
3079        return mSuggest.hasMainDictionary();
3080    }
3081
3082    // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
3083    @UsedForTesting
3084    /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
3085        mSuggest.resetMainDict(this, locale, null);
3086    }
3087
3088    public void debugDumpStateAndCrashWithException(final String context) {
3089        final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
3090        s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
3091                .append("\nContext : ").append(context);
3092        throw new RuntimeException(s.toString());
3093    }
3094
3095    @Override
3096    protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) {
3097        super.dump(fd, fout, args);
3098
3099        final Printer p = new PrintWriterPrinter(fout);
3100        p.println("LatinIME state :");
3101        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
3102        final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
3103        p.println("  Keyboard mode = " + keyboardMode);
3104        final SettingsValues settingsValues = mSettings.getCurrent();
3105        p.println("  mIsSuggestionsSuggestionsRequested = "
3106                + settingsValues.isSuggestionsRequested(mDisplayOrientation));
3107        p.println("  mCorrectionEnabled=" + settingsValues.mCorrectionEnabled);
3108        p.println("  isComposingWord=" + mWordComposer.isComposingWord());
3109        p.println("  mSoundOn=" + settingsValues.mSoundOn);
3110        p.println("  mVibrateOn=" + settingsValues.mVibrateOn);
3111        p.println("  mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn);
3112        p.println("  inputAttributes=" + settingsValues.mInputAttributes);
3113    }
3114}
3115