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