LatinIME.java revision b715299125e8fbaaa941d994217baf823e6c4013
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 android.app.AlertDialog;
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.DialogInterface;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.SharedPreferences;
26import android.content.res.Configuration;
27import android.content.res.Resources;
28import android.inputmethodservice.InputMethodService;
29import android.media.AudioManager;
30import android.net.ConnectivityManager;
31import android.os.Debug;
32import android.os.Message;
33import android.os.SystemClock;
34import android.preference.PreferenceActivity;
35import android.preference.PreferenceManager;
36import android.text.InputType;
37import android.text.TextUtils;
38import android.util.Log;
39import android.util.PrintWriterPrinter;
40import android.util.Printer;
41import android.view.HapticFeedbackConstants;
42import android.view.KeyEvent;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.ViewParent;
46import android.view.inputmethod.CompletionInfo;
47import android.view.inputmethod.EditorInfo;
48import android.view.inputmethod.ExtractedText;
49import android.view.inputmethod.InputConnection;
50
51import com.android.inputmethod.accessibility.AccessibilityUtils;
52import com.android.inputmethod.compat.CompatUtils;
53import com.android.inputmethod.compat.EditorInfoCompatUtils;
54import com.android.inputmethod.compat.InputConnectionCompatUtils;
55import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
56import com.android.inputmethod.compat.InputMethodServiceCompatWrapper;
57import com.android.inputmethod.compat.InputTypeCompatUtils;
58import com.android.inputmethod.compat.SuggestionSpanUtils;
59import com.android.inputmethod.compat.VibratorCompatWrapper;
60import com.android.inputmethod.deprecated.LanguageSwitcherProxy;
61import com.android.inputmethod.deprecated.VoiceProxy;
62import com.android.inputmethod.keyboard.Key;
63import com.android.inputmethod.keyboard.Keyboard;
64import com.android.inputmethod.keyboard.KeyboardActionListener;
65import com.android.inputmethod.keyboard.KeyboardSwitcher;
66import com.android.inputmethod.keyboard.KeyboardView;
67import com.android.inputmethod.keyboard.LatinKeyboard;
68import com.android.inputmethod.keyboard.LatinKeyboardView;
69
70import java.io.FileDescriptor;
71import java.io.PrintWriter;
72import java.util.Locale;
73
74/**
75 * Input method implementation for Qwerty'ish keyboard.
76 */
77public class LatinIME extends InputMethodServiceCompatWrapper implements KeyboardActionListener,
78        SuggestionsView.Listener {
79    private static final String TAG = LatinIME.class.getSimpleName();
80    private static final boolean PERF_DEBUG = false;
81    private static final boolean TRACE = false;
82    private static boolean DEBUG;
83
84    /**
85     * The private IME option used to indicate that no microphone should be
86     * shown for a given text field. For instance, this is specified by the
87     * search dialog when the dialog is already showing a voice search button.
88     *
89     * @deprecated Use {@link LatinIME#IME_OPTION_NO_MICROPHONE} with package name prefixed.
90     */
91    @SuppressWarnings("dep-ann")
92    public static final String IME_OPTION_NO_MICROPHONE_COMPAT = "nm";
93
94    /**
95     * The private IME option used to indicate that no microphone should be
96     * shown for a given text field. For instance, this is specified by the
97     * search dialog when the dialog is already showing a voice search button.
98     */
99    public static final String IME_OPTION_NO_MICROPHONE = "noMicrophoneKey";
100
101    /**
102     * The private IME option used to indicate that no settings key should be
103     * shown for a given text field.
104     */
105    public static final String IME_OPTION_NO_SETTINGS_KEY = "noSettingsKey";
106
107    /**
108     * The private IME option used to indicate that the given text field needs
109     * ASCII code points input.
110     */
111    public static final String IME_OPTION_FORCE_ASCII = "forceAscii";
112
113    /**
114     * The subtype extra value used to indicate that the subtype keyboard layout is capable for
115     * typing ASCII characters.
116     */
117    public static final String SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE = "AsciiCapable";
118
119    /**
120     * The subtype extra value used to indicate that the subtype keyboard layout supports touch
121     * position correction.
122     */
123    public static final String SUBTYPE_EXTRA_VALUE_SUPPORT_TOUCH_POSITION_CORRECTION =
124            "SupportTouchPositionCorrection";
125    /**
126     * The subtype extra value used to indicate that the subtype keyboard layout should be loaded
127     * from the specified locale.
128     */
129    public static final String SUBTYPE_EXTRA_VALUE_KEYBOARD_LOCALE = "KeyboardLocale";
130
131    private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
132
133    // How many continuous deletes at which to start deleting at a higher speed.
134    private static final int DELETE_ACCELERATE_AT = 20;
135    // Key events coming any faster than this are long-presses.
136    private static final int QUICK_PRESS = 200;
137
138    private static final int PENDING_IMS_CALLBACK_DURATION = 800;
139
140    /**
141     * The name of the scheme used by the Package Manager to warn of a new package installation,
142     * replacement or removal.
143     */
144    private static final String SCHEME_PACKAGE = "package";
145
146    private int mSuggestionVisibility;
147    private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
148            = R.string.prefs_suggestion_visibility_show_value;
149    private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
150            = R.string.prefs_suggestion_visibility_show_only_portrait_value;
151    private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
152            = R.string.prefs_suggestion_visibility_hide_value;
153
154    private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
155        SUGGESTION_VISIBILILTY_SHOW_VALUE,
156        SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
157        SUGGESTION_VISIBILILTY_HIDE_VALUE
158    };
159
160    private Settings.Values mSettingsValues;
161
162    private View mExtractArea;
163    private View mKeyPreviewBackingView;
164    private View mSuggestionsContainer;
165    private SuggestionsView mSuggestionsView;
166    private Suggest mSuggest;
167    private CompletionInfo[] mApplicationSpecifiedCompletions;
168
169    private InputMethodManagerCompatWrapper mImm;
170    private Resources mResources;
171    private SharedPreferences mPrefs;
172    private String mInputMethodId;
173    private KeyboardSwitcher mKeyboardSwitcher;
174    private SubtypeSwitcher mSubtypeSwitcher;
175    private VoiceProxy mVoiceProxy;
176
177    private UserDictionary mUserDictionary;
178    private UserBigramDictionary mUserBigramDictionary;
179    private UserUnigramDictionary mUserUnigramDictionary;
180    private boolean mIsUserDictionaryAvaliable;
181
182    // TODO: Create an inner class to group options and pseudo-options to improve readability.
183    // These variables are initialized according to the {@link EditorInfo#inputType}.
184    private boolean mInsertSpaceOnPickSuggestionManually;
185    private boolean mInputTypeNoAutoCorrect;
186    private boolean mIsSettingsSuggestionStripOn;
187    private boolean mApplicationSpecifiedCompletionOn;
188
189    private final StringBuilder mComposingStringBuilder = new StringBuilder();
190    private WordComposer mWordComposer = new WordComposer();
191    private CharSequence mBestWord;
192    private boolean mHasUncommittedTypedChars;
193    // Magic space: a space that should disappear on space/apostrophe insertion, move after the
194    // punctuation on punctuation insertion, and become a real space on alpha char insertion.
195    private boolean mJustAddedMagicSpace; // This indicates whether the last char is a magic space.
196    // This indicates whether the last keypress resulted in processing of double space replacement
197    // with period-space.
198    private boolean mJustReplacedDoubleSpace;
199
200    private int mCorrectionMode;
201    private int mCommittedLength;
202    // Keep track of the last selection range to decide if we need to show word alternatives
203    private int mLastSelectionStart;
204    private int mLastSelectionEnd;
205
206    // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
207    // "expect" it, it means the user actually moved the cursor.
208    private boolean mExpectingUpdateSelection;
209    private int mDeleteCount;
210    private long mLastKeyTime;
211
212    private AudioManager mAudioManager;
213    private float mFxVolume = -1.0f; // default volume
214    private boolean mSilentModeOn; // System-wide current configuration
215
216    private VibratorCompatWrapper mVibrator;
217    private long mKeypressVibrationDuration = -1;
218
219    // TODO: Move this flag to VoiceProxy
220    private boolean mConfigurationChanging;
221
222    // Member variables for remembering the current device orientation.
223    private int mDisplayOrientation;
224
225    // Object for reacting to adding/removing a dictionary pack.
226    private BroadcastReceiver mDictionaryPackInstallReceiver =
227            new DictionaryPackInstallBroadcastReceiver(this);
228
229    // Keeps track of most recently inserted text (multi-character key) for reverting
230    private CharSequence mEnteredText;
231
232    private final ComposingStateManager mComposingStateManager =
233            ComposingStateManager.getInstance();
234
235    public final UIHandler mHandler = new UIHandler(this);
236
237    public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
238        private static final int MSG_UPDATE_SUGGESTIONS = 0;
239        private static final int MSG_UPDATE_SHIFT_STATE = 1;
240        private static final int MSG_VOICE_RESULTS = 2;
241        private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 3;
242        private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 4;
243        private static final int MSG_SPACE_TYPED = 5;
244        private static final int MSG_SET_BIGRAM_PREDICTIONS = 6;
245        private static final int MSG_PENDING_IMS_CALLBACK = 7;
246
247        public UIHandler(LatinIME outerInstance) {
248            super(outerInstance);
249        }
250
251        @Override
252        public void handleMessage(Message msg) {
253            final LatinIME latinIme = getOuterInstance();
254            final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
255            final LatinKeyboardView inputView = switcher.getKeyboardView();
256            switch (msg.what) {
257            case MSG_UPDATE_SUGGESTIONS:
258                latinIme.updateSuggestions();
259                break;
260            case MSG_UPDATE_SHIFT_STATE:
261                switcher.updateShiftState();
262                break;
263            case MSG_SET_BIGRAM_PREDICTIONS:
264                latinIme.updateBigramPredictions();
265                break;
266            case MSG_VOICE_RESULTS:
267                latinIme.mVoiceProxy.handleVoiceResults(latinIme.preferCapitalization()
268                        || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked()));
269                break;
270            case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR:
271                if (inputView != null) {
272                    inputView.setSpacebarTextFadeFactor(
273                            (1.0f + latinIme.mSettingsValues.
274                                    mFinalFadeoutFactorOfLanguageOnSpacebar) / 2,
275                            (LatinKeyboard)msg.obj);
276                }
277                sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj),
278                        latinIme.mSettingsValues.mDurationOfFadeoutLanguageOnSpacebar);
279                break;
280            case MSG_DISMISS_LANGUAGE_ON_SPACEBAR:
281                if (inputView != null) {
282                    inputView.setSpacebarTextFadeFactor(
283                            latinIme.mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar,
284                            (LatinKeyboard)msg.obj);
285                }
286                break;
287            }
288        }
289
290        public void postUpdateSuggestions() {
291            removeMessages(MSG_UPDATE_SUGGESTIONS);
292            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS),
293                    getOuterInstance().mSettingsValues.mDelayUpdateSuggestions);
294        }
295
296        public void cancelUpdateSuggestions() {
297            removeMessages(MSG_UPDATE_SUGGESTIONS);
298        }
299
300        public boolean hasPendingUpdateSuggestions() {
301            return hasMessages(MSG_UPDATE_SUGGESTIONS);
302        }
303
304        public void postUpdateShiftKeyState() {
305            removeMessages(MSG_UPDATE_SHIFT_STATE);
306            sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE),
307                    getOuterInstance().mSettingsValues.mDelayUpdateShiftState);
308        }
309
310        public void cancelUpdateShiftState() {
311            removeMessages(MSG_UPDATE_SHIFT_STATE);
312        }
313
314        public void postUpdateBigramPredictions() {
315            removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
316            sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS),
317                    getOuterInstance().mSettingsValues.mDelayUpdateSuggestions);
318        }
319
320        public void cancelUpdateBigramPredictions() {
321            removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
322        }
323
324        public void updateVoiceResults() {
325            sendMessage(obtainMessage(MSG_VOICE_RESULTS));
326        }
327
328        public void startDisplayLanguageOnSpacebar(boolean localeChanged) {
329            final LatinIME latinIme = getOuterInstance();
330            removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR);
331            removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR);
332            final LatinKeyboardView inputView = latinIme.mKeyboardSwitcher.getKeyboardView();
333            if (inputView != null) {
334                final LatinKeyboard keyboard = latinIme.mKeyboardSwitcher.getLatinKeyboard();
335                // The language is always displayed when the delay is negative.
336                final boolean needsToDisplayLanguage = localeChanged
337                        || latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar < 0;
338                // The language is never displayed when the delay is zero.
339                if (latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar != 0) {
340                    inputView.setSpacebarTextFadeFactor(needsToDisplayLanguage ? 1.0f
341                            : latinIme.mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar,
342                            keyboard);
343                }
344                // The fadeout animation will start when the delay is positive.
345                if (localeChanged
346                        && latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar > 0) {
347                    sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard),
348                            latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar);
349                }
350            }
351        }
352
353        public void startDoubleSpacesTimer() {
354            removeMessages(MSG_SPACE_TYPED);
355            sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED),
356                    getOuterInstance().mSettingsValues.mDoubleSpacesTurnIntoPeriodTimeout);
357        }
358
359        public void cancelDoubleSpacesTimer() {
360            removeMessages(MSG_SPACE_TYPED);
361        }
362
363        public boolean isAcceptingDoubleSpaces() {
364            return hasMessages(MSG_SPACE_TYPED);
365        }
366
367        // Working variables for the following methods.
368        private boolean mIsOrientationChanging;
369        private boolean mPendingSuccesiveImsCallback;
370        private boolean mHasPendingStartInput;
371        private boolean mHasPendingFinishInputView;
372        private boolean mHasPendingFinishInput;
373
374        public void startOrientationChanging() {
375            mIsOrientationChanging = true;
376            final LatinIME latinIme = getOuterInstance();
377            latinIme.mKeyboardSwitcher.saveKeyboardState();
378        }
379
380        private void resetPendingImsCallback() {
381            mHasPendingFinishInputView = false;
382            mHasPendingFinishInput = false;
383            mHasPendingStartInput = false;
384        }
385
386        private void executePendingImsCallback(LatinIME latinIme, EditorInfo attribute,
387                boolean restarting) {
388            if (mHasPendingFinishInputView)
389                latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
390            if (mHasPendingFinishInput)
391                latinIme.onFinishInputInternal();
392            if (mHasPendingStartInput)
393                latinIme.onStartInputInternal(attribute, restarting);
394            resetPendingImsCallback();
395        }
396
397        public void onStartInput(EditorInfo attribute, boolean restarting) {
398            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
399                // Typically this is the second onStartInput after orientation changed.
400                mHasPendingStartInput = true;
401            } else {
402                if (mIsOrientationChanging && restarting) {
403                    // This is the first onStartInput after orientation changed.
404                    mIsOrientationChanging = false;
405                    mPendingSuccesiveImsCallback = true;
406                }
407                final LatinIME latinIme = getOuterInstance();
408                executePendingImsCallback(latinIme, attribute, restarting);
409                latinIme.onStartInputInternal(attribute, restarting);
410            }
411        }
412
413        public void onStartInputView(EditorInfo attribute, boolean restarting) {
414             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
415                 // Typically this is the second onStartInputView after orientation changed.
416                 resetPendingImsCallback();
417             } else {
418                 if (mPendingSuccesiveImsCallback) {
419                     // This is the first onStartInputView after orientation changed.
420                     mPendingSuccesiveImsCallback = false;
421                     resetPendingImsCallback();
422                     sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
423                             PENDING_IMS_CALLBACK_DURATION);
424                 }
425                 final LatinIME latinIme = getOuterInstance();
426                 executePendingImsCallback(latinIme, attribute, restarting);
427                 latinIme.onStartInputViewInternal(attribute, restarting);
428             }
429        }
430
431        public void onFinishInputView(boolean finishingInput) {
432            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
433                // Typically this is the first onFinishInputView after orientation changed.
434                mHasPendingFinishInputView = true;
435            } else {
436                final LatinIME latinIme = getOuterInstance();
437                latinIme.onFinishInputViewInternal(finishingInput);
438            }
439        }
440
441        public void onFinishInput() {
442            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
443                // Typically this is the first onFinishInput after orientation changed.
444                mHasPendingFinishInput = true;
445            } else {
446                final LatinIME latinIme = getOuterInstance();
447                executePendingImsCallback(latinIme, null, false);
448                latinIme.onFinishInputInternal();
449            }
450        }
451    }
452
453    @Override
454    public void onCreate() {
455        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
456        mPrefs = prefs;
457        LatinImeLogger.init(this, prefs);
458        LanguageSwitcherProxy.init(this, prefs);
459        InputMethodManagerCompatWrapper.init(this);
460        SubtypeSwitcher.init(this);
461        KeyboardSwitcher.init(this, prefs);
462        AccessibilityUtils.init(this, prefs);
463
464        super.onCreate();
465
466        mImm = InputMethodManagerCompatWrapper.getInstance();
467        mInputMethodId = Utils.getInputMethodId(mImm, getPackageName());
468        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
469        mKeyboardSwitcher = KeyboardSwitcher.getInstance();
470        mVibrator = VibratorCompatWrapper.getInstance(this);
471        DEBUG = LatinImeLogger.sDBG;
472
473        final Resources res = getResources();
474        mResources = res;
475
476        loadSettings();
477
478        Utils.GCUtils.getInstance().reset();
479        boolean tryGC = true;
480        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
481            try {
482                initSuggest();
483                tryGC = false;
484            } catch (OutOfMemoryError e) {
485                tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
486            }
487        }
488
489        mDisplayOrientation = res.getConfiguration().orientation;
490
491        // Register to receive ringer mode change and network state change.
492        // Also receive installation and removal of a dictionary pack.
493        final IntentFilter filter = new IntentFilter();
494        filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
495        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
496        registerReceiver(mReceiver, filter);
497        mVoiceProxy = VoiceProxy.init(this, prefs, mHandler);
498
499        final IntentFilter packageFilter = new IntentFilter();
500        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
501        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
502        packageFilter.addDataScheme(SCHEME_PACKAGE);
503        registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
504
505        final IntentFilter newDictFilter = new IntentFilter();
506        newDictFilter.addAction(
507                DictionaryPackInstallBroadcastReceiver.NEW_DICTIONARY_INTENT_ACTION);
508        registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
509    }
510
511    // Has to be package-visible for unit tests
512    /* package */ void loadSettings() {
513        if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
514        if (null == mSubtypeSwitcher) mSubtypeSwitcher = SubtypeSwitcher.getInstance();
515        mSettingsValues = new Settings.Values(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr());
516        resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
517        updateSoundEffectVolume();
518        updateKeypressVibrationDuration();
519    }
520
521    private void initSuggest() {
522        final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
523        final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr);
524
525        final Resources res = mResources;
526        final Locale savedLocale = LocaleUtils.setSystemLocale(res, keyboardLocale);
527        final ContactsDictionary oldContactsDictionary;
528        if (mSuggest != null) {
529            oldContactsDictionary = mSuggest.getContactsDictionary();
530            mSuggest.close();
531        } else {
532            oldContactsDictionary = null;
533        }
534
535        int mainDicResId = Utils.getMainDictionaryResourceId(res);
536        mSuggest = new Suggest(this, mainDicResId, keyboardLocale);
537        if (mSettingsValues.mAutoCorrectEnabled) {
538            mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
539        }
540
541        mUserDictionary = new UserDictionary(this, localeStr);
542        mSuggest.setUserDictionary(mUserDictionary);
543        mIsUserDictionaryAvaliable = mUserDictionary.isEnabled();
544
545        resetContactsDictionary(oldContactsDictionary);
546
547        mUserUnigramDictionary
548                = new UserUnigramDictionary(this, this, localeStr, Suggest.DIC_USER_UNIGRAM);
549        mSuggest.setUserUnigramDictionary(mUserUnigramDictionary);
550
551        mUserBigramDictionary
552                = new UserBigramDictionary(this, this, localeStr, Suggest.DIC_USER_BIGRAM);
553        mSuggest.setUserBigramDictionary(mUserBigramDictionary);
554
555        updateCorrectionMode();
556
557        LocaleUtils.setSystemLocale(res, savedLocale);
558    }
559
560    /**
561     * Resets the contacts dictionary in mSuggest according to the user settings.
562     *
563     * This method takes an optional contacts dictionary to use. Since the contacts dictionary
564     * does not depend on the locale, it can be reused across different instances of Suggest.
565     * The dictionary will also be opened or closed as necessary depending on the settings.
566     *
567     * @param oldContactsDictionary an optional dictionary to use, or null
568     */
569    private void resetContactsDictionary(final ContactsDictionary oldContactsDictionary) {
570        final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict);
571
572        final ContactsDictionary dictionaryToUse;
573        if (!shouldSetDictionary) {
574            // Make sure the dictionary is closed. If it is already closed, this is a no-op,
575            // so it's safe to call it anyways.
576            if (null != oldContactsDictionary) oldContactsDictionary.close();
577            dictionaryToUse = null;
578        } else if (null != oldContactsDictionary) {
579            // Make sure the old contacts dictionary is opened. If it is already open, this is a
580            // no-op, so it's safe to call it anyways.
581            oldContactsDictionary.reopen(this);
582            dictionaryToUse = oldContactsDictionary;
583        } else {
584            dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
585        }
586
587        if (null != mSuggest) {
588            mSuggest.setContactsDictionary(dictionaryToUse);
589        }
590    }
591
592    /* package private */ void resetSuggestMainDict() {
593        final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
594        final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr);
595        int mainDicResId = Utils.getMainDictionaryResourceId(mResources);
596        mSuggest.resetMainDict(this, mainDicResId, keyboardLocale);
597    }
598
599    @Override
600    public void onDestroy() {
601        if (mSuggest != null) {
602            mSuggest.close();
603            mSuggest = null;
604        }
605        unregisterReceiver(mReceiver);
606        unregisterReceiver(mDictionaryPackInstallReceiver);
607        mVoiceProxy.destroy();
608        LatinImeLogger.commit();
609        LatinImeLogger.onDestroy();
610        super.onDestroy();
611    }
612
613    @Override
614    public void onConfigurationChanged(Configuration conf) {
615        mSubtypeSwitcher.onConfigurationChanged(conf);
616        mComposingStateManager.onFinishComposingText();
617        // If orientation changed while predicting, commit the change
618        if (mDisplayOrientation != conf.orientation) {
619            mDisplayOrientation = conf.orientation;
620            mHandler.startOrientationChanging();
621            final InputConnection ic = getCurrentInputConnection();
622            commitTyped(ic);
623            if (ic != null) ic.finishComposingText(); // For voice input
624            if (isShowingOptionDialog())
625                mOptionsDialog.dismiss();
626        }
627
628        mConfigurationChanging = true;
629        super.onConfigurationChanged(conf);
630        mVoiceProxy.onConfigurationChanged(conf);
631        mConfigurationChanging = false;
632
633        // This will work only when the subtype is not supported.
634        LanguageSwitcherProxy.onConfigurationChanged(conf);
635    }
636
637    @Override
638    public View onCreateInputView() {
639        return mKeyboardSwitcher.onCreateInputView();
640    }
641
642    @Override
643    public void setInputView(View view) {
644        super.setInputView(view);
645        mExtractArea = getWindow().getWindow().getDecorView()
646                .findViewById(android.R.id.extractArea);
647        mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
648        mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
649        mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view);
650        if (mSuggestionsView != null)
651            mSuggestionsView.setListener(this, view);
652        if (LatinImeLogger.sVISUALDEBUG) {
653            mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
654        }
655    }
656
657    @Override
658    public void setCandidatesView(View view) {
659        // To ensure that CandidatesView will never be set.
660        return;
661    }
662
663    @Override
664    public void onStartInput(EditorInfo attribute, boolean restarting) {
665        mHandler.onStartInput(attribute, restarting);
666    }
667
668    @Override
669    public void onStartInputView(EditorInfo attribute, boolean restarting) {
670        mHandler.onStartInputView(attribute, restarting);
671    }
672
673    @Override
674    public void onFinishInputView(boolean finishingInput) {
675        mHandler.onFinishInputView(finishingInput);
676    }
677
678    @Override
679    public void onFinishInput() {
680        mHandler.onFinishInput();
681    }
682
683    private void onStartInputInternal(EditorInfo attribute, boolean restarting) {
684        super.onStartInput(attribute, restarting);
685    }
686
687    private void onStartInputViewInternal(EditorInfo attribute, boolean restarting) {
688        super.onStartInputView(attribute, restarting);
689        final KeyboardSwitcher switcher = mKeyboardSwitcher;
690        LatinKeyboardView inputView = switcher.getKeyboardView();
691
692        if (DEBUG) {
693            Log.d(TAG, "onStartInputView: attribute:" + ((attribute == null) ? "none"
694                    : String.format("inputType=0x%08x imeOptions=0x%08x",
695                            attribute.inputType, attribute.imeOptions)));
696        }
697        // In landscape mode, this method gets called without the input view being created.
698        if (inputView == null) {
699            return;
700        }
701
702        // Forward this event to the accessibility utilities, if enabled.
703        final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
704        if (accessUtils.isTouchExplorationEnabled()) {
705            accessUtils.onStartInputViewInternal(attribute, restarting);
706        }
707
708        mSubtypeSwitcher.updateParametersOnStartInputView();
709
710        TextEntryState.reset();
711
712        // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to
713        // know now whether this is a password text field, because we need to know now whether we
714        // want to enable the voice button.
715        final VoiceProxy voiceIme = mVoiceProxy;
716        final int inputType = (attribute != null) ? attribute.inputType : 0;
717        voiceIme.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(inputType)
718                || InputTypeCompatUtils.isVisiblePasswordInputType(inputType));
719
720        // The EditorInfo might have a flag that affects fullscreen mode.
721        // Note: This call should be done by InputMethodService?
722        updateFullscreenMode();
723        initializeInputAttributes(attribute);
724
725        inputView.closing();
726        mEnteredText = null;
727        mComposingStringBuilder.setLength(0);
728        mHasUncommittedTypedChars = false;
729        mDeleteCount = 0;
730        mJustAddedMagicSpace = false;
731        mJustReplacedDoubleSpace = false;
732
733        loadSettings();
734        updateCorrectionMode();
735        updateSuggestionVisibility(mPrefs, mResources);
736
737        if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
738            mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
739         }
740        mVoiceProxy.loadSettings(attribute, mPrefs);
741        // This will work only when the subtype is not supported.
742        LanguageSwitcherProxy.loadSettings();
743
744        if (mSubtypeSwitcher.isKeyboardMode()) {
745            switcher.loadKeyboard(attribute, mSettingsValues);
746        }
747
748        if (mSuggestionsView != null)
749            mSuggestionsView.clear();
750        setSuggestionStripShownInternal(
751                isSuggestionsStripVisible(), /* needsInputViewShown */ false);
752        // Delay updating suggestions because keyboard input view may not be shown at this point.
753        mHandler.postUpdateSuggestions();
754
755        inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
756                mSettingsValues.mKeyPreviewPopupDismissDelay);
757        inputView.setProximityCorrectionEnabled(true);
758
759        voiceIme.onStartInputView(inputView.getWindowToken());
760
761        if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
762    }
763
764    private void initializeInputAttributes(EditorInfo attribute) {
765        if (attribute == null)
766            return;
767        final int inputType = attribute.inputType;
768        if (inputType == InputType.TYPE_NULL) {
769            // TODO: We should honor TYPE_NULL specification.
770            Log.i(TAG, "InputType.TYPE_NULL is specified");
771        }
772        final int inputClass = inputType & InputType.TYPE_MASK_CLASS;
773        final int variation = inputType & InputType.TYPE_MASK_VARIATION;
774        if (inputClass == 0) {
775            Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x imeOptions=0x%08x",
776                    inputType, attribute.imeOptions));
777        }
778
779        mInsertSpaceOnPickSuggestionManually = false;
780        mInputTypeNoAutoCorrect = false;
781        mIsSettingsSuggestionStripOn = false;
782        mApplicationSpecifiedCompletionOn = false;
783        mApplicationSpecifiedCompletions = null;
784
785        if (inputClass == InputType.TYPE_CLASS_TEXT) {
786            mIsSettingsSuggestionStripOn = true;
787            // Make sure that passwords are not displayed in {@link SuggestionsView}.
788            if (InputTypeCompatUtils.isPasswordInputType(inputType)
789                    || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)) {
790                mIsSettingsSuggestionStripOn = false;
791            }
792            if (InputTypeCompatUtils.isEmailVariation(variation)
793                    || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) {
794                // The point in turning this off is that we don't want to insert a space after
795                // a name when filling a form: we can't delete trailing spaces when changing fields
796                mInsertSpaceOnPickSuggestionManually = false;
797            } else {
798                mInsertSpaceOnPickSuggestionManually = true;
799            }
800            if (InputTypeCompatUtils.isEmailVariation(variation)) {
801                mIsSettingsSuggestionStripOn = false;
802            } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
803                mIsSettingsSuggestionStripOn = false;
804            } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
805                mIsSettingsSuggestionStripOn = false;
806            } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) {
807                // If it's a browser edit field and auto correct is not ON explicitly, then
808                // disable auto correction, but keep suggestions on.
809                if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) {
810                    mInputTypeNoAutoCorrect = true;
811                }
812            }
813
814            // If NO_SUGGESTIONS is set, don't do prediction.
815            if ((inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
816                mIsSettingsSuggestionStripOn = false;
817                mInputTypeNoAutoCorrect = true;
818            }
819            // If it's not multiline and the autoCorrect flag is not set, then don't correct
820            if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0
821                    && (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
822                mInputTypeNoAutoCorrect = true;
823            }
824            if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
825                mIsSettingsSuggestionStripOn = false;
826                mApplicationSpecifiedCompletionOn = isFullscreenMode();
827            }
828        }
829    }
830
831    @Override
832    public void onWindowHidden() {
833        super.onWindowHidden();
834        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
835        if (inputView != null) inputView.closing();
836    }
837
838    private void onFinishInputInternal() {
839        super.onFinishInput();
840
841        LatinImeLogger.commit();
842
843        mVoiceProxy.flushVoiceInputLogs(mConfigurationChanging);
844
845        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
846        if (inputView != null) inputView.closing();
847        if (mUserUnigramDictionary != null) mUserUnigramDictionary.flushPendingWrites();
848        if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites();
849    }
850
851    private void onFinishInputViewInternal(boolean finishingInput) {
852        super.onFinishInputView(finishingInput);
853        mKeyboardSwitcher.onFinishInputView();
854        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
855        if (inputView != null) inputView.cancelAllMessages();
856        // Remove pending messages related to update suggestions
857        mHandler.cancelUpdateSuggestions();
858    }
859
860    @Override
861    public void onUpdateExtractedText(int token, ExtractedText text) {
862        super.onUpdateExtractedText(token, text);
863        mVoiceProxy.showPunctuationHintIfNecessary();
864    }
865
866    @Override
867    public void onUpdateSelection(int oldSelStart, int oldSelEnd,
868            int newSelStart, int newSelEnd,
869            int candidatesStart, int candidatesEnd) {
870        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
871                candidatesStart, candidatesEnd);
872
873        if (DEBUG) {
874            Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
875                    + ", ose=" + oldSelEnd
876                    + ", lss=" + mLastSelectionStart
877                    + ", lse=" + mLastSelectionEnd
878                    + ", nss=" + newSelStart
879                    + ", nse=" + newSelEnd
880                    + ", cs=" + candidatesStart
881                    + ", ce=" + candidatesEnd);
882        }
883
884        mVoiceProxy.setCursorAndSelection(newSelEnd, newSelStart);
885
886        // If the current selection in the text view changes, we should
887        // clear whatever candidate text we have.
888        final boolean selectionChanged = (newSelStart != candidatesEnd
889                || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart;
890        final boolean candidatesCleared = candidatesStart == -1 && candidatesEnd == -1;
891        if (!mExpectingUpdateSelection) {
892            if (((mComposingStringBuilder.length() > 0 && mHasUncommittedTypedChars)
893                    || mVoiceProxy.isVoiceInputHighlighted())
894                    && (selectionChanged || candidatesCleared)) {
895                mComposingStringBuilder.setLength(0);
896                mHasUncommittedTypedChars = false;
897                TextEntryState.reset();
898                updateSuggestions();
899                final InputConnection ic = getCurrentInputConnection();
900                if (ic != null) {
901                    ic.finishComposingText();
902                }
903                mComposingStateManager.onFinishComposingText();
904                mVoiceProxy.setVoiceInputHighlighted(false);
905            } else if (!mHasUncommittedTypedChars) {
906                TextEntryState.reset();
907                updateSuggestions();
908            }
909            mJustAddedMagicSpace = false; // The user moved the cursor.
910            mJustReplacedDoubleSpace = false;
911        }
912        mExpectingUpdateSelection = false;
913        mHandler.postUpdateShiftKeyState();
914
915        // Make a note of the cursor position
916        mLastSelectionStart = newSelStart;
917        mLastSelectionEnd = newSelEnd;
918    }
919
920    public void setLastSelection(int start, int end) {
921        mLastSelectionStart = start;
922        mLastSelectionEnd = end;
923    }
924
925    /**
926     * This is called when the user has clicked on the extracted text view,
927     * when running in fullscreen mode.  The default implementation hides
928     * the suggestions view when this happens, but only if the extracted text
929     * editor has a vertical scroll bar because its text doesn't fit.
930     * Here we override the behavior due to the possibility that a re-correction could
931     * cause the suggestions strip to disappear and re-appear.
932     */
933    @Override
934    public void onExtractedTextClicked() {
935        if (isSuggestionsRequested()) return;
936
937        super.onExtractedTextClicked();
938    }
939
940    /**
941     * This is called when the user has performed a cursor movement in the
942     * extracted text view, when it is running in fullscreen mode.  The default
943     * implementation hides the suggestions view when a vertical movement
944     * happens, but only if the extracted text editor has a vertical scroll bar
945     * because its text doesn't fit.
946     * Here we override the behavior due to the possibility that a re-correction could
947     * cause the suggestions strip to disappear and re-appear.
948     */
949    @Override
950    public void onExtractedCursorMovement(int dx, int dy) {
951        if (isSuggestionsRequested()) return;
952
953        super.onExtractedCursorMovement(dx, dy);
954    }
955
956    @Override
957    public void hideWindow() {
958        LatinImeLogger.commit();
959        mKeyboardSwitcher.onHideWindow();
960
961        if (TRACE) Debug.stopMethodTracing();
962        if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
963            mOptionsDialog.dismiss();
964            mOptionsDialog = null;
965        }
966        mVoiceProxy.hideVoiceWindow(mConfigurationChanging);
967        super.hideWindow();
968    }
969
970    @Override
971    public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
972        if (DEBUG) {
973            Log.i(TAG, "Received completions:");
974            if (applicationSpecifiedCompletions != null) {
975                for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
976                    Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
977                }
978            }
979        }
980        if (mApplicationSpecifiedCompletionOn) {
981            mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
982            if (applicationSpecifiedCompletions == null) {
983                clearSuggestions();
984                return;
985            }
986
987            SuggestedWords.Builder builder = new SuggestedWords.Builder()
988                    .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions)
989                    .setTypedWordValid(false)
990                    .setHasMinimalSuggestion(false);
991            // When in fullscreen mode, show completions generated by the application
992            setSuggestions(builder.build());
993            mBestWord = null;
994            setSuggestionStripShown(true);
995        }
996    }
997
998    private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
999        // TODO: Modify this if we support suggestions with hard keyboard
1000        if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
1001            final boolean shouldShowSuggestions = shown
1002                    && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true);
1003            if (isFullscreenMode()) {
1004                mSuggestionsContainer.setVisibility(
1005                        shouldShowSuggestions ? View.VISIBLE : View.GONE);
1006            } else {
1007                mSuggestionsContainer.setVisibility(
1008                        shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
1009            }
1010        }
1011    }
1012
1013    private void setSuggestionStripShown(boolean shown) {
1014        setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
1015    }
1016
1017    @Override
1018    public void onComputeInsets(InputMethodService.Insets outInsets) {
1019        super.onComputeInsets(outInsets);
1020        final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1021        if (inputView == null || mSuggestionsContainer == null)
1022            return;
1023        // In fullscreen mode, the height of the extract area managed by InputMethodService should
1024        // be considered.
1025        // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
1026        final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
1027        final int backingHeight = (mKeyPreviewBackingView.getVisibility() == View.GONE) ? 0
1028                : mKeyPreviewBackingView.getHeight();
1029        final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0
1030                : mSuggestionsContainer.getHeight();
1031        final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
1032        int touchY = extraHeight;
1033        // Need to set touchable region only if input view is being shown
1034        if (mKeyboardSwitcher.isInputViewShown()) {
1035            if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
1036                touchY -= suggestionsHeight;
1037            }
1038            final int touchWidth = inputView.getWidth();
1039            final int touchHeight = inputView.getHeight() + extraHeight
1040                    // Extend touchable region below the keyboard.
1041                    + EXTENDED_TOUCHABLE_REGION_HEIGHT;
1042            if (DEBUG) {
1043                Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth
1044                        + " height=" + touchHeight);
1045            }
1046            setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight);
1047        }
1048        outInsets.contentTopInsets = touchY;
1049        outInsets.visibleTopInsets = touchY;
1050    }
1051
1052    @Override
1053    public boolean onEvaluateFullscreenMode() {
1054        return super.onEvaluateFullscreenMode()
1055                && mResources.getBoolean(R.bool.config_use_fullscreen_mode);
1056    }
1057
1058    @Override
1059    public void updateFullscreenMode() {
1060        super.updateFullscreenMode();
1061
1062        if (mKeyPreviewBackingView == null) return;
1063        // In extract mode, no need to have extra space to show the key preview.
1064        // If not, we should have extra space above the keyboard to show the key preview.
1065        mKeyPreviewBackingView.setVisibility(isExtractViewShown() ? View.GONE : View.VISIBLE);
1066    }
1067
1068    @Override
1069    public boolean onKeyDown(int keyCode, KeyEvent event) {
1070        switch (keyCode) {
1071        case KeyEvent.KEYCODE_BACK:
1072            if (event.getRepeatCount() == 0) {
1073                if (mSuggestionsView != null && mSuggestionsView.handleBack()) {
1074                    return true;
1075                }
1076                final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
1077                if (keyboardView != null && keyboardView.handleBack()) {
1078                    return true;
1079                }
1080            }
1081            break;
1082        }
1083        return super.onKeyDown(keyCode, event);
1084    }
1085
1086    @Override
1087    public boolean onKeyUp(int keyCode, KeyEvent event) {
1088        switch (keyCode) {
1089        case KeyEvent.KEYCODE_DPAD_DOWN:
1090        case KeyEvent.KEYCODE_DPAD_UP:
1091        case KeyEvent.KEYCODE_DPAD_LEFT:
1092        case KeyEvent.KEYCODE_DPAD_RIGHT:
1093            // Enable shift key and DPAD to do selections
1094            if (mKeyboardSwitcher.isInputViewShown()
1095                    && mKeyboardSwitcher.isShiftedOrShiftLocked()) {
1096                KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
1097                        event.getAction(), event.getKeyCode(), event.getRepeatCount(),
1098                        event.getDeviceId(), event.getScanCode(),
1099                        KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
1100                final InputConnection ic = getCurrentInputConnection();
1101                if (ic != null)
1102                    ic.sendKeyEvent(newEvent);
1103                return true;
1104            }
1105            break;
1106        }
1107        return super.onKeyUp(keyCode, event);
1108    }
1109
1110    public void commitTyped(final InputConnection ic) {
1111        if (!mHasUncommittedTypedChars) return;
1112        mHasUncommittedTypedChars = false;
1113        if (mComposingStringBuilder.length() > 0) {
1114            if (ic != null) {
1115                ic.commitText(mComposingStringBuilder, 1);
1116            }
1117            mCommittedLength = mComposingStringBuilder.length();
1118            TextEntryState.acceptedTyped(mComposingStringBuilder);
1119            addToUserUnigramAndBigramDictionaries(mComposingStringBuilder,
1120                    UserUnigramDictionary.FREQUENCY_FOR_TYPED);
1121        }
1122        updateSuggestions();
1123    }
1124
1125    public boolean getCurrentAutoCapsState() {
1126        final InputConnection ic = getCurrentInputConnection();
1127        EditorInfo ei = getCurrentInputEditorInfo();
1128        if (mSettingsValues.mAutoCap && ic != null && ei != null
1129                && ei.inputType != InputType.TYPE_NULL) {
1130            return ic.getCursorCapsMode(ei.inputType) != 0;
1131        }
1132        return false;
1133    }
1134
1135    // "ic" may be null
1136    private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) {
1137        if (null == ic) return;
1138        CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
1139        // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
1140        if (lastTwo != null && lastTwo.length() == 2
1141                && lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
1142            ic.deleteSurroundingText(2, 0);
1143            ic.commitText(lastTwo.charAt(1) + " ", 1);
1144            mKeyboardSwitcher.updateShiftState();
1145        }
1146    }
1147
1148    private void maybeDoubleSpace() {
1149        if (mCorrectionMode == Suggest.CORRECTION_NONE) return;
1150        final InputConnection ic = getCurrentInputConnection();
1151        if (ic == null) return;
1152        final CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
1153        if (lastThree != null && lastThree.length() == 3
1154                && Utils.canBeFollowedByPeriod(lastThree.charAt(0))
1155                && lastThree.charAt(1) == Keyboard.CODE_SPACE
1156                && lastThree.charAt(2) == Keyboard.CODE_SPACE
1157                && mHandler.isAcceptingDoubleSpaces()) {
1158            mHandler.cancelDoubleSpacesTimer();
1159            ic.beginBatchEdit();
1160            ic.deleteSurroundingText(2, 0);
1161            ic.commitText(". ", 1);
1162            ic.endBatchEdit();
1163            mKeyboardSwitcher.updateShiftState();
1164            mJustReplacedDoubleSpace = true;
1165        } else {
1166            mHandler.startDoubleSpacesTimer();
1167        }
1168    }
1169
1170    // "ic" must not be null
1171    private void maybeRemovePreviousPeriod(final InputConnection ic, CharSequence text) {
1172        // When the text's first character is '.', remove the previous period
1173        // if there is one.
1174        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1175        if (lastOne != null && lastOne.length() == 1
1176                && lastOne.charAt(0) == Keyboard.CODE_PERIOD
1177                && text.charAt(0) == Keyboard.CODE_PERIOD) {
1178            ic.deleteSurroundingText(1, 0);
1179        }
1180    }
1181
1182    // "ic" may be null
1183    private void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) {
1184        if (ic == null) return;
1185        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1186        if (lastOne != null && lastOne.length() == 1
1187                && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
1188            ic.deleteSurroundingText(1, 0);
1189        }
1190    }
1191
1192    @Override
1193    public boolean addWordToDictionary(String word) {
1194        mUserDictionary.addWord(word, 128);
1195        // Suggestion strip should be updated after the operation of adding word to the
1196        // user dictionary
1197        mHandler.postUpdateSuggestions();
1198        return true;
1199    }
1200
1201    private boolean isAlphabet(int code) {
1202        if (Character.isLetter(code)) {
1203            return true;
1204        } else {
1205            return false;
1206        }
1207    }
1208
1209    private void onSettingsKeyPressed() {
1210        if (isShowingOptionDialog()) return;
1211        if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
1212            showSubtypeSelectorAndSettings();
1213        } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm, false /* exclude aux subtypes */)) {
1214            showOptionsMenu();
1215        } else {
1216            launchSettings();
1217        }
1218    }
1219
1220    // Virtual codes representing custom requests.  These are used in onCustomRequest() below.
1221    public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
1222
1223    @Override
1224    public boolean onCustomRequest(int requestCode) {
1225        if (isShowingOptionDialog()) return false;
1226        switch (requestCode) {
1227        case CODE_SHOW_INPUT_METHOD_PICKER:
1228            if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm, true /* include aux subtypes */)) {
1229                mImm.showInputMethodPicker();
1230                return true;
1231            }
1232            return false;
1233        }
1234        return false;
1235    }
1236
1237    private boolean isShowingOptionDialog() {
1238        return mOptionsDialog != null && mOptionsDialog.isShowing();
1239    }
1240
1241    // Implementation of {@link KeyboardActionListener}.
1242    @Override
1243    public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
1244        long when = SystemClock.uptimeMillis();
1245        if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
1246            mDeleteCount = 0;
1247        }
1248        mLastKeyTime = when;
1249        KeyboardSwitcher switcher = mKeyboardSwitcher;
1250        final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
1251        final boolean lastStateOfJustReplacedDoubleSpace = mJustReplacedDoubleSpace;
1252        mJustReplacedDoubleSpace = false;
1253        switch (primaryCode) {
1254        case Keyboard.CODE_DELETE:
1255            handleBackspace(lastStateOfJustReplacedDoubleSpace);
1256            mDeleteCount++;
1257            mExpectingUpdateSelection = true;
1258            LatinImeLogger.logOnDelete();
1259            break;
1260        case Keyboard.CODE_SHIFT:
1261            // Shift key is handled in onPress() when device has distinct multi-touch panel.
1262            if (!distinctMultiTouch)
1263                switcher.toggleShift();
1264            break;
1265        case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
1266            // Symbol key is handled in onPress() when device has distinct multi-touch panel.
1267            if (!distinctMultiTouch)
1268                switcher.changeKeyboardMode();
1269            break;
1270        case Keyboard.CODE_CANCEL:
1271            if (!isShowingOptionDialog()) {
1272                handleClose();
1273            }
1274            break;
1275        case Keyboard.CODE_SETTINGS:
1276            onSettingsKeyPressed();
1277            break;
1278        case Keyboard.CODE_CAPSLOCK:
1279            switcher.toggleCapsLock();
1280            //$FALL-THROUGH$
1281        case Keyboard.CODE_HAPTIC_AND_AUDIO_FEEDBACK_ONLY:
1282            // Dummy code for haptic and audio feedbacks.
1283            vibrate();
1284            playKeyClick(primaryCode);
1285            break;
1286        case Keyboard.CODE_SHORTCUT:
1287            mSubtypeSwitcher.switchToShortcutIME();
1288            break;
1289        case Keyboard.CODE_TAB:
1290            handleTab();
1291            // There are two cases for tab. Either we send a "next" event, that may change the
1292            // focus but will never move the cursor. Or, we send a real tab keycode, which some
1293            // applications may accept or ignore, and we don't know whether this will move the
1294            // cursor or not. So actually, we don't really know.
1295            // So to go with the safer option, we'd rather behave as if the user moved the
1296            // cursor when they didn't than the opposite. We also expect that most applications
1297            // will actually use tab only for focus movement.
1298            // To sum it up: do not update mExpectingUpdateSelection here.
1299            break;
1300        default:
1301            if (mSettingsValues.isWordSeparator(primaryCode)) {
1302                handleSeparator(primaryCode, x, y);
1303            } else {
1304                handleCharacter(primaryCode, keyCodes, x, y);
1305            }
1306            mExpectingUpdateSelection = true;
1307            break;
1308        }
1309        switcher.onKey(primaryCode);
1310        // Reset after any single keystroke
1311        mEnteredText = null;
1312    }
1313
1314    @Override
1315    public void onTextInput(CharSequence text) {
1316        mVoiceProxy.commitVoiceInput();
1317        final InputConnection ic = getCurrentInputConnection();
1318        if (ic == null) return;
1319        ic.beginBatchEdit();
1320        commitTyped(ic);
1321        maybeRemovePreviousPeriod(ic, text);
1322        ic.commitText(text, 1);
1323        ic.endBatchEdit();
1324        mKeyboardSwitcher.updateShiftState();
1325        mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY);
1326        mJustAddedMagicSpace = false;
1327        mEnteredText = text;
1328    }
1329
1330    @Override
1331    public void onCancelInput() {
1332        // User released a finger outside any key
1333        mKeyboardSwitcher.onCancelInput();
1334    }
1335
1336    private void handleBackspace(boolean justReplacedDoubleSpace) {
1337        if (mVoiceProxy.logAndRevertVoiceInput()) return;
1338
1339        final InputConnection ic = getCurrentInputConnection();
1340        if (ic == null) return;
1341        ic.beginBatchEdit();
1342
1343        mVoiceProxy.handleBackspace();
1344
1345        final boolean deleteChar = !mHasUncommittedTypedChars;
1346        if (mHasUncommittedTypedChars) {
1347            final int length = mComposingStringBuilder.length();
1348            if (length > 0) {
1349                mComposingStringBuilder.delete(length - 1, length);
1350                mWordComposer.deleteLast();
1351                final CharSequence textWithUnderline =
1352                        mComposingStateManager.isAutoCorrectionIndicatorOn()
1353                                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(
1354                                            this, mComposingStringBuilder)
1355                                : mComposingStringBuilder;
1356                ic.setComposingText(textWithUnderline, 1);
1357                if (mComposingStringBuilder.length() == 0) {
1358                    mHasUncommittedTypedChars = false;
1359                }
1360                if (1 == length) {
1361                    // 1 == length means we are about to erase the last character of the word,
1362                    // so we can show bigrams.
1363                    mHandler.postUpdateBigramPredictions();
1364                } else {
1365                    // length > 1, so we still have letters to deduce a suggestion from.
1366                    mHandler.postUpdateSuggestions();
1367                }
1368            } else {
1369                ic.deleteSurroundingText(1, 0);
1370            }
1371        }
1372        mHandler.postUpdateShiftKeyState();
1373
1374        TextEntryState.backspace();
1375        if (TextEntryState.isUndoCommit()) {
1376            revertLastWord(ic);
1377            ic.endBatchEdit();
1378            return;
1379        }
1380        if (justReplacedDoubleSpace) {
1381            if (revertDoubleSpace(ic)) {
1382                ic.endBatchEdit();
1383                return;
1384            }
1385        }
1386
1387        if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
1388            ic.deleteSurroundingText(mEnteredText.length(), 0);
1389        } else if (deleteChar) {
1390            if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
1391                // Go back to the suggestion mode if the user canceled the
1392                // "Touch again to save".
1393                // NOTE: In gerenal, we don't revert the word when backspacing
1394                // from a manual suggestion pick.  We deliberately chose a
1395                // different behavior only in the case of picking the first
1396                // suggestion (typed word).  It's intentional to have made this
1397                // inconsistent with backspacing after selecting other suggestions.
1398                revertLastWord(ic);
1399            } else {
1400                sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1401                if (mDeleteCount > DELETE_ACCELERATE_AT) {
1402                    sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1403                }
1404            }
1405        }
1406        ic.endBatchEdit();
1407    }
1408
1409    private void handleTab() {
1410        final int imeOptions = getCurrentInputEditorInfo().imeOptions;
1411        if (!EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
1412                && !EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)) {
1413            sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
1414            return;
1415        }
1416
1417        final InputConnection ic = getCurrentInputConnection();
1418        if (ic == null)
1419            return;
1420
1421        // True if keyboard is in either chording shift or manual temporary upper case mode.
1422        final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase();
1423        if (EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
1424                && !isManualTemporaryUpperCase) {
1425            EditorInfoCompatUtils.performEditorActionNext(ic);
1426        } else if (EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)
1427                && isManualTemporaryUpperCase) {
1428            EditorInfoCompatUtils.performEditorActionPrevious(ic);
1429        }
1430    }
1431
1432    private void handleCharacter(int primaryCode, int[] keyCodes, int x, int y) {
1433        mVoiceProxy.handleCharacter();
1434
1435        final InputConnection ic = getCurrentInputConnection();
1436        if (ic != null) ic.beginBatchEdit();
1437        if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceStripper(primaryCode)) {
1438            removeTrailingSpaceWhileInBatchEdit(ic);
1439        }
1440
1441        int code = primaryCode;
1442        if ((isAlphabet(code) || mSettingsValues.isSymbolExcludedFromWordSeparators(code))
1443                && isSuggestionsRequested() && !isCursorTouchingWord()) {
1444            if (!mHasUncommittedTypedChars) {
1445                mHasUncommittedTypedChars = true;
1446                mComposingStringBuilder.setLength(0);
1447                mWordComposer.reset();
1448                clearSuggestions();
1449                mComposingStateManager.onFinishComposingText();
1450            }
1451        }
1452        final KeyboardSwitcher switcher = mKeyboardSwitcher;
1453        if (switcher.isShiftedOrShiftLocked()) {
1454            if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
1455                    || keyCodes[0] > Character.MAX_CODE_POINT) {
1456                if (null != ic) ic.endBatchEdit();
1457                return;
1458            }
1459            code = keyCodes[0];
1460            if (switcher.isAlphabetMode() && Character.isLowerCase(code)) {
1461                // In some locales, such as Turkish, Character.toUpperCase() may return a wrong
1462                // character because it doesn't take care of locale.
1463                final String upperCaseString = new String(new int[] {code}, 0, 1)
1464                        .toUpperCase(mSubtypeSwitcher.getInputLocale());
1465                if (upperCaseString.codePointCount(0, upperCaseString.length()) == 1) {
1466                    code = upperCaseString.codePointAt(0);
1467                } else {
1468                    // Some keys, such as [eszett], have upper case as multi-characters.
1469                    onTextInput(upperCaseString);
1470                    if (null != ic) ic.endBatchEdit();
1471                    return;
1472                }
1473            }
1474        }
1475        if (mHasUncommittedTypedChars) {
1476            mComposingStringBuilder.append((char) code);
1477            mWordComposer.add(code, keyCodes, x, y);
1478            if (ic != null) {
1479                // If it's the first letter, make note of auto-caps state
1480                if (mWordComposer.size() == 1) {
1481                    mWordComposer.setAutoCapitalized(getCurrentAutoCapsState());
1482                    mComposingStateManager.onStartComposingText();
1483                }
1484                final CharSequence textWithUnderline =
1485                        mComposingStateManager.isAutoCorrectionIndicatorOn()
1486                                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(
1487                                        this, mComposingStringBuilder)
1488                                : mComposingStringBuilder;
1489                ic.setComposingText(textWithUnderline, 1);
1490            }
1491            mHandler.postUpdateSuggestions();
1492        } else {
1493            sendKeyChar((char)code);
1494        }
1495        if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
1496            if (null != ic) swapSwapperAndSpaceWhileInBatchEdit(ic);
1497        } else {
1498            mJustAddedMagicSpace = false;
1499        }
1500
1501        switcher.updateShiftState();
1502        if (LatinIME.PERF_DEBUG) measureCps();
1503        TextEntryState.typedCharacter((char) code, mSettingsValues.isWordSeparator(code), x, y);
1504        if (null != ic) ic.endBatchEdit();
1505    }
1506
1507    private void handleSeparator(int primaryCode, int x, int y) {
1508        mVoiceProxy.handleSeparator();
1509        mComposingStateManager.onFinishComposingText();
1510
1511        // Should dismiss the "Touch again to save" message when handling separator
1512        if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
1513            mHandler.cancelUpdateBigramPredictions();
1514            mHandler.postUpdateSuggestions();
1515        }
1516
1517        boolean pickedDefault = false;
1518        // Handle separator
1519        final InputConnection ic = getCurrentInputConnection();
1520        if (ic != null) {
1521            ic.beginBatchEdit();
1522        }
1523        if (mHasUncommittedTypedChars) {
1524            // In certain languages where single quote is a separator, it's better
1525            // not to auto correct, but accept the typed word. For instance,
1526            // in Italian dov' should not be expanded to dove' because the elision
1527            // requires the last vowel to be removed.
1528            final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
1529                    && !mInputTypeNoAutoCorrect;
1530            if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
1531                pickedDefault = pickDefaultSuggestion(primaryCode);
1532            } else {
1533                commitTyped(ic);
1534            }
1535        }
1536
1537        if (mJustAddedMagicSpace) {
1538            if (mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
1539                sendKeyChar((char)primaryCode);
1540                swapSwapperAndSpaceWhileInBatchEdit(ic);
1541            } else {
1542                if (mSettingsValues.isMagicSpaceStripper(primaryCode)) {
1543                    removeTrailingSpaceWhileInBatchEdit(ic);
1544                }
1545                sendKeyChar((char)primaryCode);
1546                mJustAddedMagicSpace = false;
1547            }
1548        } else {
1549            sendKeyChar((char)primaryCode);
1550        }
1551
1552        if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) {
1553            maybeDoubleSpace();
1554        }
1555
1556        TextEntryState.typedCharacter((char) primaryCode, true, x, y);
1557
1558        if (pickedDefault) {
1559            CharSequence typedWord = mWordComposer.getTypedWord();
1560            TextEntryState.backToAcceptedDefault(typedWord);
1561            if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) {
1562                InputConnectionCompatUtils.commitCorrection(
1563                        ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord);
1564            }
1565        }
1566        if (Keyboard.CODE_SPACE == primaryCode) {
1567            if (!isCursorTouchingWord()) {
1568                mHandler.cancelUpdateSuggestions();
1569                mHandler.postUpdateBigramPredictions();
1570            }
1571        } else {
1572            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
1573            // already displayed or not, so it's okay.
1574            setPunctuationSuggestions();
1575        }
1576        mKeyboardSwitcher.updateShiftState();
1577        if (ic != null) {
1578            ic.endBatchEdit();
1579        }
1580    }
1581
1582    private void handleClose() {
1583        commitTyped(getCurrentInputConnection());
1584        mVoiceProxy.handleClose();
1585        requestHideSelf(0);
1586        LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1587        if (inputView != null)
1588            inputView.closing();
1589    }
1590
1591    public boolean isSuggestionsRequested() {
1592        return mIsSettingsSuggestionStripOn
1593                && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
1594    }
1595
1596    public boolean isShowingPunctuationList() {
1597        return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
1598    }
1599
1600    public boolean isShowingSuggestionsStrip() {
1601        return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
1602                || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
1603                        && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
1604    }
1605
1606    public boolean isSuggestionsStripVisible() {
1607        if (mSuggestionsView == null)
1608            return false;
1609        if (mSuggestionsView.isShowingAddToDictionaryHint() || TextEntryState.isRecorrecting())
1610            return true;
1611        if (!isShowingSuggestionsStrip())
1612            return false;
1613        if (mApplicationSpecifiedCompletionOn)
1614            return true;
1615        return isSuggestionsRequested();
1616    }
1617
1618    public void switchToKeyboardView() {
1619        if (DEBUG) {
1620            Log.d(TAG, "Switch to keyboard view.");
1621        }
1622        View v = mKeyboardSwitcher.getKeyboardView();
1623        if (v != null) {
1624            // Confirms that the keyboard view doesn't have parent view.
1625            ViewParent p = v.getParent();
1626            if (p != null && p instanceof ViewGroup) {
1627                ((ViewGroup) p).removeView(v);
1628            }
1629            setInputView(v);
1630        }
1631        setSuggestionStripShown(isSuggestionsStripVisible());
1632        updateInputViewShown();
1633        mHandler.postUpdateSuggestions();
1634    }
1635
1636    public void clearSuggestions() {
1637        setSuggestions(SuggestedWords.EMPTY);
1638    }
1639
1640    public void setSuggestions(SuggestedWords words) {
1641        if (mSuggestionsView != null) {
1642            mSuggestionsView.setSuggestions(words);
1643            mKeyboardSwitcher.onAutoCorrectionStateChanged(
1644                    words.hasWordAboveAutoCorrectionScoreThreshold());
1645        }
1646
1647        // Put a blue underline to a word in TextView which will be auto-corrected.
1648        final InputConnection ic = getCurrentInputConnection();
1649        if (ic != null) {
1650            final boolean oldAutoCorrectionIndicator =
1651                    mComposingStateManager.isAutoCorrectionIndicatorOn();
1652            final boolean newAutoCorrectionIndicator = Utils.willAutoCorrect(words);
1653            if (oldAutoCorrectionIndicator != newAutoCorrectionIndicator) {
1654                if (LatinImeLogger.sDBG) {
1655                    Log.d(TAG, "Flip the indicator. " + oldAutoCorrectionIndicator
1656                            + " -> " + newAutoCorrectionIndicator);
1657                }
1658                final CharSequence textWithUnderline = newAutoCorrectionIndicator
1659                        ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(
1660                                this, mComposingStringBuilder)
1661                        : mComposingStringBuilder;
1662                if (!TextUtils.isEmpty(textWithUnderline)) {
1663                    ic.setComposingText(textWithUnderline, 1);
1664                }
1665                mComposingStateManager.setAutoCorrectionIndicatorOn(newAutoCorrectionIndicator);
1666            }
1667        }
1668    }
1669
1670    public void updateSuggestions() {
1671        // Check if we have a suggestion engine attached.
1672        if ((mSuggest == null || !isSuggestionsRequested())
1673                && !mVoiceProxy.isVoiceInputHighlighted()) {
1674            return;
1675        }
1676
1677        mHandler.cancelUpdateSuggestions();
1678        mHandler.cancelUpdateBigramPredictions();
1679
1680        if (!mHasUncommittedTypedChars) {
1681            setPunctuationSuggestions();
1682            return;
1683        }
1684
1685        final WordComposer wordComposer = mWordComposer;
1686        // TODO: May need a better way of retrieving previous word
1687        final InputConnection ic = getCurrentInputConnection();
1688        final CharSequence prevWord;
1689        if (null == ic) {
1690            prevWord = null;
1691        } else {
1692            prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
1693        }
1694        // getSuggestedWordBuilder handles gracefully a null value of prevWord
1695        final SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
1696                wordComposer, prevWord, mKeyboardSwitcher.getLatinKeyboard().getProximityInfo());
1697
1698        boolean autoCorrectionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection();
1699        final CharSequence typedWord = wordComposer.getTypedWord();
1700        // Here, we want to promote a whitelisted word if exists.
1701        // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid"
1702        // but still autocorrected from - in the case the whitelist only capitalizes the word.
1703        // The whitelist should be case-insensitive, so it's not possible to be consistent with
1704        // a boolean flag. Right now this is handled with a slight hack in
1705        // WhitelistDictionary#shouldForciblyAutoCorrectFrom.
1706        final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected(
1707                mSuggest.getUnigramDictionaries(), typedWord, preferCapitalization());
1708        if (mCorrectionMode == Suggest.CORRECTION_FULL
1709                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
1710            autoCorrectionAvailable |= (!allowsToBeAutoCorrected);
1711        }
1712        // Don't auto-correct words with multiple capital letter
1713        autoCorrectionAvailable &= !wordComposer.isMostlyCaps();
1714        autoCorrectionAvailable &= !TextEntryState.isRecorrecting();
1715
1716        // Basically, we update the suggestion strip only when suggestion count > 1.  However,
1717        // there is an exception: We update the suggestion strip whenever typed word's length
1718        // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
1719        // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
1720        // need to clear the previous state when the user starts typing a word (i.e. typed word's
1721        // length == 1).
1722        if (typedWord != null) {
1723            if (builder.size() > 1 || typedWord.length() == 1 || (!allowsToBeAutoCorrected)
1724                    || mSuggestionsView.isShowingAddToDictionaryHint()) {
1725                builder.setTypedWordValid(!allowsToBeAutoCorrected).setHasMinimalSuggestion(
1726                        autoCorrectionAvailable);
1727            } else {
1728                SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
1729                if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
1730                    if (builder.size() == 0) {
1731                        return;
1732                    }
1733                    previousSuggestions = SuggestedWords.EMPTY;
1734                }
1735                builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions);
1736            }
1737        }
1738        showSuggestions(builder.build(), typedWord);
1739    }
1740
1741    public void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) {
1742        final boolean shouldBlockAutoCorrectionBySafetyNet =
1743                Utils.shouldBlockAutoCorrectionBySafetyNet(suggestedWords, mSuggest);
1744        if (shouldBlockAutoCorrectionBySafetyNet) {
1745            suggestedWords.setShouldBlockAutoCorrection();
1746        }
1747        setSuggestions(suggestedWords);
1748        if (suggestedWords.size() > 0) {
1749            if (shouldBlockAutoCorrectionBySafetyNet) {
1750                mBestWord = typedWord;
1751            } else if (suggestedWords.hasAutoCorrectionWord()) {
1752                mBestWord = suggestedWords.getWord(1);
1753            } else {
1754                mBestWord = typedWord;
1755            }
1756        } else {
1757            mBestWord = null;
1758        }
1759        setSuggestionStripShown(isSuggestionsStripVisible());
1760    }
1761
1762    private boolean pickDefaultSuggestion(int separatorCode) {
1763        // Complete any pending suggestions query first
1764        if (mHandler.hasPendingUpdateSuggestions()) {
1765            mHandler.cancelUpdateSuggestions();
1766            updateSuggestions();
1767        }
1768        if (mBestWord != null && mBestWord.length() > 0) {
1769            TextEntryState.acceptedDefault(mWordComposer.getTypedWord(), mBestWord, separatorCode);
1770            mExpectingUpdateSelection = true;
1771            commitBestWord(mBestWord);
1772            // Add the word to the user unigram dictionary if it's not a known word
1773            addToUserUnigramAndBigramDictionaries(mBestWord,
1774                    UserUnigramDictionary.FREQUENCY_FOR_TYPED);
1775            return true;
1776        }
1777        return false;
1778    }
1779
1780    @Override
1781    public void pickSuggestionManually(int index, CharSequence suggestion) {
1782        mComposingStateManager.onFinishComposingText();
1783        SuggestedWords suggestions = mSuggestionsView.getSuggestions();
1784        mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion,
1785                mSettingsValues.mWordSeparators);
1786
1787        final boolean recorrecting = TextEntryState.isRecorrecting();
1788        final InputConnection ic = getCurrentInputConnection();
1789        if (ic != null) {
1790            ic.beginBatchEdit();
1791        }
1792        if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null
1793                && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
1794            if (ic != null) {
1795                final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
1796                ic.commitCompletion(completionInfo);
1797            }
1798            mCommittedLength = suggestion.length();
1799            if (mSuggestionsView != null) {
1800                mSuggestionsView.clear();
1801            }
1802            mKeyboardSwitcher.updateShiftState();
1803            if (ic != null) {
1804                ic.endBatchEdit();
1805            }
1806            return;
1807        }
1808
1809        // If this is a punctuation, apply it through the normal key press
1810        if (suggestion.length() == 1 && (mSettingsValues.isWordSeparator(suggestion.charAt(0))
1811                || mSettingsValues.isSuggestedPunctuation(suggestion.charAt(0)))) {
1812            // Word separators are suggested before the user inputs something.
1813            // So, LatinImeLogger logs "" as a user's input.
1814            LatinImeLogger.logOnManualSuggestion(
1815                    "", suggestion.toString(), index, suggestions.mWords);
1816            // Find out whether the previous character is a space. If it is, as a special case
1817            // for punctuation entered through the suggestion strip, it should be considered
1818            // a magic space even if it was a normal space. This is meant to help in case the user
1819            // pressed space on purpose of displaying the suggestion strip punctuation.
1820            final int rawPrimaryCode = suggestion.charAt(0);
1821            // Maybe apply the "bidi mirrored" conversions for parentheses
1822            final LatinKeyboard keyboard = mKeyboardSwitcher.getLatinKeyboard();
1823            final boolean isRtl = keyboard != null && keyboard.mIsRtlKeyboard;
1824            final int primaryCode = Key.getRtlParenthesisCode(rawPrimaryCode, isRtl);
1825
1826            final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : "";
1827            final int toLeft = (ic == null || TextUtils.isEmpty(beforeText))
1828                    ? 0 : beforeText.charAt(0);
1829            final boolean oldMagicSpace = mJustAddedMagicSpace;
1830            if (Keyboard.CODE_SPACE == toLeft) mJustAddedMagicSpace = true;
1831            onCodeInput(primaryCode, new int[] { primaryCode },
1832                    KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
1833                    KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
1834            mJustAddedMagicSpace = oldMagicSpace;
1835            if (ic != null) {
1836                ic.endBatchEdit();
1837            }
1838            return;
1839        }
1840        if (!mHasUncommittedTypedChars) {
1841            // If we are not composing a word, then it was a suggestion inferred from
1842            // context - no user input. We should reset the word composer.
1843            mWordComposer.reset();
1844        }
1845        mExpectingUpdateSelection = true;
1846        commitBestWord(suggestion);
1847        // Add the word to the auto dictionary if it's not a known word
1848        if (index == 0) {
1849            addToUserUnigramAndBigramDictionaries(suggestion,
1850                    UserUnigramDictionary.FREQUENCY_FOR_PICKED);
1851        } else {
1852            addToOnlyBigramDictionary(suggestion, 1);
1853        }
1854        LatinImeLogger.logOnManualSuggestion(mComposingStringBuilder.toString(),
1855                suggestion.toString(), index, suggestions.mWords);
1856        TextEntryState.acceptedSuggestion(mComposingStringBuilder.toString(), suggestion);
1857        // Follow it with a space
1858        if (mInsertSpaceOnPickSuggestionManually && !recorrecting) {
1859            sendMagicSpace();
1860        }
1861
1862        // We should show the "Touch again to save" hint if the user pressed the first entry
1863        // AND either:
1864        // - There is no dictionary (we know that because we tried to load it => null != mSuggest
1865        //   AND mSuggest.hasMainDictionary() is false)
1866        // - There is a dictionary and the word is not in it
1867        // Please note that if mSuggest is null, it means that everything is off: suggestion
1868        // and correction, so we shouldn't try to show the hint
1869        // We used to look at mCorrectionMode here, but showing the hint should have nothing
1870        // to do with the autocorrection setting.
1871        final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
1872                // If there is no dictionary the hint should be shown.
1873                && (!mSuggest.hasMainDictionary()
1874                        // If "suggestion" is not in the dictionary, the hint should be shown.
1875                        || !AutoCorrection.isValidWord(
1876                                mSuggest.getUnigramDictionaries(), suggestion, true));
1877
1878        if (!recorrecting) {
1879            // Fool the state watcher so that a subsequent backspace will not do a revert, unless
1880            // we just did a correction, in which case we need to stay in
1881            // TextEntryState.State.PICKED_SUGGESTION state.
1882            TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true,
1883                    WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
1884        }
1885        if (!showingAddToDictionaryHint) {
1886            // If we're not showing the "Touch again to save", then show corrections again.
1887            // In case the cursor position doesn't change, make sure we show the suggestions again.
1888            updateBigramPredictions();
1889            // Updating the predictions right away may be slow and feel unresponsive on slower
1890            // terminals. On the other hand if we just postUpdateBigramPredictions() it will
1891            // take a noticeable delay to update them which may feel uneasy.
1892        }
1893        if (showingAddToDictionaryHint) {
1894            if (mIsUserDictionaryAvaliable) {
1895                mSuggestionsView.showAddToDictionaryHint(suggestion);
1896            } else {
1897                mHandler.postUpdateSuggestions();
1898            }
1899        }
1900        if (ic != null) {
1901            ic.endBatchEdit();
1902        }
1903    }
1904
1905    /**
1906     * Commits the chosen word to the text field and saves it for later retrieval.
1907     */
1908    private void commitBestWord(CharSequence bestWord) {
1909        final KeyboardSwitcher switcher = mKeyboardSwitcher;
1910        if (!switcher.isKeyboardAvailable())
1911            return;
1912        final InputConnection ic = getCurrentInputConnection();
1913        if (ic != null) {
1914            mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators);
1915            if (mSettingsValues.mEnableSuggestionSpanInsertion) {
1916                final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
1917                ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
1918                        this, bestWord, suggestedWords), 1);
1919            } else {
1920                ic.commitText(bestWord, 1);
1921            }
1922        }
1923        mHasUncommittedTypedChars = false;
1924        mCommittedLength = bestWord.length();
1925    }
1926
1927    private static final WordComposer sEmptyWordComposer = new WordComposer();
1928    public void updateBigramPredictions() {
1929        if (mSuggest == null || !isSuggestionsRequested())
1930            return;
1931
1932        if (!mSettingsValues.mBigramPredictionEnabled) {
1933            setPunctuationSuggestions();
1934            return;
1935        }
1936
1937        final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
1938                mSettingsValues.mWordSeparators);
1939        SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(sEmptyWordComposer,
1940                prevWord, mKeyboardSwitcher.getLatinKeyboard().getProximityInfo());
1941
1942        if (builder.size() > 0) {
1943            // Explicitly supply an empty typed word (the no-second-arg version of
1944            // showSuggestions will retrieve the word near the cursor, we don't want that here)
1945            showSuggestions(builder.build(), "");
1946        } else {
1947            if (!isShowingPunctuationList()) setPunctuationSuggestions();
1948        }
1949    }
1950
1951    public void setPunctuationSuggestions() {
1952        setSuggestions(mSettingsValues.mSuggestPuncList);
1953        setSuggestionStripShown(isSuggestionsStripVisible());
1954    }
1955
1956    private void addToUserUnigramAndBigramDictionaries(CharSequence suggestion,
1957            int frequencyDelta) {
1958        checkAddToDictionary(suggestion, frequencyDelta, false);
1959    }
1960
1961    private void addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta) {
1962        checkAddToDictionary(suggestion, frequencyDelta, true);
1963    }
1964
1965    /**
1966     * Adds to the UserBigramDictionary and/or UserUnigramDictionary
1967     * @param selectedANotTypedWord true if it should be added to bigram dictionary if possible
1968     */
1969    private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta,
1970            boolean selectedANotTypedWord) {
1971        if (suggestion == null || suggestion.length() < 1) return;
1972
1973        // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
1974        // adding words in situations where the user or application really didn't
1975        // want corrections enabled or learned.
1976        if (!(mCorrectionMode == Suggest.CORRECTION_FULL
1977                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
1978            return;
1979        }
1980
1981        if (null != mSuggest && null != mUserUnigramDictionary) {
1982            final boolean selectedATypedWordAndItsInUserUnigramDic =
1983                    !selectedANotTypedWord && mUserUnigramDictionary.isValidWord(suggestion);
1984            final boolean isValidWord = AutoCorrection.isValidWord(
1985                    mSuggest.getUnigramDictionaries(), suggestion, true);
1986            final boolean needsToAddToUserUnigramDictionary =
1987                    selectedATypedWordAndItsInUserUnigramDic || !isValidWord;
1988            if (needsToAddToUserUnigramDictionary) {
1989                mUserUnigramDictionary.addWord(suggestion.toString(), frequencyDelta);
1990            }
1991        }
1992
1993        if (mUserBigramDictionary != null) {
1994            // We don't want to register as bigrams words separated by a separator.
1995            // For example "I will, and you too" : we don't want the pair ("will" "and") to be
1996            // a bigram.
1997            final InputConnection ic = getCurrentInputConnection();
1998            if (null != ic) {
1999                final CharSequence prevWord =
2000                        EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
2001                if (!TextUtils.isEmpty(prevWord)) {
2002                    mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString());
2003                }
2004            }
2005        }
2006    }
2007
2008    public boolean isCursorTouchingWord() {
2009        final InputConnection ic = getCurrentInputConnection();
2010        if (ic == null) return false;
2011        CharSequence toLeft = ic.getTextBeforeCursor(1, 0);
2012        CharSequence toRight = ic.getTextAfterCursor(1, 0);
2013        if (!TextUtils.isEmpty(toLeft)
2014                && !mSettingsValues.isWordSeparator(toLeft.charAt(0))
2015                && !mSettingsValues.isSuggestedPunctuation(toLeft.charAt(0))) {
2016            return true;
2017        }
2018        if (!TextUtils.isEmpty(toRight)
2019                && !mSettingsValues.isWordSeparator(toRight.charAt(0))
2020                && !mSettingsValues.isSuggestedPunctuation(toRight.charAt(0))) {
2021            return true;
2022        }
2023        return false;
2024    }
2025
2026    // "ic" must not null
2027    private boolean sameAsTextBeforeCursor(final InputConnection ic, CharSequence text) {
2028        CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
2029        return TextUtils.equals(text, beforeText);
2030    }
2031
2032    // "ic" must not null
2033    private void revertLastWord(final InputConnection ic) {
2034        if (mHasUncommittedTypedChars || mComposingStringBuilder.length() <= 0) {
2035            sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
2036            return;
2037        }
2038
2039        final CharSequence separator = ic.getTextBeforeCursor(1, 0);
2040        ic.deleteSurroundingText(1, 0);
2041        final CharSequence textToTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0);
2042        ic.deleteSurroundingText(mCommittedLength, 0);
2043
2044        // Re-insert "separator" only when the deleted character was word separator and the
2045        // composing text wasn't equal to the auto-corrected text which can be found before
2046        // the cursor.
2047        if (!TextUtils.isEmpty(separator)
2048                && mSettingsValues.isWordSeparator(separator.charAt(0))
2049                && !TextUtils.equals(mComposingStringBuilder, textToTheLeft)) {
2050            ic.commitText(mComposingStringBuilder, 1);
2051            TextEntryState.acceptedTyped(mComposingStringBuilder);
2052            ic.commitText(separator, 1);
2053            TextEntryState.typedCharacter(separator.charAt(0), true,
2054                    WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
2055            // Clear composing text
2056            mComposingStringBuilder.setLength(0);
2057        } else {
2058            mHasUncommittedTypedChars = true;
2059            ic.setComposingText(mComposingStringBuilder, 1);
2060            TextEntryState.backspace();
2061        }
2062        mHandler.cancelUpdateBigramPredictions();
2063        mHandler.postUpdateSuggestions();
2064    }
2065
2066    // "ic" must not null
2067    private boolean revertDoubleSpace(final InputConnection ic) {
2068        mHandler.cancelDoubleSpacesTimer();
2069        // Here we test whether we indeed have a period and a space before us. This should not
2070        // be needed, but it's there just in case something went wrong.
2071        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
2072        if (!". ".equals(textBeforeCursor))
2073            return false;
2074        ic.beginBatchEdit();
2075        ic.deleteSurroundingText(2, 0);
2076        ic.commitText("  ", 1);
2077        ic.endBatchEdit();
2078        return true;
2079    }
2080
2081    public boolean isWordSeparator(int code) {
2082        return mSettingsValues.isWordSeparator(code);
2083    }
2084
2085    private void sendMagicSpace() {
2086        sendKeyChar((char)Keyboard.CODE_SPACE);
2087        mJustAddedMagicSpace = true;
2088        mKeyboardSwitcher.updateShiftState();
2089    }
2090
2091    public boolean preferCapitalization() {
2092        return mWordComposer.isFirstCharCapitalized();
2093    }
2094
2095    // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
2096    // according to new language or mode.
2097    public void onRefreshKeyboard() {
2098        if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
2099            // Before Honeycomb, Voice IME is in LatinIME and it changes the current input view,
2100            // so that we need to re-create the keyboard input view here.
2101            setInputView(mKeyboardSwitcher.onCreateInputView());
2102        }
2103        // Reload keyboard because the current language has been changed.
2104        mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
2105        initSuggest();
2106        loadSettings();
2107    }
2108
2109    @Override
2110    public void onPress(int primaryCode, boolean withSliding) {
2111        final KeyboardSwitcher switcher = mKeyboardSwitcher;
2112        if (switcher.isVibrateAndSoundFeedbackRequired()) {
2113            vibrate();
2114            playKeyClick(primaryCode);
2115        }
2116        final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
2117        if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
2118            switcher.onPressShift(withSliding);
2119        } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
2120            switcher.onPressSymbol();
2121        } else {
2122            switcher.onOtherKeyPressed();
2123        }
2124    }
2125
2126    @Override
2127    public void onRelease(int primaryCode, boolean withSliding) {
2128        KeyboardSwitcher switcher = mKeyboardSwitcher;
2129        // Reset any drag flags in the keyboard
2130        final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
2131        if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
2132            switcher.onReleaseShift(withSliding);
2133        } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
2134            switcher.onReleaseSymbol();
2135        }
2136    }
2137
2138
2139    // receive ringer mode change and network state change.
2140    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2141        @Override
2142        public void onReceive(Context context, Intent intent) {
2143            final String action = intent.getAction();
2144            if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2145                updateRingerMode();
2146            } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2147                mSubtypeSwitcher.onNetworkStateChanged(intent);
2148            }
2149        }
2150    };
2151
2152    // update keypress sound volume
2153    private void updateSoundEffectVolume() {
2154        mFxVolume = Utils.getCurrentKeypressSoundVolume(mPrefs, mResources);
2155    }
2156
2157    // update flags for silent mode
2158    private void updateRingerMode() {
2159        if (mAudioManager == null) {
2160            mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
2161            if (mAudioManager == null) return;
2162        }
2163        mSilentModeOn = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
2164    }
2165
2166    private void updateKeypressVibrationDuration() {
2167        mKeypressVibrationDuration = Utils.getCurrentVibrationDuration(mPrefs, mResources);
2168    }
2169
2170    private void playKeyClick(int primaryCode) {
2171        // if mAudioManager is null, we don't have the ringer state yet
2172        // mAudioManager will be set by updateRingerMode
2173        if (mAudioManager == null) {
2174            if (mKeyboardSwitcher.getKeyboardView() != null) {
2175                updateRingerMode();
2176            }
2177        }
2178        if (isSoundOn()) {
2179            final int sound;
2180            switch (primaryCode) {
2181            case Keyboard.CODE_DELETE:
2182                sound = AudioManager.FX_KEYPRESS_DELETE;
2183                break;
2184            case Keyboard.CODE_ENTER:
2185                sound = AudioManager.FX_KEYPRESS_RETURN;
2186                break;
2187            case Keyboard.CODE_SPACE:
2188                sound = AudioManager.FX_KEYPRESS_SPACEBAR;
2189                break;
2190            default:
2191                sound = AudioManager.FX_KEYPRESS_STANDARD;
2192                break;
2193            }
2194            mAudioManager.playSoundEffect(sound, mFxVolume);
2195        }
2196    }
2197
2198    public void vibrate() {
2199        if (!mSettingsValues.mVibrateOn) {
2200            return;
2201        }
2202        if (mKeypressVibrationDuration < 0) {
2203            // Go ahead with the system default
2204            LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
2205            if (inputView != null) {
2206                inputView.performHapticFeedback(
2207                        HapticFeedbackConstants.KEYBOARD_TAP,
2208                        HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
2209            }
2210        } else if (mVibrator != null) {
2211            mVibrator.vibrate(mKeypressVibrationDuration);
2212        }
2213    }
2214
2215    public WordComposer getCurrentWord() {
2216        return mWordComposer;
2217    }
2218
2219    boolean isSoundOn() {
2220        return mSettingsValues.mSoundOn && !mSilentModeOn;
2221    }
2222
2223    private void updateCorrectionMode() {
2224        // TODO: cleanup messy flags
2225        final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
2226                && !mInputTypeNoAutoCorrect;
2227        mCorrectionMode = (shouldAutoCorrect && mSettingsValues.mAutoCorrectEnabled)
2228                ? Suggest.CORRECTION_FULL
2229                : (shouldAutoCorrect ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE);
2230        mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect
2231                && mSettingsValues.mAutoCorrectEnabled)
2232                ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
2233        if (mSuggest != null) {
2234            mSuggest.setCorrectionMode(mCorrectionMode);
2235        }
2236    }
2237
2238    private void updateSuggestionVisibility(final SharedPreferences prefs, final Resources res) {
2239        final String suggestionVisiblityStr = prefs.getString(
2240                Settings.PREF_SHOW_SUGGESTIONS_SETTING,
2241                res.getString(R.string.prefs_suggestion_visibility_default_value));
2242        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
2243            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
2244                mSuggestionVisibility = visibility;
2245                break;
2246            }
2247        }
2248    }
2249
2250    protected void launchSettings() {
2251        launchSettingsClass(Settings.class);
2252    }
2253
2254    public void launchDebugSettings() {
2255        launchSettingsClass(DebugSettings.class);
2256    }
2257
2258    protected void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
2259        handleClose();
2260        Intent intent = new Intent();
2261        intent.setClass(LatinIME.this, settingsClass);
2262        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2263        startActivity(intent);
2264    }
2265
2266    private void showSubtypeSelectorAndSettings() {
2267        final CharSequence title = getString(R.string.english_ime_input_options);
2268        final CharSequence[] items = new CharSequence[] {
2269                // TODO: Should use new string "Select active input modes".
2270                getString(R.string.language_selection_title),
2271                getString(R.string.english_ime_settings),
2272        };
2273        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2274            @Override
2275            public void onClick(DialogInterface di, int position) {
2276                di.dismiss();
2277                switch (position) {
2278                case 0:
2279                    Intent intent = CompatUtils.getInputLanguageSelectionIntent(
2280                            mInputMethodId, Intent.FLAG_ACTIVITY_NEW_TASK
2281                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2282                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2283                    startActivity(intent);
2284                    break;
2285                case 1:
2286                    launchSettings();
2287                    break;
2288                }
2289            }
2290        };
2291        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2292                .setItems(items, listener)
2293                .setTitle(title);
2294        showOptionDialogInternal(builder.create());
2295    }
2296
2297    private void showOptionsMenu() {
2298        final CharSequence title = getString(R.string.english_ime_input_options);
2299        final CharSequence[] items = new CharSequence[] {
2300                getString(R.string.selectInputMethod),
2301                getString(R.string.english_ime_settings),
2302        };
2303        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2304            @Override
2305            public void onClick(DialogInterface di, int position) {
2306                di.dismiss();
2307                switch (position) {
2308                case 0:
2309                    mImm.showInputMethodPicker();
2310                    break;
2311                case 1:
2312                    launchSettings();
2313                    break;
2314                }
2315            }
2316        };
2317        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2318                .setItems(items, listener)
2319                .setTitle(title);
2320        showOptionDialogInternal(builder.create());
2321    }
2322
2323    @Override
2324    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
2325        super.dump(fd, fout, args);
2326
2327        final Printer p = new PrintWriterPrinter(fout);
2328        p.println("LatinIME state :");
2329        p.println("  Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode());
2330        p.println("  mComposingStringBuilder=" + mComposingStringBuilder.toString());
2331        p.println("  mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn);
2332        p.println("  mCorrectionMode=" + mCorrectionMode);
2333        p.println("  mHasUncommittedTypedChars=" + mHasUncommittedTypedChars);
2334        p.println("  mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
2335        p.println("  mInsertSpaceOnPickSuggestionManually=" + mInsertSpaceOnPickSuggestionManually);
2336        p.println("  mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn);
2337        p.println("  TextEntryState.state=" + TextEntryState.getState());
2338        p.println("  mSoundOn=" + mSettingsValues.mSoundOn);
2339        p.println("  mVibrateOn=" + mSettingsValues.mVibrateOn);
2340        p.println("  mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
2341    }
2342
2343    // Characters per second measurement
2344
2345    private long mLastCpsTime;
2346    private static final int CPS_BUFFER_SIZE = 16;
2347    private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE];
2348    private int mCpsIndex;
2349
2350    private void measureCps() {
2351        long now = System.currentTimeMillis();
2352        if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial
2353        mCpsIntervals[mCpsIndex] = now - mLastCpsTime;
2354        mLastCpsTime = now;
2355        mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE;
2356        long total = 0;
2357        for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i];
2358        System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total));
2359    }
2360}
2361