LatinIME.java revision d418580a7185754df3c9d3c65a5cd529b4bc5e25
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_VISIBILITY_SHOW_VALUE
115            = R.string.prefs_suggestion_visibility_show_value;
116    private static final int SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
117            = R.string.prefs_suggestion_visibility_show_only_portrait_value;
118    private static final int SUGGESTION_VISIBILITY_HIDE_VALUE
119            = R.string.prefs_suggestion_visibility_hide_value;
120
121    private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
122        SUGGESTION_VISIBILITY_SHOW_VALUE,
123        SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE,
124        SUGGESTION_VISIBILITY_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 = UserHistoryDictionary.getInstance(
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        case Keyboard.CODE_RESEARCH:
1349            if (ProductionFlag.IS_EXPERIMENTAL) {
1350                ResearchLogger.getInstance().presentResearchDialog(this);
1351            }
1352            break;
1353        default:
1354            if (primaryCode == Keyboard.CODE_TAB
1355                    && mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT) {
1356                performEditorAction(EditorInfo.IME_ACTION_NEXT);
1357                break;
1358            }
1359            mSpaceState = SPACE_STATE_NONE;
1360            if (mSettingsValues.isWordSeparator(primaryCode)) {
1361                didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
1362            } else {
1363                final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1364                if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
1365                    handleCharacter(primaryCode, x, y, spaceState);
1366                } else {
1367                    handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE,
1368                            spaceState);
1369                }
1370            }
1371            mExpectingUpdateSelection = true;
1372            mShouldSwitchToLastSubtype = true;
1373            break;
1374        }
1375        switcher.onCodeInput(primaryCode);
1376        // Reset after any single keystroke, except shift and symbol-shift
1377        if (!didAutoCorrect && primaryCode != Keyboard.CODE_SHIFT
1378                && primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL)
1379            mLastComposedWord.deactivate();
1380        mEnteredText = null;
1381    }
1382
1383    @Override
1384    public void onTextInput(CharSequence text) {
1385        final InputConnection ic = getCurrentInputConnection();
1386        if (ic == null) return;
1387        ic.beginBatchEdit();
1388        commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
1389        text = specificTldProcessingOnTextInput(ic, text);
1390        if (SPACE_STATE_PHANTOM == mSpaceState) {
1391            sendKeyCodePoint(Keyboard.CODE_SPACE);
1392        }
1393        ic.commitText(text, 1);
1394        if (ProductionFlag.IS_EXPERIMENTAL) {
1395            ResearchLogger.latinIME_commitText(text);
1396        }
1397        ic.endBatchEdit();
1398        mKeyboardSwitcher.updateShiftState();
1399        mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
1400        mSpaceState = SPACE_STATE_NONE;
1401        mEnteredText = text;
1402        resetComposingState(true /* alsoResetLastComposedWord */);
1403    }
1404
1405    // ic may not be null
1406    private CharSequence specificTldProcessingOnTextInput(final InputConnection ic,
1407            final CharSequence text) {
1408        if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
1409                || !Character.isLetter(text.charAt(1))) {
1410            // Not a tld: do nothing.
1411            return text;
1412        }
1413        // We have a TLD (or something that looks like this): make sure we don't add
1414        // a space even if currently in phantom mode.
1415        mSpaceState = SPACE_STATE_NONE;
1416        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1417        if (lastOne != null && lastOne.length() == 1
1418                && lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
1419            return text.subSequence(1, text.length());
1420        } else {
1421            return text;
1422        }
1423    }
1424
1425    @Override
1426    public void onCancelInput() {
1427        // User released a finger outside any key
1428        mKeyboardSwitcher.onCancelInput();
1429    }
1430
1431    private void handleBackspace(final int spaceState) {
1432        final InputConnection ic = getCurrentInputConnection();
1433        if (ic == null) return;
1434        ic.beginBatchEdit();
1435        handleBackspaceWhileInBatchEdit(spaceState, ic);
1436        ic.endBatchEdit();
1437    }
1438
1439    // "ic" may not be null.
1440    private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) {
1441        // In many cases, we may have to put the keyboard in auto-shift state again.
1442        mHandler.postUpdateShiftState();
1443
1444        if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
1445            // Cancel multi-character input: remove the text we just entered.
1446            // This is triggered on backspace after a key that inputs multiple characters,
1447            // like the smiley key or the .com key.
1448            final int length = mEnteredText.length();
1449            ic.deleteSurroundingText(length, 0);
1450            if (ProductionFlag.IS_EXPERIMENTAL) {
1451                ResearchLogger.latinIME_deleteSurroundingText(length);
1452            }
1453            // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
1454            // In addition we know that spaceState is false, and that we should not be
1455            // reverting any autocorrect at this point. So we can safely return.
1456            return;
1457        }
1458
1459        if (mWordComposer.isComposingWord()) {
1460            final int length = mWordComposer.size();
1461            if (length > 0) {
1462                mWordComposer.deleteLast();
1463                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1464                // If we have deleted the last remaining character of a word, then we are not
1465                // isComposingWord() any more.
1466                if (!mWordComposer.isComposingWord()) {
1467                    // Not composing word any more, so we can show bigrams.
1468                    mHandler.postUpdateBigramPredictions();
1469                } else {
1470                    // Still composing a word, so we still have letters to deduce a suggestion from.
1471                    mHandler.postUpdateSuggestions();
1472                }
1473            } else {
1474                ic.deleteSurroundingText(1, 0);
1475                if (ProductionFlag.IS_EXPERIMENTAL) {
1476                    ResearchLogger.latinIME_deleteSurroundingText(1);
1477                }
1478            }
1479        } else {
1480            if (mLastComposedWord.canRevertCommit()) {
1481                Utils.Stats.onAutoCorrectionCancellation();
1482                revertCommit(ic);
1483                return;
1484            }
1485            if (SPACE_STATE_DOUBLE == spaceState) {
1486                if (revertDoubleSpaceWhileInBatchEdit(ic)) {
1487                    // No need to reset mSpaceState, it has already be done (that's why we
1488                    // receive it as a parameter)
1489                    return;
1490                }
1491            } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1492                if (revertSwapPunctuation(ic)) {
1493                    // Likewise
1494                    return;
1495                }
1496            }
1497
1498            // No cancelling of commit/double space/swap: we have a regular backspace.
1499            // We should backspace one char and restart suggestion if at the end of a word.
1500            if (mLastSelectionStart != mLastSelectionEnd) {
1501                // If there is a selection, remove it.
1502                final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
1503                ic.setSelection(mLastSelectionEnd, mLastSelectionEnd);
1504                ic.deleteSurroundingText(lengthToDelete, 0);
1505                if (ProductionFlag.IS_EXPERIMENTAL) {
1506                    ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
1507                }
1508            } else {
1509                // There is no selection, just delete one character.
1510                if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
1511                    // This should never happen.
1512                    Log.e(TAG, "Backspace when we don't know the selection position");
1513                }
1514                // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
1515                // we want to be able to compile against the Ice Cream Sandwich SDK.
1516                if (mTargetApplicationInfo != null
1517                        && mTargetApplicationInfo.targetSdkVersion < 16) {
1518                    // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
1519                    // a hardware keyboard event on pressing enter or delete. This is bad for many
1520                    // reasons (there are race conditions with commits) but some applications are
1521                    // relying on this behavior so we continue to support it for older apps.
1522                    sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL, ic);
1523                } else {
1524                    ic.deleteSurroundingText(1, 0);
1525                }
1526                if (ProductionFlag.IS_EXPERIMENTAL) {
1527                    ResearchLogger.latinIME_deleteSurroundingText(1);
1528                }
1529                if (mDeleteCount > DELETE_ACCELERATE_AT) {
1530                    ic.deleteSurroundingText(1, 0);
1531                    if (ProductionFlag.IS_EXPERIMENTAL) {
1532                        ResearchLogger.latinIME_deleteSurroundingText(1);
1533                    }
1534                }
1535            }
1536            if (isSuggestionsRequested()) {
1537                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic);
1538            }
1539        }
1540    }
1541
1542    // ic may be null
1543    private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code,
1544            final int spaceState, final boolean isFromSuggestionStrip) {
1545        if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1546            removeTrailingSpaceWhileInBatchEdit(ic);
1547            return false;
1548        } else if ((SPACE_STATE_WEAK == spaceState
1549                || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
1550                && isFromSuggestionStrip) {
1551            if (mSettingsValues.isWeakSpaceSwapper(code)) {
1552                return true;
1553            } else {
1554                if (mSettingsValues.isWeakSpaceStripper(code)) {
1555                    removeTrailingSpaceWhileInBatchEdit(ic);
1556                }
1557                return false;
1558            }
1559        } else {
1560            return false;
1561        }
1562    }
1563
1564    private void handleCharacter(final int primaryCode, final int x,
1565            final int y, final int spaceState) {
1566        final InputConnection ic = getCurrentInputConnection();
1567        if (null != ic) ic.beginBatchEdit();
1568        // TODO: if ic is null, does it make any sense to call this?
1569        handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic);
1570        if (null != ic) ic.endBatchEdit();
1571    }
1572
1573    // "ic" may be null without this crashing, but the behavior will be really strange
1574    private void handleCharacterWhileInBatchEdit(final int primaryCode,
1575            final int x, final int y, final int spaceState, final InputConnection ic) {
1576        boolean isComposingWord = mWordComposer.isComposingWord();
1577
1578        if (SPACE_STATE_PHANTOM == spaceState &&
1579                !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) {
1580            if (isComposingWord) {
1581                // Sanity check
1582                throw new RuntimeException("Should not be composing here");
1583            }
1584            sendKeyCodePoint(Keyboard.CODE_SPACE);
1585        }
1586
1587        // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
1588        // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
1589        // thread here.
1590        if (!isComposingWord && (isAlphabet(primaryCode)
1591                || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode))
1592                && isSuggestionsRequested() && !isCursorTouchingWord()) {
1593            // Reset entirely the composing state anyway, then start composing a new word unless
1594            // the character is a single quote. The idea here is, single quote is not a
1595            // separator and it should be treated as a normal character, except in the first
1596            // position where it should not start composing a word.
1597            isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode);
1598            // Here we don't need to reset the last composed word. It will be reset
1599            // when we commit this one, if we ever do; if on the other hand we backspace
1600            // it entirely and resume suggestions on the previous word, we'd like to still
1601            // have touch coordinates for it.
1602            resetComposingState(false /* alsoResetLastComposedWord */);
1603            clearSuggestions();
1604        }
1605        if (isComposingWord) {
1606            mWordComposer.add(
1607                    primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
1608            if (ic != null) {
1609                // If it's the first letter, make note of auto-caps state
1610                if (mWordComposer.size() == 1) {
1611                    mWordComposer.setAutoCapitalized(
1612                            getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
1613                }
1614                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1615            }
1616            mHandler.postUpdateSuggestions();
1617        } else {
1618            final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode,
1619                    spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
1620
1621            sendKeyCodePoint(primaryCode);
1622
1623            if (swapWeakSpace) {
1624                swapSwapperAndSpaceWhileInBatchEdit(ic);
1625                mSpaceState = SPACE_STATE_WEAK;
1626            }
1627            // Some characters are not word separators, yet they don't start a new
1628            // composing span. For these, we haven't changed the suggestion strip, and
1629            // if the "add to dictionary" hint is shown, we should do so now. Examples of
1630            // such characters include single quote, dollar, and others; the exact list is
1631            // the list of characters for which we enter handleCharacterWhileInBatchEdit
1632            // that don't match the test if ((isAlphabet...)) at the top of this method.
1633            if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) {
1634                mHandler.postUpdateBigramPredictions();
1635            }
1636        }
1637        Utils.Stats.onNonSeparator((char)primaryCode, x, y);
1638    }
1639
1640    // Returns true if we did an autocorrection, false otherwise.
1641    private boolean handleSeparator(final int primaryCode, final int x, final int y,
1642            final int spaceState) {
1643        // Should dismiss the "Touch again to save" message when handling separator
1644        if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
1645            mHandler.cancelUpdateBigramPredictions();
1646            mHandler.postUpdateSuggestions();
1647        }
1648
1649        boolean didAutoCorrect = false;
1650        // Handle separator
1651        final InputConnection ic = getCurrentInputConnection();
1652        if (ic != null) {
1653            ic.beginBatchEdit();
1654        }
1655        if (mWordComposer.isComposingWord()) {
1656            // In certain languages where single quote is a separator, it's better
1657            // not to auto correct, but accept the typed word. For instance,
1658            // in Italian dov' should not be expanded to dove' because the elision
1659            // requires the last vowel to be removed.
1660            final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
1661                    && !mInputAttributes.mInputTypeNoAutoCorrect;
1662            if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
1663                commitCurrentAutoCorrection(primaryCode, ic);
1664                didAutoCorrect = true;
1665            } else {
1666                commitTyped(ic, primaryCode);
1667            }
1668        }
1669
1670        final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState,
1671                KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
1672
1673        if (SPACE_STATE_PHANTOM == spaceState &&
1674                mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) {
1675            sendKeyCodePoint(Keyboard.CODE_SPACE);
1676        }
1677        sendKeyCodePoint(primaryCode);
1678
1679        if (Keyboard.CODE_SPACE == primaryCode) {
1680            if (isSuggestionsRequested()) {
1681                if (maybeDoubleSpaceWhileInBatchEdit(ic)) {
1682                    mSpaceState = SPACE_STATE_DOUBLE;
1683                } else if (!isShowingPunctuationList()) {
1684                    mSpaceState = SPACE_STATE_WEAK;
1685                }
1686            }
1687
1688            mHandler.startDoubleSpacesTimer();
1689            if (!isCursorTouchingWord()) {
1690                mHandler.cancelUpdateSuggestions();
1691                mHandler.postUpdateBigramPredictions();
1692            }
1693        } else {
1694            if (swapWeakSpace) {
1695                swapSwapperAndSpaceWhileInBatchEdit(ic);
1696                mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
1697            } else if (SPACE_STATE_PHANTOM == spaceState) {
1698                // If we are in phantom space state, and the user presses a separator, we want to
1699                // stay in phantom space state so that the next keypress has a chance to add the
1700                // space. For example, if I type "Good dat", pick "day" from the suggestion strip
1701                // then insert a comma and go on to typing the next word, I want the space to be
1702                // inserted automatically before the next word, the same way it is when I don't
1703                // input the comma.
1704                mSpaceState = SPACE_STATE_PHANTOM;
1705            }
1706
1707            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
1708            // already displayed or not, so it's okay.
1709            setPunctuationSuggestions();
1710        }
1711
1712        Utils.Stats.onSeparator((char)primaryCode, x, y);
1713
1714        if (ic != null) {
1715            ic.endBatchEdit();
1716        }
1717        return didAutoCorrect;
1718    }
1719
1720    private CharSequence getTextWithUnderline(final CharSequence text) {
1721        return mIsAutoCorrectionIndicatorOn
1722                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
1723                : text;
1724    }
1725
1726    private void handleClose() {
1727        commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR);
1728        requestHideSelf(0);
1729        LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1730        if (inputView != null)
1731            inputView.closing();
1732    }
1733
1734    public boolean isSuggestionsRequested() {
1735        return mInputAttributes.mIsSettingsSuggestionStripOn
1736                && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
1737    }
1738
1739    public boolean isShowingPunctuationList() {
1740        if (mSuggestionsView == null) return false;
1741        return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
1742    }
1743
1744    public boolean isShowingSuggestionsStrip() {
1745        return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE)
1746                || (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
1747                        && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
1748    }
1749
1750    public boolean isSuggestionsStripVisible() {
1751        if (mSuggestionsView == null)
1752            return false;
1753        if (mSuggestionsView.isShowingAddToDictionaryHint())
1754            return true;
1755        if (!isShowingSuggestionsStrip())
1756            return false;
1757        if (mInputAttributes.mApplicationSpecifiedCompletionOn)
1758            return true;
1759        return isSuggestionsRequested();
1760    }
1761
1762    public void switchToKeyboardView() {
1763        if (DEBUG) {
1764            Log.d(TAG, "Switch to keyboard view.");
1765        }
1766        if (ProductionFlag.IS_EXPERIMENTAL) {
1767            ResearchLogger.latinIME_switchToKeyboardView();
1768        }
1769        View v = mKeyboardSwitcher.getKeyboardView();
1770        if (v != null) {
1771            // Confirms that the keyboard view doesn't have parent view.
1772            ViewParent p = v.getParent();
1773            if (p != null && p instanceof ViewGroup) {
1774                ((ViewGroup) p).removeView(v);
1775            }
1776            setInputView(v);
1777        }
1778        setSuggestionStripShown(isSuggestionsStripVisible());
1779        updateInputViewShown();
1780        mHandler.postUpdateSuggestions();
1781    }
1782
1783    public void clearSuggestions() {
1784        setSuggestions(SuggestedWords.EMPTY, false);
1785        setAutoCorrectionIndicator(false);
1786    }
1787
1788    private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) {
1789        if (mSuggestionsView != null) {
1790            mSuggestionsView.setSuggestions(words);
1791            mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
1792        }
1793    }
1794
1795    private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
1796        // Put a blue underline to a word in TextView which will be auto-corrected.
1797        final InputConnection ic = getCurrentInputConnection();
1798        if (ic == null) return;
1799        if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
1800                && mWordComposer.isComposingWord()) {
1801            mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
1802            final CharSequence textWithUnderline =
1803                    getTextWithUnderline(mWordComposer.getTypedWord());
1804            ic.setComposingText(textWithUnderline, 1);
1805        }
1806    }
1807
1808    public void updateSuggestions() {
1809        // Check if we have a suggestion engine attached.
1810        if ((mSuggest == null || !isSuggestionsRequested())) {
1811            if (mWordComposer.isComposingWord()) {
1812                Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
1813                mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
1814            }
1815            return;
1816        }
1817
1818        mHandler.cancelUpdateSuggestions();
1819        mHandler.cancelUpdateBigramPredictions();
1820
1821        if (!mWordComposer.isComposingWord()) {
1822            setPunctuationSuggestions();
1823            return;
1824        }
1825
1826        // TODO: May need a better way of retrieving previous word
1827        final InputConnection ic = getCurrentInputConnection();
1828        final CharSequence prevWord;
1829        if (null == ic) {
1830            prevWord = null;
1831        } else {
1832            prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
1833        }
1834
1835        final CharSequence typedWord = mWordComposer.getTypedWord();
1836        // getSuggestedWords handles gracefully a null value of prevWord
1837        final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
1838                prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode);
1839
1840        // Basically, we update the suggestion strip only when suggestion count > 1.  However,
1841        // there is an exception: We update the suggestion strip whenever typed word's length
1842        // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
1843        // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
1844        // need to clear the previous state when the user starts typing a word (i.e. typed word's
1845        // length == 1).
1846        if (suggestedWords.size() > 1 || typedWord.length() == 1
1847                || !suggestedWords.mAllowsToBeAutoCorrected
1848                || mSuggestionsView.isShowingAddToDictionaryHint()) {
1849            showSuggestions(suggestedWords, typedWord);
1850        } else {
1851            SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
1852            if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
1853                previousSuggestions = SuggestedWords.EMPTY;
1854            }
1855            final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
1856                    SuggestedWords.getTypedWordAndPreviousSuggestions(
1857                            typedWord, previousSuggestions);
1858            final SuggestedWords obsoleteSuggestedWords =
1859                    new SuggestedWords(typedWordAndPreviousSuggestions,
1860                            false /* typedWordValid */,
1861                            false /* hasAutoCorrectionCandidate */,
1862                            false /* allowsToBeAutoCorrected */,
1863                            false /* isPunctuationSuggestions */,
1864                            true /* isObsoleteSuggestions */,
1865                            false /* isPrediction */);
1866            showSuggestions(obsoleteSuggestedWords, typedWord);
1867        }
1868    }
1869
1870    public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) {
1871        final CharSequence autoCorrection;
1872        if (suggestedWords.size() > 0) {
1873            if (suggestedWords.hasAutoCorrectionWord()) {
1874                autoCorrection = suggestedWords.getWord(1);
1875            } else {
1876                autoCorrection = typedWord;
1877            }
1878        } else {
1879            autoCorrection = null;
1880        }
1881        mWordComposer.setAutoCorrection(autoCorrection);
1882        final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
1883        setSuggestions(suggestedWords, isAutoCorrection);
1884        setAutoCorrectionIndicator(isAutoCorrection);
1885        setSuggestionStripShown(isSuggestionsStripVisible());
1886    }
1887
1888    private void commitCurrentAutoCorrection(final int separatorCodePoint,
1889            final InputConnection ic) {
1890        // Complete any pending suggestions query first
1891        if (mHandler.hasPendingUpdateSuggestions()) {
1892            mHandler.cancelUpdateSuggestions();
1893            updateSuggestions();
1894        }
1895        final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
1896        if (autoCorrection != null) {
1897            final String typedWord = mWordComposer.getTypedWord();
1898            if (TextUtils.isEmpty(typedWord)) {
1899                throw new RuntimeException("We have an auto-correction but the typed word "
1900                        + "is empty? Impossible! I must commit suicide.");
1901            }
1902            Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
1903            if (ProductionFlag.IS_EXPERIMENTAL) {
1904                ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord,
1905                        autoCorrection.toString());
1906            }
1907            mExpectingUpdateSelection = true;
1908            commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
1909                    separatorCodePoint);
1910            if (!typedWord.equals(autoCorrection) && null != ic) {
1911                // This will make the correction flash for a short while as a visual clue
1912                // to the user that auto-correction happened.
1913                ic.commitCorrection(new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
1914                        typedWord, autoCorrection));
1915            }
1916        }
1917    }
1918
1919    @Override
1920    public void pickSuggestionManually(final int index, final CharSequence suggestion,
1921            int x, int y) {
1922        final InputConnection ic = getCurrentInputConnection();
1923        if (null != ic) ic.beginBatchEdit();
1924        pickSuggestionManuallyWhileInBatchEdit(index, suggestion, x, y, ic);
1925        if (null != ic) ic.endBatchEdit();
1926    }
1927
1928    public void pickSuggestionManuallyWhileInBatchEdit(final int index,
1929        final CharSequence suggestion, final int x, final int y, final InputConnection ic) {
1930        final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
1931        // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
1932        if (suggestion.length() == 1 && isShowingPunctuationList()) {
1933            // Word separators are suggested before the user inputs something.
1934            // So, LatinImeLogger logs "" as a user's input.
1935            LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
1936            // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
1937            if (ProductionFlag.IS_EXPERIMENTAL) {
1938                ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y);
1939            }
1940            final int primaryCode = suggestion.charAt(0);
1941            onCodeInput(primaryCode,
1942                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
1943                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
1944            return;
1945        }
1946
1947        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
1948            int firstChar = Character.codePointAt(suggestion, 0);
1949            if ((!mSettingsValues.isWeakSpaceStripper(firstChar))
1950                    && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) {
1951                sendKeyCodePoint(Keyboard.CODE_SPACE);
1952            }
1953        }
1954
1955        if (mInputAttributes.mApplicationSpecifiedCompletionOn
1956                && mApplicationSpecifiedCompletions != null
1957                && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
1958            if (mSuggestionsView != null) {
1959                mSuggestionsView.clear();
1960            }
1961            mKeyboardSwitcher.updateShiftState();
1962            resetComposingState(true /* alsoResetLastComposedWord */);
1963            if (ic != null) {
1964                final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
1965                ic.commitCompletion(completionInfo);
1966                if (ProductionFlag.IS_EXPERIMENTAL) {
1967                    ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
1968                            completionInfo.getText(), x, y);
1969                }
1970            }
1971            return;
1972        }
1973
1974        // We need to log before we commit, because the word composer will store away the user
1975        // typed word.
1976        final String replacedWord = mWordComposer.getTypedWord().toString();
1977        LatinImeLogger.logOnManualSuggestion(replacedWord,
1978                suggestion.toString(), index, suggestedWords);
1979        if (ProductionFlag.IS_EXPERIMENTAL) {
1980            ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y);
1981        }
1982        mExpectingUpdateSelection = true;
1983        commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
1984                LastComposedWord.NOT_A_SEPARATOR);
1985        // Don't allow cancellation of manual pick
1986        mLastComposedWord.deactivate();
1987        mSpaceState = SPACE_STATE_PHANTOM;
1988        // TODO: is this necessary?
1989        mKeyboardSwitcher.updateShiftState();
1990
1991        // We should show the "Touch again to save" hint if the user pressed the first entry
1992        // AND either:
1993        // - There is no dictionary (we know that because we tried to load it => null != mSuggest
1994        //   AND mSuggest.hasMainDictionary() is false)
1995        // - There is a dictionary and the word is not in it
1996        // Please note that if mSuggest is null, it means that everything is off: suggestion
1997        // and correction, so we shouldn't try to show the hint
1998        // We used to look at mCorrectionMode here, but showing the hint should have nothing
1999        // to do with the autocorrection setting.
2000        final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
2001                // If there is no dictionary the hint should be shown.
2002                && (!mSuggest.hasMainDictionary()
2003                        // If "suggestion" is not in the dictionary, the hint should be shown.
2004                        || !AutoCorrection.isValidWord(
2005                                mSuggest.getUnigramDictionaries(), suggestion, true));
2006
2007        Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE,
2008                WordComposer.NOT_A_COORDINATE);
2009        if (!showingAddToDictionaryHint) {
2010            // If we're not showing the "Touch again to save", then show corrections again.
2011            // In case the cursor position doesn't change, make sure we show the suggestions again.
2012            updateBigramPredictions();
2013            // Updating the predictions right away may be slow and feel unresponsive on slower
2014            // terminals. On the other hand if we just postUpdateBigramPredictions() it will
2015            // take a noticeable delay to update them which may feel uneasy.
2016        } else {
2017            if (mIsUserDictionaryAvailable) {
2018                mSuggestionsView.showAddToDictionaryHint(
2019                        suggestion, mSettingsValues.mHintToSaveText);
2020            } else {
2021                mHandler.postUpdateSuggestions();
2022            }
2023        }
2024    }
2025
2026    /**
2027     * Commits the chosen word to the text field and saves it for later retrieval.
2028     */
2029    private void commitChosenWord(final CharSequence chosenWord, final int commitType,
2030            final int separatorCode) {
2031        final InputConnection ic = getCurrentInputConnection();
2032        if (ic != null) {
2033            if (mSettingsValues.mEnableSuggestionSpanInsertion) {
2034                final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
2035                ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
2036                        this, chosenWord, suggestedWords, mIsMainDictionaryAvailable),
2037                        1);
2038                if (ProductionFlag.IS_EXPERIMENTAL) {
2039                    ResearchLogger.latinIME_commitText(chosenWord);
2040                }
2041            } else {
2042                ic.commitText(chosenWord, 1);
2043                if (ProductionFlag.IS_EXPERIMENTAL) {
2044                    ResearchLogger.latinIME_commitText(chosenWord);
2045                }
2046            }
2047        }
2048        // Add the word to the user history dictionary
2049        final CharSequence prevWord = addToUserHistoryDictionary(chosenWord);
2050        // TODO: figure out here if this is an auto-correct or if the best word is actually
2051        // what user typed. Note: currently this is done much later in
2052        // LastComposedWord#didCommitTypedWord by string equality of the remembered
2053        // strings.
2054        mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord.toString(),
2055                separatorCode, prevWord);
2056    }
2057
2058    public void updateBigramPredictions() {
2059        if (mSuggest == null || !isSuggestionsRequested())
2060            return;
2061
2062        if (!mSettingsValues.mBigramPredictionEnabled) {
2063            setPunctuationSuggestions();
2064            return;
2065        }
2066
2067        final SuggestedWords suggestedWords;
2068        if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
2069            final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
2070                    mSettingsValues.mWordSeparators);
2071            if (!TextUtils.isEmpty(prevWord)) {
2072                suggestedWords = mSuggest.getBigramPredictions(prevWord);
2073            } else {
2074                suggestedWords = null;
2075            }
2076        } else {
2077            suggestedWords = null;
2078        }
2079
2080        if (null != suggestedWords && suggestedWords.size() > 0) {
2081            // Explicitly supply an empty typed word (the no-second-arg version of
2082            // showSuggestions will retrieve the word near the cursor, we don't want that here)
2083            showSuggestions(suggestedWords, "");
2084        } else {
2085            if (!isShowingPunctuationList()) setPunctuationSuggestions();
2086        }
2087    }
2088
2089    public void setPunctuationSuggestions() {
2090        setSuggestions(mSettingsValues.mSuggestPuncList, false);
2091        setAutoCorrectionIndicator(false);
2092        setSuggestionStripShown(isSuggestionsStripVisible());
2093    }
2094
2095    private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) {
2096        if (TextUtils.isEmpty(suggestion)) return null;
2097
2098        // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
2099        // adding words in situations where the user or application really didn't
2100        // want corrections enabled or learned.
2101        if (!(mCorrectionMode == Suggest.CORRECTION_FULL
2102                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
2103            return null;
2104        }
2105
2106        if (mUserHistoryDictionary != null) {
2107            final InputConnection ic = getCurrentInputConnection();
2108            final CharSequence prevWord;
2109            if (null != ic) {
2110                prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
2111            } else {
2112                prevWord = null;
2113            }
2114            final String secondWord;
2115            if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
2116                secondWord = suggestion.toString().toLowerCase(
2117                        mSubtypeSwitcher.getCurrentSubtypeLocale());
2118            } else {
2119                secondWord = suggestion.toString();
2120            }
2121            // We demote unrecognized word and words with 0-frequency (assuming they would be
2122            // profanity etc.) by specifying them as "invalid".
2123            final int maxFreq = AutoCorrection.getMaxFrequency(
2124                    mSuggest.getUnigramDictionaries(), suggestion);
2125            mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
2126                    secondWord, maxFreq > 0);
2127            return prevWord;
2128        }
2129        return null;
2130    }
2131
2132    public boolean isCursorTouchingWord() {
2133        final InputConnection ic = getCurrentInputConnection();
2134        if (ic == null) return false;
2135        CharSequence before = ic.getTextBeforeCursor(1, 0);
2136        CharSequence after = ic.getTextAfterCursor(1, 0);
2137        if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0))
2138                && !mSettingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
2139            return true;
2140        }
2141        if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0))
2142                && !mSettingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
2143            return true;
2144        }
2145        return false;
2146    }
2147
2148    // "ic" must not be null
2149    private static boolean sameAsTextBeforeCursor(final InputConnection ic,
2150            final CharSequence text) {
2151        final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
2152        return TextUtils.equals(text, beforeText);
2153    }
2154
2155    // "ic" must not be null
2156    /**
2157     * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
2158     * word, else do nothing.
2159     */
2160    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
2161            final InputConnection ic) {
2162        // Bail out if the cursor is not at the end of a word (cursor must be preceded by
2163        // non-whitespace, non-separator, non-start-of-text)
2164        // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
2165        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0);
2166        if (TextUtils.isEmpty(textBeforeCursor)
2167                || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return;
2168
2169        // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
2170        // separator or end of line/text)
2171        // Example: "test|"<EOL> "te|st" get rejected here
2172        final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0);
2173        if (!TextUtils.isEmpty(textAfterCursor)
2174                && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return;
2175
2176        // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
2177        // Example: " -|" gets rejected here but "e-|" and "e|" are okay
2178        CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators);
2179        // We don't suggest on leading single quotes, so we have to remove them from the word if
2180        // it starts with single quotes.
2181        while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
2182            word = word.subSequence(1, word.length());
2183        }
2184        if (TextUtils.isEmpty(word)) return;
2185        final char firstChar = word.charAt(0); // we just tested that word is not empty
2186        if (word.length() == 1 && !Character.isLetter(firstChar)) return;
2187
2188        // We only suggest on words that start with a letter or a symbol that is excluded from
2189        // word separators (see #handleCharacterWhileInBatchEdit).
2190        if (!(isAlphabet(firstChar)
2191                || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) {
2192            return;
2193        }
2194
2195        // Okay, we are at the end of a word. Restart suggestions.
2196        restartSuggestionsOnWordBeforeCursor(ic, word);
2197    }
2198
2199    // "ic" must not be null
2200    private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic,
2201            final CharSequence word) {
2202        mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
2203        final int length = word.length();
2204        ic.deleteSurroundingText(length, 0);
2205        if (ProductionFlag.IS_EXPERIMENTAL) {
2206            ResearchLogger.latinIME_deleteSurroundingText(length);
2207        }
2208        ic.setComposingText(word, 1);
2209        mHandler.postUpdateSuggestions();
2210    }
2211
2212    // "ic" must not be null
2213    private void revertCommit(final InputConnection ic) {
2214        final CharSequence previousWord = mLastComposedWord.mPrevWord;
2215        final String originallyTypedWord = mLastComposedWord.mTypedWord;
2216        final CharSequence committedWord = mLastComposedWord.mCommittedWord;
2217        final int cancelLength = committedWord.length();
2218        final int separatorLength = LastComposedWord.getSeparatorLength(
2219                mLastComposedWord.mSeparatorCode);
2220        // TODO: should we check our saved separator against the actual contents of the text view?
2221        final int deleteLength = cancelLength + separatorLength;
2222        if (DEBUG) {
2223            if (mWordComposer.isComposingWord()) {
2224                throw new RuntimeException("revertCommit, but we are composing a word");
2225            }
2226            final String wordBeforeCursor =
2227                    ic.getTextBeforeCursor(deleteLength, 0)
2228                            .subSequence(0, cancelLength).toString();
2229            if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
2230                throw new RuntimeException("revertCommit check failed: we thought we were "
2231                        + "reverting \"" + committedWord
2232                        + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
2233            }
2234        }
2235        ic.deleteSurroundingText(deleteLength, 0);
2236        if (ProductionFlag.IS_EXPERIMENTAL) {
2237            ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
2238        }
2239        if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
2240            mUserHistoryDictionary.cancelAddingUserHistory(
2241                    previousWord.toString(), committedWord.toString());
2242        }
2243        if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) {
2244            // This is the case when we cancel a manual pick.
2245            // We should restart suggestion on the word right away.
2246            mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
2247            ic.setComposingText(originallyTypedWord, 1);
2248        } else {
2249            ic.commitText(originallyTypedWord, 1);
2250            // Re-insert the separator
2251            sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
2252            Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
2253                    WordComposer.NOT_A_COORDINATE);
2254            if (ProductionFlag.IS_EXPERIMENTAL) {
2255                ResearchLogger.latinIME_revertCommit(originallyTypedWord);
2256            }
2257            // Don't restart suggestion yet. We'll restart if the user deletes the
2258            // separator.
2259        }
2260        mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
2261        mHandler.cancelUpdateBigramPredictions();
2262        mHandler.postUpdateSuggestions();
2263    }
2264
2265    // "ic" must not be null
2266    private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
2267        mHandler.cancelDoubleSpacesTimer();
2268        // Here we test whether we indeed have a period and a space before us. This should not
2269        // be needed, but it's there just in case something went wrong.
2270        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
2271        if (!". ".equals(textBeforeCursor)) {
2272            // Theoretically we should not be coming here if there isn't ". " before the
2273            // cursor, but the application may be changing the text while we are typing, so
2274            // anything goes. We should not crash.
2275            Log.d(TAG, "Tried to revert double-space combo but we didn't find "
2276                    + "\". \" just before the cursor.");
2277            return false;
2278        }
2279        ic.deleteSurroundingText(2, 0);
2280        if (ProductionFlag.IS_EXPERIMENTAL) {
2281            ResearchLogger.latinIME_deleteSurroundingText(2);
2282        }
2283        ic.commitText("  ", 1);
2284        if (ProductionFlag.IS_EXPERIMENTAL) {
2285            ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
2286        }
2287        return true;
2288    }
2289
2290    private static boolean revertSwapPunctuation(final InputConnection ic) {
2291        // Here we test whether we indeed have a space and something else before us. This should not
2292        // be needed, but it's there just in case something went wrong.
2293        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
2294        // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
2295        // enter surrogate pairs this code will have been removed.
2296        if (TextUtils.isEmpty(textBeforeCursor)
2297                || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
2298            // We may only come here if the application is changing the text while we are typing.
2299            // This is quite a broken case, but not logically impossible, so we shouldn't crash,
2300            // but some debugging log may be in order.
2301            Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
2302                    + "find a space just before the cursor.");
2303            return false;
2304        }
2305        ic.beginBatchEdit();
2306        ic.deleteSurroundingText(2, 0);
2307        if (ProductionFlag.IS_EXPERIMENTAL) {
2308            ResearchLogger.latinIME_deleteSurroundingText(2);
2309        }
2310        ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
2311        if (ProductionFlag.IS_EXPERIMENTAL) {
2312            ResearchLogger.latinIME_revertSwapPunctuation();
2313        }
2314        ic.endBatchEdit();
2315        return true;
2316    }
2317
2318    public boolean isWordSeparator(int code) {
2319        return mSettingsValues.isWordSeparator(code);
2320    }
2321
2322    public boolean preferCapitalization() {
2323        return mWordComposer.isFirstCharCapitalized();
2324    }
2325
2326    // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
2327    // according to new language or mode.
2328    public void onRefreshKeyboard() {
2329        // When the device locale is changed in SetupWizard etc., this method may get called via
2330        // onConfigurationChanged before SoftInputWindow is shown.
2331        if (mKeyboardSwitcher.getKeyboardView() != null) {
2332            // Reload keyboard because the current language has been changed.
2333            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
2334        }
2335        initSuggest();
2336        updateCorrectionMode();
2337        loadSettings();
2338        // Since we just changed languages, we should re-evaluate suggestions with whatever word
2339        // we are currently composing. If we are not composing anything, we may want to display
2340        // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
2341        if (isCursorTouchingWord()) {
2342            mHandler.postUpdateSuggestions();
2343        } else {
2344            mHandler.postUpdateBigramPredictions();
2345        }
2346    }
2347
2348    // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to
2349    // {@link KeyboardSwitcher}.
2350    public void hapticAndAudioFeedback(final int primaryCode) {
2351        mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView());
2352    }
2353
2354    @Override
2355    public void onPressKey(int primaryCode) {
2356        mKeyboardSwitcher.onPressKey(primaryCode);
2357    }
2358
2359    @Override
2360    public void onReleaseKey(int primaryCode, boolean withSliding) {
2361        mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
2362
2363        // If accessibility is on, ensure the user receives keyboard state updates.
2364        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
2365            switch (primaryCode) {
2366            case Keyboard.CODE_SHIFT:
2367                AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
2368                break;
2369            case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
2370                AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
2371                break;
2372            }
2373        }
2374
2375        if (Keyboard.CODE_DELETE == primaryCode) {
2376            // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
2377            // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
2378            // pair-friendly way of deleting characters in InputConnection.
2379            final InputConnection ic = getCurrentInputConnection();
2380            if (null != ic) {
2381                final CharSequence lastChar = ic.getTextBeforeCursor(1, 0);
2382                if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
2383                    ic.deleteSurroundingText(1, 0);
2384                }
2385            }
2386        }
2387    }
2388
2389    // receive ringer mode change and network state change.
2390    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2391        @Override
2392        public void onReceive(Context context, Intent intent) {
2393            final String action = intent.getAction();
2394            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2395                mSubtypeSwitcher.onNetworkStateChanged(intent);
2396            } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2397                mFeedbackManager.onRingerModeChanged();
2398            }
2399        }
2400    };
2401
2402    private void updateCorrectionMode() {
2403        // TODO: cleanup messy flags
2404        final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
2405                && !mInputAttributes.mInputTypeNoAutoCorrect;
2406        mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE;
2407        mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect)
2408                ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
2409    }
2410
2411    private void updateSuggestionVisibility(final Resources res) {
2412        final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting;
2413        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
2414            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
2415                mSuggestionVisibility = visibility;
2416                break;
2417            }
2418        }
2419    }
2420
2421    private void launchSettings() {
2422        launchSettingsClass(SettingsActivity.class);
2423    }
2424
2425    public void launchDebugSettings() {
2426        launchSettingsClass(DebugSettingsActivity.class);
2427    }
2428
2429    private void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
2430        handleClose();
2431        Intent intent = new Intent();
2432        intent.setClass(LatinIME.this, settingsClass);
2433        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2434        startActivity(intent);
2435    }
2436
2437    private void showSubtypeSelectorAndSettings() {
2438        final CharSequence title = getString(R.string.english_ime_input_options);
2439        final CharSequence[] items = new CharSequence[] {
2440                // TODO: Should use new string "Select active input modes".
2441                getString(R.string.language_selection_title),
2442                getString(R.string.english_ime_settings),
2443        };
2444        final Context context = this;
2445        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2446            @Override
2447            public void onClick(DialogInterface di, int position) {
2448                di.dismiss();
2449                switch (position) {
2450                case 0:
2451                    Intent intent = CompatUtils.getInputLanguageSelectionIntent(
2452                            ImfUtils.getInputMethodIdOfThisIme(context),
2453                            Intent.FLAG_ACTIVITY_NEW_TASK
2454                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2455                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2456                    startActivity(intent);
2457                    break;
2458                case 1:
2459                    launchSettings();
2460                    break;
2461                }
2462            }
2463        };
2464        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2465                .setItems(items, listener)
2466                .setTitle(title);
2467        showOptionDialog(builder.create());
2468    }
2469
2470    /* package */ void showOptionDialog(AlertDialog dialog) {
2471        final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
2472        if (windowToken == null) return;
2473
2474        dialog.setCancelable(true);
2475        dialog.setCanceledOnTouchOutside(true);
2476
2477        final Window window = dialog.getWindow();
2478        final WindowManager.LayoutParams lp = window.getAttributes();
2479        lp.token = windowToken;
2480        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
2481        window.setAttributes(lp);
2482        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
2483
2484        mOptionsDialog = dialog;
2485        dialog.show();
2486    }
2487
2488    @Override
2489    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
2490        super.dump(fd, fout, args);
2491
2492        final Printer p = new PrintWriterPrinter(fout);
2493        p.println("LatinIME state :");
2494        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
2495        final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
2496        p.println("  Keyboard mode = " + keyboardMode);
2497        p.println("  mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn);
2498        p.println("  mCorrectionMode=" + mCorrectionMode);
2499        p.println("  isComposingWord=" + mWordComposer.isComposingWord());
2500        p.println("  mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
2501        p.println("  mSoundOn=" + mSettingsValues.mSoundOn);
2502        p.println("  mVibrateOn=" + mSettingsValues.mVibrateOn);
2503        p.println("  mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
2504        p.println("  mInputAttributes=" + mInputAttributes.toString());
2505    }
2506}
2507