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