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