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");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of 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,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under 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.Activity;
     24 import android.app.AlertDialog;
     25 import android.content.BroadcastReceiver;
     26 import android.content.Context;
     27 import android.content.DialogInterface;
     28 import android.content.Intent;
     29 import android.content.IntentFilter;
     30 import android.content.SharedPreferences;
     31 import android.content.pm.PackageInfo;
     32 import android.content.res.Configuration;
     33 import android.content.res.Resources;
     34 import android.graphics.Rect;
     35 import android.inputmethodservice.InputMethodService;
     36 import android.media.AudioManager;
     37 import android.net.ConnectivityManager;
     38 import android.os.Debug;
     39 import android.os.Handler;
     40 import android.os.HandlerThread;
     41 import android.os.IBinder;
     42 import android.os.Message;
     43 import android.os.SystemClock;
     44 import android.preference.PreferenceManager;
     45 import android.text.InputType;
     46 import android.text.TextUtils;
     47 import android.text.style.SuggestionSpan;
     48 import android.util.Log;
     49 import android.util.Pair;
     50 import android.util.PrintWriterPrinter;
     51 import android.util.Printer;
     52 import android.view.KeyCharacterMap;
     53 import android.view.KeyEvent;
     54 import android.view.View;
     55 import android.view.ViewGroup.LayoutParams;
     56 import android.view.Window;
     57 import android.view.WindowManager;
     58 import android.view.inputmethod.CompletionInfo;
     59 import android.view.inputmethod.CorrectionInfo;
     60 import android.view.inputmethod.EditorInfo;
     61 import android.view.inputmethod.InputMethodSubtype;
     62 
     63 import com.android.inputmethod.accessibility.AccessibilityUtils;
     64 import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
     65 import com.android.inputmethod.annotations.UsedForTesting;
     66 import com.android.inputmethod.compat.AppWorkaroundsUtils;
     67 import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
     68 import com.android.inputmethod.compat.SuggestionSpanUtils;
     69 import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
     70 import com.android.inputmethod.event.EventInterpreter;
     71 import com.android.inputmethod.keyboard.KeyDetector;
     72 import com.android.inputmethod.keyboard.Keyboard;
     73 import com.android.inputmethod.keyboard.KeyboardActionListener;
     74 import com.android.inputmethod.keyboard.KeyboardId;
     75 import com.android.inputmethod.keyboard.KeyboardSwitcher;
     76 import com.android.inputmethod.keyboard.MainKeyboardView;
     77 import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
     78 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
     79 import com.android.inputmethod.latin.define.ProductionFlag;
     80 import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever;
     81 import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
     82 import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister;
     83 import com.android.inputmethod.latin.personalization.PersonalizationHelper;
     84 import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary;
     85 import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
     86 import com.android.inputmethod.latin.settings.Settings;
     87 import com.android.inputmethod.latin.settings.SettingsActivity;
     88 import com.android.inputmethod.latin.settings.SettingsValues;
     89 import com.android.inputmethod.latin.suggestions.SuggestionStripView;
     90 import com.android.inputmethod.latin.utils.ApplicationUtils;
     91 import com.android.inputmethod.latin.utils.AsyncResultHolder;
     92 import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
     93 import com.android.inputmethod.latin.utils.CapsModeUtils;
     94 import com.android.inputmethod.latin.utils.CollectionUtils;
     95 import com.android.inputmethod.latin.utils.CompletionInfoUtils;
     96 import com.android.inputmethod.latin.utils.InputTypeUtils;
     97 import com.android.inputmethod.latin.utils.IntentUtils;
     98 import com.android.inputmethod.latin.utils.JniUtils;
     99 import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
    100 import com.android.inputmethod.latin.utils.RecapitalizeStatus;
    101 import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
    102 import com.android.inputmethod.latin.utils.StringUtils;
    103 import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
    104 import com.android.inputmethod.latin.utils.TextRange;
    105 import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils;
    106 import com.android.inputmethod.research.ResearchLogger;
    107 
    108 import java.io.FileDescriptor;
    109 import java.io.PrintWriter;
    110 import java.util.ArrayList;
    111 import java.util.Locale;
    112 import java.util.TreeSet;
    113 
    114 /**
    115  * Input method implementation for Qwerty'ish keyboard.
    116  */
    117 public class LatinIME extends InputMethodService implements KeyboardActionListener,
    118         SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener,
    119         Suggest.SuggestInitializationListener {
    120     private static final String TAG = LatinIME.class.getSimpleName();
    121     private static final boolean TRACE = false;
    122     private static boolean DEBUG;
    123 
    124     private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
    125 
    126     // How many continuous deletes at which to start deleting at a higher speed.
    127     private static final int DELETE_ACCELERATE_AT = 20;
    128     // Key events coming any faster than this are long-presses.
    129     private static final int QUICK_PRESS = 200;
    130 
    131     private static final int PENDING_IMS_CALLBACK_DURATION = 800;
    132 
    133     private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2;
    134 
    135     // TODO: Set this value appropriately.
    136     private static final int GET_SUGGESTED_WORDS_TIMEOUT = 200;
    137 
    138     /**
    139      * The name of the scheme used by the Package Manager to warn of a new package installation,
    140      * replacement or removal.
    141      */
    142     private static final String SCHEME_PACKAGE = "package";
    143 
    144     private static final int SPACE_STATE_NONE = 0;
    145     // Double space: the state where the user pressed space twice quickly, which LatinIME
    146     // resolved as period-space. Undoing this converts the period to a space.
    147     private static final int SPACE_STATE_DOUBLE = 1;
    148     // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
    149     // have just been swapped. Undoing this swaps them back; the space is still considered weak.
    150     private static final int SPACE_STATE_SWAP_PUNCTUATION = 2;
    151     // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
    152     // spaces happen when the user presses space, accepting the current suggestion (whether
    153     // it's an auto-correction or not).
    154     private static final int SPACE_STATE_WEAK = 3;
    155     // Phantom space: a not-yet-inserted space that should get inserted on the next input,
    156     // character provided it's not a separator. If it's a separator, the phantom space is dropped.
    157     // Phantom spaces happen when a user chooses a word from the suggestion strip.
    158     private static final int SPACE_STATE_PHANTOM = 4;
    159 
    160     // Current space state of the input method. This can be any of the above constants.
    161     private int mSpaceState;
    162 
    163     private final Settings mSettings;
    164 
    165     private View mExtractArea;
    166     private View mKeyPreviewBackingView;
    167     private SuggestionStripView mSuggestionStripView;
    168     // Never null
    169     private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
    170     private Suggest mSuggest;
    171     private CompletionInfo[] mApplicationSpecifiedCompletions;
    172     private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils();
    173 
    174     private RichInputMethodManager mRichImm;
    175     @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
    176     private final SubtypeSwitcher mSubtypeSwitcher;
    177     private final SubtypeState mSubtypeState = new SubtypeState();
    178     // At start, create a default event interpreter that does nothing by passing it no decoder spec.
    179     // The event interpreter should never be null.
    180     private EventInterpreter mEventInterpreter = new EventInterpreter(this);
    181 
    182     private boolean mIsMainDictionaryAvailable;
    183     private UserBinaryDictionary mUserDictionary;
    184     private UserHistoryDictionary mUserHistoryDictionary;
    185     private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary;
    186     private PersonalizationDictionary mPersonalizationDictionary;
    187     private boolean mIsUserDictionaryAvailable;
    188 
    189     private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
    190     private final WordComposer mWordComposer = new WordComposer();
    191     private final RichInputConnection mConnection = new RichInputConnection(this);
    192     private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
    193 
    194     // Keep track of the last selection range to decide if we need to show word alternatives
    195     private static final int NOT_A_CURSOR_POSITION = -1;
    196     private int mLastSelectionStart = NOT_A_CURSOR_POSITION;
    197     private int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
    198 
    199     // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
    200     // "expect" it, it means the user actually moved the cursor.
    201     private boolean mExpectingUpdateSelection;
    202     private int mDeleteCount;
    203     private long mLastKeyTime;
    204     private final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
    205     // Personalization debugging params
    206     private boolean mUseOnlyPersonalizationDictionaryForDebug = false;
    207     private boolean mBoostPersonalizationDictionaryForDebug = false;
    208 
    209     // Member variables for remembering the current device orientation.
    210     private int mDisplayOrientation;
    211 
    212     // Object for reacting to adding/removing a dictionary pack.
    213     private BroadcastReceiver mDictionaryPackInstallReceiver =
    214             new DictionaryPackInstallBroadcastReceiver(this);
    215 
    216     // Keeps track of most recently inserted text (multi-character key) for reverting
    217     private String mEnteredText;
    218 
    219     // TODO: This boolean is persistent state and causes large side effects at unexpected times.
    220     // Find a way to remove it for readability.
    221     private boolean mIsAutoCorrectionIndicatorOn;
    222 
    223     private AlertDialog mOptionsDialog;
    224 
    225     private final boolean mIsHardwareAcceleratedDrawingEnabled;
    226 
    227     public final UIHandler mHandler = new UIHandler(this);
    228     private InputUpdater mInputUpdater;
    229 
    230     public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
    231         private static final int MSG_UPDATE_SHIFT_STATE = 0;
    232         private static final int MSG_PENDING_IMS_CALLBACK = 1;
    233         private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
    234         private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
    235         private static final int MSG_RESUME_SUGGESTIONS = 4;
    236         private static final int MSG_REOPEN_DICTIONARIES = 5;
    237         private static final int MSG_ON_END_BATCH_INPUT = 6;
    238         private static final int MSG_RESET_CACHES = 7;
    239 
    240         private static final int ARG1_NOT_GESTURE_INPUT = 0;
    241         private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
    242         private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2;
    243         private static final int ARG2_WITHOUT_TYPED_WORD = 0;
    244         private static final int ARG2_WITH_TYPED_WORD = 1;
    245 
    246         private int mDelayUpdateSuggestions;
    247         private int mDelayUpdateShiftState;
    248         private long mDoubleSpacePeriodTimeout;
    249         private long mDoubleSpacePeriodTimerStart;
    250 
    251         public UIHandler(final LatinIME outerInstance) {
    252             super(outerInstance);
    253         }
    254 
    255         public void onCreate() {
    256             final Resources res = getOuterInstance().getResources();
    257             mDelayUpdateSuggestions =
    258                     res.getInteger(R.integer.config_delay_update_suggestions);
    259             mDelayUpdateShiftState =
    260                     res.getInteger(R.integer.config_delay_update_shift_state);
    261             mDoubleSpacePeriodTimeout =
    262                     res.getInteger(R.integer.config_double_space_period_timeout);
    263         }
    264 
    265         @Override
    266         public void handleMessage(final Message msg) {
    267             final LatinIME latinIme = getOuterInstance();
    268             final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
    269             switch (msg.what) {
    270             case MSG_UPDATE_SUGGESTION_STRIP:
    271                 latinIme.updateSuggestionStrip();
    272                 break;
    273             case MSG_UPDATE_SHIFT_STATE:
    274                 switcher.updateShiftState();
    275                 break;
    276             case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
    277                 if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) {
    278                     if (msg.arg2 == ARG2_WITH_TYPED_WORD) {
    279                         final Pair<SuggestedWords, String> p =
    280                                 (Pair<SuggestedWords, String>) msg.obj;
    281                         latinIme.showSuggestionStripWithTypedWord(p.first, p.second);
    282                     } else {
    283                         latinIme.showSuggestionStrip((SuggestedWords) msg.obj);
    284                     }
    285                 } else {
    286                     latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj,
    287                             msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
    288                 }
    289                 break;
    290             case MSG_RESUME_SUGGESTIONS:
    291                 latinIme.restartSuggestionsOnWordTouchedByCursor();
    292                 break;
    293             case MSG_REOPEN_DICTIONARIES:
    294                 latinIme.initSuggest();
    295                 // In theory we could call latinIme.updateSuggestionStrip() right away, but
    296                 // in the practice, the dictionary is not finished opening yet so we wouldn't
    297                 // get any suggestions. Wait one frame.
    298                 postUpdateSuggestionStrip();
    299                 break;
    300             case MSG_ON_END_BATCH_INPUT:
    301                 latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj);
    302                 break;
    303             case MSG_RESET_CACHES:
    304                 latinIme.retryResetCaches(msg.arg1 == 1 /* tryResumeSuggestions */,
    305                         msg.arg2 /* remainingTries */);
    306                 break;
    307             }
    308         }
    309 
    310         public void postUpdateSuggestionStrip() {
    311             sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
    312         }
    313 
    314         public void postReopenDictionaries() {
    315             sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES));
    316         }
    317 
    318         public void postResumeSuggestions() {
    319             removeMessages(MSG_RESUME_SUGGESTIONS);
    320             sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
    321         }
    322 
    323         public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
    324             removeMessages(MSG_RESET_CACHES);
    325             sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0,
    326                     remainingTries, null));
    327         }
    328 
    329         public void cancelUpdateSuggestionStrip() {
    330             removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
    331         }
    332 
    333         public boolean hasPendingUpdateSuggestions() {
    334             return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
    335         }
    336 
    337         public boolean hasPendingReopenDictionaries() {
    338             return hasMessages(MSG_REOPEN_DICTIONARIES);
    339         }
    340 
    341         public void postUpdateShiftState() {
    342             removeMessages(MSG_UPDATE_SHIFT_STATE);
    343             sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
    344         }
    345 
    346         public void cancelUpdateShiftState() {
    347             removeMessages(MSG_UPDATE_SHIFT_STATE);
    348         }
    349 
    350         public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
    351                 final boolean dismissGestureFloatingPreviewText) {
    352             removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
    353             final int arg1 = dismissGestureFloatingPreviewText
    354                     ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT
    355                     : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT;
    356             obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1,
    357                     ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget();
    358         }
    359 
    360         public void showSuggestionStrip(final SuggestedWords suggestedWords) {
    361             removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
    362             obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP,
    363                     ARG1_NOT_GESTURE_INPUT, ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget();
    364         }
    365 
    366         // TODO: Remove this method.
    367         public void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
    368                 final String typedWord) {
    369             removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
    370             obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, ARG1_NOT_GESTURE_INPUT,
    371                     ARG2_WITH_TYPED_WORD,
    372                     new Pair<SuggestedWords, String>(suggestedWords, typedWord)).sendToTarget();
    373         }
    374 
    375         public void onEndBatchInput(final SuggestedWords suggestedWords) {
    376             obtainMessage(MSG_ON_END_BATCH_INPUT, suggestedWords).sendToTarget();
    377         }
    378 
    379         public void startDoubleSpacePeriodTimer() {
    380             mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis();
    381         }
    382 
    383         public void cancelDoubleSpacePeriodTimer() {
    384             mDoubleSpacePeriodTimerStart = 0;
    385         }
    386 
    387         public boolean isAcceptingDoubleSpacePeriod() {
    388             return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart
    389                     < mDoubleSpacePeriodTimeout;
    390         }
    391 
    392         // Working variables for the following methods.
    393         private boolean mIsOrientationChanging;
    394         private boolean mPendingSuccessiveImsCallback;
    395         private boolean mHasPendingStartInput;
    396         private boolean mHasPendingFinishInputView;
    397         private boolean mHasPendingFinishInput;
    398         private EditorInfo mAppliedEditorInfo;
    399 
    400         public void startOrientationChanging() {
    401             removeMessages(MSG_PENDING_IMS_CALLBACK);
    402             resetPendingImsCallback();
    403             mIsOrientationChanging = true;
    404             final LatinIME latinIme = getOuterInstance();
    405             if (latinIme.isInputViewShown()) {
    406                 latinIme.mKeyboardSwitcher.saveKeyboardState();
    407             }
    408         }
    409 
    410         private void resetPendingImsCallback() {
    411             mHasPendingFinishInputView = false;
    412             mHasPendingFinishInput = false;
    413             mHasPendingStartInput = false;
    414         }
    415 
    416         private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo,
    417                 boolean restarting) {
    418             if (mHasPendingFinishInputView)
    419                 latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
    420             if (mHasPendingFinishInput)
    421                 latinIme.onFinishInputInternal();
    422             if (mHasPendingStartInput)
    423                 latinIme.onStartInputInternal(editorInfo, restarting);
    424             resetPendingImsCallback();
    425         }
    426 
    427         public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
    428             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
    429                 // Typically this is the second onStartInput after orientation changed.
    430                 mHasPendingStartInput = true;
    431             } else {
    432                 if (mIsOrientationChanging && restarting) {
    433                     // This is the first onStartInput after orientation changed.
    434                     mIsOrientationChanging = false;
    435                     mPendingSuccessiveImsCallback = true;
    436                 }
    437                 final LatinIME latinIme = getOuterInstance();
    438                 executePendingImsCallback(latinIme, editorInfo, restarting);
    439                 latinIme.onStartInputInternal(editorInfo, restarting);
    440             }
    441         }
    442 
    443         public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
    444             if (hasMessages(MSG_PENDING_IMS_CALLBACK)
    445                     && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
    446                 // Typically this is the second onStartInputView after orientation changed.
    447                 resetPendingImsCallback();
    448             } else {
    449                 if (mPendingSuccessiveImsCallback) {
    450                     // This is the first onStartInputView after orientation changed.
    451                     mPendingSuccessiveImsCallback = false;
    452                     resetPendingImsCallback();
    453                     sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
    454                             PENDING_IMS_CALLBACK_DURATION);
    455                 }
    456                 final LatinIME latinIme = getOuterInstance();
    457                 executePendingImsCallback(latinIme, editorInfo, restarting);
    458                 latinIme.onStartInputViewInternal(editorInfo, restarting);
    459                 mAppliedEditorInfo = editorInfo;
    460             }
    461         }
    462 
    463         public void onFinishInputView(final boolean finishingInput) {
    464             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
    465                 // Typically this is the first onFinishInputView after orientation changed.
    466                 mHasPendingFinishInputView = true;
    467             } else {
    468                 final LatinIME latinIme = getOuterInstance();
    469                 latinIme.onFinishInputViewInternal(finishingInput);
    470                 mAppliedEditorInfo = null;
    471             }
    472         }
    473 
    474         public void onFinishInput() {
    475             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
    476                 // Typically this is the first onFinishInput after orientation changed.
    477                 mHasPendingFinishInput = true;
    478             } else {
    479                 final LatinIME latinIme = getOuterInstance();
    480                 executePendingImsCallback(latinIme, null, false);
    481                 latinIme.onFinishInputInternal();
    482             }
    483         }
    484     }
    485 
    486     static final class SubtypeState {
    487         private InputMethodSubtype mLastActiveSubtype;
    488         private boolean mCurrentSubtypeUsed;
    489 
    490         public void currentSubtypeUsed() {
    491             mCurrentSubtypeUsed = true;
    492         }
    493 
    494         public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) {
    495             final InputMethodSubtype currentSubtype = richImm.getInputMethodManager()
    496                     .getCurrentInputMethodSubtype();
    497             final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype;
    498             final boolean currentSubtypeUsed = mCurrentSubtypeUsed;
    499             if (currentSubtypeUsed) {
    500                 mLastActiveSubtype = currentSubtype;
    501                 mCurrentSubtypeUsed = false;
    502             }
    503             if (currentSubtypeUsed
    504                     && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
    505                     && !currentSubtype.equals(lastActiveSubtype)) {
    506                 richImm.setInputMethodAndSubtype(token, lastActiveSubtype);
    507                 return;
    508             }
    509             richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */);
    510         }
    511     }
    512 
    513     // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial
    514     // JNI call as much as possible.
    515     static {
    516         JniUtils.loadNativeLibrary();
    517     }
    518 
    519     public LatinIME() {
    520         super();
    521         mSettings = Settings.getInstance();
    522         mSubtypeSwitcher = SubtypeSwitcher.getInstance();
    523         mKeyboardSwitcher = KeyboardSwitcher.getInstance();
    524         mIsHardwareAcceleratedDrawingEnabled =
    525                 InputMethodServiceCompatUtils.enableHardwareAcceleration(this);
    526         Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled);
    527     }
    528 
    529     @Override
    530     public void onCreate() {
    531         Settings.init(this);
    532         LatinImeLogger.init(this);
    533         RichInputMethodManager.init(this);
    534         mRichImm = RichInputMethodManager.getInstance();
    535         SubtypeSwitcher.init(this);
    536         KeyboardSwitcher.init(this);
    537         AudioAndHapticFeedbackManager.init(this);
    538         AccessibilityUtils.init(this);
    539         PersonalizationDictionarySessionRegister.init(this);
    540 
    541         super.onCreate();
    542 
    543         mHandler.onCreate();
    544         DEBUG = LatinImeLogger.sDBG;
    545 
    546         // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}.
    547         loadSettings();
    548         initSuggest();
    549 
    550         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
    551             ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest);
    552         }
    553         mDisplayOrientation = getResources().getConfiguration().orientation;
    554 
    555         // Register to receive ringer mode change and network state change.
    556         // Also receive installation and removal of a dictionary pack.
    557         final IntentFilter filter = new IntentFilter();
    558         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
    559         filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
    560         registerReceiver(mReceiver, filter);
    561 
    562         final IntentFilter packageFilter = new IntentFilter();
    563         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
    564         packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
    565         packageFilter.addDataScheme(SCHEME_PACKAGE);
    566         registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
    567 
    568         final IntentFilter newDictFilter = new IntentFilter();
    569         newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
    570         registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
    571 
    572         DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this);
    573 
    574         mInputUpdater = new InputUpdater(this);
    575     }
    576 
    577     // Has to be package-visible for unit tests
    578     @UsedForTesting
    579     void loadSettings() {
    580         final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
    581         final InputAttributes inputAttributes =
    582                 new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
    583         mSettings.loadSettings(locale, inputAttributes);
    584         AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent());
    585         // To load the keyboard we need to load all the settings once, but resetting the
    586         // contacts dictionary should be deferred until after the new layout has been displayed
    587         // to improve responsivity. In the language switching process, we post a reopenDictionaries
    588         // message, then come here to read the settings for the new language before we change
    589         // the layout; at this time, we need to skip resetting the contacts dictionary. It will
    590         // be done later inside {@see #initSuggest()} when the reopenDictionaries message is
    591         // processed.
    592         if (!mHandler.hasPendingReopenDictionaries()) {
    593             // May need to reset the contacts dictionary depending on the user settings.
    594             resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
    595         }
    596     }
    597 
    598     // Note that this method is called from a non-UI thread.
    599     @Override
    600     public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) {
    601         mIsMainDictionaryAvailable = isMainDictionaryAvailable;
    602         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
    603         if (mainKeyboardView != null) {
    604             mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
    605         }
    606     }
    607 
    608     private void initSuggest() {
    609         final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
    610         final String switcherLocaleStr = switcherSubtypeLocale.toString();
    611         final Locale subtypeLocale;
    612         final String localeStr;
    613         if (TextUtils.isEmpty(switcherLocaleStr)) {
    614             // This happens in very rare corner cases - for example, immediately after a switch
    615             // to LatinIME has been requested, about a frame later another switch happens. In this
    616             // case, we are about to go down but we still don't know it, however the system tells
    617             // us there is no current subtype so the locale is the empty string. Take the best
    618             // possible guess instead -- it's bound to have no consequences, and we have no way
    619             // of knowing anyway.
    620             Log.e(TAG, "System is reporting no current subtype.");
    621             subtypeLocale = getResources().getConfiguration().locale;
    622             localeStr = subtypeLocale.toString();
    623         } else {
    624             subtypeLocale = switcherSubtypeLocale;
    625             localeStr = switcherLocaleStr;
    626         }
    627 
    628         final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale,
    629                 this /* SuggestInitializationListener */);
    630         final SettingsValues settingsValues = mSettings.getCurrent();
    631         if (settingsValues.mCorrectionEnabled) {
    632             newSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
    633         }
    634 
    635         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
    636         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
    637             ResearchLogger.getInstance().initSuggest(newSuggest);
    638         }
    639 
    640         mUserDictionary = new UserBinaryDictionary(this, localeStr);
    641         mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
    642         newSuggest.setUserDictionary(mUserDictionary);
    643 
    644         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    645 
    646         mUserHistoryDictionary = PersonalizationHelper.getUserHistoryDictionary(
    647                 this, localeStr, prefs);
    648         newSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
    649         mPersonalizationDictionary = PersonalizationHelper
    650                 .getPersonalizationDictionary(this, localeStr, prefs);
    651         newSuggest.setPersonalizationDictionary(mPersonalizationDictionary);
    652         mPersonalizationPredictionDictionary = PersonalizationHelper
    653                 .getPersonalizationPredictionDictionary(this, localeStr, prefs);
    654         newSuggest.setPersonalizationPredictionDictionary(mPersonalizationPredictionDictionary);
    655 
    656         final Suggest oldSuggest = mSuggest;
    657         resetContactsDictionary(null != oldSuggest ? oldSuggest.getContactsDictionary() : null);
    658         mSuggest = newSuggest;
    659         if (oldSuggest != null) oldSuggest.close();
    660     }
    661 
    662     /**
    663      * Resets the contacts dictionary in mSuggest according to the user settings.
    664      *
    665      * This method takes an optional contacts dictionary to use when the locale hasn't changed
    666      * since the contacts dictionary can be opened or closed as necessary depending on the settings.
    667      *
    668      * @param oldContactsDictionary an optional dictionary to use, or null
    669      */
    670     private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
    671         final Suggest suggest = mSuggest;
    672         final boolean shouldSetDictionary =
    673                 (null != suggest && mSettings.getCurrent().mUseContactsDict);
    674 
    675         final ContactsBinaryDictionary dictionaryToUse;
    676         if (!shouldSetDictionary) {
    677             // Make sure the dictionary is closed. If it is already closed, this is a no-op,
    678             // so it's safe to call it anyways.
    679             if (null != oldContactsDictionary) oldContactsDictionary.close();
    680             dictionaryToUse = null;
    681         } else {
    682             final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
    683             if (null != oldContactsDictionary) {
    684                 if (!oldContactsDictionary.mLocale.equals(locale)) {
    685                     // If the locale has changed then recreate the contacts dictionary. This
    686                     // allows locale dependent rules for handling bigram name predictions.
    687                     oldContactsDictionary.close();
    688                     dictionaryToUse = new ContactsBinaryDictionary(this, locale);
    689                 } else {
    690                     // Make sure the old contacts dictionary is opened. If it is already open,
    691                     // this is a no-op, so it's safe to call it anyways.
    692                     oldContactsDictionary.reopen(this);
    693                     dictionaryToUse = oldContactsDictionary;
    694                 }
    695             } else {
    696                 dictionaryToUse = new ContactsBinaryDictionary(this, locale);
    697             }
    698         }
    699 
    700         if (null != suggest) {
    701             suggest.setContactsDictionary(dictionaryToUse);
    702         }
    703     }
    704 
    705     /* package private */ void resetSuggestMainDict() {
    706         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
    707         mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
    708         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
    709     }
    710 
    711     @Override
    712     public void onDestroy() {
    713         final Suggest suggest = mSuggest;
    714         if (suggest != null) {
    715             suggest.close();
    716             mSuggest = null;
    717         }
    718         mSettings.onDestroy();
    719         unregisterReceiver(mReceiver);
    720         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
    721             ResearchLogger.getInstance().onDestroy();
    722         }
    723         unregisterReceiver(mDictionaryPackInstallReceiver);
    724         PersonalizationDictionarySessionRegister.onDestroy(this);
    725         LatinImeLogger.commit();
    726         LatinImeLogger.onDestroy();
    727         if (mInputUpdater != null) {
    728             mInputUpdater.quitLooper();
    729         }
    730         super.onDestroy();
    731     }
    732 
    733     @Override
    734     public void onConfigurationChanged(final Configuration conf) {
    735         // If orientation changed while predicting, commit the change
    736         if (mDisplayOrientation != conf.orientation) {
    737             mDisplayOrientation = conf.orientation;
    738             mHandler.startOrientationChanging();
    739             mConnection.beginBatchEdit();
    740             commitTyped(LastComposedWord.NOT_A_SEPARATOR);
    741             mConnection.finishComposingText();
    742             mConnection.endBatchEdit();
    743             if (isShowingOptionDialog()) {
    744                 mOptionsDialog.dismiss();
    745             }
    746         }
    747         PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf);
    748         super.onConfigurationChanged(conf);
    749     }
    750 
    751     @Override
    752     public View onCreateInputView() {
    753         return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
    754     }
    755 
    756     @Override
    757     public void setInputView(final View view) {
    758         super.setInputView(view);
    759         mExtractArea = getWindow().getWindow().getDecorView()
    760                 .findViewById(android.R.id.extractArea);
    761         mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
    762         mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
    763         if (mSuggestionStripView != null)
    764             mSuggestionStripView.setListener(this, view);
    765         if (LatinImeLogger.sVISUALDEBUG) {
    766             mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
    767         }
    768     }
    769 
    770     @Override
    771     public void setCandidatesView(final View view) {
    772         // To ensure that CandidatesView will never be set.
    773         return;
    774     }
    775 
    776     @Override
    777     public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
    778         mHandler.onStartInput(editorInfo, restarting);
    779     }
    780 
    781     @Override
    782     public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
    783         mHandler.onStartInputView(editorInfo, restarting);
    784     }
    785 
    786     @Override
    787     public void onFinishInputView(final boolean finishingInput) {
    788         mHandler.onFinishInputView(finishingInput);
    789     }
    790 
    791     @Override
    792     public void onFinishInput() {
    793         mHandler.onFinishInput();
    794     }
    795 
    796     @Override
    797     public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) {
    798         // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
    799         // is not guaranteed. It may even be called at the same time on a different thread.
    800         mSubtypeSwitcher.onSubtypeChanged(subtype);
    801         loadKeyboard();
    802     }
    803 
    804     private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
    805         super.onStartInput(editorInfo, restarting);
    806     }
    807 
    808     @SuppressWarnings("deprecation")
    809     private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
    810         super.onStartInputView(editorInfo, restarting);
    811         mRichImm.clearSubtypeCaches();
    812         final KeyboardSwitcher switcher = mKeyboardSwitcher;
    813         switcher.updateKeyboardTheme();
    814         final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
    815         // If we are starting input in a different text field from before, we'll have to reload
    816         // settings, so currentSettingsValues can't be final.
    817         SettingsValues currentSettingsValues = mSettings.getCurrent();
    818 
    819         if (editorInfo == null) {
    820             Log.e(TAG, "Null EditorInfo in onStartInputView()");
    821             if (LatinImeLogger.sDBG) {
    822                 throw new NullPointerException("Null EditorInfo in onStartInputView()");
    823             }
    824             return;
    825         }
    826         if (DEBUG) {
    827             Log.d(TAG, "onStartInputView: editorInfo:"
    828                     + String.format("inputType=0x%08x imeOptions=0x%08x",
    829                             editorInfo.inputType, editorInfo.imeOptions));
    830             Log.d(TAG, "All caps = "
    831                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0)
    832                     + ", sentence caps = "
    833                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0)
    834                     + ", word caps = "
    835                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
    836         }
    837         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
    838             final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    839             ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs);
    840         }
    841         if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
    842             Log.w(TAG, "Deprecated private IME option specified: "
    843                     + editorInfo.privateImeOptions);
    844             Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead");
    845         }
    846         if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) {
    847             Log.w(TAG, "Deprecated private IME option specified: "
    848                     + editorInfo.privateImeOptions);
    849             Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
    850         }
    851 
    852         final PackageInfo packageInfo =
    853                 TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName);
    854         mAppWorkAroundsUtils.setPackageInfo(packageInfo);
    855         if (null == packageInfo) {
    856             new TargetPackageInfoGetterTask(this /* context */, this /* listener */)
    857                     .execute(editorInfo.packageName);
    858         }
    859 
    860         LatinImeLogger.onStartInputView(editorInfo);
    861         // In landscape mode, this method gets called without the input view being created.
    862         if (mainKeyboardView == null) {
    863             return;
    864         }
    865 
    866         // Forward this event to the accessibility utilities, if enabled.
    867         final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
    868         if (accessUtils.isTouchExplorationEnabled()) {
    869             accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
    870         }
    871 
    872         final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo);
    873         final boolean isDifferentTextField = !restarting || inputTypeChanged;
    874         if (isDifferentTextField) {
    875             mSubtypeSwitcher.updateParametersOnStartInputView();
    876         }
    877 
    878         // The EditorInfo might have a flag that affects fullscreen mode.
    879         // Note: This call should be done by InputMethodService?
    880         updateFullscreenMode();
    881         mApplicationSpecifiedCompletions = null;
    882 
    883         // The app calling setText() has the effect of clearing the composing
    884         // span, so we should reset our state unconditionally, even if restarting is true.
    885         mEnteredText = null;
    886         resetComposingState(true /* alsoResetLastComposedWord */);
    887         mDeleteCount = 0;
    888         mSpaceState = SPACE_STATE_NONE;
    889         mRecapitalizeStatus.deactivate();
    890         mCurrentlyPressedHardwareKeys.clear();
    891 
    892         // Note: the following does a round-trip IPC on the main thread: be careful
    893         final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
    894         final Suggest suggest = mSuggest;
    895         if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) {
    896             initSuggest();
    897         }
    898         if (mSuggestionStripView != null) {
    899             // This will set the punctuation suggestions if next word suggestion is off;
    900             // otherwise it will clear the suggestion strip.
    901             setPunctuationSuggestions();
    902         }
    903         mSuggestedWords = SuggestedWords.EMPTY;
    904 
    905         // Sometimes, while rotating, for some reason the framework tells the app we are not
    906         // connected to it and that means we can't refresh the cache. In this case, schedule a
    907         // refresh later.
    908         final boolean canReachInputConnection;
    909         if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart,
    910                 false /* shouldFinishComposition */)) {
    911             // We try resetting the caches up to 5 times before giving up.
    912             mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
    913             // mLastSelection{Start,End} are reset later in this method, don't need to do it here
    914             canReachInputConnection = false;
    915         } else {
    916             if (isDifferentTextField) {
    917                 mHandler.postResumeSuggestions();
    918             }
    919             canReachInputConnection = true;
    920         }
    921 
    922         if (isDifferentTextField) {
    923             mainKeyboardView.closing();
    924             loadSettings();
    925             currentSettingsValues = mSettings.getCurrent();
    926 
    927             if (suggest != null && currentSettingsValues.mCorrectionEnabled) {
    928                 suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
    929             }
    930 
    931             switcher.loadKeyboard(editorInfo, currentSettingsValues);
    932             if (!canReachInputConnection) {
    933                 // If we can't reach the input connection, we will call loadKeyboard again later,
    934                 // so we need to save its state now. The call will be done in #retryResetCaches.
    935                 switcher.saveKeyboardState();
    936             }
    937         } else if (restarting) {
    938             // TODO: Come up with a more comprehensive way to reset the keyboard layout when
    939             // a keyboard layout set doesn't get reloaded in this method.
    940             switcher.resetKeyboardStateToAlphabet();
    941             // In apps like Talk, we come here when the text is sent and the field gets emptied and
    942             // we need to re-evaluate the shift state, but not the whole layout which would be
    943             // disruptive.
    944             // Space state must be updated before calling updateShiftState
    945             switcher.updateShiftState();
    946         }
    947         setSuggestionStripShownInternal(
    948                 isSuggestionsStripVisible(), /* needsInputViewShown */ false);
    949 
    950         mLastSelectionStart = editorInfo.initialSelStart;
    951         mLastSelectionEnd = editorInfo.initialSelEnd;
    952         // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying
    953         // so we try using some heuristics to find out about these and fix them.
    954         tryFixLyingCursorPosition();
    955 
    956         mHandler.cancelUpdateSuggestionStrip();
    957         mHandler.cancelDoubleSpacePeriodTimer();
    958 
    959         mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
    960         mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
    961                 currentSettingsValues.mKeyPreviewPopupDismissDelay);
    962         mainKeyboardView.setSlidingKeyInputPreviewEnabled(
    963                 currentSettingsValues.mSlidingKeyInputPreviewEnabled);
    964         mainKeyboardView.setGestureHandlingEnabledByUser(
    965                 currentSettingsValues.mGestureInputEnabled,
    966                 currentSettingsValues.mGestureTrailEnabled,
    967                 currentSettingsValues.mGestureFloatingPreviewTextEnabled);
    968 
    969         initPersonalizationDebugSettings(currentSettingsValues);
    970 
    971         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
    972     }
    973 
    974     /**
    975      * Try to get the text from the editor to expose lies the framework may have been
    976      * telling us. Concretely, when the device rotates, the frameworks tells us about where the
    977      * cursor used to be initially in the editor at the time it first received the focus; this
    978      * may be completely different from the place it is upon rotation. Since we don't have any
    979      * means to get the real value, try at least to ask the text view for some characters and
    980      * detect the most damaging cases: when the cursor position is declared to be much smaller
    981      * than it really is.
    982      */
    983     private void tryFixLyingCursorPosition() {
    984         final CharSequence textBeforeCursor =
    985                 mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
    986         if (null == textBeforeCursor) {
    987             mLastSelectionStart = mLastSelectionEnd = NOT_A_CURSOR_POSITION;
    988         } else {
    989             final int textLength = textBeforeCursor.length();
    990             if (textLength > mLastSelectionStart
    991                     || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
    992                             && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
    993                 // It should not be possible to have only one of those variables be
    994                 // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
    995                 // (simple cursor, no selection) or there is no cursor/we don't know its pos
    996                 final boolean wasEqual = mLastSelectionStart == mLastSelectionEnd;
    997                 mLastSelectionStart = textLength;
    998                 // We can't figure out the value of mLastSelectionEnd :(
    999                 // But at least if it's smaller than mLastSelectionStart something is wrong,
   1000                 // and if they used to be equal we also don't want to make it look like there is a
   1001                 // selection.
   1002                 if (wasEqual || mLastSelectionStart > mLastSelectionEnd) {
   1003                     mLastSelectionEnd = mLastSelectionStart;
   1004                 }
   1005             }
   1006         }
   1007     }
   1008 
   1009     // Initialization of personalization debug settings. This must be called inside
   1010     // onStartInputView.
   1011     private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) {
   1012         if (mUseOnlyPersonalizationDictionaryForDebug
   1013                 != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
   1014             // Only for debug
   1015             initSuggest();
   1016             mUseOnlyPersonalizationDictionaryForDebug =
   1017                     currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug;
   1018         }
   1019 
   1020         if (mBoostPersonalizationDictionaryForDebug !=
   1021                 currentSettingsValues.mBoostPersonalizationDictionaryForDebug) {
   1022             // Only for debug
   1023             mBoostPersonalizationDictionaryForDebug =
   1024                     currentSettingsValues.mBoostPersonalizationDictionaryForDebug;
   1025             if (mBoostPersonalizationDictionaryForDebug) {
   1026                 UserHistoryForgettingCurveUtils.boostMaxFreqForDebug();
   1027             } else {
   1028                 UserHistoryForgettingCurveUtils.resetMaxFreqForDebug();
   1029             }
   1030         }
   1031     }
   1032 
   1033     // Callback for the TargetPackageInfoGetterTask
   1034     @Override
   1035     public void onTargetPackageInfoKnown(final PackageInfo info) {
   1036         mAppWorkAroundsUtils.setPackageInfo(info);
   1037     }
   1038 
   1039     @Override
   1040     public void onWindowHidden() {
   1041         super.onWindowHidden();
   1042         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   1043         if (mainKeyboardView != null) {
   1044             mainKeyboardView.closing();
   1045         }
   1046     }
   1047 
   1048     private void onFinishInputInternal() {
   1049         super.onFinishInput();
   1050 
   1051         LatinImeLogger.commit();
   1052         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   1053         if (mainKeyboardView != null) {
   1054             mainKeyboardView.closing();
   1055         }
   1056     }
   1057 
   1058     private void onFinishInputViewInternal(final boolean finishingInput) {
   1059         super.onFinishInputView(finishingInput);
   1060         mKeyboardSwitcher.onFinishInputView();
   1061         mKeyboardSwitcher.deallocateMemory();
   1062         // Remove pending messages related to update suggestions
   1063         mHandler.cancelUpdateSuggestionStrip();
   1064         // Should do the following in onFinishInputInternal but until JB MR2 it's not called :(
   1065         if (mWordComposer.isComposingWord()) mConnection.finishComposingText();
   1066         resetComposingState(true /* alsoResetLastComposedWord */);
   1067         // Notify ResearchLogger
   1068         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1069             ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart,
   1070                     mLastSelectionEnd, getCurrentInputConnection());
   1071         }
   1072     }
   1073 
   1074     @Override
   1075     public void onUpdateSelection(final int oldSelStart, final int oldSelEnd,
   1076             final int newSelStart, final int newSelEnd,
   1077             final int composingSpanStart, final int composingSpanEnd) {
   1078         super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
   1079                 composingSpanStart, composingSpanEnd);
   1080         if (DEBUG) {
   1081             Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
   1082                     + ", ose=" + oldSelEnd
   1083                     + ", lss=" + mLastSelectionStart
   1084                     + ", lse=" + mLastSelectionEnd
   1085                     + ", nss=" + newSelStart
   1086                     + ", nse=" + newSelEnd
   1087                     + ", cs=" + composingSpanStart
   1088                     + ", ce=" + composingSpanEnd);
   1089         }
   1090         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1091             final boolean expectingUpdateSelectionFromLogger =
   1092                     ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
   1093             ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
   1094                     oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
   1095                     composingSpanEnd, mExpectingUpdateSelection,
   1096                     expectingUpdateSelectionFromLogger, mConnection);
   1097             if (expectingUpdateSelectionFromLogger) {
   1098                 // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
   1099                 return;
   1100             }
   1101         }
   1102 
   1103         final boolean selectionChanged = mLastSelectionStart != newSelStart
   1104                 || mLastSelectionEnd != newSelEnd;
   1105 
   1106         // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
   1107         // span in the view - we can use that to narrow down whether the cursor was moved
   1108         // by us or not. If we are composing a word but there is no composing span, then
   1109         // we know for sure the cursor moved while we were composing and we should reset
   1110         // the state. TODO: rescind this policy: the framework never removes the composing
   1111         // span on its own accord while editing. This test is useless.
   1112         final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
   1113 
   1114         // If the keyboard is not visible, we don't need to do all the housekeeping work, as it
   1115         // will be reset when the keyboard shows up anyway.
   1116         // TODO: revisit this when LatinIME supports hardware keyboards.
   1117         // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
   1118         // TODO: find a better way to simulate actual execution.
   1119         if (isInputViewShown() && !mExpectingUpdateSelection
   1120                 && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) {
   1121             // TAKE CARE: there is a race condition when we enter this test even when the user
   1122             // did not explicitly move the cursor. This happens when typing fast, where two keys
   1123             // turn this flag on in succession and both onUpdateSelection() calls arrive after
   1124             // the second one - the first call successfully avoids this test, but the second one
   1125             // enters. For the moment we rely on noComposingSpan to further reduce the impact.
   1126 
   1127             // TODO: the following is probably better done in resetEntireInputState().
   1128             // it should only happen when the cursor moved, and the very purpose of the
   1129             // test below is to narrow down whether this happened or not. Likewise with
   1130             // the call to updateShiftState.
   1131             // We set this to NONE because after a cursor move, we don't want the space
   1132             // state-related special processing to kick in.
   1133             mSpaceState = SPACE_STATE_NONE;
   1134 
   1135             // TODO: is it still necessary to test for composingSpan related stuff?
   1136             final boolean selectionChangedOrSafeToReset = selectionChanged
   1137                     || (!mWordComposer.isComposingWord()) || noComposingSpan;
   1138             final boolean hasOrHadSelection = (oldSelStart != oldSelEnd
   1139                     || newSelStart != newSelEnd);
   1140             final int moveAmount = newSelStart - oldSelStart;
   1141             if (selectionChangedOrSafeToReset && (hasOrHadSelection
   1142                     || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) {
   1143                 // If we are composing a word and moving the cursor, we would want to set a
   1144                 // suggestion span for recorrection to work correctly. Unfortunately, that
   1145                 // would involve the keyboard committing some new text, which would move the
   1146                 // cursor back to where it was. Latin IME could then fix the position of the cursor
   1147                 // again, but the asynchronous nature of the calls results in this wreaking havoc
   1148                 // with selection on double tap and the like.
   1149                 // Another option would be to send suggestions each time we set the composing
   1150                 // text, but that is probably too expensive to do, so we decided to leave things
   1151                 // as is.
   1152                 resetEntireInputState(newSelStart);
   1153             } else {
   1154                 // resetEntireInputState calls resetCachesUponCursorMove, but with the second
   1155                 // argument as true. But in all cases where we don't reset the entire input state,
   1156                 // we still want to tell the rich input connection about the new cursor position so
   1157                 // that it can update its caches.
   1158                 mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart,
   1159                         false /* shouldFinishComposition */);
   1160             }
   1161 
   1162             // We moved the cursor. If we are touching a word, we need to resume suggestion,
   1163             // unless suggestions are off.
   1164             if (isSuggestionsStripVisible()) {
   1165                 mHandler.postResumeSuggestions();
   1166             }
   1167             // Reset the last recapitalization.
   1168             mRecapitalizeStatus.deactivate();
   1169             mKeyboardSwitcher.updateShiftState();
   1170         }
   1171         mExpectingUpdateSelection = false;
   1172 
   1173         // Make a note of the cursor position
   1174         mLastSelectionStart = newSelStart;
   1175         mLastSelectionEnd = newSelEnd;
   1176         mSubtypeState.currentSubtypeUsed();
   1177     }
   1178 
   1179     /**
   1180      * This is called when the user has clicked on the extracted text view,
   1181      * when running in fullscreen mode.  The default implementation hides
   1182      * the suggestions view when this happens, but only if the extracted text
   1183      * editor has a vertical scroll bar because its text doesn't fit.
   1184      * Here we override the behavior due to the possibility that a re-correction could
   1185      * cause the suggestions strip to disappear and re-appear.
   1186      */
   1187     @Override
   1188     public void onExtractedTextClicked() {
   1189         if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
   1190 
   1191         super.onExtractedTextClicked();
   1192     }
   1193 
   1194     /**
   1195      * This is called when the user has performed a cursor movement in the
   1196      * extracted text view, when it is running in fullscreen mode.  The default
   1197      * implementation hides the suggestions view when a vertical movement
   1198      * happens, but only if the extracted text editor has a vertical scroll bar
   1199      * because its text doesn't fit.
   1200      * Here we override the behavior due to the possibility that a re-correction could
   1201      * cause the suggestions strip to disappear and re-appear.
   1202      */
   1203     @Override
   1204     public void onExtractedCursorMovement(final int dx, final int dy) {
   1205         if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
   1206 
   1207         super.onExtractedCursorMovement(dx, dy);
   1208     }
   1209 
   1210     @Override
   1211     public void hideWindow() {
   1212         LatinImeLogger.commit();
   1213         mKeyboardSwitcher.onHideWindow();
   1214 
   1215         if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
   1216             AccessibleKeyboardViewProxy.getInstance().onHideWindow();
   1217         }
   1218 
   1219         if (TRACE) Debug.stopMethodTracing();
   1220         if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
   1221             mOptionsDialog.dismiss();
   1222             mOptionsDialog = null;
   1223         }
   1224         super.hideWindow();
   1225     }
   1226 
   1227     @Override
   1228     public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
   1229         if (DEBUG) {
   1230             Log.i(TAG, "Received completions:");
   1231             if (applicationSpecifiedCompletions != null) {
   1232                 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
   1233                     Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
   1234                 }
   1235             }
   1236         }
   1237         if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return;
   1238         if (applicationSpecifiedCompletions == null) {
   1239             clearSuggestionStrip();
   1240             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1241                 ResearchLogger.latinIME_onDisplayCompletions(null);
   1242             }
   1243             return;
   1244         }
   1245         mApplicationSpecifiedCompletions =
   1246                 CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions);
   1247 
   1248         final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
   1249                 SuggestedWords.getFromApplicationSpecifiedCompletions(
   1250                         applicationSpecifiedCompletions);
   1251         final SuggestedWords suggestedWords = new SuggestedWords(
   1252                 applicationSuggestedWords,
   1253                 false /* typedWordValid */,
   1254                 false /* hasAutoCorrectionCandidate */,
   1255                 false /* isPunctuationSuggestions */,
   1256                 false /* isObsoleteSuggestions */,
   1257                 false /* isPrediction */);
   1258         // When in fullscreen mode, show completions generated by the application
   1259         final boolean isAutoCorrection = false;
   1260         setSuggestedWords(suggestedWords, isAutoCorrection);
   1261         setAutoCorrectionIndicator(isAutoCorrection);
   1262         setSuggestionStripShown(true);
   1263         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1264             ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
   1265         }
   1266     }
   1267 
   1268     private void setSuggestionStripShownInternal(final boolean shown,
   1269             final boolean needsInputViewShown) {
   1270         // TODO: Modify this if we support suggestions with hard keyboard
   1271         if (onEvaluateInputViewShown() && mSuggestionStripView != null) {
   1272             final boolean inputViewShown = mKeyboardSwitcher.isShowingMainKeyboardOrEmojiPalettes();
   1273             final boolean shouldShowSuggestions = shown
   1274                     && (needsInputViewShown ? inputViewShown : true);
   1275             if (isFullscreenMode()) {
   1276                 mSuggestionStripView.setVisibility(
   1277                         shouldShowSuggestions ? View.VISIBLE : View.GONE);
   1278             } else {
   1279                 mSuggestionStripView.setVisibility(
   1280                         shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
   1281             }
   1282         }
   1283     }
   1284 
   1285     private void setSuggestionStripShown(final boolean shown) {
   1286         setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
   1287     }
   1288 
   1289     private int getAdjustedBackingViewHeight() {
   1290         final int currentHeight = mKeyPreviewBackingView.getHeight();
   1291         if (currentHeight > 0) {
   1292             return currentHeight;
   1293         }
   1294 
   1295         final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
   1296         if (visibleKeyboardView == null) {
   1297             return 0;
   1298         }
   1299         // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main   !!!
   1300         // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   1301         final int keyboardHeight = visibleKeyboardView.getHeight();
   1302         final int suggestionsHeight = mSuggestionStripView.getHeight();
   1303         final int displayHeight = getResources().getDisplayMetrics().heightPixels;
   1304         final Rect rect = new Rect();
   1305         mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
   1306         final int notificationBarHeight = rect.top;
   1307         final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
   1308                 - keyboardHeight;
   1309 
   1310         final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
   1311         params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight);
   1312         mKeyPreviewBackingView.setLayoutParams(params);
   1313         return params.height;
   1314     }
   1315 
   1316     @Override
   1317     public void onComputeInsets(final InputMethodService.Insets outInsets) {
   1318         super.onComputeInsets(outInsets);
   1319         final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
   1320         if (visibleKeyboardView == null || mSuggestionStripView == null) {
   1321             return;
   1322         }
   1323         final int adjustedBackingHeight = getAdjustedBackingViewHeight();
   1324         final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
   1325         final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
   1326         // In fullscreen mode, the height of the extract area managed by InputMethodService should
   1327         // be considered.
   1328         // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
   1329         final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
   1330         final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0
   1331                 : mSuggestionStripView.getHeight();
   1332         final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
   1333         int visibleTopY = extraHeight;
   1334         // Need to set touchable region only if input view is being shown
   1335         if (visibleKeyboardView.isShown()) {
   1336             // Note that the height of Emoji layout is the same as the height of the main keyboard
   1337             // and the suggestion strip
   1338             if (mKeyboardSwitcher.isShowingEmojiPalettes()
   1339                     || mSuggestionStripView.getVisibility() == View.VISIBLE) {
   1340                 visibleTopY -= suggestionsHeight;
   1341             }
   1342             final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
   1343             final int touchWidth = visibleKeyboardView.getWidth();
   1344             final int touchHeight = visibleKeyboardView.getHeight() + extraHeight
   1345                     // Extend touchable region below the keyboard.
   1346                     + EXTENDED_TOUCHABLE_REGION_HEIGHT;
   1347             outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
   1348             outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight);
   1349         }
   1350         outInsets.contentTopInsets = visibleTopY;
   1351         outInsets.visibleTopInsets = visibleTopY;
   1352     }
   1353 
   1354     @Override
   1355     public boolean onEvaluateFullscreenMode() {
   1356         // Reread resource value here, because this method is called by framework anytime as needed.
   1357         final boolean isFullscreenModeAllowed =
   1358                 Settings.readUseFullscreenMode(getResources());
   1359         if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) {
   1360             // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI
   1361             // implies NO_FULLSCREEN. However, the framework mistakenly does.  i.e. NO_EXTRACT_UI
   1362             // without NO_FULLSCREEN doesn't work as expected. Because of this we need this
   1363             // hack for now.  Let's get rid of this once the framework gets fixed.
   1364             final EditorInfo ei = getCurrentInputEditorInfo();
   1365             return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0));
   1366         } else {
   1367             return false;
   1368         }
   1369     }
   1370 
   1371     @Override
   1372     public void updateFullscreenMode() {
   1373         super.updateFullscreenMode();
   1374 
   1375         if (mKeyPreviewBackingView == null) return;
   1376         // In fullscreen mode, no need to have extra space to show the key preview.
   1377         // If not, we should have extra space above the keyboard to show the key preview.
   1378         mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
   1379     }
   1380 
   1381     // This will reset the whole input state to the starting state. It will clear
   1382     // the composing word, reset the last composed word, tell the inputconnection about it.
   1383     private void resetEntireInputState(final int newCursorPosition) {
   1384         final boolean shouldFinishComposition = mWordComposer.isComposingWord();
   1385         resetComposingState(true /* alsoResetLastComposedWord */);
   1386         final SettingsValues settingsValues = mSettings.getCurrent();
   1387         if (settingsValues.mBigramPredictionEnabled) {
   1388             clearSuggestionStrip();
   1389         } else {
   1390             setSuggestedWords(settingsValues.mSuggestPuncList, false);
   1391         }
   1392         mConnection.resetCachesUponCursorMoveAndReturnSuccess(newCursorPosition,
   1393                 shouldFinishComposition);
   1394     }
   1395 
   1396     private void resetComposingState(final boolean alsoResetLastComposedWord) {
   1397         mWordComposer.reset();
   1398         if (alsoResetLastComposedWord)
   1399             mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
   1400     }
   1401 
   1402     private void commitTyped(final String separatorString) {
   1403         if (!mWordComposer.isComposingWord()) return;
   1404         final String typedWord = mWordComposer.getTypedWord();
   1405         if (typedWord.length() > 0) {
   1406             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1407                 ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
   1408             }
   1409             commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
   1410                     separatorString);
   1411         }
   1412     }
   1413 
   1414     // Called from the KeyboardSwitcher which needs to know auto caps state to display
   1415     // the right layout.
   1416     public int getCurrentAutoCapsState() {
   1417         final SettingsValues currentSettingsValues = mSettings.getCurrent();
   1418         if (!currentSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
   1419 
   1420         final EditorInfo ei = getCurrentInputEditorInfo();
   1421         if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
   1422         final int inputType = ei.inputType;
   1423         // Warning: this depends on mSpaceState, which may not be the most current value. If
   1424         // mSpaceState gets updated later, whoever called this may need to be told about it.
   1425         return mConnection.getCursorCapsMode(inputType, currentSettingsValues,
   1426                 SPACE_STATE_PHANTOM == mSpaceState);
   1427     }
   1428 
   1429     public int getCurrentRecapitalizeState() {
   1430         if (!mRecapitalizeStatus.isActive()
   1431                 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
   1432             // Not recapitalizing at the moment
   1433             return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
   1434         }
   1435         return mRecapitalizeStatus.getCurrentMode();
   1436     }
   1437 
   1438     // Factor in auto-caps and manual caps and compute the current caps mode.
   1439     private int getActualCapsMode() {
   1440         final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
   1441         if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
   1442         final int auto = getCurrentAutoCapsState();
   1443         if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
   1444             return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
   1445         }
   1446         if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED;
   1447         return WordComposer.CAPS_MODE_OFF;
   1448     }
   1449 
   1450     private void swapSwapperAndSpace() {
   1451         final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
   1452         // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
   1453         if (lastTwo != null && lastTwo.length() == 2
   1454                 && lastTwo.charAt(0) == Constants.CODE_SPACE) {
   1455             mConnection.deleteSurroundingText(2, 0);
   1456             final String text = lastTwo.charAt(1) + " ";
   1457             mConnection.commitText(text, 1);
   1458             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1459                 ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
   1460             }
   1461             mKeyboardSwitcher.updateShiftState();
   1462         }
   1463     }
   1464 
   1465     private boolean maybeDoubleSpacePeriod() {
   1466         final SettingsValues currentSettingsValues = mSettings.getCurrent();
   1467         if (!currentSettingsValues.mUseDoubleSpacePeriod) return false;
   1468         if (!mHandler.isAcceptingDoubleSpacePeriod()) return false;
   1469         // We only do this when we see two spaces and an accepted code point before the cursor.
   1470         // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars.
   1471         final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0);
   1472         if (null == lastThree) return false;
   1473         final int length = lastThree.length();
   1474         if (length < 3) return false;
   1475         if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false;
   1476         if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false;
   1477         // We know there are spaces in pos -1 and -2, and we have at least three chars.
   1478         // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space,
   1479         // so this is fine.
   1480         final int firstCodePoint =
   1481                 Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ?
   1482                         Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3);
   1483         if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
   1484             mHandler.cancelDoubleSpacePeriodTimer();
   1485             mConnection.deleteSurroundingText(2, 0);
   1486             final String textToInsert = new String(
   1487                     new int[] { currentSettingsValues.mSentenceSeparator, Constants.CODE_SPACE },
   1488                     0, 2);
   1489             mConnection.commitText(textToInsert, 1);
   1490             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1491                 ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
   1492                         false /* isBatchMode */);
   1493             }
   1494             mKeyboardSwitcher.updateShiftState();
   1495             return true;
   1496         }
   1497         return false;
   1498     }
   1499 
   1500     private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
   1501         // TODO: Check again whether there really ain't a better way to check this.
   1502         // TODO: This should probably be language-dependant...
   1503         return Character.isLetterOrDigit(codePoint)
   1504                 || codePoint == Constants.CODE_SINGLE_QUOTE
   1505                 || codePoint == Constants.CODE_DOUBLE_QUOTE
   1506                 || codePoint == Constants.CODE_CLOSING_PARENTHESIS
   1507                 || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
   1508                 || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
   1509                 || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
   1510                 || codePoint == Constants.CODE_PLUS
   1511                 || codePoint == Constants.CODE_PERCENT
   1512                 || Character.getType(codePoint) == Character.OTHER_SYMBOL;
   1513     }
   1514 
   1515     // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
   1516     // pressed.
   1517     @Override
   1518     public void addWordToUserDictionary(final String word) {
   1519         if (TextUtils.isEmpty(word)) {
   1520             // Probably never supposed to happen, but just in case.
   1521             return;
   1522         }
   1523         final String wordToEdit;
   1524         if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) {
   1525             wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
   1526         } else {
   1527             wordToEdit = word;
   1528         }
   1529         mUserDictionary.addWordToUserDictionary(wordToEdit);
   1530     }
   1531 
   1532     private void onSettingsKeyPressed() {
   1533         if (isShowingOptionDialog()) return;
   1534         showSubtypeSelectorAndSettings();
   1535     }
   1536 
   1537     @Override
   1538     public boolean onCustomRequest(final int requestCode) {
   1539         if (isShowingOptionDialog()) return false;
   1540         switch (requestCode) {
   1541         case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
   1542             if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
   1543                 mRichImm.getInputMethodManager().showInputMethodPicker();
   1544                 return true;
   1545             }
   1546             return false;
   1547         }
   1548         return false;
   1549     }
   1550 
   1551     private boolean isShowingOptionDialog() {
   1552         return mOptionsDialog != null && mOptionsDialog.isShowing();
   1553     }
   1554 
   1555     private void performEditorAction(final int actionId) {
   1556         mConnection.performEditorAction(actionId);
   1557     }
   1558 
   1559     // TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
   1560     private void handleLanguageSwitchKey() {
   1561         final IBinder token = getWindow().getWindow().getAttributes().token;
   1562         if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) {
   1563             mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
   1564             return;
   1565         }
   1566         mSubtypeState.switchSubtype(token, mRichImm);
   1567     }
   1568 
   1569     private void sendDownUpKeyEvent(final int code) {
   1570         final long eventTime = SystemClock.uptimeMillis();
   1571         mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
   1572                 KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
   1573                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
   1574         mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
   1575                 KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
   1576                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
   1577     }
   1578 
   1579     private void sendKeyCodePoint(final int code) {
   1580         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1581             ResearchLogger.latinIME_sendKeyCodePoint(code);
   1582         }
   1583         // TODO: Remove this special handling of digit letters.
   1584         // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
   1585         if (code >= '0' && code <= '9') {
   1586             sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0);
   1587             return;
   1588         }
   1589 
   1590         if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) {
   1591             // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
   1592             // a hardware keyboard event on pressing enter or delete. This is bad for many
   1593             // reasons (there are race conditions with commits) but some applications are
   1594             // relying on this behavior so we continue to support it for older apps.
   1595             sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
   1596         } else {
   1597             mConnection.commitText(StringUtils.newSingleCodePointString(code), 1);
   1598         }
   1599     }
   1600 
   1601     // Implementation of {@link KeyboardActionListener}.
   1602     @Override
   1603     public void onCodeInput(final int primaryCode, final int x, final int y) {
   1604         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1605             ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
   1606         }
   1607         final long when = SystemClock.uptimeMillis();
   1608         if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
   1609             mDeleteCount = 0;
   1610         }
   1611         mLastKeyTime = when;
   1612         mConnection.beginBatchEdit();
   1613         final KeyboardSwitcher switcher = mKeyboardSwitcher;
   1614         // The space state depends only on the last character pressed and its own previous
   1615         // state. Here, we revert the space state to neutral if the key is actually modifying
   1616         // the input contents (any non-shift key), which is what we should do for
   1617         // all inputs that do not result in a special state. Each character handling is then
   1618         // free to override the state as they see fit.
   1619         final int spaceState = mSpaceState;
   1620         if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
   1621 
   1622         // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
   1623         if (primaryCode != Constants.CODE_SPACE) {
   1624             mHandler.cancelDoubleSpacePeriodTimer();
   1625         }
   1626 
   1627         boolean didAutoCorrect = false;
   1628         switch (primaryCode) {
   1629         case Constants.CODE_DELETE:
   1630             mSpaceState = SPACE_STATE_NONE;
   1631             handleBackspace(spaceState);
   1632             LatinImeLogger.logOnDelete(x, y);
   1633             break;
   1634         case Constants.CODE_SHIFT:
   1635             // Note: Calling back to the keyboard on Shift key is handled in
   1636             // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
   1637             final Keyboard currentKeyboard = switcher.getKeyboard();
   1638             if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
   1639                 // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
   1640                 // alphabetic shift and shift while in symbol layout.
   1641                 handleRecapitalize();
   1642             }
   1643             break;
   1644         case Constants.CODE_CAPSLOCK:
   1645             // Note: Changing keyboard to shift lock state is handled in
   1646             // {@link KeyboardSwitcher#onCodeInput(int)}.
   1647             break;
   1648         case Constants.CODE_SWITCH_ALPHA_SYMBOL:
   1649             // Note: Calling back to the keyboard on symbol key is handled in
   1650             // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
   1651             break;
   1652         case Constants.CODE_SETTINGS:
   1653             onSettingsKeyPressed();
   1654             break;
   1655         case Constants.CODE_SHORTCUT:
   1656             mSubtypeSwitcher.switchToShortcutIME(this);
   1657             break;
   1658         case Constants.CODE_ACTION_NEXT:
   1659             performEditorAction(EditorInfo.IME_ACTION_NEXT);
   1660             break;
   1661         case Constants.CODE_ACTION_PREVIOUS:
   1662             performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
   1663             break;
   1664         case Constants.CODE_LANGUAGE_SWITCH:
   1665             handleLanguageSwitchKey();
   1666             break;
   1667         case Constants.CODE_EMOJI:
   1668             // Note: Switching emoji keyboard is being handled in
   1669             // {@link KeyboardState#onCodeInput(int,int)}.
   1670             break;
   1671         case Constants.CODE_ENTER:
   1672             final EditorInfo editorInfo = getCurrentInputEditorInfo();
   1673             final int imeOptionsActionId =
   1674                     InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
   1675             if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
   1676                 // Either we have an actionLabel and we should performEditorAction with actionId
   1677                 // regardless of its value.
   1678                 performEditorAction(editorInfo.actionId);
   1679             } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
   1680                 // We didn't have an actionLabel, but we had another action to execute.
   1681                 // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
   1682                 // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
   1683                 // means there should be an action and the app didn't bother to set a specific
   1684                 // code for it - presumably it only handles one. It does not have to be treated
   1685                 // in any specific way: anything that is not IME_ACTION_NONE should be sent to
   1686                 // performEditorAction.
   1687                 performEditorAction(imeOptionsActionId);
   1688             } else {
   1689                 // No action label, and the action from imeOptions is NONE: this is a regular
   1690                 // enter key that should input a carriage return.
   1691                 didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
   1692             }
   1693             break;
   1694         case Constants.CODE_SHIFT_ENTER:
   1695             didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
   1696             break;
   1697         default:
   1698             didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState);
   1699             break;
   1700         }
   1701         switcher.onCodeInput(primaryCode);
   1702         // Reset after any single keystroke, except shift, capslock, and symbol-shift
   1703         if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT
   1704                 && primaryCode != Constants.CODE_CAPSLOCK
   1705                 && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
   1706             mLastComposedWord.deactivate();
   1707         if (Constants.CODE_DELETE != primaryCode) {
   1708             mEnteredText = null;
   1709         }
   1710         mConnection.endBatchEdit();
   1711     }
   1712 
   1713     private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y,
   1714             final int spaceState) {
   1715         mSpaceState = SPACE_STATE_NONE;
   1716         final boolean didAutoCorrect;
   1717         final SettingsValues settingsValues = mSettings.getCurrent();
   1718         if (settingsValues.isWordSeparator(primaryCode)
   1719                 || Character.getType(primaryCode) == Character.OTHER_SYMBOL) {
   1720             didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
   1721         } else {
   1722             didAutoCorrect = false;
   1723             if (SPACE_STATE_PHANTOM == spaceState) {
   1724                 if (settingsValues.mIsInternal) {
   1725                     if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
   1726                         LatinImeLoggerUtils.onAutoCorrection(
   1727                                 "", mWordComposer.getTypedWord(), " ", mWordComposer);
   1728                     }
   1729                 }
   1730                 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   1731                     // If we are in the middle of a recorrection, we need to commit the recorrection
   1732                     // first so that we can insert the character at the current cursor position.
   1733                     resetEntireInputState(mLastSelectionStart);
   1734                 } else {
   1735                     commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   1736                 }
   1737             }
   1738             final int keyX, keyY;
   1739             final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   1740             if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
   1741                 keyX = x;
   1742                 keyY = y;
   1743             } else {
   1744                 keyX = Constants.NOT_A_COORDINATE;
   1745                 keyY = Constants.NOT_A_COORDINATE;
   1746             }
   1747             handleCharacter(primaryCode, keyX, keyY, spaceState);
   1748         }
   1749         mExpectingUpdateSelection = true;
   1750         return didAutoCorrect;
   1751     }
   1752 
   1753     // Called from PointerTracker through the KeyboardActionListener interface
   1754     @Override
   1755     public void onTextInput(final String rawText) {
   1756         mConnection.beginBatchEdit();
   1757         if (mWordComposer.isComposingWord()) {
   1758             commitCurrentAutoCorrection(rawText);
   1759         } else {
   1760             resetComposingState(true /* alsoResetLastComposedWord */);
   1761         }
   1762         mHandler.postUpdateSuggestionStrip();
   1763         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
   1764                 && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) {
   1765             ResearchLogger.getInstance().onResearchKeySelected(this);
   1766             return;
   1767         }
   1768         final String text = specificTldProcessingOnTextInput(rawText);
   1769         if (SPACE_STATE_PHANTOM == mSpaceState) {
   1770             promotePhantomSpace();
   1771         }
   1772         mConnection.commitText(text, 1);
   1773         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1774             ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */);
   1775         }
   1776         mConnection.endBatchEdit();
   1777         // Space state must be updated before calling updateShiftState
   1778         mSpaceState = SPACE_STATE_NONE;
   1779         mKeyboardSwitcher.updateShiftState();
   1780         mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT);
   1781         mEnteredText = text;
   1782     }
   1783 
   1784     @Override
   1785     public void onStartBatchInput() {
   1786         mInputUpdater.onStartBatchInput();
   1787         mHandler.cancelUpdateSuggestionStrip();
   1788         mConnection.beginBatchEdit();
   1789         final SettingsValues settingsValues = mSettings.getCurrent();
   1790         if (mWordComposer.isComposingWord()) {
   1791             if (settingsValues.mIsInternal) {
   1792                 if (mWordComposer.isBatchMode()) {
   1793                     LatinImeLoggerUtils.onAutoCorrection(
   1794                             "", mWordComposer.getTypedWord(), " ", mWordComposer);
   1795                 }
   1796             }
   1797             final int wordComposerSize = mWordComposer.size();
   1798             // Since isComposingWord() is true, the size is at least 1.
   1799             if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   1800                 // If we are in the middle of a recorrection, we need to commit the recorrection
   1801                 // first so that we can insert the batch input at the current cursor position.
   1802                 resetEntireInputState(mLastSelectionStart);
   1803             } else if (wordComposerSize <= 1) {
   1804                 // We auto-correct the previous (typed, not gestured) string iff it's one character
   1805                 // long. The reason for this is, even in the middle of gesture typing, you'll still
   1806                 // tap one-letter words and you want them auto-corrected (typically, "i" in English
   1807                 // should become "I"). However for any longer word, we assume that the reason for
   1808                 // tapping probably is that the word you intend to type is not in the dictionary,
   1809                 // so we do not attempt to correct, on the assumption that if that was a dictionary
   1810                 // word, the user would probably have gestured instead.
   1811                 commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
   1812             } else {
   1813                 commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   1814             }
   1815             mExpectingUpdateSelection = true;
   1816         }
   1817         final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
   1818         if (Character.isLetterOrDigit(codePointBeforeCursor)
   1819                 || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
   1820             mSpaceState = SPACE_STATE_PHANTOM;
   1821         }
   1822         mConnection.endBatchEdit();
   1823         mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
   1824     }
   1825 
   1826     static final class InputUpdater implements Handler.Callback {
   1827         private final Handler mHandler;
   1828         private final LatinIME mLatinIme;
   1829         private final Object mLock = new Object();
   1830         private boolean mInBatchInput; // synchronized using {@link #mLock}.
   1831 
   1832         InputUpdater(final LatinIME latinIme) {
   1833             final HandlerThread handlerThread = new HandlerThread(
   1834                     InputUpdater.class.getSimpleName());
   1835             handlerThread.start();
   1836             mHandler = new Handler(handlerThread.getLooper(), this);
   1837             mLatinIme = latinIme;
   1838         }
   1839 
   1840         private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1;
   1841         private static final int MSG_GET_SUGGESTED_WORDS = 2;
   1842 
   1843         @Override
   1844         public boolean handleMessage(final Message msg) {
   1845             // TODO: straighten message passing - we don't need two kinds of messages calling
   1846             // each other.
   1847             switch (msg.what) {
   1848                 case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
   1849                     updateBatchInput((InputPointers)msg.obj, msg.arg2 /* sequenceNumber */);
   1850                     break;
   1851                 case MSG_GET_SUGGESTED_WORDS:
   1852                     mLatinIme.getSuggestedWords(msg.arg1 /* sessionId */,
   1853                             msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj);
   1854                     break;
   1855             }
   1856             return true;
   1857         }
   1858 
   1859         // Run in the UI thread.
   1860         public void onStartBatchInput() {
   1861             synchronized (mLock) {
   1862                 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
   1863                 mInBatchInput = true;
   1864                 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
   1865                         SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
   1866             }
   1867         }
   1868 
   1869         // Run in the Handler thread.
   1870         private void updateBatchInput(final InputPointers batchPointers, final int sequenceNumber) {
   1871             synchronized (mLock) {
   1872                 if (!mInBatchInput) {
   1873                     // Batch input has ended or canceled while the message was being delivered.
   1874                     return;
   1875                 }
   1876 
   1877                 getSuggestedWordsGestureLocked(batchPointers, sequenceNumber,
   1878                         new OnGetSuggestedWordsCallback() {
   1879                     @Override
   1880                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
   1881                         mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
   1882                                 suggestedWords, false /* dismissGestureFloatingPreviewText */);
   1883                     }
   1884                 });
   1885             }
   1886         }
   1887 
   1888         // Run in the UI thread.
   1889         public void onUpdateBatchInput(final InputPointers batchPointers,
   1890                 final int sequenceNumber) {
   1891             if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
   1892                 return;
   1893             }
   1894             mHandler.obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 0 /* arg1 */,
   1895                     sequenceNumber /* arg2 */, batchPointers /* obj */).sendToTarget();
   1896         }
   1897 
   1898         public void onCancelBatchInput() {
   1899             synchronized (mLock) {
   1900                 mInBatchInput = false;
   1901                 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
   1902                         SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
   1903             }
   1904         }
   1905 
   1906         // Run in the UI thread.
   1907         public void onEndBatchInput(final InputPointers batchPointers) {
   1908             synchronized(mLock) {
   1909                 getSuggestedWordsGestureLocked(batchPointers, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
   1910                         new OnGetSuggestedWordsCallback() {
   1911                     @Override
   1912                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
   1913                         mInBatchInput = false;
   1914                         mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords,
   1915                                 true /* dismissGestureFloatingPreviewText */);
   1916                         mLatinIme.mHandler.onEndBatchInput(suggestedWords);
   1917                     }
   1918                 });
   1919             }
   1920         }
   1921 
   1922         // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
   1923         // be synchronized.
   1924         private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
   1925                 final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
   1926             mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
   1927             mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
   1928                     sequenceNumber, new OnGetSuggestedWordsCallback() {
   1929                 @Override
   1930                 public void onGetSuggestedWords(SuggestedWords suggestedWords) {
   1931                     final int suggestionCount = suggestedWords.size();
   1932                     if (suggestionCount <= 1) {
   1933                         final String mostProbableSuggestion = (suggestionCount == 0) ? null
   1934                                 : suggestedWords.getWord(0);
   1935                         callback.onGetSuggestedWords(
   1936                                 mLatinIme.getOlderSuggestions(mostProbableSuggestion));
   1937                     }
   1938                     callback.onGetSuggestedWords(suggestedWords);
   1939                 }
   1940             });
   1941         }
   1942 
   1943         public void getSuggestedWords(final int sessionId, final int sequenceNumber,
   1944                 final OnGetSuggestedWordsCallback callback) {
   1945             mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback)
   1946                     .sendToTarget();
   1947         }
   1948 
   1949         void quitLooper() {
   1950             mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS);
   1951             mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
   1952             mHandler.getLooper().quit();
   1953         }
   1954     }
   1955 
   1956     // This method must run in UI Thread.
   1957     private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
   1958             final boolean dismissGestureFloatingPreviewText) {
   1959         showSuggestionStrip(suggestedWords);
   1960         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   1961         mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
   1962         if (dismissGestureFloatingPreviewText) {
   1963             mainKeyboardView.dismissGestureFloatingPreviewText();
   1964         }
   1965     }
   1966 
   1967     /* The sequence number member is only used in onUpdateBatchInput. It is increased each time
   1968      * auto-commit happens. The reason we need this is, when auto-commit happens we trim the
   1969      * input pointers that are held in a singleton, and to know how much to trim we rely on the
   1970      * results of the suggestion process that is held in mSuggestedWords.
   1971      * However, the suggestion process is asynchronous, and sometimes we may enter the
   1972      * onUpdateBatchInput method twice without having recomputed suggestions yet, or having
   1973      * received new suggestions generated from not-yet-trimmed input pointers. In this case, the
   1974      * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we
   1975      * remove an unrelated number of pointers (possibly even more than are left in the input
   1976      * pointers, leading to a crash).
   1977      * To avoid that, we increase the sequence number each time we auto-commit and trim the
   1978      * input pointers, and we do not use any suggested words that have been generated with an
   1979      * earlier sequence number.
   1980      */
   1981     private int mAutoCommitSequenceNumber = 1;
   1982     @Override
   1983     public void onUpdateBatchInput(final InputPointers batchPointers) {
   1984         if (mSettings.getCurrent().mPhraseGestureEnabled) {
   1985             final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
   1986             // If these suggested words have been generated with out of date input pointers, then
   1987             // we skip auto-commit (see comments above on the mSequenceNumber member).
   1988             if (null != candidate && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) {
   1989                 if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
   1990                     final String[] commitParts = candidate.mWord.split(" ", 2);
   1991                     batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
   1992                     promotePhantomSpace();
   1993                     mConnection.commitText(commitParts[0], 0);
   1994                     mSpaceState = SPACE_STATE_PHANTOM;
   1995                     mKeyboardSwitcher.updateShiftState();
   1996                     mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
   1997                     ++mAutoCommitSequenceNumber;
   1998                 }
   1999             }
   2000         }
   2001         mInputUpdater.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
   2002     }
   2003 
   2004     // This method must run in UI Thread.
   2005     public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) {
   2006         final String batchInputText = suggestedWords.isEmpty()
   2007                 ? null : suggestedWords.getWord(0);
   2008         if (TextUtils.isEmpty(batchInputText)) {
   2009             return;
   2010         }
   2011         mConnection.beginBatchEdit();
   2012         if (SPACE_STATE_PHANTOM == mSpaceState) {
   2013             promotePhantomSpace();
   2014         }
   2015         if (mSettings.getCurrent().mPhraseGestureEnabled) {
   2016             // Find the last space
   2017             final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
   2018             if (0 != indexOfLastSpace) {
   2019                 mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
   2020                 showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture());
   2021             }
   2022             final String lastWord = batchInputText.substring(indexOfLastSpace);
   2023             mWordComposer.setBatchInputWord(lastWord);
   2024             mConnection.setComposingText(lastWord, 1);
   2025         } else {
   2026             mWordComposer.setBatchInputWord(batchInputText);
   2027             mConnection.setComposingText(batchInputText, 1);
   2028         }
   2029         mExpectingUpdateSelection = true;
   2030         mConnection.endBatchEdit();
   2031         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2032             ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords);
   2033         }
   2034         // Space state must be updated before calling updateShiftState
   2035         mSpaceState = SPACE_STATE_PHANTOM;
   2036         mKeyboardSwitcher.updateShiftState();
   2037     }
   2038 
   2039     @Override
   2040     public void onEndBatchInput(final InputPointers batchPointers) {
   2041         mInputUpdater.onEndBatchInput(batchPointers);
   2042     }
   2043 
   2044     private String specificTldProcessingOnTextInput(final String text) {
   2045         if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
   2046                 || !Character.isLetter(text.charAt(1))) {
   2047             // Not a tld: do nothing.
   2048             return text;
   2049         }
   2050         // We have a TLD (or something that looks like this): make sure we don't add
   2051         // a space even if currently in phantom mode.
   2052         mSpaceState = SPACE_STATE_NONE;
   2053         // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code
   2054         final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
   2055         if (lastOne != null && lastOne.length() == 1
   2056                 && lastOne.charAt(0) == Constants.CODE_PERIOD) {
   2057             return text.substring(1);
   2058         } else {
   2059             return text;
   2060         }
   2061     }
   2062 
   2063     // Called from PointerTracker through the KeyboardActionListener interface
   2064     @Override
   2065     public void onFinishSlidingInput() {
   2066         // User finished sliding input.
   2067         mKeyboardSwitcher.onFinishSlidingInput();
   2068     }
   2069 
   2070     // Called from PointerTracker through the KeyboardActionListener interface
   2071     @Override
   2072     public void onCancelInput() {
   2073         // User released a finger outside any key
   2074         // Nothing to do so far.
   2075     }
   2076 
   2077     @Override
   2078     public void onCancelBatchInput() {
   2079         mInputUpdater.onCancelBatchInput();
   2080     }
   2081 
   2082     private void handleBackspace(final int spaceState) {
   2083         // We revert these in this method if the deletion doesn't happen.
   2084         mDeleteCount++;
   2085         mExpectingUpdateSelection = true;
   2086 
   2087         // In many cases, we may have to put the keyboard in auto-shift state again. However
   2088         // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
   2089         // during key repeat.
   2090         mHandler.postUpdateShiftState();
   2091 
   2092         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   2093             // If we are in the middle of a recorrection, we need to commit the recorrection
   2094             // first so that we can remove the character at the current cursor position.
   2095             resetEntireInputState(mLastSelectionStart);
   2096             // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
   2097         }
   2098         if (mWordComposer.isComposingWord()) {
   2099             if (mWordComposer.isBatchMode()) {
   2100                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2101                     final String word = mWordComposer.getTypedWord();
   2102                     ResearchLogger.latinIME_handleBackspace_batch(word, 1);
   2103                 }
   2104                 final String rejectedSuggestion = mWordComposer.getTypedWord();
   2105                 mWordComposer.reset();
   2106                 mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
   2107             } else {
   2108                 mWordComposer.deleteLast();
   2109             }
   2110             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
   2111             mHandler.postUpdateSuggestionStrip();
   2112             if (!mWordComposer.isComposingWord()) {
   2113                 // If we just removed the last character, auto-caps mode may have changed so we
   2114                 // need to re-evaluate.
   2115                 mKeyboardSwitcher.updateShiftState();
   2116             }
   2117         } else {
   2118             final SettingsValues currentSettings = mSettings.getCurrent();
   2119             if (mLastComposedWord.canRevertCommit()) {
   2120                 if (currentSettings.mIsInternal) {
   2121                     LatinImeLoggerUtils.onAutoCorrectionCancellation();
   2122                 }
   2123                 revertCommit();
   2124                 return;
   2125             }
   2126             if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
   2127                 // Cancel multi-character input: remove the text we just entered.
   2128                 // This is triggered on backspace after a key that inputs multiple characters,
   2129                 // like the smiley key or the .com key.
   2130                 mConnection.deleteSurroundingText(mEnteredText.length(), 0);
   2131                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2132                     ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
   2133                 }
   2134                 mEnteredText = null;
   2135                 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
   2136                 // In addition we know that spaceState is false, and that we should not be
   2137                 // reverting any autocorrect at this point. So we can safely return.
   2138                 return;
   2139             }
   2140             if (SPACE_STATE_DOUBLE == spaceState) {
   2141                 mHandler.cancelDoubleSpacePeriodTimer();
   2142                 if (mConnection.revertDoubleSpacePeriod()) {
   2143                     // No need to reset mSpaceState, it has already be done (that's why we
   2144                     // receive it as a parameter)
   2145                     return;
   2146                 }
   2147             } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
   2148                 if (mConnection.revertSwapPunctuation()) {
   2149                     // Likewise
   2150                     return;
   2151                 }
   2152             }
   2153 
   2154             // No cancelling of commit/double space/swap: we have a regular backspace.
   2155             // We should backspace one char and restart suggestion if at the end of a word.
   2156             if (mLastSelectionStart != mLastSelectionEnd) {
   2157                 // If there is a selection, remove it.
   2158                 final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
   2159                 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
   2160                 // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
   2161                 // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
   2162                 // but we want to set it right away to avoid it being used with the wrong values
   2163                 // later (typically, in a subsequent press on backspace).
   2164                 mLastSelectionEnd = mLastSelectionStart;
   2165                 mConnection.deleteSurroundingText(numCharsDeleted, 0);
   2166             } else {
   2167                 // There is no selection, just delete one character.
   2168                 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
   2169                     // This should never happen.
   2170                     Log.e(TAG, "Backspace when we don't know the selection position");
   2171                 }
   2172                 if (mAppWorkAroundsUtils.isBeforeJellyBean() ||
   2173                         currentSettings.mInputAttributes.isTypeNull()) {
   2174                     // There are two possible reasons to send a key event: either the field has
   2175                     // type TYPE_NULL, in which case the keyboard should send events, or we are
   2176                     // running in backward compatibility mode. Before Jelly bean, the keyboard
   2177                     // would simulate a hardware keyboard event on pressing enter or delete. This
   2178                     // is bad for many reasons (there are race conditions with commits) but some
   2179                     // applications are relying on this behavior so we continue to support it for
   2180                     // older apps, so we retain this behavior if the app has target SDK < JellyBean.
   2181                     sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
   2182                     if (mDeleteCount > DELETE_ACCELERATE_AT) {
   2183                         sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
   2184                     }
   2185                 } else {
   2186                     final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
   2187                     if (codePointBeforeCursor == Constants.NOT_A_CODE) {
   2188                         // Nothing to delete before the cursor. We have to revert the deletion
   2189                         // states that were updated at the beginning of this method.
   2190                         mDeleteCount--;
   2191                         mExpectingUpdateSelection = false;
   2192                         return;
   2193                     }
   2194                     final int lengthToDelete =
   2195                             Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
   2196                     mConnection.deleteSurroundingText(lengthToDelete, 0);
   2197                     if (mDeleteCount > DELETE_ACCELERATE_AT) {
   2198                         final int codePointBeforeCursorToDeleteAgain =
   2199                                 mConnection.getCodePointBeforeCursor();
   2200                         if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
   2201                             final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
   2202                                    codePointBeforeCursorToDeleteAgain) ? 2 : 1;
   2203                             mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
   2204                         }
   2205                     }
   2206                 }
   2207             }
   2208             if (currentSettings.isSuggestionsRequested(mDisplayOrientation)
   2209                     && currentSettings.mCurrentLanguageHasSpaces) {
   2210                 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
   2211             }
   2212             // We just removed a character. We need to update the auto-caps state.
   2213             mKeyboardSwitcher.updateShiftState();
   2214         }
   2215     }
   2216 
   2217     /*
   2218      * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
   2219      */
   2220     private boolean maybeStripSpace(final int code,
   2221             final int spaceState, final boolean isFromSuggestionStrip) {
   2222         if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
   2223             mConnection.removeTrailingSpace();
   2224             return false;
   2225         }
   2226         if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
   2227                 && isFromSuggestionStrip) {
   2228             final SettingsValues currentSettings = mSettings.getCurrent();
   2229             if (currentSettings.isUsuallyPrecededBySpace(code)) return false;
   2230             if (currentSettings.isUsuallyFollowedBySpace(code)) return true;
   2231             mConnection.removeTrailingSpace();
   2232         }
   2233         return false;
   2234     }
   2235 
   2236     private void handleCharacter(final int primaryCode, final int x,
   2237             final int y, final int spaceState) {
   2238         // TODO: refactor this method to stop flipping isComposingWord around all the time, and
   2239         // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
   2240         // which has the same name as other handle* methods but is not the same.
   2241         boolean isComposingWord = mWordComposer.isComposingWord();
   2242 
   2243         // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
   2244         // See onStartBatchInput() to see how to do it.
   2245         final SettingsValues currentSettings = mSettings.getCurrent();
   2246         if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) {
   2247             if (isComposingWord) {
   2248                 // Sanity check
   2249                 throw new RuntimeException("Should not be composing here");
   2250             }
   2251             promotePhantomSpace();
   2252         }
   2253 
   2254         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   2255             // If we are in the middle of a recorrection, we need to commit the recorrection
   2256             // first so that we can insert the character at the current cursor position.
   2257             resetEntireInputState(mLastSelectionStart);
   2258             isComposingWord = false;
   2259         }
   2260         // We want to find out whether to start composing a new word with this character. If so,
   2261         // we need to reset the composing state and switch isComposingWord. The order of the
   2262         // tests is important for good performance.
   2263         // We only start composing if we're not already composing.
   2264         if (!isComposingWord
   2265         // We only start composing if this is a word code point. Essentially that means it's a
   2266         // a letter or a word connector.
   2267                 && currentSettings.isWordCodePoint(primaryCode)
   2268         // We never go into composing state if suggestions are not requested.
   2269                 && currentSettings.isSuggestionsRequested(mDisplayOrientation) &&
   2270         // In languages with spaces, we only start composing a word when we are not already
   2271         // touching a word. In languages without spaces, the above conditions are sufficient.
   2272                 (!mConnection.isCursorTouchingWord(currentSettings)
   2273                         || !currentSettings.mCurrentLanguageHasSpaces)) {
   2274             // Reset entirely the composing state anyway, then start composing a new word unless
   2275             // the character is a single quote or a dash. The idea here is, single quote and dash
   2276             // are not separators and they should be treated as normal characters, except in the
   2277             // first position where they should not start composing a word.
   2278             isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode
   2279                     && Constants.CODE_DASH != primaryCode);
   2280             // Here we don't need to reset the last composed word. It will be reset
   2281             // when we commit this one, if we ever do; if on the other hand we backspace
   2282             // it entirely and resume suggestions on the previous word, we'd like to still
   2283             // have touch coordinates for it.
   2284             resetComposingState(false /* alsoResetLastComposedWord */);
   2285         }
   2286         if (isComposingWord) {
   2287             final int keyX, keyY;
   2288             if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) {
   2289                 final KeyDetector keyDetector =
   2290                         mKeyboardSwitcher.getMainKeyboardView().getKeyDetector();
   2291                 keyX = keyDetector.getTouchX(x);
   2292                 keyY = keyDetector.getTouchY(y);
   2293             } else {
   2294                 keyX = x;
   2295                 keyY = y;
   2296             }
   2297             mWordComposer.add(primaryCode, keyX, keyY);
   2298             // If it's the first letter, make note of auto-caps state
   2299             if (mWordComposer.size() == 1) {
   2300                 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
   2301             }
   2302             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
   2303         } else {
   2304             final boolean swapWeakSpace = maybeStripSpace(primaryCode,
   2305                     spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
   2306 
   2307             sendKeyCodePoint(primaryCode);
   2308 
   2309             if (swapWeakSpace) {
   2310                 swapSwapperAndSpace();
   2311                 mSpaceState = SPACE_STATE_WEAK;
   2312             }
   2313             // In case the "add to dictionary" hint was still displayed.
   2314             if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
   2315         }
   2316         mHandler.postUpdateSuggestionStrip();
   2317         if (currentSettings.mIsInternal) {
   2318             LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y);
   2319         }
   2320     }
   2321 
   2322     private void handleRecapitalize() {
   2323         if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
   2324         // If we have a recapitalize in progress, use it; otherwise, create a new one.
   2325         if (!mRecapitalizeStatus.isActive()
   2326                 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
   2327             final CharSequence selectedText =
   2328                     mConnection.getSelectedText(0 /* flags, 0 for no styles */);
   2329             if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
   2330             final SettingsValues currentSettings = mSettings.getCurrent();
   2331             mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
   2332                     selectedText.toString(), currentSettings.mLocale,
   2333                     currentSettings.mWordSeparators);
   2334             // We trim leading and trailing whitespace.
   2335             mRecapitalizeStatus.trim();
   2336             // Trimming the object may have changed the length of the string, and we need to
   2337             // reposition the selection handles accordingly. As this result in an IPC call,
   2338             // only do it if it's actually necessary, in other words if the recapitalize status
   2339             // is not set at the same place as before.
   2340             if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
   2341                 mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
   2342                 mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
   2343             }
   2344         }
   2345         mConnection.finishComposingText();
   2346         mRecapitalizeStatus.rotate();
   2347         final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
   2348         mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
   2349         mConnection.deleteSurroundingText(numCharsDeleted, 0);
   2350         mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
   2351         mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
   2352         mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
   2353         mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
   2354         // Match the keyboard to the new state.
   2355         mKeyboardSwitcher.updateShiftState();
   2356     }
   2357 
   2358     // Returns true if we do an autocorrection, false otherwise.
   2359     private boolean handleSeparator(final int primaryCode, final int x, final int y,
   2360             final int spaceState) {
   2361         boolean didAutoCorrect = false;
   2362         final SettingsValues currentSettings = mSettings.getCurrent();
   2363         // We avoid sending spaces in languages without spaces if we were composing.
   2364         final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode
   2365                 && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord();
   2366         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   2367             // If we are in the middle of a recorrection, we need to commit the recorrection
   2368             // first so that we can insert the separator at the current cursor position.
   2369             resetEntireInputState(mLastSelectionStart);
   2370         }
   2371         if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
   2372             if (currentSettings.mCorrectionEnabled) {
   2373                 final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
   2374                         : StringUtils.newSingleCodePointString(primaryCode);
   2375                 commitCurrentAutoCorrection(separator);
   2376                 didAutoCorrect = true;
   2377             } else {
   2378                 commitTyped(StringUtils.newSingleCodePointString(primaryCode));
   2379             }
   2380         }
   2381 
   2382         final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
   2383                 Constants.SUGGESTION_STRIP_COORDINATE == x);
   2384 
   2385         if (SPACE_STATE_PHANTOM == spaceState &&
   2386                 currentSettings.isUsuallyPrecededBySpace(primaryCode)) {
   2387             promotePhantomSpace();
   2388         }
   2389         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2390             ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
   2391         }
   2392 
   2393         if (!shouldAvoidSendingCode) {
   2394             sendKeyCodePoint(primaryCode);
   2395         }
   2396 
   2397         if (Constants.CODE_SPACE == primaryCode) {
   2398             if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
   2399                 if (maybeDoubleSpacePeriod()) {
   2400                     mSpaceState = SPACE_STATE_DOUBLE;
   2401                 } else if (!isShowingPunctuationList()) {
   2402                     mSpaceState = SPACE_STATE_WEAK;
   2403                 }
   2404             }
   2405 
   2406             mHandler.startDoubleSpacePeriodTimer();
   2407             mHandler.postUpdateSuggestionStrip();
   2408         } else {
   2409             if (swapWeakSpace) {
   2410                 swapSwapperAndSpace();
   2411                 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
   2412             } else if (SPACE_STATE_PHANTOM == spaceState
   2413                     && currentSettings.isUsuallyFollowedBySpace(primaryCode)) {
   2414                 // If we are in phantom space state, and the user presses a separator, we want to
   2415                 // stay in phantom space state so that the next keypress has a chance to add the
   2416                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
   2417                 // then insert a comma and go on to typing the next word, I want the space to be
   2418                 // inserted automatically before the next word, the same way it is when I don't
   2419                 // input the comma.
   2420                 // The case is a little different if the separator is a space stripper. Such a
   2421                 // separator does not normally need a space on the right (that's the difference
   2422                 // between swappers and strippers), so we should not stay in phantom space state if
   2423                 // the separator is a stripper. Hence the additional test above.
   2424                 mSpaceState = SPACE_STATE_PHANTOM;
   2425             }
   2426 
   2427             // Set punctuation right away. onUpdateSelection will fire but tests whether it is
   2428             // already displayed or not, so it's okay.
   2429             setPunctuationSuggestions();
   2430         }
   2431         if (currentSettings.mIsInternal) {
   2432             LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y);
   2433         }
   2434 
   2435         mKeyboardSwitcher.updateShiftState();
   2436         return didAutoCorrect;
   2437     }
   2438 
   2439     private CharSequence getTextWithUnderline(final String text) {
   2440         return mIsAutoCorrectionIndicatorOn
   2441                 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
   2442                 : text;
   2443     }
   2444 
   2445     private void handleClose() {
   2446         // TODO: Verify that words are logged properly when IME is closed.
   2447         commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   2448         requestHideSelf(0);
   2449         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   2450         if (mainKeyboardView != null) {
   2451             mainKeyboardView.closing();
   2452         }
   2453     }
   2454 
   2455     // TODO: make this private
   2456     // Outside LatinIME, only used by the test suite.
   2457     @UsedForTesting
   2458     boolean isShowingPunctuationList() {
   2459         if (mSuggestedWords == null) return false;
   2460         return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords;
   2461     }
   2462 
   2463     private boolean isSuggestionsStripVisible() {
   2464         final SettingsValues currentSettings = mSettings.getCurrent();
   2465         if (mSuggestionStripView == null)
   2466             return false;
   2467         if (mSuggestionStripView.isShowingAddToDictionaryHint())
   2468             return true;
   2469         if (null == currentSettings)
   2470             return false;
   2471         if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
   2472             return false;
   2473         if (currentSettings.isApplicationSpecifiedCompletionsOn())
   2474             return true;
   2475         return currentSettings.isSuggestionsRequested(mDisplayOrientation);
   2476     }
   2477 
   2478     private void clearSuggestionStrip() {
   2479         setSuggestedWords(SuggestedWords.EMPTY, false);
   2480         setAutoCorrectionIndicator(false);
   2481     }
   2482 
   2483     private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) {
   2484         mSuggestedWords = words;
   2485         if (mSuggestionStripView != null) {
   2486             mSuggestionStripView.setSuggestions(words);
   2487             mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
   2488         }
   2489     }
   2490 
   2491     private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
   2492         // Put a blue underline to a word in TextView which will be auto-corrected.
   2493         if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
   2494                 && mWordComposer.isComposingWord()) {
   2495             mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
   2496             final CharSequence textWithUnderline =
   2497                     getTextWithUnderline(mWordComposer.getTypedWord());
   2498             // TODO: when called from an updateSuggestionStrip() call that results from a posted
   2499             // message, this is called outside any batch edit. Potentially, this may result in some
   2500             // janky flickering of the screen, although the display speed makes it unlikely in
   2501             // the practice.
   2502             mConnection.setComposingText(textWithUnderline, 1);
   2503         }
   2504     }
   2505 
   2506     private void updateSuggestionStrip() {
   2507         mHandler.cancelUpdateSuggestionStrip();
   2508         final SettingsValues currentSettings = mSettings.getCurrent();
   2509 
   2510         // Check if we have a suggestion engine attached.
   2511         if (mSuggest == null
   2512                 || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
   2513             if (mWordComposer.isComposingWord()) {
   2514                 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
   2515                         + "requested!");
   2516             }
   2517             return;
   2518         }
   2519 
   2520         if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) {
   2521             setPunctuationSuggestions();
   2522             return;
   2523         }
   2524 
   2525         final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
   2526         getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
   2527                 SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
   2528                     @Override
   2529                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
   2530                         holder.set(suggestedWords);
   2531                     }
   2532                 }
   2533         );
   2534 
   2535         // This line may cause the current thread to wait.
   2536         final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT);
   2537         if (suggestedWords != null) {
   2538             showSuggestionStrip(suggestedWords);
   2539         }
   2540     }
   2541 
   2542     private void getSuggestedWords(final int sessionId, final int sequenceNumber,
   2543             final OnGetSuggestedWordsCallback callback) {
   2544         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   2545         final Suggest suggest = mSuggest;
   2546         if (keyboard == null || suggest == null) {
   2547             callback.onGetSuggestedWords(SuggestedWords.EMPTY);
   2548             return;
   2549         }
   2550         // Get the word on which we should search the bigrams. If we are composing a word, it's
   2551         // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
   2552         // should just skip whitespace if any, so 1.
   2553         final SettingsValues currentSettings = mSettings.getCurrent();
   2554         final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
   2555         final String prevWord;
   2556         if (currentSettings.mCurrentLanguageHasSpaces) {
   2557             // If we are typing in a language with spaces we can just look up the previous
   2558             // word from textview.
   2559             prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators,
   2560                     mWordComposer.isComposingWord() ? 2 : 1);
   2561         } else {
   2562             prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
   2563                     : mLastComposedWord.mCommittedWord;
   2564         }
   2565         suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
   2566                 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
   2567                 additionalFeaturesOptions, sessionId, sequenceNumber, callback);
   2568     }
   2569 
   2570     private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId,
   2571             final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
   2572         mInputUpdater.getSuggestedWords(sessionId, sequenceNumber,
   2573                 new OnGetSuggestedWordsCallback() {
   2574                     @Override
   2575                     public void onGetSuggestedWords(SuggestedWords suggestedWords) {
   2576                         callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
   2577                                 mWordComposer.getTypedWord(), suggestedWords));
   2578                     }
   2579                 });
   2580     }
   2581 
   2582     private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
   2583             final SuggestedWords suggestedWords) {
   2584         // TODO: consolidate this into getSuggestedWords
   2585         // We update the suggestion strip only when we have some suggestions to show, i.e. when
   2586         // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
   2587         // replaced with the new one. However, when the word is a dictionary word, or when the
   2588         // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
   2589         // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
   2590         // revert to suggestions - although it is unclear how we can come here if it's displayed.
   2591         if (suggestedWords.size() > 1 || typedWord.length() <= 1
   2592                 || suggestedWords.mTypedWordValid || null == mSuggestionStripView
   2593                 || mSuggestionStripView.isShowingAddToDictionaryHint()) {
   2594             return suggestedWords;
   2595         } else {
   2596             return getOlderSuggestions(typedWord);
   2597         }
   2598     }
   2599 
   2600     private SuggestedWords getOlderSuggestions(final String typedWord) {
   2601         SuggestedWords previousSuggestedWords = mSuggestedWords;
   2602         if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) {
   2603             previousSuggestedWords = SuggestedWords.EMPTY;
   2604         }
   2605         if (typedWord == null) {
   2606             return previousSuggestedWords;
   2607         }
   2608         final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
   2609                 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord,
   2610                         previousSuggestedWords);
   2611         return new SuggestedWords(typedWordAndPreviousSuggestions,
   2612                 false /* typedWordValid */,
   2613                 false /* hasAutoCorrectionCandidate */,
   2614                 false /* isPunctuationSuggestions */,
   2615                 true /* isObsoleteSuggestions */,
   2616                 false /* isPrediction */);
   2617     }
   2618 
   2619     private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) {
   2620         if (suggestedWords.isEmpty()) return;
   2621         final String autoCorrection;
   2622         if (suggestedWords.mWillAutoCorrect) {
   2623             autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
   2624         } else {
   2625             // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)
   2626             // because it may differ from mWordComposer.mTypedWord.
   2627             autoCorrection = typedWord;
   2628         }
   2629         mWordComposer.setAutoCorrection(autoCorrection);
   2630     }
   2631 
   2632     private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
   2633             final String typedWord) {
   2634       if (suggestedWords.isEmpty()) {
   2635           // No auto-correction is available, clear the cached values.
   2636           AccessibilityUtils.getInstance().setAutoCorrection(null, null);
   2637           clearSuggestionStrip();
   2638           return;
   2639       }
   2640       setAutoCorrection(suggestedWords, typedWord);
   2641       final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
   2642       setSuggestedWords(suggestedWords, isAutoCorrection);
   2643       setAutoCorrectionIndicator(isAutoCorrection);
   2644       setSuggestionStripShown(isSuggestionsStripVisible());
   2645       // An auto-correction is available, cache it in accessibility code so
   2646       // we can be speak it if the user touches a key that will insert it.
   2647       AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord);
   2648     }
   2649 
   2650     private void showSuggestionStrip(final SuggestedWords suggestedWords) {
   2651         if (suggestedWords.isEmpty()) {
   2652             clearSuggestionStrip();
   2653             return;
   2654         }
   2655         showSuggestionStripWithTypedWord(suggestedWords,
   2656             suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
   2657     }
   2658 
   2659     private void commitCurrentAutoCorrection(final String separator) {
   2660         // Complete any pending suggestions query first
   2661         if (mHandler.hasPendingUpdateSuggestions()) {
   2662             updateSuggestionStrip();
   2663         }
   2664         final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
   2665         final String typedWord = mWordComposer.getTypedWord();
   2666         final String autoCorrection = (typedAutoCorrection != null)
   2667                 ? typedAutoCorrection : typedWord;
   2668         if (autoCorrection != null) {
   2669             if (TextUtils.isEmpty(typedWord)) {
   2670                 throw new RuntimeException("We have an auto-correction but the typed word "
   2671                         + "is empty? Impossible! I must commit suicide.");
   2672             }
   2673             if (mSettings.isInternal()) {
   2674                 LatinImeLoggerUtils.onAutoCorrection(
   2675                         typedWord, autoCorrection, separator, mWordComposer);
   2676             }
   2677             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2678                 final SuggestedWords suggestedWords = mSuggestedWords;
   2679                 ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
   2680                         separator, mWordComposer.isBatchMode(), suggestedWords);
   2681             }
   2682             mExpectingUpdateSelection = true;
   2683             commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
   2684                     separator);
   2685             if (!typedWord.equals(autoCorrection)) {
   2686                 // This will make the correction flash for a short while as a visual clue
   2687                 // to the user that auto-correction happened. It has no other effect; in particular
   2688                 // note that this won't affect the text inside the text field AT ALL: it only makes
   2689                 // the segment of text starting at the supplied index and running for the length
   2690                 // of the auto-correction flash. At this moment, the "typedWord" argument is
   2691                 // ignored by TextView.
   2692                 mConnection.commitCorrection(
   2693                         new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
   2694                         typedWord, autoCorrection));
   2695             }
   2696         }
   2697     }
   2698 
   2699     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
   2700     // interface
   2701     @Override
   2702     public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) {
   2703         final SuggestedWords suggestedWords = mSuggestedWords;
   2704         final String suggestion = suggestionInfo.mWord;
   2705         // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
   2706         if (suggestion.length() == 1 && isShowingPunctuationList()) {
   2707             // Word separators are suggested before the user inputs something.
   2708             // So, LatinImeLogger logs "" as a user's input.
   2709             LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords);
   2710             // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
   2711             final int primaryCode = suggestion.charAt(0);
   2712             onCodeInput(primaryCode,
   2713                     Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
   2714             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2715                 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion,
   2716                         false /* isBatchMode */, suggestedWords.mIsPrediction);
   2717             }
   2718             return;
   2719         }
   2720 
   2721         mConnection.beginBatchEdit();
   2722         final SettingsValues currentSettings = mSettings.getCurrent();
   2723         if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
   2724                 // In the batch input mode, a manually picked suggested word should just replace
   2725                 // the current batch input text and there is no need for a phantom space.
   2726                 && !mWordComposer.isBatchMode()) {
   2727             final int firstChar = Character.codePointAt(suggestion, 0);
   2728             if (!currentSettings.isWordSeparator(firstChar)
   2729                     || currentSettings.isUsuallyPrecededBySpace(firstChar)) {
   2730                 promotePhantomSpace();
   2731             }
   2732         }
   2733 
   2734         if (currentSettings.isApplicationSpecifiedCompletionsOn()
   2735                 && mApplicationSpecifiedCompletions != null
   2736                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
   2737             mSuggestedWords = SuggestedWords.EMPTY;
   2738             if (mSuggestionStripView != null) {
   2739                 mSuggestionStripView.clear();
   2740             }
   2741             mKeyboardSwitcher.updateShiftState();
   2742             resetComposingState(true /* alsoResetLastComposedWord */);
   2743             final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
   2744             mConnection.commitCompletion(completionInfo);
   2745             mConnection.endBatchEdit();
   2746             return;
   2747         }
   2748 
   2749         // We need to log before we commit, because the word composer will store away the user
   2750         // typed word.
   2751         final String replacedWord = mWordComposer.getTypedWord();
   2752         LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords);
   2753         mExpectingUpdateSelection = true;
   2754         commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
   2755                 LastComposedWord.NOT_A_SEPARATOR);
   2756         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2757             ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
   2758                     mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind,
   2759                     suggestionInfo.mSourceDict.mDictType);
   2760         }
   2761         mConnection.endBatchEdit();
   2762         // Don't allow cancellation of manual pick
   2763         mLastComposedWord.deactivate();
   2764         // Space state must be updated before calling updateShiftState
   2765         mSpaceState = SPACE_STATE_PHANTOM;
   2766         mKeyboardSwitcher.updateShiftState();
   2767 
   2768         // We should show the "Touch again to save" hint if the user pressed the first entry
   2769         // AND it's in none of our current dictionaries (main, user or otherwise).
   2770         // Please note that if mSuggest is null, it means that everything is off: suggestion
   2771         // and correction, so we shouldn't try to show the hint
   2772         final Suggest suggest = mSuggest;
   2773         final boolean showingAddToDictionaryHint =
   2774                 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
   2775                         || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
   2776                         && suggest != null
   2777                         // If the suggestion is not in the dictionary, the hint should be shown.
   2778                         && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true);
   2779 
   2780         if (currentSettings.mIsInternal) {
   2781             LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
   2782                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
   2783         }
   2784         if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
   2785             mSuggestionStripView.showAddToDictionaryHint(
   2786                     suggestion, currentSettings.mHintToSaveText);
   2787         } else {
   2788             // If we're not showing the "Touch again to save", then update the suggestion strip.
   2789             mHandler.postUpdateSuggestionStrip();
   2790         }
   2791     }
   2792 
   2793     /**
   2794      * Commits the chosen word to the text field and saves it for later retrieval.
   2795      */
   2796     private void commitChosenWord(final String chosenWord, final int commitType,
   2797             final String separatorString) {
   2798         final SuggestedWords suggestedWords = mSuggestedWords;
   2799         mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
   2800                 this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
   2801         // Add the word to the user history dictionary
   2802         final String prevWord = addToUserHistoryDictionary(chosenWord);
   2803         // TODO: figure out here if this is an auto-correct or if the best word is actually
   2804         // what user typed. Note: currently this is done much later in
   2805         // LastComposedWord#didCommitTypedWord by string equality of the remembered
   2806         // strings.
   2807         mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString,
   2808                 prevWord);
   2809     }
   2810 
   2811     private void setPunctuationSuggestions() {
   2812         final SettingsValues currentSettings = mSettings.getCurrent();
   2813         if (currentSettings.mBigramPredictionEnabled) {
   2814             clearSuggestionStrip();
   2815         } else {
   2816             setSuggestedWords(currentSettings.mSuggestPuncList, false);
   2817         }
   2818         setAutoCorrectionIndicator(false);
   2819         setSuggestionStripShown(isSuggestionsStripVisible());
   2820     }
   2821 
   2822     private String addToUserHistoryDictionary(final String suggestion) {
   2823         if (TextUtils.isEmpty(suggestion)) return null;
   2824         final Suggest suggest = mSuggest;
   2825         if (suggest == null) return null;
   2826 
   2827         // If correction is not enabled, we don't add words to the user history dictionary.
   2828         // That's to avoid unintended additions in some sensitive fields, or fields that
   2829         // expect to receive non-words.
   2830         final SettingsValues currentSettings = mSettings.getCurrent();
   2831         if (!currentSettings.mCorrectionEnabled) return null;
   2832 
   2833         final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
   2834         if (userHistoryDictionary == null) return null;
   2835 
   2836         final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2);
   2837         final String secondWord;
   2838         if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
   2839             secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
   2840         } else {
   2841             secondWord = suggestion;
   2842         }
   2843         // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
   2844         // We don't add words with 0-frequency (assuming they would be profanity etc.).
   2845         final int maxFreq = AutoCorrectionUtils.getMaxFrequency(
   2846                 suggest.getUnigramDictionaries(), suggestion);
   2847         if (maxFreq == 0) return null;
   2848         userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0);
   2849         return prevWord;
   2850     }
   2851 
   2852     private boolean isResumableWord(final String word, final SettingsValues settings) {
   2853         final int firstCodePoint = word.codePointAt(0);
   2854         return settings.isWordCodePoint(firstCodePoint)
   2855                 && Constants.CODE_SINGLE_QUOTE != firstCodePoint
   2856                 && Constants.CODE_DASH != firstCodePoint;
   2857     }
   2858 
   2859     /**
   2860      * Check if the cursor is touching a word. If so, restart suggestions on this word, else
   2861      * do nothing.
   2862      */
   2863     private void restartSuggestionsOnWordTouchedByCursor() {
   2864         // HACK: We may want to special-case some apps that exhibit bad behavior in case of
   2865         // recorrection. This is a temporary, stopgap measure that will be removed later.
   2866         // TODO: remove this.
   2867         if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
   2868         // A simple way to test for support from the TextView.
   2869         if (!isSuggestionsStripVisible()) return;
   2870         // Recorrection is not supported in languages without spaces because we don't know
   2871         // how to segment them yet.
   2872         if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
   2873         // If the cursor is not touching a word, or if there is a selection, return right away.
   2874         if (mLastSelectionStart != mLastSelectionEnd) return;
   2875         // If we don't know the cursor location, return.
   2876         if (mLastSelectionStart < 0) return;
   2877         final SettingsValues currentSettings = mSettings.getCurrent();
   2878         if (!mConnection.isCursorTouchingWord(currentSettings)) return;
   2879         final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators,
   2880                 0 /* additionalPrecedingWordsCount */);
   2881         if (null == range) return; // Happens if we don't have an input connection at all
   2882         if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out.
   2883         // If for some strange reason (editor bug or so) we measure the text before the cursor as
   2884         // longer than what the entire text is supposed to be, the safe thing to do is bail out.
   2885         final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
   2886         if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return;
   2887         final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
   2888         final String typedWord = range.mWord.toString();
   2889         if (!isResumableWord(typedWord, currentSettings)) return;
   2890         int i = 0;
   2891         for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) {
   2892             for (final String s : span.getSuggestions()) {
   2893                 ++i;
   2894                 if (!TextUtils.equals(s, typedWord)) {
   2895                     suggestions.add(new SuggestedWordInfo(s,
   2896                             SuggestionStripView.MAX_SUGGESTIONS - i,
   2897                             SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
   2898                             SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
   2899                             SuggestedWordInfo.NOT_A_CONFIDENCE
   2900                                     /* autoCommitFirstWordConfidence */));
   2901                 }
   2902             }
   2903         }
   2904         mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
   2905         mWordComposer.setCursorPositionWithinWord(
   2906                 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
   2907         mConnection.setComposingRegion(
   2908                 mLastSelectionStart - numberOfCharsInWordBeforeCursor,
   2909                 mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
   2910         if (suggestions.isEmpty()) {
   2911             // We come here if there weren't any suggestion spans on this word. We will try to
   2912             // compute suggestions for it instead.
   2913             mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
   2914                     SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
   2915                         @Override
   2916                         public void onGetSuggestedWords(
   2917                                 final SuggestedWords suggestedWordsIncludingTypedWord) {
   2918                             final SuggestedWords suggestedWords;
   2919                             if (suggestedWordsIncludingTypedWord.size() > 1) {
   2920                                 // We were able to compute new suggestions for this word.
   2921                                 // Remove the typed word, since we don't want to display it in this
   2922                                 // case. The #getSuggestedWordsExcludingTypedWord() method sets
   2923                                 // willAutoCorrect to false.
   2924                                 suggestedWords = suggestedWordsIncludingTypedWord
   2925                                         .getSuggestedWordsExcludingTypedWord();
   2926                             } else {
   2927                                 // No saved suggestions, and we were unable to compute any good one
   2928                                 // either. Rather than displaying an empty suggestion strip, we'll
   2929                                 // display the original word alone in the middle.
   2930                                 // Since there is only one word, willAutoCorrect is false.
   2931                                 suggestedWords = suggestedWordsIncludingTypedWord;
   2932                             }
   2933                             // We need to pass typedWord because mWordComposer.mTypedWord may
   2934                             // differ from typedWord.
   2935                             unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
   2936                                     suggestedWords, typedWord);
   2937                         }});
   2938         } else {
   2939             // We found suggestion spans in the word. We'll create the SuggestedWords out of
   2940             // them, and make willAutoCorrect false.
   2941             final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
   2942                     true /* typedWordValid */, false /* willAutoCorrect */,
   2943                     false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
   2944                     false /* isPrediction */);
   2945             // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord.
   2946             unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, typedWord);
   2947         }
   2948     }
   2949 
   2950     public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
   2951             final SuggestedWords suggestedWords, final String typedWord) {
   2952         // Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
   2953         // We never want to auto-correct on a resumed suggestion. Please refer to the three places
   2954         // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected.
   2955         // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching
   2956         // the text to adapt it.
   2957         // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition)
   2958         mIsAutoCorrectionIndicatorOn = false;
   2959         mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord);
   2960     }
   2961 
   2962     /**
   2963      * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
   2964      * word, else do nothing.
   2965      */
   2966     private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
   2967         final CharSequence word =
   2968                 mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
   2969         if (null != word) {
   2970             final String wordString = word.toString();
   2971             restartSuggestionsOnWordBeforeCursor(wordString);
   2972             // TODO: Handle the case where the user manually moves the cursor and then backs up over
   2973             // a separator.  In that case, the current log unit should not be uncommitted.
   2974             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2975                 ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
   2976                         true /* dumpCurrentLogUnit */);
   2977             }
   2978         }
   2979     }
   2980 
   2981     private void restartSuggestionsOnWordBeforeCursor(final String word) {
   2982         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
   2983         final int length = word.length();
   2984         mConnection.deleteSurroundingText(length, 0);
   2985         mConnection.setComposingText(word, 1);
   2986         mHandler.postUpdateSuggestionStrip();
   2987     }
   2988 
   2989     /**
   2990      * Retry resetting caches in the rich input connection.
   2991      *
   2992      * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
   2993      * This method handles the retry, and re-schedules a new retry if we still can't access.
   2994      * We only retry up to 5 times before giving up.
   2995      *
   2996      * @param tryResumeSuggestions Whether we should resume suggestions or not.
   2997      * @param remainingTries How many times we may try again before giving up.
   2998      */
   2999     private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
   3000         if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart, false)) {
   3001             if (0 < remainingTries) {
   3002                 mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
   3003                 return;
   3004             }
   3005             // If remainingTries is 0, we should stop waiting for new tries, but it's still
   3006             // better to load the keyboard (less things will be broken).
   3007         }
   3008         tryFixLyingCursorPosition();
   3009         mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
   3010         if (tryResumeSuggestions) mHandler.postResumeSuggestions();
   3011     }
   3012 
   3013     private void revertCommit() {
   3014         final String previousWord = mLastComposedWord.mPrevWord;
   3015         final String originallyTypedWord = mLastComposedWord.mTypedWord;
   3016         final String committedWord = mLastComposedWord.mCommittedWord;
   3017         final int cancelLength = committedWord.length();
   3018         // We want java chars, not codepoints for the following.
   3019         final int separatorLength = mLastComposedWord.mSeparatorString.length();
   3020         // TODO: should we check our saved separator against the actual contents of the text view?
   3021         final int deleteLength = cancelLength + separatorLength;
   3022         if (DEBUG) {
   3023             if (mWordComposer.isComposingWord()) {
   3024                 throw new RuntimeException("revertCommit, but we are composing a word");
   3025             }
   3026             final CharSequence wordBeforeCursor =
   3027                     mConnection.getTextBeforeCursor(deleteLength, 0)
   3028                             .subSequence(0, cancelLength);
   3029             if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
   3030                 throw new RuntimeException("revertCommit check failed: we thought we were "
   3031                         + "reverting \"" + committedWord
   3032                         + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
   3033             }
   3034         }
   3035         mConnection.deleteSurroundingText(deleteLength, 0);
   3036         if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
   3037             mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
   3038         }
   3039         final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
   3040         if (mSettings.getCurrent().mCurrentLanguageHasSpaces) {
   3041             // For languages with spaces, we revert to the typed string, but the cursor is still
   3042             // after the separator so we don't resume suggestions. If the user wants to correct
   3043             // the word, they have to press backspace again.
   3044             mConnection.commitText(stringToCommit, 1);
   3045         } else {
   3046             // For languages without spaces, we revert the typed string but the cursor is flush
   3047             // with the typed word, so we need to resume suggestions right away.
   3048             mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard());
   3049             mConnection.setComposingText(stringToCommit, 1);
   3050         }
   3051         if (mSettings.isInternal()) {
   3052             LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
   3053                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
   3054         }
   3055         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   3056             ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
   3057                     mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
   3058         }
   3059         // Don't restart suggestion yet. We'll restart if the user deletes the
   3060         // separator.
   3061         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
   3062         // We have a separator between the word and the cursor: we should show predictions.
   3063         mHandler.postUpdateSuggestionStrip();
   3064     }
   3065 
   3066     // This essentially inserts a space, and that's it.
   3067     public void promotePhantomSpace() {
   3068         final SettingsValues currentSettings = mSettings.getCurrent();
   3069         if (currentSettings.shouldInsertSpacesAutomatically()
   3070                 && currentSettings.mCurrentLanguageHasSpaces
   3071                 && !mConnection.textBeforeCursorLooksLikeURL()) {
   3072             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   3073                 ResearchLogger.latinIME_promotePhantomSpace();
   3074             }
   3075             sendKeyCodePoint(Constants.CODE_SPACE);
   3076         }
   3077     }
   3078 
   3079     // TODO: Make this private
   3080     // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
   3081     @UsedForTesting
   3082     void loadKeyboard() {
   3083         // Since we are switching languages, the most urgent thing is to let the keyboard graphics
   3084         // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on
   3085         // the screen. Anything we do right now will delay this, so wait until the next frame
   3086         // before we do the rest, like reopening dictionaries and updating suggestions. So we
   3087         // post a message.
   3088         mHandler.postReopenDictionaries();
   3089         loadSettings();
   3090         if (mKeyboardSwitcher.getMainKeyboardView() != null) {
   3091             // Reload keyboard because the current language has been changed.
   3092             mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
   3093         }
   3094     }
   3095 
   3096     private void hapticAndAudioFeedback(final int code, final int repeatCount) {
   3097         final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView();
   3098         if (keyboardView != null && keyboardView.isInSlidingKeyInput()) {
   3099             // No need to feedback while sliding input.
   3100             return;
   3101         }
   3102         if (repeatCount > 0) {
   3103             if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
   3104                 // No need to feedback when repeat delete key will have no effect.
   3105                 return;
   3106             }
   3107             // TODO: Use event time that the last feedback has been generated instead of relying on
   3108             // a repeat count to thin out feedback.
   3109             if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) {
   3110                 return;
   3111             }
   3112         }
   3113         final AudioAndHapticFeedbackManager feedbackManager =
   3114                 AudioAndHapticFeedbackManager.getInstance();
   3115         if (repeatCount == 0) {
   3116             // TODO: Reconsider how to perform haptic feedback when repeating key.
   3117             feedbackManager.performHapticFeedback(keyboardView);
   3118         }
   3119         feedbackManager.performAudioFeedback(code);
   3120     }
   3121 
   3122     // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
   3123     // release matching call is {@link #onReleaseKey(int,boolean)} below.
   3124     @Override
   3125     public void onPressKey(final int primaryCode, final int repeatCount,
   3126             final boolean isSinglePointer) {
   3127         mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
   3128         hapticAndAudioFeedback(primaryCode, repeatCount);
   3129     }
   3130 
   3131     // Callback of the {@link KeyboardActionListener}. This is called when a key is released;
   3132     // press matching call is {@link #onPressKey(int,int,boolean)} above.
   3133     @Override
   3134     public void onReleaseKey(final int primaryCode, final boolean withSliding) {
   3135         mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
   3136 
   3137         // If accessibility is on, ensure the user receives keyboard state updates.
   3138         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
   3139             switch (primaryCode) {
   3140             case Constants.CODE_SHIFT:
   3141                 AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
   3142                 break;
   3143             case Constants.CODE_SWITCH_ALPHA_SYMBOL:
   3144                 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
   3145                 break;
   3146             }
   3147         }
   3148     }
   3149 
   3150     // Hooks for hardware keyboard
   3151     @Override
   3152     public boolean onKeyDown(final int keyCode, final KeyEvent event) {
   3153         if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event);
   3154         // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if
   3155         // it doesn't know what to do with it and leave it to the application. For example,
   3156         // hardware key events for adjusting the screen's brightness are passed as is.
   3157         if (mEventInterpreter.onHardwareKeyEvent(event)) {
   3158             final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
   3159             mCurrentlyPressedHardwareKeys.add(keyIdentifier);
   3160             return true;
   3161         }
   3162         return super.onKeyDown(keyCode, event);
   3163     }
   3164 
   3165     @Override
   3166     public boolean onKeyUp(final int keyCode, final KeyEvent event) {
   3167         final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
   3168         if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
   3169             return true;
   3170         }
   3171         return super.onKeyUp(keyCode, event);
   3172     }
   3173 
   3174     // onKeyDown and onKeyUp are the main events we are interested in. There are two more events
   3175     // related to handling of hardware key events that we may want to implement in the future:
   3176     // boolean onKeyLongPress(final int keyCode, final KeyEvent event);
   3177     // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event);
   3178 
   3179     // receive ringer mode change and network state change.
   3180     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
   3181         @Override
   3182         public void onReceive(final Context context, final Intent intent) {
   3183             final String action = intent.getAction();
   3184             if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
   3185                 mSubtypeSwitcher.onNetworkStateChanged(intent);
   3186             } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
   3187                 AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
   3188             }
   3189         }
   3190     };
   3191 
   3192     private void launchSettings() {
   3193         handleClose();
   3194         launchSubActivity(SettingsActivity.class);
   3195     }
   3196 
   3197     public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
   3198         // Put the text in the attached EditText into a safe, saved state before switching to a
   3199         // new activity that will also use the soft keyboard.
   3200         commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   3201         launchSubActivity(activityClass);
   3202     }
   3203 
   3204     private void launchSubActivity(final Class<? extends Activity> activityClass) {
   3205         Intent intent = new Intent();
   3206         intent.setClass(LatinIME.this, activityClass);
   3207         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
   3208                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
   3209                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
   3210         startActivity(intent);
   3211     }
   3212 
   3213     private void showSubtypeSelectorAndSettings() {
   3214         final CharSequence title = getString(R.string.english_ime_input_options);
   3215         final CharSequence[] items = new CharSequence[] {
   3216                 // TODO: Should use new string "Select active input modes".
   3217                 getString(R.string.language_selection_title),
   3218                 getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)),
   3219         };
   3220         final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
   3221             @Override
   3222             public void onClick(DialogInterface di, int position) {
   3223                 di.dismiss();
   3224                 switch (position) {
   3225                 case 0:
   3226                     final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
   3227                             mRichImm.getInputMethodIdOfThisIme(),
   3228                             Intent.FLAG_ACTIVITY_NEW_TASK
   3229                             | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
   3230                             | Intent.FLAG_ACTIVITY_CLEAR_TOP);
   3231                     startActivity(intent);
   3232                     break;
   3233                 case 1:
   3234                     launchSettings();
   3235                     break;
   3236                 }
   3237             }
   3238         };
   3239         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
   3240                 .setItems(items, listener)
   3241                 .setTitle(title);
   3242         showOptionDialog(builder.create());
   3243     }
   3244 
   3245     public void showOptionDialog(final AlertDialog dialog) {
   3246         final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
   3247         if (windowToken == null) {
   3248             return;
   3249         }
   3250 
   3251         dialog.setCancelable(true);
   3252         dialog.setCanceledOnTouchOutside(true);
   3253 
   3254         final Window window = dialog.getWindow();
   3255         final WindowManager.LayoutParams lp = window.getAttributes();
   3256         lp.token = windowToken;
   3257         lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
   3258         window.setAttributes(lp);
   3259         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
   3260 
   3261         mOptionsDialog = dialog;
   3262         dialog.show();
   3263     }
   3264 
   3265     // TODO: can this be removed somehow without breaking the tests?
   3266     @UsedForTesting
   3267     /* package for test */ String getFirstSuggestedWord() {
   3268         return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null;
   3269     }
   3270 
   3271     // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
   3272     @UsedForTesting
   3273     /* package for test */ boolean isCurrentlyWaitingForMainDictionary() {
   3274         return mSuggest.isCurrentlyWaitingForMainDictionary();
   3275     }
   3276 
   3277     // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
   3278     @UsedForTesting
   3279     /* package for test */ boolean hasMainDictionary() {
   3280         return mSuggest.hasMainDictionary();
   3281     }
   3282 
   3283     // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
   3284     @UsedForTesting
   3285     /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
   3286         mSuggest.resetMainDict(this, locale, null);
   3287     }
   3288 
   3289     public void debugDumpStateAndCrashWithException(final String context) {
   3290         final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
   3291         s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
   3292                 .append("\nContext : ").append(context);
   3293         throw new RuntimeException(s.toString());
   3294     }
   3295 
   3296     @Override
   3297     protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) {
   3298         super.dump(fd, fout, args);
   3299 
   3300         final Printer p = new PrintWriterPrinter(fout);
   3301         p.println("LatinIME state :");
   3302         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   3303         final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
   3304         p.println("  Keyboard mode = " + keyboardMode);
   3305         final SettingsValues settingsValues = mSettings.getCurrent();
   3306         p.println("  mIsSuggestionsSuggestionsRequested = "
   3307                 + settingsValues.isSuggestionsRequested(mDisplayOrientation));
   3308         p.println("  mCorrectionEnabled=" + settingsValues.mCorrectionEnabled);
   3309         p.println("  isComposingWord=" + mWordComposer.isComposingWord());
   3310         p.println("  mSoundOn=" + settingsValues.mSoundOn);
   3311         p.println("  mVibrateOn=" + settingsValues.mVibrateOn);
   3312         p.println("  mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn);
   3313         p.println("  inputAttributes=" + settingsValues.mInputAttributes);
   3314     }
   3315 }
   3316