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