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