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