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