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