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