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