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