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