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