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