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