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