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