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