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