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