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