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