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