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