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 static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
     20 import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
     21 import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
     22 
     23 import android.app.AlertDialog;
     24 import android.content.BroadcastReceiver;
     25 import android.content.Context;
     26 import android.content.DialogInterface;
     27 import android.content.Intent;
     28 import android.content.IntentFilter;
     29 import android.content.SharedPreferences;
     30 import android.content.pm.ApplicationInfo;
     31 import android.content.res.Configuration;
     32 import android.content.res.Resources;
     33 import android.graphics.Rect;
     34 import android.inputmethodservice.InputMethodService;
     35 import android.media.AudioManager;
     36 import android.net.ConnectivityManager;
     37 import android.os.Debug;
     38 import android.os.IBinder;
     39 import android.os.Message;
     40 import android.os.SystemClock;
     41 import android.preference.PreferenceActivity;
     42 import android.preference.PreferenceManager;
     43 import android.text.InputType;
     44 import android.text.TextUtils;
     45 import android.util.Log;
     46 import android.util.PrintWriterPrinter;
     47 import android.util.Printer;
     48 import android.view.KeyCharacterMap;
     49 import android.view.KeyEvent;
     50 import android.view.View;
     51 import android.view.ViewGroup;
     52 import android.view.ViewGroup.LayoutParams;
     53 import android.view.ViewParent;
     54 import android.view.Window;
     55 import android.view.WindowManager;
     56 import android.view.inputmethod.CompletionInfo;
     57 import android.view.inputmethod.CorrectionInfo;
     58 import android.view.inputmethod.EditorInfo;
     59 import android.view.inputmethod.InputConnection;
     60 import android.view.inputmethod.InputMethodSubtype;
     61 
     62 import com.android.inputmethod.accessibility.AccessibilityUtils;
     63 import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
     64 import com.android.inputmethod.compat.CompatUtils;
     65 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
     66 import com.android.inputmethod.compat.SuggestionSpanUtils;
     67 import com.android.inputmethod.keyboard.Keyboard;
     68 import com.android.inputmethod.keyboard.KeyboardActionListener;
     69 import com.android.inputmethod.keyboard.KeyboardId;
     70 import com.android.inputmethod.keyboard.KeyboardSwitcher;
     71 import com.android.inputmethod.keyboard.KeyboardView;
     72 import com.android.inputmethod.keyboard.LatinKeyboardView;
     73 import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
     74 import com.android.inputmethod.latin.define.ProductionFlag;
     75 import com.android.inputmethod.latin.suggestions.SuggestionsView;
     76 
     77 import java.io.FileDescriptor;
     78 import java.io.PrintWriter;
     79 import java.util.ArrayList;
     80 import java.util.Locale;
     81 
     82 /**
     83  * Input method implementation for Qwerty'ish keyboard.
     84  */
     85 public class LatinIME extends InputMethodService implements KeyboardActionListener,
     86         SuggestionsView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener {
     87     private static final String TAG = LatinIME.class.getSimpleName();
     88     private static final boolean TRACE = false;
     89     private static boolean DEBUG;
     90 
     91     private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
     92 
     93     // How many continuous deletes at which to start deleting at a higher speed.
     94     private static final int DELETE_ACCELERATE_AT = 20;
     95     // Key events coming any faster than this are long-presses.
     96     private static final int QUICK_PRESS = 200;
     97 
     98     private static final int PENDING_IMS_CALLBACK_DURATION = 800;
     99 
    100     /**
    101      * The name of the scheme used by the Package Manager to warn of a new package installation,
    102      * replacement or removal.
    103      */
    104     private static final String SCHEME_PACKAGE = "package";
    105 
    106     /** Whether to use the binary version of the contacts dictionary */
    107     public static final boolean USE_BINARY_CONTACTS_DICTIONARY = true;
    108 
    109     /** Whether to use the binary version of the user dictionary */
    110     public static final boolean USE_BINARY_USER_DICTIONARY = true;
    111 
    112     // TODO: migrate this to SettingsValues
    113     private int mSuggestionVisibility;
    114     private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
    115             = R.string.prefs_suggestion_visibility_show_value;
    116     private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
    117             = R.string.prefs_suggestion_visibility_show_only_portrait_value;
    118     private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
    119             = R.string.prefs_suggestion_visibility_hide_value;
    120 
    121     private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
    122         SUGGESTION_VISIBILILTY_SHOW_VALUE,
    123         SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
    124         SUGGESTION_VISIBILILTY_HIDE_VALUE
    125     };
    126 
    127     private static final int SPACE_STATE_NONE = 0;
    128     // Double space: the state where the user pressed space twice quickly, which LatinIME
    129     // resolved as period-space. Undoing this converts the period to a space.
    130     private static final int SPACE_STATE_DOUBLE = 1;
    131     // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
    132     // have just been swapped. Undoing this swaps them back; the space is still considered weak.
    133     private static final int SPACE_STATE_SWAP_PUNCTUATION = 2;
    134     // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
    135     // spaces happen when the user presses space, accepting the current suggestion (whether
    136     // it's an auto-correction or not).
    137     private static final int SPACE_STATE_WEAK = 3;
    138     // Phantom space: a not-yet-inserted space that should get inserted on the next input,
    139     // character provided it's not a separator. If it's a separator, the phantom space is dropped.
    140     // Phantom spaces happen when a user chooses a word from the suggestion strip.
    141     private static final int SPACE_STATE_PHANTOM = 4;
    142 
    143     // Current space state of the input method. This can be any of the above constants.
    144     private int mSpaceState;
    145 
    146     private SettingsValues mSettingsValues;
    147     private InputAttributes mInputAttributes;
    148 
    149     private View mExtractArea;
    150     private View mKeyPreviewBackingView;
    151     private View mSuggestionsContainer;
    152     private SuggestionsView mSuggestionsView;
    153     /* package for tests */ Suggest mSuggest;
    154     private CompletionInfo[] mApplicationSpecifiedCompletions;
    155     private ApplicationInfo mTargetApplicationInfo;
    156 
    157     private InputMethodManagerCompatWrapper mImm;
    158     private Resources mResources;
    159     private SharedPreferences mPrefs;
    160     /* package for tests */ final KeyboardSwitcher mKeyboardSwitcher;
    161     private final SubtypeSwitcher mSubtypeSwitcher;
    162     private boolean mShouldSwitchToLastSubtype = true;
    163 
    164     private boolean mIsMainDictionaryAvailable;
    165     // TODO: revert this back to the concrete class after transition.
    166     private Dictionary mUserDictionary;
    167     private UserHistoryDictionary mUserHistoryDictionary;
    168     private boolean mIsUserDictionaryAvailable;
    169 
    170     private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
    171     private WordComposer mWordComposer = new WordComposer();
    172 
    173     private int mCorrectionMode;
    174 
    175     // Keep track of the last selection range to decide if we need to show word alternatives
    176     private static final int NOT_A_CURSOR_POSITION = -1;
    177     private int mLastSelectionStart = NOT_A_CURSOR_POSITION;
    178     private int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
    179 
    180     // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
    181     // "expect" it, it means the user actually moved the cursor.
    182     private boolean mExpectingUpdateSelection;
    183     private int mDeleteCount;
    184     private long mLastKeyTime;
    185 
    186     private AudioAndHapticFeedbackManager mFeedbackManager;
    187 
    188     // Member variables for remembering the current device orientation.
    189     private int mDisplayOrientation;
    190 
    191     // Object for reacting to adding/removing a dictionary pack.
    192     private BroadcastReceiver mDictionaryPackInstallReceiver =
    193             new DictionaryPackInstallBroadcastReceiver(this);
    194 
    195     // Keeps track of most recently inserted text (multi-character key) for reverting
    196     private CharSequence mEnteredText;
    197 
    198     private boolean mIsAutoCorrectionIndicatorOn;
    199 
    200     private AlertDialog mOptionsDialog;
    201 
    202     public final UIHandler mHandler = new UIHandler(this);
    203 
    204     public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
    205         private static final int MSG_UPDATE_SHIFT_STATE = 1;
    206         private static final int MSG_SPACE_TYPED = 4;
    207         private static final int MSG_SET_BIGRAM_PREDICTIONS = 5;
    208         private static final int MSG_PENDING_IMS_CALLBACK = 6;
    209         private static final int MSG_UPDATE_SUGGESTIONS = 7;
    210 
    211         private int mDelayUpdateSuggestions;
    212         private int mDelayUpdateShiftState;
    213         private long mDoubleSpacesTurnIntoPeriodTimeout;
    214 
    215         public UIHandler(LatinIME outerInstance) {
    216             super(outerInstance);
    217         }
    218 
    219         public void onCreate() {
    220             final Resources res = getOuterInstance().getResources();
    221             mDelayUpdateSuggestions =
    222                     res.getInteger(R.integer.config_delay_update_suggestions);
    223             mDelayUpdateShiftState =
    224                     res.getInteger(R.integer.config_delay_update_shift_state);
    225             mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger(
    226                     R.integer.config_double_spaces_turn_into_period_timeout);
    227         }
    228 
    229         @Override
    230         public void handleMessage(Message msg) {
    231             final LatinIME latinIme = getOuterInstance();
    232             final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
    233             switch (msg.what) {
    234             case MSG_UPDATE_SUGGESTIONS:
    235                 latinIme.updateSuggestions();
    236                 break;
    237             case MSG_UPDATE_SHIFT_STATE:
    238                 switcher.updateShiftState();
    239                 break;
    240             case MSG_SET_BIGRAM_PREDICTIONS:
    241                 latinIme.updateBigramPredictions();
    242                 break;
    243             }
    244         }
    245 
    246         public void postUpdateSuggestions() {
    247             removeMessages(MSG_UPDATE_SUGGESTIONS);
    248             sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions);
    249         }
    250 
    251         public void cancelUpdateSuggestions() {
    252             removeMessages(MSG_UPDATE_SUGGESTIONS);
    253         }
    254 
    255         public boolean hasPendingUpdateSuggestions() {
    256             return hasMessages(MSG_UPDATE_SUGGESTIONS);
    257         }
    258 
    259         public void postUpdateShiftState() {
    260             removeMessages(MSG_UPDATE_SHIFT_STATE);
    261             sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
    262         }
    263 
    264         public void cancelUpdateShiftState() {
    265             removeMessages(MSG_UPDATE_SHIFT_STATE);
    266         }
    267 
    268         public void postUpdateBigramPredictions() {
    269             removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
    270             sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions);
    271         }
    272 
    273         public void cancelUpdateBigramPredictions() {
    274             removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
    275         }
    276 
    277         public void startDoubleSpacesTimer() {
    278             removeMessages(MSG_SPACE_TYPED);
    279             sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), mDoubleSpacesTurnIntoPeriodTimeout);
    280         }
    281 
    282         public void cancelDoubleSpacesTimer() {
    283             removeMessages(MSG_SPACE_TYPED);
    284         }
    285 
    286         public boolean isAcceptingDoubleSpaces() {
    287             return hasMessages(MSG_SPACE_TYPED);
    288         }
    289 
    290         // Working variables for the following methods.
    291         private boolean mIsOrientationChanging;
    292         private boolean mPendingSuccessiveImsCallback;
    293         private boolean mHasPendingStartInput;
    294         private boolean mHasPendingFinishInputView;
    295         private boolean mHasPendingFinishInput;
    296         private EditorInfo mAppliedEditorInfo;
    297 
    298         public void startOrientationChanging() {
    299             removeMessages(MSG_PENDING_IMS_CALLBACK);
    300             resetPendingImsCallback();
    301             mIsOrientationChanging = true;
    302             final LatinIME latinIme = getOuterInstance();
    303             if (latinIme.isInputViewShown()) {
    304                 latinIme.mKeyboardSwitcher.saveKeyboardState();
    305             }
    306         }
    307 
    308         private void resetPendingImsCallback() {
    309             mHasPendingFinishInputView = false;
    310             mHasPendingFinishInput = false;
    311             mHasPendingStartInput = false;
    312         }
    313 
    314         private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo,
    315                 boolean restarting) {
    316             if (mHasPendingFinishInputView)
    317                 latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
    318             if (mHasPendingFinishInput)
    319                 latinIme.onFinishInputInternal();
    320             if (mHasPendingStartInput)
    321                 latinIme.onStartInputInternal(editorInfo, restarting);
    322             resetPendingImsCallback();
    323         }
    324 
    325         public void onStartInput(EditorInfo editorInfo, boolean restarting) {
    326             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
    327                 // Typically this is the second onStartInput after orientation changed.
    328                 mHasPendingStartInput = true;
    329             } else {
    330                 if (mIsOrientationChanging && restarting) {
    331                     // This is the first onStartInput after orientation changed.
    332                     mIsOrientationChanging = false;
    333                     mPendingSuccessiveImsCallback = true;
    334                 }
    335                 final LatinIME latinIme = getOuterInstance();
    336                 executePendingImsCallback(latinIme, editorInfo, restarting);
    337                 latinIme.onStartInputInternal(editorInfo, restarting);
    338             }
    339         }
    340 
    341         public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
    342             if (hasMessages(MSG_PENDING_IMS_CALLBACK)
    343                     && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
    344                 // Typically this is the second onStartInputView after orientation changed.
    345                 resetPendingImsCallback();
    346             } else {
    347                 if (mPendingSuccessiveImsCallback) {
    348                     // This is the first onStartInputView after orientation changed.
    349                     mPendingSuccessiveImsCallback = false;
    350                     resetPendingImsCallback();
    351                     sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
    352                             PENDING_IMS_CALLBACK_DURATION);
    353                 }
    354                 final LatinIME latinIme = getOuterInstance();
    355                 executePendingImsCallback(latinIme, editorInfo, restarting);
    356                 latinIme.onStartInputViewInternal(editorInfo, restarting);
    357                 mAppliedEditorInfo = editorInfo;
    358             }
    359         }
    360 
    361         public void onFinishInputView(boolean finishingInput) {
    362             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
    363                 // Typically this is the first onFinishInputView after orientation changed.
    364                 mHasPendingFinishInputView = true;
    365             } else {
    366                 final LatinIME latinIme = getOuterInstance();
    367                 latinIme.onFinishInputViewInternal(finishingInput);
    368                 mAppliedEditorInfo = null;
    369             }
    370         }
    371 
    372         public void onFinishInput() {
    373             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
    374                 // Typically this is the first onFinishInput after orientation changed.
    375                 mHasPendingFinishInput = true;
    376             } else {
    377                 final LatinIME latinIme = getOuterInstance();
    378                 executePendingImsCallback(latinIme, null, false);
    379                 latinIme.onFinishInputInternal();
    380             }
    381         }
    382     }
    383 
    384     public LatinIME() {
    385         super();
    386         mSubtypeSwitcher = SubtypeSwitcher.getInstance();
    387         mKeyboardSwitcher = KeyboardSwitcher.getInstance();
    388     }
    389 
    390     @Override
    391     public void onCreate() {
    392         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    393         mPrefs = prefs;
    394         LatinImeLogger.init(this, prefs);
    395         if (ProductionFlag.IS_EXPERIMENTAL) {
    396             ResearchLogger.init(this, prefs);
    397         }
    398         InputMethodManagerCompatWrapper.init(this);
    399         SubtypeSwitcher.init(this);
    400         KeyboardSwitcher.init(this, prefs);
    401         AccessibilityUtils.init(this);
    402 
    403         super.onCreate();
    404 
    405         mImm = InputMethodManagerCompatWrapper.getInstance();
    406         mHandler.onCreate();
    407         DEBUG = LatinImeLogger.sDBG;
    408 
    409         final Resources res = getResources();
    410         mResources = res;
    411 
    412         loadSettings();
    413 
    414         ImfUtils.setAdditionalInputMethodSubtypes(this, mSettingsValues.getAdditionalSubtypes());
    415 
    416         // TODO: remove the following when it's not needed by updateCorrectionMode() any more
    417         mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
    418         updateCorrectionMode();
    419 
    420         Utils.GCUtils.getInstance().reset();
    421         boolean tryGC = true;
    422         for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
    423             try {
    424                 initSuggest();
    425                 tryGC = false;
    426             } catch (OutOfMemoryError e) {
    427                 tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
    428             }
    429         }
    430 
    431         mDisplayOrientation = res.getConfiguration().orientation;
    432 
    433         // Register to receive ringer mode change and network state change.
    434         // Also receive installation and removal of a dictionary pack.
    435         final IntentFilter filter = new IntentFilter();
    436         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
    437         filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
    438         registerReceiver(mReceiver, filter);
    439 
    440         final IntentFilter packageFilter = new IntentFilter();
    441         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
    442         packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
    443         packageFilter.addDataScheme(SCHEME_PACKAGE);
    444         registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
    445 
    446         final IntentFilter newDictFilter = new IntentFilter();
    447         newDictFilter.addAction(
    448                 DictionaryPackInstallBroadcastReceiver.NEW_DICTIONARY_INTENT_ACTION);
    449         registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
    450     }
    451 
    452     // Has to be package-visible for unit tests
    453     /* package */ void loadSettings() {
    454         // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
    455         // is not guaranteed. It may even be called at the same time on a different thread.
    456         if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    457         final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
    458             @Override
    459             protected SettingsValues job(Resources res) {
    460                 return new SettingsValues(mPrefs, LatinIME.this);
    461             }
    462         };
    463         mSettingsValues = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
    464         mFeedbackManager = new AudioAndHapticFeedbackManager(this, mSettingsValues);
    465         resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
    466     }
    467 
    468     private void initSuggest() {
    469         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
    470         final String localeStr = subtypeLocale.toString();
    471 
    472         final Dictionary oldContactsDictionary;
    473         if (mSuggest != null) {
    474             oldContactsDictionary = mSuggest.getContactsDictionary();
    475             mSuggest.close();
    476         } else {
    477             oldContactsDictionary = null;
    478         }
    479         mSuggest = new Suggest(this, subtypeLocale);
    480         if (mSettingsValues.mAutoCorrectEnabled) {
    481             mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
    482         }
    483 
    484         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
    485 
    486         if (USE_BINARY_USER_DICTIONARY) {
    487             mUserDictionary = new UserBinaryDictionary(this, localeStr);
    488             mIsUserDictionaryAvailable = ((UserBinaryDictionary)mUserDictionary).isEnabled();
    489         } else {
    490             mUserDictionary = new UserDictionary(this, localeStr);
    491             mIsUserDictionaryAvailable = ((UserDictionary)mUserDictionary).isEnabled();
    492         }
    493         mSuggest.setUserDictionary(mUserDictionary);
    494 
    495         resetContactsDictionary(oldContactsDictionary);
    496 
    497         // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
    498         // is not guaranteed. It may even be called at the same time on a different thread.
    499         if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    500         mUserHistoryDictionary = UserHistoryDictionary.getInstance(
    501                 this, localeStr, Suggest.DIC_USER_HISTORY, mPrefs);
    502         mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
    503     }
    504 
    505     /**
    506      * Resets the contacts dictionary in mSuggest according to the user settings.
    507      *
    508      * This method takes an optional contacts dictionary to use. Since the contacts dictionary
    509      * does not depend on the locale, it can be reused across different instances of Suggest.
    510      * The dictionary will also be opened or closed as necessary depending on the settings.
    511      *
    512      * @param oldContactsDictionary an optional dictionary to use, or null
    513      */
    514     private void resetContactsDictionary(final Dictionary oldContactsDictionary) {
    515         final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict);
    516 
    517         final Dictionary dictionaryToUse;
    518         if (!shouldSetDictionary) {
    519             // Make sure the dictionary is closed. If it is already closed, this is a no-op,
    520             // so it's safe to call it anyways.
    521             if (null != oldContactsDictionary) oldContactsDictionary.close();
    522             dictionaryToUse = null;
    523         } else if (null != oldContactsDictionary) {
    524             // Make sure the old contacts dictionary is opened. If it is already open, this is a
    525             // no-op, so it's safe to call it anyways.
    526             if (USE_BINARY_CONTACTS_DICTIONARY) {
    527                 ((ContactsBinaryDictionary)oldContactsDictionary).reopen(this);
    528             } else {
    529                 ((ContactsDictionary)oldContactsDictionary).reopen(this);
    530             }
    531             dictionaryToUse = oldContactsDictionary;
    532         } else {
    533             if (USE_BINARY_CONTACTS_DICTIONARY) {
    534                 dictionaryToUse = new ContactsBinaryDictionary(this, Suggest.DIC_CONTACTS,
    535                         mSubtypeSwitcher.getCurrentSubtypeLocale());
    536             } else {
    537                 dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
    538             }
    539         }
    540 
    541         if (null != mSuggest) {
    542             mSuggest.setContactsDictionary(dictionaryToUse);
    543         }
    544     }
    545 
    546     /* package private */ void resetSuggestMainDict() {
    547         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
    548         mSuggest.resetMainDict(this, subtypeLocale);
    549         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
    550     }
    551 
    552     @Override
    553     public void onDestroy() {
    554         if (mSuggest != null) {
    555             mSuggest.close();
    556             mSuggest = null;
    557         }
    558         unregisterReceiver(mReceiver);
    559         unregisterReceiver(mDictionaryPackInstallReceiver);
    560         LatinImeLogger.commit();
    561         LatinImeLogger.onDestroy();
    562         super.onDestroy();
    563     }
    564 
    565     @Override
    566     public void onConfigurationChanged(Configuration conf) {
    567         mSubtypeSwitcher.onConfigurationChanged(conf);
    568         // If orientation changed while predicting, commit the change
    569         if (mDisplayOrientation != conf.orientation) {
    570             mDisplayOrientation = conf.orientation;
    571             mHandler.startOrientationChanging();
    572             final InputConnection ic = getCurrentInputConnection();
    573             commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
    574             if (ic != null) ic.finishComposingText(); // For voice input
    575             if (isShowingOptionDialog())
    576                 mOptionsDialog.dismiss();
    577         }
    578         super.onConfigurationChanged(conf);
    579     }
    580 
    581     @Override
    582     public View onCreateInputView() {
    583         return mKeyboardSwitcher.onCreateInputView();
    584     }
    585 
    586     @Override
    587     public void setInputView(View view) {
    588         super.setInputView(view);
    589         mExtractArea = getWindow().getWindow().getDecorView()
    590                 .findViewById(android.R.id.extractArea);
    591         mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
    592         mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
    593         mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view);
    594         if (mSuggestionsView != null)
    595             mSuggestionsView.setListener(this, view);
    596         if (LatinImeLogger.sVISUALDEBUG) {
    597             mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
    598         }
    599     }
    600 
    601     @Override
    602     public void setCandidatesView(View view) {
    603         // To ensure that CandidatesView will never be set.
    604         return;
    605     }
    606 
    607     @Override
    608     public void onStartInput(EditorInfo editorInfo, boolean restarting) {
    609         mHandler.onStartInput(editorInfo, restarting);
    610     }
    611 
    612     @Override
    613     public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
    614         mHandler.onStartInputView(editorInfo, restarting);
    615     }
    616 
    617     @Override
    618     public void onFinishInputView(boolean finishingInput) {
    619         mHandler.onFinishInputView(finishingInput);
    620     }
    621 
    622     @Override
    623     public void onFinishInput() {
    624         mHandler.onFinishInput();
    625     }
    626 
    627     @Override
    628     public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) {
    629         // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
    630         // is not guaranteed. It may even be called at the same time on a different thread.
    631         mSubtypeSwitcher.updateSubtype(subtype);
    632     }
    633 
    634     private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) {
    635         super.onStartInput(editorInfo, restarting);
    636     }
    637 
    638     @SuppressWarnings("deprecation")
    639     private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
    640         super.onStartInputView(editorInfo, restarting);
    641         final KeyboardSwitcher switcher = mKeyboardSwitcher;
    642         LatinKeyboardView inputView = switcher.getKeyboardView();
    643 
    644         if (editorInfo == null) {
    645             Log.e(TAG, "Null EditorInfo in onStartInputView()");
    646             if (LatinImeLogger.sDBG) {
    647                 throw new NullPointerException("Null EditorInfo in onStartInputView()");
    648             }
    649             return;
    650         }
    651         if (DEBUG) {
    652             Log.d(TAG, "onStartInputView: editorInfo:"
    653                     + String.format("inputType=0x%08x imeOptions=0x%08x",
    654                             editorInfo.inputType, editorInfo.imeOptions));
    655             Log.d(TAG, "All caps = "
    656                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0)
    657                     + ", sentence caps = "
    658                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0)
    659                     + ", word caps = "
    660                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
    661         }
    662         if (ProductionFlag.IS_EXPERIMENTAL) {
    663             ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, mPrefs);
    664         }
    665         if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
    666             Log.w(TAG, "Deprecated private IME option specified: "
    667                     + editorInfo.privateImeOptions);
    668             Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead");
    669         }
    670         if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) {
    671             Log.w(TAG, "Deprecated private IME option specified: "
    672                     + editorInfo.privateImeOptions);
    673             Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
    674         }
    675 
    676         mTargetApplicationInfo =
    677                 TargetApplicationGetter.getCachedApplicationInfo(editorInfo.packageName);
    678         if (null == mTargetApplicationInfo) {
    679             new TargetApplicationGetter(this /* context */, this /* listener */)
    680                     .execute(editorInfo.packageName);
    681         }
    682 
    683         LatinImeLogger.onStartInputView(editorInfo);
    684         // In landscape mode, this method gets called without the input view being created.
    685         if (inputView == null) {
    686             return;
    687         }
    688 
    689         // Forward this event to the accessibility utilities, if enabled.
    690         final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
    691         if (accessUtils.isTouchExplorationEnabled()) {
    692             accessUtils.onStartInputViewInternal(editorInfo, restarting);
    693         }
    694 
    695         mSubtypeSwitcher.updateParametersOnStartInputView();
    696 
    697         // The EditorInfo might have a flag that affects fullscreen mode.
    698         // Note: This call should be done by InputMethodService?
    699         updateFullscreenMode();
    700         mLastSelectionStart = editorInfo.initialSelStart;
    701         mLastSelectionEnd = editorInfo.initialSelEnd;
    702         mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode());
    703         mApplicationSpecifiedCompletions = null;
    704 
    705         inputView.closing();
    706         mEnteredText = null;
    707         resetComposingState(true /* alsoResetLastComposedWord */);
    708         mDeleteCount = 0;
    709         mSpaceState = SPACE_STATE_NONE;
    710 
    711         loadSettings();
    712         updateCorrectionMode();
    713         updateSuggestionVisibility(mResources);
    714 
    715         if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
    716             mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
    717         }
    718 
    719         switcher.loadKeyboard(editorInfo, mSettingsValues);
    720 
    721         if (mSuggestionsView != null)
    722             mSuggestionsView.clear();
    723         setSuggestionStripShownInternal(
    724                 isSuggestionsStripVisible(), /* needsInputViewShown */ false);
    725         // Delay updating suggestions because keyboard input view may not be shown at this point.
    726         mHandler.postUpdateSuggestions();
    727         mHandler.cancelDoubleSpacesTimer();
    728 
    729         inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
    730                 mSettingsValues.mKeyPreviewPopupDismissDelay);
    731         inputView.setProximityCorrectionEnabled(true);
    732 
    733         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
    734     }
    735 
    736     public void onTargetApplicationKnown(final ApplicationInfo info) {
    737         mTargetApplicationInfo = info;
    738     }
    739 
    740     @Override
    741     public void onWindowHidden() {
    742         super.onWindowHidden();
    743         KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
    744         if (inputView != null) inputView.closing();
    745     }
    746 
    747     private void onFinishInputInternal() {
    748         super.onFinishInput();
    749 
    750         LatinImeLogger.commit();
    751 
    752         KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
    753         if (inputView != null) inputView.closing();
    754     }
    755 
    756     private void onFinishInputViewInternal(boolean finishingInput) {
    757         super.onFinishInputView(finishingInput);
    758         mKeyboardSwitcher.onFinishInputView();
    759         KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
    760         if (inputView != null) inputView.cancelAllMessages();
    761         // Remove pending messages related to update suggestions
    762         mHandler.cancelUpdateSuggestions();
    763     }
    764 
    765     @Override
    766     public void onUpdateSelection(int oldSelStart, int oldSelEnd,
    767             int newSelStart, int newSelEnd,
    768             int composingSpanStart, int composingSpanEnd) {
    769         super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
    770                 composingSpanStart, composingSpanEnd);
    771 
    772         if (DEBUG) {
    773             Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
    774                     + ", ose=" + oldSelEnd
    775                     + ", lss=" + mLastSelectionStart
    776                     + ", lse=" + mLastSelectionEnd
    777                     + ", nss=" + newSelStart
    778                     + ", nse=" + newSelEnd
    779                     + ", cs=" + composingSpanStart
    780                     + ", ce=" + composingSpanEnd);
    781         }
    782         if (ProductionFlag.IS_EXPERIMENTAL) {
    783             ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
    784                     oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
    785                     composingSpanEnd);
    786         }
    787 
    788         // TODO: refactor the following code to be less contrived.
    789         // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means
    790         // that the cursor is not at the end of the composing span, or there is a selection.
    791         // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place
    792         // as last time we were called (if there is a selection, it means the start hasn't
    793         // changed, so it's the end that did).
    794         final boolean selectionChanged = (newSelStart != composingSpanEnd
    795                 || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart;
    796         // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
    797         // span in the view - we can use that to narrow down whether the cursor was moved
    798         // by us or not. If we are composing a word but there is no composing span, then
    799         // we know for sure the cursor moved while we were composing and we should reset
    800         // the state.
    801         final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
    802         if (!mExpectingUpdateSelection) {
    803             // TAKE CARE: there is a race condition when we enter this test even when the user
    804             // did not explicitly move the cursor. This happens when typing fast, where two keys
    805             // turn this flag on in succession and both onUpdateSelection() calls arrive after
    806             // the second one - the first call successfully avoids this test, but the second one
    807             // enters. For the moment we rely on noComposingSpan to further reduce the impact.
    808 
    809             // TODO: the following is probably better done in resetEntireInputState().
    810             // it should only happen when the cursor moved, and the very purpose of the
    811             // test below is to narrow down whether this happened or not. Likewise with
    812             // the call to postUpdateShiftState.
    813             // We set this to NONE because after a cursor move, we don't want the space
    814             // state-related special processing to kick in.
    815             mSpaceState = SPACE_STATE_NONE;
    816 
    817             if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
    818                 resetEntireInputState();
    819             }
    820 
    821             mHandler.postUpdateShiftState();
    822         }
    823         mExpectingUpdateSelection = false;
    824         // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not
    825         // here. It would probably be too expensive to call directly here but we may want to post a
    826         // message to delay it. The point would be to unify behavior between backspace to the
    827         // end of a word and manually put the pointer at the end of the word.
    828 
    829         // Make a note of the cursor position
    830         mLastSelectionStart = newSelStart;
    831         mLastSelectionEnd = newSelEnd;
    832     }
    833 
    834     /**
    835      * This is called when the user has clicked on the extracted text view,
    836      * when running in fullscreen mode.  The default implementation hides
    837      * the suggestions view when this happens, but only if the extracted text
    838      * editor has a vertical scroll bar because its text doesn't fit.
    839      * Here we override the behavior due to the possibility that a re-correction could
    840      * cause the suggestions strip to disappear and re-appear.
    841      */
    842     @Override
    843     public void onExtractedTextClicked() {
    844         if (isSuggestionsRequested()) return;
    845 
    846         super.onExtractedTextClicked();
    847     }
    848 
    849     /**
    850      * This is called when the user has performed a cursor movement in the
    851      * extracted text view, when it is running in fullscreen mode.  The default
    852      * implementation hides the suggestions view when a vertical movement
    853      * happens, but only if the extracted text editor has a vertical scroll bar
    854      * because its text doesn't fit.
    855      * Here we override the behavior due to the possibility that a re-correction could
    856      * cause the suggestions strip to disappear and re-appear.
    857      */
    858     @Override
    859     public void onExtractedCursorMovement(int dx, int dy) {
    860         if (isSuggestionsRequested()) return;
    861 
    862         super.onExtractedCursorMovement(dx, dy);
    863     }
    864 
    865     @Override
    866     public void hideWindow() {
    867         LatinImeLogger.commit();
    868         mKeyboardSwitcher.onHideWindow();
    869 
    870         if (TRACE) Debug.stopMethodTracing();
    871         if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
    872             mOptionsDialog.dismiss();
    873             mOptionsDialog = null;
    874         }
    875         super.hideWindow();
    876     }
    877 
    878     @Override
    879     public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
    880         if (DEBUG) {
    881             Log.i(TAG, "Received completions:");
    882             if (applicationSpecifiedCompletions != null) {
    883                 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
    884                     Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
    885                 }
    886             }
    887         }
    888         if (ProductionFlag.IS_EXPERIMENTAL) {
    889             ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
    890         }
    891         if (mInputAttributes.mApplicationSpecifiedCompletionOn) {
    892             mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
    893             if (applicationSpecifiedCompletions == null) {
    894                 clearSuggestions();
    895                 return;
    896             }
    897 
    898             final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
    899                     SuggestedWords.getFromApplicationSpecifiedCompletions(
    900                             applicationSpecifiedCompletions);
    901             final SuggestedWords suggestedWords = new SuggestedWords(
    902                     applicationSuggestedWords,
    903                     false /* typedWordValid */,
    904                     false /* hasAutoCorrectionCandidate */,
    905                     false /* allowsToBeAutoCorrected */,
    906                     false /* isPunctuationSuggestions */,
    907                     false /* isObsoleteSuggestions */,
    908                     false /* isPrediction */);
    909             // When in fullscreen mode, show completions generated by the application
    910             final boolean isAutoCorrection = false;
    911             setSuggestions(suggestedWords, isAutoCorrection);
    912             setAutoCorrectionIndicator(isAutoCorrection);
    913             // TODO: is this the right thing to do? What should we auto-correct to in
    914             // this case? This says to keep whatever the user typed.
    915             mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
    916             setSuggestionStripShown(true);
    917         }
    918     }
    919 
    920     private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
    921         // TODO: Modify this if we support suggestions with hard keyboard
    922         if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
    923             final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
    924             final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false;
    925             final boolean shouldShowSuggestions = shown
    926                     && (needsInputViewShown ? inputViewShown : true);
    927             if (isFullscreenMode()) {
    928                 mSuggestionsContainer.setVisibility(
    929                         shouldShowSuggestions ? View.VISIBLE : View.GONE);
    930             } else {
    931                 mSuggestionsContainer.setVisibility(
    932                         shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
    933             }
    934         }
    935     }
    936 
    937     private void setSuggestionStripShown(boolean shown) {
    938         setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
    939     }
    940 
    941     private int getAdjustedBackingViewHeight() {
    942         final int currentHeight = mKeyPreviewBackingView.getHeight();
    943         if (currentHeight > 0) {
    944             return currentHeight;
    945         }
    946 
    947         final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
    948         if (keyboardView == null) {
    949             return 0;
    950         }
    951         final int keyboardHeight = keyboardView.getHeight();
    952         final int suggestionsHeight = mSuggestionsContainer.getHeight();
    953         final int displayHeight = mResources.getDisplayMetrics().heightPixels;
    954         final Rect rect = new Rect();
    955         mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
    956         final int notificationBarHeight = rect.top;
    957         final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
    958                 - keyboardHeight;
    959 
    960         final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
    961         params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight);
    962         mKeyPreviewBackingView.setLayoutParams(params);
    963         return params.height;
    964     }
    965 
    966     @Override
    967     public void onComputeInsets(InputMethodService.Insets outInsets) {
    968         super.onComputeInsets(outInsets);
    969         final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
    970         if (inputView == null || mSuggestionsContainer == null)
    971             return;
    972         final int adjustedBackingHeight = getAdjustedBackingViewHeight();
    973         final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
    974         final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
    975         // In fullscreen mode, the height of the extract area managed by InputMethodService should
    976         // be considered.
    977         // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
    978         final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
    979         final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0
    980                 : mSuggestionsContainer.getHeight();
    981         final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
    982         int touchY = extraHeight;
    983         // Need to set touchable region only if input view is being shown
    984         final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
    985         if (keyboardView != null && keyboardView.isShown()) {
    986             if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
    987                 touchY -= suggestionsHeight;
    988             }
    989             final int touchWidth = inputView.getWidth();
    990             final int touchHeight = inputView.getHeight() + extraHeight
    991                     // Extend touchable region below the keyboard.
    992                     + EXTENDED_TOUCHABLE_REGION_HEIGHT;
    993             outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
    994             outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight);
    995         }
    996         outInsets.contentTopInsets = touchY;
    997         outInsets.visibleTopInsets = touchY;
    998     }
    999 
   1000     @Override
   1001     public boolean onEvaluateFullscreenMode() {
   1002         // Reread resource value here, because this method is called by framework anytime as needed.
   1003         final boolean isFullscreenModeAllowed =
   1004                 mSettingsValues.isFullscreenModeAllowed(getResources());
   1005         return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed;
   1006     }
   1007 
   1008     @Override
   1009     public void updateFullscreenMode() {
   1010         super.updateFullscreenMode();
   1011 
   1012         if (mKeyPreviewBackingView == null) return;
   1013         // In fullscreen mode, no need to have extra space to show the key preview.
   1014         // If not, we should have extra space above the keyboard to show the key preview.
   1015         mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
   1016     }
   1017 
   1018     // This will reset the whole input state to the starting state. It will clear
   1019     // the composing word, reset the last composed word, tell the inputconnection
   1020     // and the composingStateManager about it.
   1021     private void resetEntireInputState() {
   1022         resetComposingState(true /* alsoResetLastComposedWord */);
   1023         updateSuggestions();
   1024         final InputConnection ic = getCurrentInputConnection();
   1025         if (ic != null) {
   1026             ic.finishComposingText();
   1027         }
   1028     }
   1029 
   1030     private void resetComposingState(final boolean alsoResetLastComposedWord) {
   1031         mWordComposer.reset();
   1032         if (alsoResetLastComposedWord)
   1033             mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
   1034     }
   1035 
   1036     public void commitTyped(final InputConnection ic, final int separatorCode) {
   1037         if (!mWordComposer.isComposingWord()) return;
   1038         final CharSequence typedWord = mWordComposer.getTypedWord();
   1039         if (typedWord.length() > 0) {
   1040             if (ic != null) {
   1041                 ic.commitText(typedWord, 1);
   1042                 if (ProductionFlag.IS_EXPERIMENTAL) {
   1043                     ResearchLogger.latinIME_commitText(typedWord);
   1044                 }
   1045             }
   1046             final CharSequence prevWord = addToUserHistoryDictionary(typedWord);
   1047             mLastComposedWord = mWordComposer.commitWord(
   1048                     LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
   1049                     separatorCode, prevWord);
   1050         }
   1051         updateSuggestions();
   1052     }
   1053 
   1054     public int getCurrentAutoCapsState() {
   1055         if (!mSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
   1056 
   1057         final EditorInfo ei = getCurrentInputEditorInfo();
   1058         if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
   1059 
   1060         final int inputType = ei.inputType;
   1061         if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
   1062             return TextUtils.CAP_MODE_CHARACTERS;
   1063         }
   1064 
   1065         final boolean noNeedToCheckCapsMode = (inputType & (InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
   1066                 | InputType.TYPE_TEXT_FLAG_CAP_WORDS)) == 0;
   1067         if (noNeedToCheckCapsMode) return Constants.TextUtils.CAP_MODE_OFF;
   1068 
   1069         // Avoid making heavy round-trip IPC calls of {@link InputConnection#getCursorCapsMode}
   1070         // unless needed.
   1071         if (mWordComposer.isComposingWord()) return Constants.TextUtils.CAP_MODE_OFF;
   1072 
   1073         final InputConnection ic = getCurrentInputConnection();
   1074         if (ic == null) return Constants.TextUtils.CAP_MODE_OFF;
   1075         // TODO: This blocking IPC call is heavy. Consider doing this without using IPC calls.
   1076         // Note: getCursorCapsMode() returns the current capitalization mode that is any
   1077         // combination of CAP_MODE_CHARACTERS, CAP_MODE_WORDS, and CAP_MODE_SENTENCES. 0 means none
   1078         // of them.
   1079         return ic.getCursorCapsMode(inputType);
   1080     }
   1081 
   1082     // "ic" may be null
   1083     private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) {
   1084         if (null == ic) return;
   1085         CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
   1086         // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
   1087         if (lastTwo != null && lastTwo.length() == 2
   1088                 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
   1089             ic.deleteSurroundingText(2, 0);
   1090             if (ProductionFlag.IS_EXPERIMENTAL) {
   1091                 ResearchLogger.latinIME_deleteSurroundingText(2);
   1092             }
   1093             ic.commitText(lastTwo.charAt(1) + " ", 1);
   1094             if (ProductionFlag.IS_EXPERIMENTAL) {
   1095                 ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit();
   1096             }
   1097             mKeyboardSwitcher.updateShiftState();
   1098         }
   1099     }
   1100 
   1101     private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
   1102         if (mCorrectionMode == Suggest.CORRECTION_NONE) return false;
   1103         if (ic == null) return false;
   1104         final CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
   1105         if (lastThree != null && lastThree.length() == 3
   1106                 && canBeFollowedByPeriod(lastThree.charAt(0))
   1107                 && lastThree.charAt(1) == Keyboard.CODE_SPACE
   1108                 && lastThree.charAt(2) == Keyboard.CODE_SPACE
   1109                 && mHandler.isAcceptingDoubleSpaces()) {
   1110             mHandler.cancelDoubleSpacesTimer();
   1111             ic.deleteSurroundingText(2, 0);
   1112             ic.commitText(". ", 1);
   1113             if (ProductionFlag.IS_EXPERIMENTAL) {
   1114                 ResearchLogger.latinIME_doubleSpaceAutoPeriod();
   1115             }
   1116             mKeyboardSwitcher.updateShiftState();
   1117             return true;
   1118         }
   1119         return false;
   1120     }
   1121 
   1122     private static boolean canBeFollowedByPeriod(final int codePoint) {
   1123         // TODO: Check again whether there really ain't a better way to check this.
   1124         // TODO: This should probably be language-dependant...
   1125         return Character.isLetterOrDigit(codePoint)
   1126                 || codePoint == Keyboard.CODE_SINGLE_QUOTE
   1127                 || codePoint == Keyboard.CODE_DOUBLE_QUOTE
   1128                 || codePoint == Keyboard.CODE_CLOSING_PARENTHESIS
   1129                 || codePoint == Keyboard.CODE_CLOSING_SQUARE_BRACKET
   1130                 || codePoint == Keyboard.CODE_CLOSING_CURLY_BRACKET
   1131                 || codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET;
   1132     }
   1133 
   1134     // "ic" may be null
   1135     private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) {
   1136         if (ic == null) return;
   1137         final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
   1138         if (lastOne != null && lastOne.length() == 1
   1139                 && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
   1140             ic.deleteSurroundingText(1, 0);
   1141             if (ProductionFlag.IS_EXPERIMENTAL) {
   1142                 ResearchLogger.latinIME_deleteSurroundingText(1);
   1143             }
   1144         }
   1145     }
   1146 
   1147     @Override
   1148     public boolean addWordToDictionary(String word) {
   1149         if (USE_BINARY_USER_DICTIONARY) {
   1150             ((UserBinaryDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
   1151         } else {
   1152             ((UserDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
   1153         }
   1154         // Suggestion strip should be updated after the operation of adding word to the
   1155         // user dictionary
   1156         mHandler.postUpdateSuggestions();
   1157         return true;
   1158     }
   1159 
   1160     private static boolean isAlphabet(int code) {
   1161         return Character.isLetter(code);
   1162     }
   1163 
   1164     private void onSettingsKeyPressed() {
   1165         if (isShowingOptionDialog()) return;
   1166         showSubtypeSelectorAndSettings();
   1167     }
   1168 
   1169     // Virtual codes representing custom requests.  These are used in onCustomRequest() below.
   1170     public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
   1171 
   1172     @Override
   1173     public boolean onCustomRequest(int requestCode) {
   1174         if (isShowingOptionDialog()) return false;
   1175         switch (requestCode) {
   1176         case CODE_SHOW_INPUT_METHOD_PICKER:
   1177             if (ImfUtils.hasMultipleEnabledIMEsOrSubtypes(
   1178                     this, true /* include aux subtypes */)) {
   1179                 mImm.showInputMethodPicker();
   1180                 return true;
   1181             }
   1182             return false;
   1183         }
   1184         return false;
   1185     }
   1186 
   1187     private boolean isShowingOptionDialog() {
   1188         return mOptionsDialog != null && mOptionsDialog.isShowing();
   1189     }
   1190 
   1191     private static int getActionId(Keyboard keyboard) {
   1192         return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE;
   1193     }
   1194 
   1195     private void performEditorAction(int actionId) {
   1196         final InputConnection ic = getCurrentInputConnection();
   1197         if (ic != null) {
   1198             ic.performEditorAction(actionId);
   1199             if (ProductionFlag.IS_EXPERIMENTAL) {
   1200                 ResearchLogger.latinIME_performEditorAction(actionId);
   1201             }
   1202         }
   1203     }
   1204 
   1205     private void handleLanguageSwitchKey() {
   1206         final boolean includesOtherImes = mSettingsValues.mIncludesOtherImesInLanguageSwitchList;
   1207         final IBinder token = getWindow().getWindow().getAttributes().token;
   1208         if (mShouldSwitchToLastSubtype) {
   1209             final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype();
   1210             final boolean lastSubtypeBelongsToThisIme =
   1211                     ImfUtils.checkIfSubtypeBelongsToThisImeAndEnabled(this, lastSubtype);
   1212             if ((includesOtherImes || lastSubtypeBelongsToThisIme)
   1213                     && mImm.switchToLastInputMethod(token)) {
   1214                 mShouldSwitchToLastSubtype = false;
   1215             } else {
   1216                 mImm.switchToNextInputMethod(token, !includesOtherImes);
   1217                 mShouldSwitchToLastSubtype = true;
   1218             }
   1219         } else {
   1220             mImm.switchToNextInputMethod(token, !includesOtherImes);
   1221         }
   1222     }
   1223 
   1224     static private void sendUpDownEnterOrBackspace(final int code, final InputConnection ic) {
   1225         final long eventTime = SystemClock.uptimeMillis();
   1226         ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
   1227                 KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
   1228                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
   1229         ic.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
   1230                 KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
   1231                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
   1232     }
   1233 
   1234     private void sendKeyCodePoint(int code) {
   1235         // TODO: Remove this special handling of digit letters.
   1236         // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
   1237         if (code >= '0' && code <= '9') {
   1238             super.sendKeyChar((char)code);
   1239             return;
   1240         }
   1241 
   1242         final InputConnection ic = getCurrentInputConnection();
   1243         if (ic != null) {
   1244             // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
   1245             // we want to be able to compile against the Ice Cream Sandwich SDK.
   1246             if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
   1247                     && mTargetApplicationInfo.targetSdkVersion < 16) {
   1248                 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
   1249                 // a hardware keyboard event on pressing enter or delete. This is bad for many
   1250                 // reasons (there are race conditions with commits) but some applications are
   1251                 // relying on this behavior so we continue to support it for older apps.
   1252                 sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER, ic);
   1253             } else {
   1254                 final String text = new String(new int[] { code }, 0, 1);
   1255                 ic.commitText(text, text.length());
   1256             }
   1257             if (ProductionFlag.IS_EXPERIMENTAL) {
   1258                 ResearchLogger.latinIME_sendKeyCodePoint(code);
   1259             }
   1260         }
   1261     }
   1262 
   1263     // Implementation of {@link KeyboardActionListener}.
   1264     @Override
   1265     public void onCodeInput(int primaryCode, int x, int y) {
   1266         final long when = SystemClock.uptimeMillis();
   1267         if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
   1268             mDeleteCount = 0;
   1269         }
   1270         mLastKeyTime = when;
   1271 
   1272         if (ProductionFlag.IS_EXPERIMENTAL) {
   1273             if (ResearchLogger.sIsLogging) {
   1274                 ResearchLogger.getInstance().logKeyEvent(primaryCode, x, y);
   1275             }
   1276         }
   1277 
   1278         final KeyboardSwitcher switcher = mKeyboardSwitcher;
   1279         // The space state depends only on the last character pressed and its own previous
   1280         // state. Here, we revert the space state to neutral if the key is actually modifying
   1281         // the input contents (any non-shift key), which is what we should do for
   1282         // all inputs that do not result in a special state. Each character handling is then
   1283         // free to override the state as they see fit.
   1284         final int spaceState = mSpaceState;
   1285         if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
   1286 
   1287         // TODO: Consolidate the double space timer, mLastKeyTime, and the space state.
   1288         if (primaryCode != Keyboard.CODE_SPACE) {
   1289             mHandler.cancelDoubleSpacesTimer();
   1290         }
   1291 
   1292         boolean didAutoCorrect = false;
   1293         switch (primaryCode) {
   1294         case Keyboard.CODE_DELETE:
   1295             mSpaceState = SPACE_STATE_NONE;
   1296             handleBackspace(spaceState);
   1297             mDeleteCount++;
   1298             mExpectingUpdateSelection = true;
   1299             mShouldSwitchToLastSubtype = true;
   1300             LatinImeLogger.logOnDelete(x, y);
   1301             break;
   1302         case Keyboard.CODE_SHIFT:
   1303         case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
   1304             // Shift and symbol key is handled in onPressKey() and onReleaseKey().
   1305             break;
   1306         case Keyboard.CODE_SETTINGS:
   1307             onSettingsKeyPressed();
   1308             break;
   1309         case Keyboard.CODE_SHORTCUT:
   1310             mSubtypeSwitcher.switchToShortcutIME();
   1311             break;
   1312         case Keyboard.CODE_ACTION_ENTER:
   1313             performEditorAction(getActionId(switcher.getKeyboard()));
   1314             break;
   1315         case Keyboard.CODE_ACTION_NEXT:
   1316             performEditorAction(EditorInfo.IME_ACTION_NEXT);
   1317             break;
   1318         case Keyboard.CODE_ACTION_PREVIOUS:
   1319             performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
   1320             break;
   1321         case Keyboard.CODE_LANGUAGE_SWITCH:
   1322             handleLanguageSwitchKey();
   1323             break;
   1324         default:
   1325             if (primaryCode == Keyboard.CODE_TAB
   1326                     && mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT) {
   1327                 performEditorAction(EditorInfo.IME_ACTION_NEXT);
   1328                 break;
   1329             }
   1330             mSpaceState = SPACE_STATE_NONE;
   1331             if (mSettingsValues.isWordSeparator(primaryCode)) {
   1332                 didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
   1333             } else {
   1334                 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   1335                 if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
   1336                     handleCharacter(primaryCode, x, y, spaceState);
   1337                 } else {
   1338                     handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE,
   1339                             spaceState);
   1340                 }
   1341             }
   1342             mExpectingUpdateSelection = true;
   1343             mShouldSwitchToLastSubtype = true;
   1344             break;
   1345         }
   1346         switcher.onCodeInput(primaryCode);
   1347         // Reset after any single keystroke, except shift and symbol-shift
   1348         if (!didAutoCorrect && primaryCode != Keyboard.CODE_SHIFT
   1349                 && primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL)
   1350             mLastComposedWord.deactivate();
   1351         mEnteredText = null;
   1352     }
   1353 
   1354     @Override
   1355     public void onTextInput(CharSequence text) {
   1356         final InputConnection ic = getCurrentInputConnection();
   1357         if (ic == null) return;
   1358         ic.beginBatchEdit();
   1359         commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
   1360         text = specificTldProcessingOnTextInput(ic, text);
   1361         if (SPACE_STATE_PHANTOM == mSpaceState) {
   1362             sendKeyCodePoint(Keyboard.CODE_SPACE);
   1363         }
   1364         ic.commitText(text, 1);
   1365         if (ProductionFlag.IS_EXPERIMENTAL) {
   1366             ResearchLogger.latinIME_commitText(text);
   1367         }
   1368         ic.endBatchEdit();
   1369         mKeyboardSwitcher.updateShiftState();
   1370         mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
   1371         mSpaceState = SPACE_STATE_NONE;
   1372         mEnteredText = text;
   1373         resetComposingState(true /* alsoResetLastComposedWord */);
   1374     }
   1375 
   1376     // ic may not be null
   1377     private CharSequence specificTldProcessingOnTextInput(final InputConnection ic,
   1378             final CharSequence text) {
   1379         if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
   1380                 || !Character.isLetter(text.charAt(1))) {
   1381             // Not a tld: do nothing.
   1382             return text;
   1383         }
   1384         // We have a TLD (or something that looks like this): make sure we don't add
   1385         // a space even if currently in phantom mode.
   1386         mSpaceState = SPACE_STATE_NONE;
   1387         final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
   1388         if (lastOne != null && lastOne.length() == 1
   1389                 && lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
   1390             return text.subSequence(1, text.length());
   1391         } else {
   1392             return text;
   1393         }
   1394     }
   1395 
   1396     @Override
   1397     public void onCancelInput() {
   1398         // User released a finger outside any key
   1399         mKeyboardSwitcher.onCancelInput();
   1400     }
   1401 
   1402     private void handleBackspace(final int spaceState) {
   1403         final InputConnection ic = getCurrentInputConnection();
   1404         if (ic == null) return;
   1405         ic.beginBatchEdit();
   1406         handleBackspaceWhileInBatchEdit(spaceState, ic);
   1407         ic.endBatchEdit();
   1408     }
   1409 
   1410     // "ic" may not be null.
   1411     private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) {
   1412         // In many cases, we may have to put the keyboard in auto-shift state again.
   1413         mHandler.postUpdateShiftState();
   1414 
   1415         if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
   1416             // Cancel multi-character input: remove the text we just entered.
   1417             // This is triggered on backspace after a key that inputs multiple characters,
   1418             // like the smiley key or the .com key.
   1419             final int length = mEnteredText.length();
   1420             ic.deleteSurroundingText(length, 0);
   1421             if (ProductionFlag.IS_EXPERIMENTAL) {
   1422                 ResearchLogger.latinIME_deleteSurroundingText(length);
   1423             }
   1424             // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
   1425             // In addition we know that spaceState is false, and that we should not be
   1426             // reverting any autocorrect at this point. So we can safely return.
   1427             return;
   1428         }
   1429 
   1430         if (mWordComposer.isComposingWord()) {
   1431             final int length = mWordComposer.size();
   1432             if (length > 0) {
   1433                 mWordComposer.deleteLast();
   1434                 ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
   1435                 // If we have deleted the last remaining character of a word, then we are not
   1436                 // isComposingWord() any more.
   1437                 if (!mWordComposer.isComposingWord()) {
   1438                     // Not composing word any more, so we can show bigrams.
   1439                     mHandler.postUpdateBigramPredictions();
   1440                 } else {
   1441                     // Still composing a word, so we still have letters to deduce a suggestion from.
   1442                     mHandler.postUpdateSuggestions();
   1443                 }
   1444             } else {
   1445                 ic.deleteSurroundingText(1, 0);
   1446                 if (ProductionFlag.IS_EXPERIMENTAL) {
   1447                     ResearchLogger.latinIME_deleteSurroundingText(1);
   1448                 }
   1449             }
   1450         } else {
   1451             if (mLastComposedWord.canRevertCommit()) {
   1452                 Utils.Stats.onAutoCorrectionCancellation();
   1453                 revertCommit(ic);
   1454                 return;
   1455             }
   1456             if (SPACE_STATE_DOUBLE == spaceState) {
   1457                 if (revertDoubleSpaceWhileInBatchEdit(ic)) {
   1458                     // No need to reset mSpaceState, it has already be done (that's why we
   1459                     // receive it as a parameter)
   1460                     return;
   1461                 }
   1462             } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
   1463                 if (revertSwapPunctuation(ic)) {
   1464                     // Likewise
   1465                     return;
   1466                 }
   1467             }
   1468 
   1469             // No cancelling of commit/double space/swap: we have a regular backspace.
   1470             // We should backspace one char and restart suggestion if at the end of a word.
   1471             if (mLastSelectionStart != mLastSelectionEnd) {
   1472                 // If there is a selection, remove it.
   1473                 final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
   1474                 ic.setSelection(mLastSelectionEnd, mLastSelectionEnd);
   1475                 ic.deleteSurroundingText(lengthToDelete, 0);
   1476                 if (ProductionFlag.IS_EXPERIMENTAL) {
   1477                     ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
   1478                 }
   1479             } else {
   1480                 // There is no selection, just delete one character.
   1481                 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
   1482                     // This should never happen.
   1483                     Log.e(TAG, "Backspace when we don't know the selection position");
   1484                 }
   1485                 // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
   1486                 // we want to be able to compile against the Ice Cream Sandwich SDK.
   1487                 if (mTargetApplicationInfo != null
   1488                         && mTargetApplicationInfo.targetSdkVersion < 16) {
   1489                     // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
   1490                     // a hardware keyboard event on pressing enter or delete. This is bad for many
   1491                     // reasons (there are race conditions with commits) but some applications are
   1492                     // relying on this behavior so we continue to support it for older apps.
   1493                     sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL, ic);
   1494                 } else {
   1495                     ic.deleteSurroundingText(1, 0);
   1496                 }
   1497                 if (ProductionFlag.IS_EXPERIMENTAL) {
   1498                     ResearchLogger.latinIME_deleteSurroundingText(1);
   1499                 }
   1500                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
   1501                     ic.deleteSurroundingText(1, 0);
   1502                     if (ProductionFlag.IS_EXPERIMENTAL) {
   1503                         ResearchLogger.latinIME_deleteSurroundingText(1);
   1504                     }
   1505                 }
   1506             }
   1507             if (isSuggestionsRequested()) {
   1508                 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic);
   1509             }
   1510         }
   1511     }
   1512 
   1513     // ic may be null
   1514     private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code,
   1515             final int spaceState, final boolean isFromSuggestionStrip) {
   1516         if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
   1517             removeTrailingSpaceWhileInBatchEdit(ic);
   1518             return false;
   1519         } else if ((SPACE_STATE_WEAK == spaceState
   1520                 || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
   1521                 && isFromSuggestionStrip) {
   1522             if (mSettingsValues.isWeakSpaceSwapper(code)) {
   1523                 return true;
   1524             } else {
   1525                 if (mSettingsValues.isWeakSpaceStripper(code)) {
   1526                     removeTrailingSpaceWhileInBatchEdit(ic);
   1527                 }
   1528                 return false;
   1529             }
   1530         } else {
   1531             return false;
   1532         }
   1533     }
   1534 
   1535     private void handleCharacter(final int primaryCode, final int x,
   1536             final int y, final int spaceState) {
   1537         final InputConnection ic = getCurrentInputConnection();
   1538         if (null != ic) ic.beginBatchEdit();
   1539         // TODO: if ic is null, does it make any sense to call this?
   1540         handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic);
   1541         if (null != ic) ic.endBatchEdit();
   1542     }
   1543 
   1544     // "ic" may be null without this crashing, but the behavior will be really strange
   1545     private void handleCharacterWhileInBatchEdit(final int primaryCode,
   1546             final int x, final int y, final int spaceState, final InputConnection ic) {
   1547         boolean isComposingWord = mWordComposer.isComposingWord();
   1548 
   1549         if (SPACE_STATE_PHANTOM == spaceState &&
   1550                 !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) {
   1551             if (isComposingWord) {
   1552                 // Sanity check
   1553                 throw new RuntimeException("Should not be composing here");
   1554             }
   1555             sendKeyCodePoint(Keyboard.CODE_SPACE);
   1556         }
   1557 
   1558         // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
   1559         // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
   1560         // thread here.
   1561         if (!isComposingWord && (isAlphabet(primaryCode)
   1562                 || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode))
   1563                 && isSuggestionsRequested() && !isCursorTouchingWord()) {
   1564             // Reset entirely the composing state anyway, then start composing a new word unless
   1565             // the character is a single quote. The idea here is, single quote is not a
   1566             // separator and it should be treated as a normal character, except in the first
   1567             // position where it should not start composing a word.
   1568             isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode);
   1569             // Here we don't need to reset the last composed word. It will be reset
   1570             // when we commit this one, if we ever do; if on the other hand we backspace
   1571             // it entirely and resume suggestions on the previous word, we'd like to still
   1572             // have touch coordinates for it.
   1573             resetComposingState(false /* alsoResetLastComposedWord */);
   1574             clearSuggestions();
   1575         }
   1576         if (isComposingWord) {
   1577             mWordComposer.add(
   1578                     primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
   1579             if (ic != null) {
   1580                 // If it's the first letter, make note of auto-caps state
   1581                 if (mWordComposer.size() == 1) {
   1582                     mWordComposer.setAutoCapitalized(
   1583                             getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
   1584                 }
   1585                 ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
   1586             }
   1587             mHandler.postUpdateSuggestions();
   1588         } else {
   1589             final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode,
   1590                     spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
   1591 
   1592             sendKeyCodePoint(primaryCode);
   1593 
   1594             if (swapWeakSpace) {
   1595                 swapSwapperAndSpaceWhileInBatchEdit(ic);
   1596                 mSpaceState = SPACE_STATE_WEAK;
   1597             }
   1598             // Some characters are not word separators, yet they don't start a new
   1599             // composing span. For these, we haven't changed the suggestion strip, and
   1600             // if the "add to dictionary" hint is shown, we should do so now. Examples of
   1601             // such characters include single quote, dollar, and others; the exact list is
   1602             // the list of characters for which we enter handleCharacterWhileInBatchEdit
   1603             // that don't match the test if ((isAlphabet...)) at the top of this method.
   1604             if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) {
   1605                 mHandler.postUpdateBigramPredictions();
   1606             }
   1607         }
   1608         Utils.Stats.onNonSeparator((char)primaryCode, x, y);
   1609     }
   1610 
   1611     // Returns true if we did an autocorrection, false otherwise.
   1612     private boolean handleSeparator(final int primaryCode, final int x, final int y,
   1613             final int spaceState) {
   1614         // Should dismiss the "Touch again to save" message when handling separator
   1615         if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
   1616             mHandler.cancelUpdateBigramPredictions();
   1617             mHandler.postUpdateSuggestions();
   1618         }
   1619 
   1620         boolean didAutoCorrect = false;
   1621         // Handle separator
   1622         final InputConnection ic = getCurrentInputConnection();
   1623         if (ic != null) {
   1624             ic.beginBatchEdit();
   1625         }
   1626         if (mWordComposer.isComposingWord()) {
   1627             // In certain languages where single quote is a separator, it's better
   1628             // not to auto correct, but accept the typed word. For instance,
   1629             // in Italian dov' should not be expanded to dove' because the elision
   1630             // requires the last vowel to be removed.
   1631             final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
   1632                     && !mInputAttributes.mInputTypeNoAutoCorrect;
   1633             if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
   1634                 commitCurrentAutoCorrection(primaryCode, ic);
   1635                 didAutoCorrect = true;
   1636             } else {
   1637                 commitTyped(ic, primaryCode);
   1638             }
   1639         }
   1640 
   1641         final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState,
   1642                 KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
   1643 
   1644         if (SPACE_STATE_PHANTOM == spaceState &&
   1645                 mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) {
   1646             sendKeyCodePoint(Keyboard.CODE_SPACE);
   1647         }
   1648         sendKeyCodePoint(primaryCode);
   1649 
   1650         if (Keyboard.CODE_SPACE == primaryCode) {
   1651             if (isSuggestionsRequested()) {
   1652                 if (maybeDoubleSpaceWhileInBatchEdit(ic)) {
   1653                     mSpaceState = SPACE_STATE_DOUBLE;
   1654                 } else if (!isShowingPunctuationList()) {
   1655                     mSpaceState = SPACE_STATE_WEAK;
   1656                 }
   1657             }
   1658 
   1659             mHandler.startDoubleSpacesTimer();
   1660             if (!isCursorTouchingWord()) {
   1661                 mHandler.cancelUpdateSuggestions();
   1662                 mHandler.postUpdateBigramPredictions();
   1663             }
   1664         } else {
   1665             if (swapWeakSpace) {
   1666                 swapSwapperAndSpaceWhileInBatchEdit(ic);
   1667                 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
   1668             } else if (SPACE_STATE_PHANTOM == spaceState) {
   1669                 // If we are in phantom space state, and the user presses a separator, we want to
   1670                 // stay in phantom space state so that the next keypress has a chance to add the
   1671                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
   1672                 // then insert a comma and go on to typing the next word, I want the space to be
   1673                 // inserted automatically before the next word, the same way it is when I don't
   1674                 // input the comma.
   1675                 mSpaceState = SPACE_STATE_PHANTOM;
   1676             }
   1677 
   1678             // Set punctuation right away. onUpdateSelection will fire but tests whether it is
   1679             // already displayed or not, so it's okay.
   1680             setPunctuationSuggestions();
   1681         }
   1682 
   1683         Utils.Stats.onSeparator((char)primaryCode, x, y);
   1684 
   1685         if (ic != null) {
   1686             ic.endBatchEdit();
   1687         }
   1688         return didAutoCorrect;
   1689     }
   1690 
   1691     private CharSequence getTextWithUnderline(final CharSequence text) {
   1692         return mIsAutoCorrectionIndicatorOn
   1693                 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
   1694                 : text;
   1695     }
   1696 
   1697     private void handleClose() {
   1698         commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR);
   1699         requestHideSelf(0);
   1700         LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
   1701         if (inputView != null)
   1702             inputView.closing();
   1703     }
   1704 
   1705     public boolean isSuggestionsRequested() {
   1706         return mInputAttributes.mIsSettingsSuggestionStripOn
   1707                 && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
   1708     }
   1709 
   1710     public boolean isShowingPunctuationList() {
   1711         if (mSuggestionsView == null) return false;
   1712         return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
   1713     }
   1714 
   1715     public boolean isShowingSuggestionsStrip() {
   1716         return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
   1717                 || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
   1718                         && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
   1719     }
   1720 
   1721     public boolean isSuggestionsStripVisible() {
   1722         if (mSuggestionsView == null)
   1723             return false;
   1724         if (mSuggestionsView.isShowingAddToDictionaryHint())
   1725             return true;
   1726         if (!isShowingSuggestionsStrip())
   1727             return false;
   1728         if (mInputAttributes.mApplicationSpecifiedCompletionOn)
   1729             return true;
   1730         return isSuggestionsRequested();
   1731     }
   1732 
   1733     public void switchToKeyboardView() {
   1734         if (DEBUG) {
   1735             Log.d(TAG, "Switch to keyboard view.");
   1736         }
   1737         if (ProductionFlag.IS_EXPERIMENTAL) {
   1738             ResearchLogger.latinIME_switchToKeyboardView();
   1739         }
   1740         View v = mKeyboardSwitcher.getKeyboardView();
   1741         if (v != null) {
   1742             // Confirms that the keyboard view doesn't have parent view.
   1743             ViewParent p = v.getParent();
   1744             if (p != null && p instanceof ViewGroup) {
   1745                 ((ViewGroup) p).removeView(v);
   1746             }
   1747             setInputView(v);
   1748         }
   1749         setSuggestionStripShown(isSuggestionsStripVisible());
   1750         updateInputViewShown();
   1751         mHandler.postUpdateSuggestions();
   1752     }
   1753 
   1754     public void clearSuggestions() {
   1755         setSuggestions(SuggestedWords.EMPTY, false);
   1756         setAutoCorrectionIndicator(false);
   1757     }
   1758 
   1759     private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) {
   1760         if (mSuggestionsView != null) {
   1761             mSuggestionsView.setSuggestions(words);
   1762             mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
   1763         }
   1764     }
   1765 
   1766     private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
   1767         // Put a blue underline to a word in TextView which will be auto-corrected.
   1768         final InputConnection ic = getCurrentInputConnection();
   1769         if (ic == null) return;
   1770         if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
   1771                 && mWordComposer.isComposingWord()) {
   1772             mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
   1773             final CharSequence textWithUnderline =
   1774                     getTextWithUnderline(mWordComposer.getTypedWord());
   1775             ic.setComposingText(textWithUnderline, 1);
   1776         }
   1777     }
   1778 
   1779     public void updateSuggestions() {
   1780         // Check if we have a suggestion engine attached.
   1781         if ((mSuggest == null || !isSuggestionsRequested())) {
   1782             if (mWordComposer.isComposingWord()) {
   1783                 Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
   1784                 mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
   1785             }
   1786             return;
   1787         }
   1788 
   1789         mHandler.cancelUpdateSuggestions();
   1790         mHandler.cancelUpdateBigramPredictions();
   1791 
   1792         if (!mWordComposer.isComposingWord()) {
   1793             setPunctuationSuggestions();
   1794             return;
   1795         }
   1796 
   1797         // TODO: May need a better way of retrieving previous word
   1798         final InputConnection ic = getCurrentInputConnection();
   1799         final CharSequence prevWord;
   1800         if (null == ic) {
   1801             prevWord = null;
   1802         } else {
   1803             prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
   1804         }
   1805 
   1806         final CharSequence typedWord = mWordComposer.getTypedWord();
   1807         // getSuggestedWords handles gracefully a null value of prevWord
   1808         final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
   1809                 prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode);
   1810 
   1811         // Basically, we update the suggestion strip only when suggestion count > 1.  However,
   1812         // there is an exception: We update the suggestion strip whenever typed word's length
   1813         // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
   1814         // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
   1815         // need to clear the previous state when the user starts typing a word (i.e. typed word's
   1816         // length == 1).
   1817         if (suggestedWords.size() > 1 || typedWord.length() == 1
   1818                 || !suggestedWords.mAllowsToBeAutoCorrected
   1819                 || mSuggestionsView.isShowingAddToDictionaryHint()) {
   1820             showSuggestions(suggestedWords, typedWord);
   1821         } else {
   1822             SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
   1823             if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
   1824                 previousSuggestions = SuggestedWords.EMPTY;
   1825             }
   1826             final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
   1827                     SuggestedWords.getTypedWordAndPreviousSuggestions(
   1828                             typedWord, previousSuggestions);
   1829             final SuggestedWords obsoleteSuggestedWords =
   1830                     new SuggestedWords(typedWordAndPreviousSuggestions,
   1831                             false /* typedWordValid */,
   1832                             false /* hasAutoCorrectionCandidate */,
   1833                             false /* allowsToBeAutoCorrected */,
   1834                             false /* isPunctuationSuggestions */,
   1835                             true /* isObsoleteSuggestions */,
   1836                             false /* isPrediction */);
   1837             showSuggestions(obsoleteSuggestedWords, typedWord);
   1838         }
   1839     }
   1840 
   1841     public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) {
   1842         final CharSequence autoCorrection;
   1843         if (suggestedWords.size() > 0) {
   1844             if (suggestedWords.hasAutoCorrectionWord()) {
   1845                 autoCorrection = suggestedWords.getWord(1);
   1846             } else {
   1847                 autoCorrection = typedWord;
   1848             }
   1849         } else {
   1850             autoCorrection = null;
   1851         }
   1852         mWordComposer.setAutoCorrection(autoCorrection);
   1853         final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
   1854         setSuggestions(suggestedWords, isAutoCorrection);
   1855         setAutoCorrectionIndicator(isAutoCorrection);
   1856         setSuggestionStripShown(isSuggestionsStripVisible());
   1857     }
   1858 
   1859     private void commitCurrentAutoCorrection(final int separatorCodePoint,
   1860             final InputConnection ic) {
   1861         // Complete any pending suggestions query first
   1862         if (mHandler.hasPendingUpdateSuggestions()) {
   1863             mHandler.cancelUpdateSuggestions();
   1864             updateSuggestions();
   1865         }
   1866         final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
   1867         if (autoCorrection != null) {
   1868             final String typedWord = mWordComposer.getTypedWord();
   1869             if (TextUtils.isEmpty(typedWord)) {
   1870                 throw new RuntimeException("We have an auto-correction but the typed word "
   1871                         + "is empty? Impossible! I must commit suicide.");
   1872             }
   1873             Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
   1874             if (ProductionFlag.IS_EXPERIMENTAL) {
   1875                 ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord,
   1876                         autoCorrection.toString());
   1877             }
   1878             mExpectingUpdateSelection = true;
   1879             commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
   1880                     separatorCodePoint);
   1881             if (!typedWord.equals(autoCorrection) && null != ic) {
   1882                 // This will make the correction flash for a short while as a visual clue
   1883                 // to the user that auto-correction happened.
   1884                 ic.commitCorrection(new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
   1885                         typedWord, autoCorrection));
   1886             }
   1887         }
   1888     }
   1889 
   1890     @Override
   1891     public void pickSuggestionManually(final int index, final CharSequence suggestion,
   1892             int x, int y) {
   1893         final InputConnection ic = getCurrentInputConnection();
   1894         if (null != ic) ic.beginBatchEdit();
   1895         pickSuggestionManuallyWhileInBatchEdit(index, suggestion, x, y, ic);
   1896         if (null != ic) ic.endBatchEdit();
   1897     }
   1898 
   1899     public void pickSuggestionManuallyWhileInBatchEdit(final int index,
   1900         final CharSequence suggestion, final int x, final int y, final InputConnection ic) {
   1901         final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
   1902         // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
   1903         if (suggestion.length() == 1 && isShowingPunctuationList()) {
   1904             // Word separators are suggested before the user inputs something.
   1905             // So, LatinImeLogger logs "" as a user's input.
   1906             LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
   1907             // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
   1908             if (ProductionFlag.IS_EXPERIMENTAL) {
   1909                 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y);
   1910             }
   1911             final int primaryCode = suggestion.charAt(0);
   1912             onCodeInput(primaryCode,
   1913                     KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
   1914                     KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
   1915             return;
   1916         }
   1917 
   1918         if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
   1919             int firstChar = Character.codePointAt(suggestion, 0);
   1920             if ((!mSettingsValues.isWeakSpaceStripper(firstChar))
   1921                     && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) {
   1922                 sendKeyCodePoint(Keyboard.CODE_SPACE);
   1923             }
   1924         }
   1925 
   1926         if (mInputAttributes.mApplicationSpecifiedCompletionOn
   1927                 && mApplicationSpecifiedCompletions != null
   1928                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
   1929             if (mSuggestionsView != null) {
   1930                 mSuggestionsView.clear();
   1931             }
   1932             mKeyboardSwitcher.updateShiftState();
   1933             resetComposingState(true /* alsoResetLastComposedWord */);
   1934             if (ic != null) {
   1935                 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
   1936                 ic.commitCompletion(completionInfo);
   1937                 if (ProductionFlag.IS_EXPERIMENTAL) {
   1938                     ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
   1939                             completionInfo.getText(), x, y);
   1940                 }
   1941             }
   1942             return;
   1943         }
   1944 
   1945         // We need to log before we commit, because the word composer will store away the user
   1946         // typed word.
   1947         final String replacedWord = mWordComposer.getTypedWord().toString();
   1948         LatinImeLogger.logOnManualSuggestion(replacedWord,
   1949                 suggestion.toString(), index, suggestedWords);
   1950         if (ProductionFlag.IS_EXPERIMENTAL) {
   1951             ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y);
   1952         }
   1953         mExpectingUpdateSelection = true;
   1954         commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
   1955                 LastComposedWord.NOT_A_SEPARATOR);
   1956         // Don't allow cancellation of manual pick
   1957         mLastComposedWord.deactivate();
   1958         mSpaceState = SPACE_STATE_PHANTOM;
   1959         // TODO: is this necessary?
   1960         mKeyboardSwitcher.updateShiftState();
   1961 
   1962         // We should show the "Touch again to save" hint if the user pressed the first entry
   1963         // AND either:
   1964         // - There is no dictionary (we know that because we tried to load it => null != mSuggest
   1965         //   AND mSuggest.hasMainDictionary() is false)
   1966         // - There is a dictionary and the word is not in it
   1967         // Please note that if mSuggest is null, it means that everything is off: suggestion
   1968         // and correction, so we shouldn't try to show the hint
   1969         // We used to look at mCorrectionMode here, but showing the hint should have nothing
   1970         // to do with the autocorrection setting.
   1971         final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
   1972                 // If there is no dictionary the hint should be shown.
   1973                 && (!mSuggest.hasMainDictionary()
   1974                         // If "suggestion" is not in the dictionary, the hint should be shown.
   1975                         || !AutoCorrection.isValidWord(
   1976                                 mSuggest.getUnigramDictionaries(), suggestion, true));
   1977 
   1978         Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE,
   1979                 WordComposer.NOT_A_COORDINATE);
   1980         if (!showingAddToDictionaryHint) {
   1981             // If we're not showing the "Touch again to save", then show corrections again.
   1982             // In case the cursor position doesn't change, make sure we show the suggestions again.
   1983             updateBigramPredictions();
   1984             // Updating the predictions right away may be slow and feel unresponsive on slower
   1985             // terminals. On the other hand if we just postUpdateBigramPredictions() it will
   1986             // take a noticeable delay to update them which may feel uneasy.
   1987         } else {
   1988             if (mIsUserDictionaryAvailable) {
   1989                 mSuggestionsView.showAddToDictionaryHint(
   1990                         suggestion, mSettingsValues.mHintToSaveText);
   1991             } else {
   1992                 mHandler.postUpdateSuggestions();
   1993             }
   1994         }
   1995     }
   1996 
   1997     /**
   1998      * Commits the chosen word to the text field and saves it for later retrieval.
   1999      */
   2000     private void commitChosenWord(final CharSequence chosenWord, final int commitType,
   2001             final int separatorCode) {
   2002         final InputConnection ic = getCurrentInputConnection();
   2003         if (ic != null) {
   2004             if (mSettingsValues.mEnableSuggestionSpanInsertion) {
   2005                 final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
   2006                 ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
   2007                         this, chosenWord, suggestedWords, mIsMainDictionaryAvailable),
   2008                         1);
   2009                 if (ProductionFlag.IS_EXPERIMENTAL) {
   2010                     ResearchLogger.latinIME_commitText(chosenWord);
   2011                 }
   2012             } else {
   2013                 ic.commitText(chosenWord, 1);
   2014                 if (ProductionFlag.IS_EXPERIMENTAL) {
   2015                     ResearchLogger.latinIME_commitText(chosenWord);
   2016                 }
   2017             }
   2018         }
   2019         // Add the word to the user history dictionary
   2020         final CharSequence prevWord = addToUserHistoryDictionary(chosenWord);
   2021         // TODO: figure out here if this is an auto-correct or if the best word is actually
   2022         // what user typed. Note: currently this is done much later in
   2023         // LastComposedWord#didCommitTypedWord by string equality of the remembered
   2024         // strings.
   2025         mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord.toString(),
   2026                 separatorCode, prevWord);
   2027     }
   2028 
   2029     public void updateBigramPredictions() {
   2030         if (mSuggest == null || !isSuggestionsRequested())
   2031             return;
   2032 
   2033         if (!mSettingsValues.mBigramPredictionEnabled) {
   2034             setPunctuationSuggestions();
   2035             return;
   2036         }
   2037 
   2038         final SuggestedWords suggestedWords;
   2039         if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
   2040             final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
   2041                     mSettingsValues.mWordSeparators);
   2042             if (!TextUtils.isEmpty(prevWord)) {
   2043                 suggestedWords = mSuggest.getBigramPredictions(prevWord);
   2044             } else {
   2045                 suggestedWords = null;
   2046             }
   2047         } else {
   2048             suggestedWords = null;
   2049         }
   2050 
   2051         if (null != suggestedWords && suggestedWords.size() > 0) {
   2052             // Explicitly supply an empty typed word (the no-second-arg version of
   2053             // showSuggestions will retrieve the word near the cursor, we don't want that here)
   2054             showSuggestions(suggestedWords, "");
   2055         } else {
   2056             clearSuggestions();
   2057         }
   2058     }
   2059 
   2060     public void setPunctuationSuggestions() {
   2061         if (mSettingsValues.mBigramPredictionEnabled) {
   2062             clearSuggestions();
   2063         } else {
   2064             setSuggestions(mSettingsValues.mSuggestPuncList, false);
   2065         }
   2066         setAutoCorrectionIndicator(false);
   2067         setSuggestionStripShown(isSuggestionsStripVisible());
   2068     }
   2069 
   2070     private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) {
   2071         if (TextUtils.isEmpty(suggestion)) return null;
   2072 
   2073         // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
   2074         // adding words in situations where the user or application really didn't
   2075         // want corrections enabled or learned.
   2076         if (!(mCorrectionMode == Suggest.CORRECTION_FULL
   2077                 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
   2078             return null;
   2079         }
   2080 
   2081         if (mUserHistoryDictionary != null) {
   2082             final InputConnection ic = getCurrentInputConnection();
   2083             final CharSequence prevWord;
   2084             if (null != ic) {
   2085                 prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
   2086             } else {
   2087                 prevWord = null;
   2088             }
   2089             final String secondWord;
   2090             if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
   2091                 secondWord = suggestion.toString().toLowerCase(
   2092                         mSubtypeSwitcher.getCurrentSubtypeLocale());
   2093             } else {
   2094                 secondWord = suggestion.toString();
   2095             }
   2096             // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
   2097             // We don't add words with 0-frequency (assuming they would be profanity etc.).
   2098             final int maxFreq = AutoCorrection.getMaxFrequency(
   2099                     mSuggest.getUnigramDictionaries(), suggestion);
   2100             if (maxFreq == 0) return null;
   2101             mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
   2102                     secondWord, maxFreq > 0);
   2103             return prevWord;
   2104         }
   2105         return null;
   2106     }
   2107 
   2108     public boolean isCursorTouchingWord() {
   2109         final InputConnection ic = getCurrentInputConnection();
   2110         if (ic == null) return false;
   2111         CharSequence before = ic.getTextBeforeCursor(1, 0);
   2112         CharSequence after = ic.getTextAfterCursor(1, 0);
   2113         if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0))
   2114                 && !mSettingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
   2115             return true;
   2116         }
   2117         if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0))
   2118                 && !mSettingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
   2119             return true;
   2120         }
   2121         return false;
   2122     }
   2123 
   2124     // "ic" must not be null
   2125     private static boolean sameAsTextBeforeCursor(final InputConnection ic,
   2126             final CharSequence text) {
   2127         final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
   2128         return TextUtils.equals(text, beforeText);
   2129     }
   2130 
   2131     // "ic" must not be null
   2132     /**
   2133      * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
   2134      * word, else do nothing.
   2135      */
   2136     private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
   2137             final InputConnection ic) {
   2138         // Bail out if the cursor is not at the end of a word (cursor must be preceded by
   2139         // non-whitespace, non-separator, non-start-of-text)
   2140         // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
   2141         final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0);
   2142         if (TextUtils.isEmpty(textBeforeCursor)
   2143                 || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return;
   2144 
   2145         // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
   2146         // separator or end of line/text)
   2147         // Example: "test|"<EOL> "te|st" get rejected here
   2148         final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0);
   2149         if (!TextUtils.isEmpty(textAfterCursor)
   2150                 && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return;
   2151 
   2152         // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
   2153         // Example: " -|" gets rejected here but "e-|" and "e|" are okay
   2154         CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators);
   2155         // We don't suggest on leading single quotes, so we have to remove them from the word if
   2156         // it starts with single quotes.
   2157         while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
   2158             word = word.subSequence(1, word.length());
   2159         }
   2160         if (TextUtils.isEmpty(word)) return;
   2161         final char firstChar = word.charAt(0); // we just tested that word is not empty
   2162         if (word.length() == 1 && !Character.isLetter(firstChar)) return;
   2163 
   2164         // We only suggest on words that start with a letter or a symbol that is excluded from
   2165         // word separators (see #handleCharacterWhileInBatchEdit).
   2166         if (!(isAlphabet(firstChar)
   2167                 || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) {
   2168             return;
   2169         }
   2170 
   2171         // Okay, we are at the end of a word. Restart suggestions.
   2172         restartSuggestionsOnWordBeforeCursor(ic, word);
   2173     }
   2174 
   2175     // "ic" must not be null
   2176     private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic,
   2177             final CharSequence word) {
   2178         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
   2179         final int length = word.length();
   2180         ic.deleteSurroundingText(length, 0);
   2181         if (ProductionFlag.IS_EXPERIMENTAL) {
   2182             ResearchLogger.latinIME_deleteSurroundingText(length);
   2183         }
   2184         ic.setComposingText(word, 1);
   2185         mHandler.postUpdateSuggestions();
   2186     }
   2187 
   2188     // "ic" must not be null
   2189     private void revertCommit(final InputConnection ic) {
   2190         final CharSequence previousWord = mLastComposedWord.mPrevWord;
   2191         final String originallyTypedWord = mLastComposedWord.mTypedWord;
   2192         final CharSequence committedWord = mLastComposedWord.mCommittedWord;
   2193         final int cancelLength = committedWord.length();
   2194         final int separatorLength = LastComposedWord.getSeparatorLength(
   2195                 mLastComposedWord.mSeparatorCode);
   2196         // TODO: should we check our saved separator against the actual contents of the text view?
   2197         final int deleteLength = cancelLength + separatorLength;
   2198         if (DEBUG) {
   2199             if (mWordComposer.isComposingWord()) {
   2200                 throw new RuntimeException("revertCommit, but we are composing a word");
   2201             }
   2202             final String wordBeforeCursor =
   2203                     ic.getTextBeforeCursor(deleteLength, 0)
   2204                             .subSequence(0, cancelLength).toString();
   2205             if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
   2206                 throw new RuntimeException("revertCommit check failed: we thought we were "
   2207                         + "reverting \"" + committedWord
   2208                         + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
   2209             }
   2210         }
   2211         ic.deleteSurroundingText(deleteLength, 0);
   2212         if (ProductionFlag.IS_EXPERIMENTAL) {
   2213             ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
   2214         }
   2215         if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
   2216             mUserHistoryDictionary.cancelAddingUserHistory(
   2217                     previousWord.toString(), committedWord.toString());
   2218         }
   2219         if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) {
   2220             // This is the case when we cancel a manual pick.
   2221             // We should restart suggestion on the word right away.
   2222             mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
   2223             ic.setComposingText(originallyTypedWord, 1);
   2224         } else {
   2225             ic.commitText(originallyTypedWord, 1);
   2226             // Re-insert the separator
   2227             sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
   2228             Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
   2229                     WordComposer.NOT_A_COORDINATE);
   2230             if (ProductionFlag.IS_EXPERIMENTAL) {
   2231                 ResearchLogger.latinIME_revertCommit(originallyTypedWord);
   2232             }
   2233             // Don't restart suggestion yet. We'll restart if the user deletes the
   2234             // separator.
   2235         }
   2236         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
   2237         mHandler.cancelUpdateBigramPredictions();
   2238         mHandler.postUpdateSuggestions();
   2239     }
   2240 
   2241     // "ic" must not be null
   2242     private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
   2243         mHandler.cancelDoubleSpacesTimer();
   2244         // Here we test whether we indeed have a period and a space before us. This should not
   2245         // be needed, but it's there just in case something went wrong.
   2246         final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
   2247         if (!". ".equals(textBeforeCursor)) {
   2248             // Theoretically we should not be coming here if there isn't ". " before the
   2249             // cursor, but the application may be changing the text while we are typing, so
   2250             // anything goes. We should not crash.
   2251             Log.d(TAG, "Tried to revert double-space combo but we didn't find "
   2252                     + "\". \" just before the cursor.");
   2253             return false;
   2254         }
   2255         ic.deleteSurroundingText(2, 0);
   2256         if (ProductionFlag.IS_EXPERIMENTAL) {
   2257             ResearchLogger.latinIME_deleteSurroundingText(2);
   2258         }
   2259         ic.commitText("  ", 1);
   2260         if (ProductionFlag.IS_EXPERIMENTAL) {
   2261             ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
   2262         }
   2263         return true;
   2264     }
   2265 
   2266     private static boolean revertSwapPunctuation(final InputConnection ic) {
   2267         // Here we test whether we indeed have a space and something else before us. This should not
   2268         // be needed, but it's there just in case something went wrong.
   2269         final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
   2270         // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
   2271         // enter surrogate pairs this code will have been removed.
   2272         if (TextUtils.isEmpty(textBeforeCursor)
   2273                 || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
   2274             // We may only come here if the application is changing the text while we are typing.
   2275             // This is quite a broken case, but not logically impossible, so we shouldn't crash,
   2276             // but some debugging log may be in order.
   2277             Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
   2278                     + "find a space just before the cursor.");
   2279             return false;
   2280         }
   2281         ic.beginBatchEdit();
   2282         ic.deleteSurroundingText(2, 0);
   2283         if (ProductionFlag.IS_EXPERIMENTAL) {
   2284             ResearchLogger.latinIME_deleteSurroundingText(2);
   2285         }
   2286         ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
   2287         if (ProductionFlag.IS_EXPERIMENTAL) {
   2288             ResearchLogger.latinIME_revertSwapPunctuation();
   2289         }
   2290         ic.endBatchEdit();
   2291         return true;
   2292     }
   2293 
   2294     public boolean isWordSeparator(int code) {
   2295         return mSettingsValues.isWordSeparator(code);
   2296     }
   2297 
   2298     public boolean preferCapitalization() {
   2299         return mWordComposer.isFirstCharCapitalized();
   2300     }
   2301 
   2302     // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
   2303     // according to new language or mode.
   2304     public void onRefreshKeyboard() {
   2305         // When the device locale is changed in SetupWizard etc., this method may get called via
   2306         // onConfigurationChanged before SoftInputWindow is shown.
   2307         if (mKeyboardSwitcher.getKeyboardView() != null) {
   2308             // Reload keyboard because the current language has been changed.
   2309             mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
   2310         }
   2311         initSuggest();
   2312         updateCorrectionMode();
   2313         loadSettings();
   2314         // Since we just changed languages, we should re-evaluate suggestions with whatever word
   2315         // we are currently composing. If we are not composing anything, we may want to display
   2316         // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
   2317         if (isCursorTouchingWord()) {
   2318             mHandler.postUpdateSuggestions();
   2319         } else {
   2320             mHandler.postUpdateBigramPredictions();
   2321         }
   2322     }
   2323 
   2324     // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to
   2325     // {@link KeyboardSwitcher}.
   2326     public void hapticAndAudioFeedback(final int primaryCode) {
   2327         mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView());
   2328     }
   2329 
   2330     @Override
   2331     public void onPressKey(int primaryCode) {
   2332         mKeyboardSwitcher.onPressKey(primaryCode);
   2333     }
   2334 
   2335     @Override
   2336     public void onReleaseKey(int primaryCode, boolean withSliding) {
   2337         mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
   2338 
   2339         // If accessibility is on, ensure the user receives keyboard state updates.
   2340         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
   2341             switch (primaryCode) {
   2342             case Keyboard.CODE_SHIFT:
   2343                 AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
   2344                 break;
   2345             case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
   2346                 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
   2347                 break;
   2348             }
   2349         }
   2350 
   2351         if (Keyboard.CODE_DELETE == primaryCode) {
   2352             // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
   2353             // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
   2354             // pair-friendly way of deleting characters in InputConnection.
   2355             final InputConnection ic = getCurrentInputConnection();
   2356             if (null != ic) {
   2357                 final CharSequence lastChar = ic.getTextBeforeCursor(1, 0);
   2358                 if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
   2359                     ic.deleteSurroundingText(1, 0);
   2360                 }
   2361             }
   2362         }
   2363     }
   2364 
   2365     // receive ringer mode change and network state change.
   2366     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
   2367         @Override
   2368         public void onReceive(Context context, Intent intent) {
   2369             final String action = intent.getAction();
   2370             if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
   2371                 mSubtypeSwitcher.onNetworkStateChanged(intent);
   2372             } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
   2373                 mFeedbackManager.onRingerModeChanged();
   2374             }
   2375         }
   2376     };
   2377 
   2378     private void updateCorrectionMode() {
   2379         // TODO: cleanup messy flags
   2380         final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
   2381                 && !mInputAttributes.mInputTypeNoAutoCorrect;
   2382         mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE;
   2383         mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect)
   2384                 ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
   2385     }
   2386 
   2387     private void updateSuggestionVisibility(final Resources res) {
   2388         final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting;
   2389         for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
   2390             if (suggestionVisiblityStr.equals(res.getString(visibility))) {
   2391                 mSuggestionVisibility = visibility;
   2392                 break;
   2393             }
   2394         }
   2395     }
   2396 
   2397     private void launchSettings() {
   2398         launchSettingsClass(SettingsActivity.class);
   2399     }
   2400 
   2401     public void launchDebugSettings() {
   2402         launchSettingsClass(DebugSettingsActivity.class);
   2403     }
   2404 
   2405     private void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
   2406         handleClose();
   2407         Intent intent = new Intent();
   2408         intent.setClass(LatinIME.this, settingsClass);
   2409         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   2410         startActivity(intent);
   2411     }
   2412 
   2413     private void showSubtypeSelectorAndSettings() {
   2414         final CharSequence title = getString(R.string.english_ime_input_options);
   2415         final CharSequence[] items = new CharSequence[] {
   2416                 // TODO: Should use new string "Select active input modes".
   2417                 getString(R.string.language_selection_title),
   2418                 getString(R.string.english_ime_settings),
   2419         };
   2420         final Context context = this;
   2421         final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
   2422             @Override
   2423             public void onClick(DialogInterface di, int position) {
   2424                 di.dismiss();
   2425                 switch (position) {
   2426                 case 0:
   2427                     Intent intent = CompatUtils.getInputLanguageSelectionIntent(
   2428                             ImfUtils.getInputMethodIdOfThisIme(context),
   2429                             Intent.FLAG_ACTIVITY_NEW_TASK
   2430                             | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
   2431                             | Intent.FLAG_ACTIVITY_CLEAR_TOP);
   2432                     startActivity(intent);
   2433                     break;
   2434                 case 1:
   2435                     launchSettings();
   2436                     break;
   2437                 }
   2438             }
   2439         };
   2440         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
   2441                 .setItems(items, listener)
   2442                 .setTitle(title);
   2443         showOptionDialogInternal(builder.create());
   2444     }
   2445 
   2446     private void showOptionDialogInternal(AlertDialog dialog) {
   2447         final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
   2448         if (windowToken == null) return;
   2449 
   2450         dialog.setCancelable(true);
   2451         dialog.setCanceledOnTouchOutside(true);
   2452 
   2453         final Window window = dialog.getWindow();
   2454         final WindowManager.LayoutParams lp = window.getAttributes();
   2455         lp.token = windowToken;
   2456         lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
   2457         window.setAttributes(lp);
   2458         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
   2459 
   2460         mOptionsDialog = dialog;
   2461         dialog.show();
   2462     }
   2463 
   2464     @Override
   2465     protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
   2466         super.dump(fd, fout, args);
   2467 
   2468         final Printer p = new PrintWriterPrinter(fout);
   2469         p.println("LatinIME state :");
   2470         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   2471         final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
   2472         p.println("  Keyboard mode = " + keyboardMode);
   2473         p.println("  mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn);
   2474         p.println("  mCorrectionMode=" + mCorrectionMode);
   2475         p.println("  isComposingWord=" + mWordComposer.isComposingWord());
   2476         p.println("  mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
   2477         p.println("  mSoundOn=" + mSettingsValues.mSoundOn);
   2478         p.println("  mVibrateOn=" + mSettingsValues.mVibrateOn);
   2479         p.println("  mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
   2480         p.println("  mInputAttributes=" + mInputAttributes.toString());
   2481     }
   2482 }
   2483