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