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