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.onDestroy();
    729             mInputUpdater = null;
    730         }
    731         super.onDestroy();
    732     }
    733 
    734     @Override
    735     public void onConfigurationChanged(final Configuration conf) {
    736         // If orientation changed while predicting, commit the change
    737         if (mDisplayOrientation != conf.orientation) {
    738             mDisplayOrientation = conf.orientation;
    739             mHandler.startOrientationChanging();
    740             mConnection.beginBatchEdit();
    741             commitTyped(LastComposedWord.NOT_A_SEPARATOR);
    742             mConnection.finishComposingText();
    743             mConnection.endBatchEdit();
    744             if (isShowingOptionDialog()) {
    745                 mOptionsDialog.dismiss();
    746             }
    747         }
    748         PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf);
    749         super.onConfigurationChanged(conf);
    750     }
    751 
    752     @Override
    753     public View onCreateInputView() {
    754         return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
    755     }
    756 
    757     @Override
    758     public void setInputView(final View view) {
    759         super.setInputView(view);
    760         mExtractArea = getWindow().getWindow().getDecorView()
    761                 .findViewById(android.R.id.extractArea);
    762         mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
    763         mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
    764         if (mSuggestionStripView != null)
    765             mSuggestionStripView.setListener(this, view);
    766         if (LatinImeLogger.sVISUALDEBUG) {
    767             mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
    768         }
    769     }
    770 
    771     @Override
    772     public void setCandidatesView(final View view) {
    773         // To ensure that CandidatesView will never be set.
    774         return;
    775     }
    776 
    777     @Override
    778     public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
    779         mHandler.onStartInput(editorInfo, restarting);
    780     }
    781 
    782     @Override
    783     public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
    784         mHandler.onStartInputView(editorInfo, restarting);
    785     }
    786 
    787     @Override
    788     public void onFinishInputView(final boolean finishingInput) {
    789         mHandler.onFinishInputView(finishingInput);
    790     }
    791 
    792     @Override
    793     public void onFinishInput() {
    794         mHandler.onFinishInput();
    795     }
    796 
    797     @Override
    798     public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) {
    799         // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
    800         // is not guaranteed. It may even be called at the same time on a different thread.
    801         mSubtypeSwitcher.onSubtypeChanged(subtype);
    802         loadKeyboard();
    803     }
    804 
    805     private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
    806         super.onStartInput(editorInfo, restarting);
    807     }
    808 
    809     @SuppressWarnings("deprecation")
    810     private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
    811         super.onStartInputView(editorInfo, restarting);
    812         mRichImm.clearSubtypeCaches();
    813         final KeyboardSwitcher switcher = mKeyboardSwitcher;
    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             canReachInputConnection = false;
    914         } else {
    915             if (isDifferentTextField) {
    916                 mHandler.postResumeSuggestions();
    917             }
    918             canReachInputConnection = true;
    919         }
    920 
    921         if (isDifferentTextField) {
    922             mainKeyboardView.closing();
    923             loadSettings();
    924             currentSettingsValues = mSettings.getCurrent();
    925 
    926             if (suggest != null && currentSettingsValues.mCorrectionEnabled) {
    927                 suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
    928             }
    929 
    930             switcher.loadKeyboard(editorInfo, currentSettingsValues);
    931             if (!canReachInputConnection) {
    932                 // If we can't reach the input connection, we will call loadKeyboard again later,
    933                 // so we need to save its state now. The call will be done in #retryResetCaches.
    934                 switcher.saveKeyboardState();
    935             }
    936         } else if (restarting) {
    937             // TODO: Come up with a more comprehensive way to reset the keyboard layout when
    938             // a keyboard layout set doesn't get reloaded in this method.
    939             switcher.resetKeyboardStateToAlphabet();
    940             // In apps like Talk, we come here when the text is sent and the field gets emptied and
    941             // we need to re-evaluate the shift state, but not the whole layout which would be
    942             // disruptive.
    943             // Space state must be updated before calling updateShiftState
    944             switcher.updateShiftState();
    945         }
    946         setSuggestionStripShownInternal(
    947                 isSuggestionsStripVisible(), /* needsInputViewShown */ false);
    948 
    949         mLastSelectionStart = editorInfo.initialSelStart;
    950         mLastSelectionEnd = editorInfo.initialSelEnd;
    951         // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying
    952         // so we try using some heuristics to find out about these and fix them.
    953         tryFixLyingCursorPosition();
    954 
    955         mHandler.cancelUpdateSuggestionStrip();
    956         mHandler.cancelDoubleSpacePeriodTimer();
    957 
    958         mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
    959         mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
    960                 currentSettingsValues.mKeyPreviewPopupDismissDelay);
    961         mainKeyboardView.setSlidingKeyInputPreviewEnabled(
    962                 currentSettingsValues.mSlidingKeyInputPreviewEnabled);
    963         mainKeyboardView.setGestureHandlingEnabledByUser(
    964                 currentSettingsValues.mGestureInputEnabled,
    965                 currentSettingsValues.mGestureTrailEnabled,
    966                 currentSettingsValues.mGestureFloatingPreviewTextEnabled);
    967 
    968         initPersonalizationDebugSettings(currentSettingsValues);
    969 
    970         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
    971     }
    972 
    973     /**
    974      * Try to get the text from the editor to expose lies the framework may have been
    975      * telling us. Concretely, when the device rotates, the frameworks tells us about where the
    976      * cursor used to be initially in the editor at the time it first received the focus; this
    977      * may be completely different from the place it is upon rotation. Since we don't have any
    978      * means to get the real value, try at least to ask the text view for some characters and
    979      * detect the most damaging cases: when the cursor position is declared to be much smaller
    980      * than it really is.
    981      */
    982     private void tryFixLyingCursorPosition() {
    983         final CharSequence textBeforeCursor =
    984                 mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
    985         if (null == textBeforeCursor) {
    986             mLastSelectionStart = mLastSelectionEnd = NOT_A_CURSOR_POSITION;
    987         } else {
    988             final int textLength = textBeforeCursor.length();
    989             if (textLength > mLastSelectionStart
    990                     || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
    991                             && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
    992                 mLastSelectionStart = textLength;
    993                 // We can't figure out the value of mLastSelectionEnd :(
    994                 // But at least if it's smaller than mLastSelectionStart something is wrong
    995                 if (mLastSelectionStart > mLastSelectionEnd) {
    996                     mLastSelectionEnd = mLastSelectionStart;
    997                 }
    998             }
    999         }
   1000     }
   1001 
   1002     // Initialization of personalization debug settings. This must be called inside
   1003     // onStartInputView.
   1004     private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) {
   1005         if (mUseOnlyPersonalizationDictionaryForDebug
   1006                 != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
   1007             // Only for debug
   1008             initSuggest();
   1009             mUseOnlyPersonalizationDictionaryForDebug =
   1010                     currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug;
   1011         }
   1012 
   1013         if (mBoostPersonalizationDictionaryForDebug !=
   1014                 currentSettingsValues.mBoostPersonalizationDictionaryForDebug) {
   1015             // Only for debug
   1016             mBoostPersonalizationDictionaryForDebug =
   1017                     currentSettingsValues.mBoostPersonalizationDictionaryForDebug;
   1018             if (mBoostPersonalizationDictionaryForDebug) {
   1019                 UserHistoryForgettingCurveUtils.boostMaxFreqForDebug();
   1020             } else {
   1021                 UserHistoryForgettingCurveUtils.resetMaxFreqForDebug();
   1022             }
   1023         }
   1024     }
   1025 
   1026     // Callback for the TargetPackageInfoGetterTask
   1027     @Override
   1028     public void onTargetPackageInfoKnown(final PackageInfo info) {
   1029         mAppWorkAroundsUtils.setPackageInfo(info);
   1030     }
   1031 
   1032     @Override
   1033     public void onWindowHidden() {
   1034         super.onWindowHidden();
   1035         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   1036         if (mainKeyboardView != null) {
   1037             mainKeyboardView.closing();
   1038         }
   1039     }
   1040 
   1041     private void onFinishInputInternal() {
   1042         super.onFinishInput();
   1043 
   1044         LatinImeLogger.commit();
   1045         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   1046         if (mainKeyboardView != null) {
   1047             mainKeyboardView.closing();
   1048         }
   1049     }
   1050 
   1051     private void onFinishInputViewInternal(final boolean finishingInput) {
   1052         super.onFinishInputView(finishingInput);
   1053         mKeyboardSwitcher.onFinishInputView();
   1054         mKeyboardSwitcher.deallocateMemory();
   1055         // Remove pending messages related to update suggestions
   1056         mHandler.cancelUpdateSuggestionStrip();
   1057         // Should do the following in onFinishInputInternal but until JB MR2 it's not called :(
   1058         if (mWordComposer.isComposingWord()) mConnection.finishComposingText();
   1059         resetComposingState(true /* alsoResetLastComposedWord */);
   1060         // Notify ResearchLogger
   1061         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1062             ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart,
   1063                     mLastSelectionEnd, getCurrentInputConnection());
   1064         }
   1065     }
   1066 
   1067     @Override
   1068     public void onUpdateSelection(final int oldSelStart, final int oldSelEnd,
   1069             final int newSelStart, final int newSelEnd,
   1070             final int composingSpanStart, final int composingSpanEnd) {
   1071         super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
   1072                 composingSpanStart, composingSpanEnd);
   1073         if (DEBUG) {
   1074             Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
   1075                     + ", ose=" + oldSelEnd
   1076                     + ", lss=" + mLastSelectionStart
   1077                     + ", lse=" + mLastSelectionEnd
   1078                     + ", nss=" + newSelStart
   1079                     + ", nse=" + newSelEnd
   1080                     + ", cs=" + composingSpanStart
   1081                     + ", ce=" + composingSpanEnd);
   1082         }
   1083         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1084             final boolean expectingUpdateSelectionFromLogger =
   1085                     ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
   1086             ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
   1087                     oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
   1088                     composingSpanEnd, mExpectingUpdateSelection,
   1089                     expectingUpdateSelectionFromLogger, mConnection);
   1090             if (expectingUpdateSelectionFromLogger) {
   1091                 // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
   1092                 return;
   1093             }
   1094         }
   1095 
   1096         final boolean selectionChanged = mLastSelectionStart != newSelStart
   1097                 || mLastSelectionEnd != newSelEnd;
   1098 
   1099         // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
   1100         // span in the view - we can use that to narrow down whether the cursor was moved
   1101         // by us or not. If we are composing a word but there is no composing span, then
   1102         // we know for sure the cursor moved while we were composing and we should reset
   1103         // the state. TODO: rescind this policy: the framework never removes the composing
   1104         // span on its own accord while editing. This test is useless.
   1105         final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
   1106 
   1107         // If the keyboard is not visible, we don't need to do all the housekeeping work, as it
   1108         // will be reset when the keyboard shows up anyway.
   1109         // TODO: revisit this when LatinIME supports hardware keyboards.
   1110         // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
   1111         // TODO: find a better way to simulate actual execution.
   1112         if (isInputViewShown() && !mExpectingUpdateSelection
   1113                 && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) {
   1114             // TAKE CARE: there is a race condition when we enter this test even when the user
   1115             // did not explicitly move the cursor. This happens when typing fast, where two keys
   1116             // turn this flag on in succession and both onUpdateSelection() calls arrive after
   1117             // the second one - the first call successfully avoids this test, but the second one
   1118             // enters. For the moment we rely on noComposingSpan to further reduce the impact.
   1119 
   1120             // TODO: the following is probably better done in resetEntireInputState().
   1121             // it should only happen when the cursor moved, and the very purpose of the
   1122             // test below is to narrow down whether this happened or not. Likewise with
   1123             // the call to updateShiftState.
   1124             // We set this to NONE because after a cursor move, we don't want the space
   1125             // state-related special processing to kick in.
   1126             mSpaceState = SPACE_STATE_NONE;
   1127 
   1128             // TODO: is it still necessary to test for composingSpan related stuff?
   1129             final boolean selectionChangedOrSafeToReset = selectionChanged
   1130                     || (!mWordComposer.isComposingWord()) || noComposingSpan;
   1131             final boolean hasOrHadSelection = (oldSelStart != oldSelEnd
   1132                     || newSelStart != newSelEnd);
   1133             final int moveAmount = newSelStart - oldSelStart;
   1134             if (selectionChangedOrSafeToReset && (hasOrHadSelection
   1135                     || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) {
   1136                 // If we are composing a word and moving the cursor, we would want to set a
   1137                 // suggestion span for recorrection to work correctly. Unfortunately, that
   1138                 // would involve the keyboard committing some new text, which would move the
   1139                 // cursor back to where it was. Latin IME could then fix the position of the cursor
   1140                 // again, but the asynchronous nature of the calls results in this wreaking havoc
   1141                 // with selection on double tap and the like.
   1142                 // Another option would be to send suggestions each time we set the composing
   1143                 // text, but that is probably too expensive to do, so we decided to leave things
   1144                 // as is.
   1145                 resetEntireInputState(newSelStart);
   1146             } else {
   1147                 // resetEntireInputState calls resetCachesUponCursorMove, but with the second
   1148                 // argument as true. But in all cases where we don't reset the entire input state,
   1149                 // we still want to tell the rich input connection about the new cursor position so
   1150                 // that it can update its caches.
   1151                 mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart,
   1152                         false /* shouldFinishComposition */);
   1153             }
   1154 
   1155             // We moved the cursor. If we are touching a word, we need to resume suggestion,
   1156             // unless suggestions are off.
   1157             if (isSuggestionsStripVisible()) {
   1158                 mHandler.postResumeSuggestions();
   1159             }
   1160             // Reset the last recapitalization.
   1161             mRecapitalizeStatus.deactivate();
   1162             mKeyboardSwitcher.updateShiftState();
   1163         }
   1164         mExpectingUpdateSelection = false;
   1165 
   1166         // Make a note of the cursor position
   1167         mLastSelectionStart = newSelStart;
   1168         mLastSelectionEnd = newSelEnd;
   1169         mSubtypeState.currentSubtypeUsed();
   1170     }
   1171 
   1172     /**
   1173      * This is called when the user has clicked on the extracted text view,
   1174      * when running in fullscreen mode.  The default implementation hides
   1175      * the suggestions view when this happens, but only if the extracted text
   1176      * editor has a vertical scroll bar because its text doesn't fit.
   1177      * Here we override the behavior due to the possibility that a re-correction could
   1178      * cause the suggestions strip to disappear and re-appear.
   1179      */
   1180     @Override
   1181     public void onExtractedTextClicked() {
   1182         if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
   1183 
   1184         super.onExtractedTextClicked();
   1185     }
   1186 
   1187     /**
   1188      * This is called when the user has performed a cursor movement in the
   1189      * extracted text view, when it is running in fullscreen mode.  The default
   1190      * implementation hides the suggestions view when a vertical movement
   1191      * happens, but only if the extracted text editor has a vertical scroll bar
   1192      * because its text doesn't fit.
   1193      * Here we override the behavior due to the possibility that a re-correction could
   1194      * cause the suggestions strip to disappear and re-appear.
   1195      */
   1196     @Override
   1197     public void onExtractedCursorMovement(final int dx, final int dy) {
   1198         if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
   1199 
   1200         super.onExtractedCursorMovement(dx, dy);
   1201     }
   1202 
   1203     @Override
   1204     public void hideWindow() {
   1205         LatinImeLogger.commit();
   1206         mKeyboardSwitcher.onHideWindow();
   1207 
   1208         if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
   1209             AccessibleKeyboardViewProxy.getInstance().onHideWindow();
   1210         }
   1211 
   1212         if (TRACE) Debug.stopMethodTracing();
   1213         if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
   1214             mOptionsDialog.dismiss();
   1215             mOptionsDialog = null;
   1216         }
   1217         super.hideWindow();
   1218     }
   1219 
   1220     @Override
   1221     public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
   1222         if (DEBUG) {
   1223             Log.i(TAG, "Received completions:");
   1224             if (applicationSpecifiedCompletions != null) {
   1225                 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
   1226                     Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
   1227                 }
   1228             }
   1229         }
   1230         if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return;
   1231         if (applicationSpecifiedCompletions == null) {
   1232             clearSuggestionStrip();
   1233             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1234                 ResearchLogger.latinIME_onDisplayCompletions(null);
   1235             }
   1236             return;
   1237         }
   1238         mApplicationSpecifiedCompletions =
   1239                 CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions);
   1240 
   1241         final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
   1242                 SuggestedWords.getFromApplicationSpecifiedCompletions(
   1243                         applicationSpecifiedCompletions);
   1244         final SuggestedWords suggestedWords = new SuggestedWords(
   1245                 applicationSuggestedWords,
   1246                 false /* typedWordValid */,
   1247                 false /* hasAutoCorrectionCandidate */,
   1248                 false /* isPunctuationSuggestions */,
   1249                 false /* isObsoleteSuggestions */,
   1250                 false /* isPrediction */);
   1251         // When in fullscreen mode, show completions generated by the application
   1252         final boolean isAutoCorrection = false;
   1253         setSuggestedWords(suggestedWords, isAutoCorrection);
   1254         setAutoCorrectionIndicator(isAutoCorrection);
   1255         setSuggestionStripShown(true);
   1256         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1257             ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
   1258         }
   1259     }
   1260 
   1261     private void setSuggestionStripShownInternal(final boolean shown,
   1262             final boolean needsInputViewShown) {
   1263         // TODO: Modify this if we support suggestions with hard keyboard
   1264         if (onEvaluateInputViewShown() && mSuggestionStripView != null) {
   1265             final boolean inputViewShown = mKeyboardSwitcher.isShowingMainKeyboardOrEmojiPalettes();
   1266             final boolean shouldShowSuggestions = shown
   1267                     && (needsInputViewShown ? inputViewShown : true);
   1268             if (isFullscreenMode()) {
   1269                 mSuggestionStripView.setVisibility(
   1270                         shouldShowSuggestions ? View.VISIBLE : View.GONE);
   1271             } else {
   1272                 mSuggestionStripView.setVisibility(
   1273                         shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
   1274             }
   1275         }
   1276     }
   1277 
   1278     private void setSuggestionStripShown(final boolean shown) {
   1279         setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
   1280     }
   1281 
   1282     private int getAdjustedBackingViewHeight() {
   1283         final int currentHeight = mKeyPreviewBackingView.getHeight();
   1284         if (currentHeight > 0) {
   1285             return currentHeight;
   1286         }
   1287 
   1288         final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
   1289         if (visibleKeyboardView == null) {
   1290             return 0;
   1291         }
   1292         // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main   !!!
   1293         // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   1294         final int keyboardHeight = visibleKeyboardView.getHeight();
   1295         final int suggestionsHeight = mSuggestionStripView.getHeight();
   1296         final int displayHeight = getResources().getDisplayMetrics().heightPixels;
   1297         final Rect rect = new Rect();
   1298         mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
   1299         final int notificationBarHeight = rect.top;
   1300         final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
   1301                 - keyboardHeight;
   1302 
   1303         final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
   1304         params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight);
   1305         mKeyPreviewBackingView.setLayoutParams(params);
   1306         return params.height;
   1307     }
   1308 
   1309     @Override
   1310     public void onComputeInsets(final InputMethodService.Insets outInsets) {
   1311         super.onComputeInsets(outInsets);
   1312         final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
   1313         if (visibleKeyboardView == null || mSuggestionStripView == null) {
   1314             return;
   1315         }
   1316         final int adjustedBackingHeight = getAdjustedBackingViewHeight();
   1317         final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
   1318         final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
   1319         // In fullscreen mode, the height of the extract area managed by InputMethodService should
   1320         // be considered.
   1321         // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
   1322         final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
   1323         final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0
   1324                 : mSuggestionStripView.getHeight();
   1325         final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
   1326         int visibleTopY = extraHeight;
   1327         // Need to set touchable region only if input view is being shown
   1328         if (visibleKeyboardView.isShown()) {
   1329             // Note that the height of Emoji layout is the same as the height of the main keyboard
   1330             // and the suggestion strip
   1331             if (mKeyboardSwitcher.isShowingEmojiPalettes()
   1332                     || mSuggestionStripView.getVisibility() == View.VISIBLE) {
   1333                 visibleTopY -= suggestionsHeight;
   1334             }
   1335             final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
   1336             final int touchWidth = visibleKeyboardView.getWidth();
   1337             final int touchHeight = visibleKeyboardView.getHeight() + extraHeight
   1338                     // Extend touchable region below the keyboard.
   1339                     + EXTENDED_TOUCHABLE_REGION_HEIGHT;
   1340             outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
   1341             outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight);
   1342         }
   1343         outInsets.contentTopInsets = visibleTopY;
   1344         outInsets.visibleTopInsets = visibleTopY;
   1345     }
   1346 
   1347     @Override
   1348     public boolean onEvaluateFullscreenMode() {
   1349         // Reread resource value here, because this method is called by framework anytime as needed.
   1350         final boolean isFullscreenModeAllowed =
   1351                 Settings.readUseFullscreenMode(getResources());
   1352         if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) {
   1353             // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI
   1354             // implies NO_FULLSCREEN. However, the framework mistakenly does.  i.e. NO_EXTRACT_UI
   1355             // without NO_FULLSCREEN doesn't work as expected. Because of this we need this
   1356             // hack for now.  Let's get rid of this once the framework gets fixed.
   1357             final EditorInfo ei = getCurrentInputEditorInfo();
   1358             return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0));
   1359         } else {
   1360             return false;
   1361         }
   1362     }
   1363 
   1364     @Override
   1365     public void updateFullscreenMode() {
   1366         super.updateFullscreenMode();
   1367 
   1368         if (mKeyPreviewBackingView == null) return;
   1369         // In fullscreen mode, no need to have extra space to show the key preview.
   1370         // If not, we should have extra space above the keyboard to show the key preview.
   1371         mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
   1372     }
   1373 
   1374     // This will reset the whole input state to the starting state. It will clear
   1375     // the composing word, reset the last composed word, tell the inputconnection about it.
   1376     private void resetEntireInputState(final int newCursorPosition) {
   1377         final boolean shouldFinishComposition = mWordComposer.isComposingWord();
   1378         resetComposingState(true /* alsoResetLastComposedWord */);
   1379         final SettingsValues settingsValues = mSettings.getCurrent();
   1380         if (settingsValues.mBigramPredictionEnabled) {
   1381             clearSuggestionStrip();
   1382         } else {
   1383             setSuggestedWords(settingsValues.mSuggestPuncList, false);
   1384         }
   1385         mConnection.resetCachesUponCursorMoveAndReturnSuccess(newCursorPosition,
   1386                 shouldFinishComposition);
   1387     }
   1388 
   1389     private void resetComposingState(final boolean alsoResetLastComposedWord) {
   1390         mWordComposer.reset();
   1391         if (alsoResetLastComposedWord)
   1392             mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
   1393     }
   1394 
   1395     private void commitTyped(final String separatorString) {
   1396         if (!mWordComposer.isComposingWord()) return;
   1397         final String typedWord = mWordComposer.getTypedWord();
   1398         if (typedWord.length() > 0) {
   1399             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1400                 ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
   1401             }
   1402             commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
   1403                     separatorString);
   1404         }
   1405     }
   1406 
   1407     // Called from the KeyboardSwitcher which needs to know auto caps state to display
   1408     // the right layout.
   1409     public int getCurrentAutoCapsState() {
   1410         final SettingsValues currentSettingsValues = mSettings.getCurrent();
   1411         if (!currentSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
   1412 
   1413         final EditorInfo ei = getCurrentInputEditorInfo();
   1414         if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
   1415         final int inputType = ei.inputType;
   1416         // Warning: this depends on mSpaceState, which may not be the most current value. If
   1417         // mSpaceState gets updated later, whoever called this may need to be told about it.
   1418         return mConnection.getCursorCapsMode(inputType, currentSettingsValues,
   1419                 SPACE_STATE_PHANTOM == mSpaceState);
   1420     }
   1421 
   1422     public int getCurrentRecapitalizeState() {
   1423         if (!mRecapitalizeStatus.isActive()
   1424                 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
   1425             // Not recapitalizing at the moment
   1426             return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
   1427         }
   1428         return mRecapitalizeStatus.getCurrentMode();
   1429     }
   1430 
   1431     // Factor in auto-caps and manual caps and compute the current caps mode.
   1432     private int getActualCapsMode() {
   1433         final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
   1434         if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
   1435         final int auto = getCurrentAutoCapsState();
   1436         if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
   1437             return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
   1438         }
   1439         if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED;
   1440         return WordComposer.CAPS_MODE_OFF;
   1441     }
   1442 
   1443     private void swapSwapperAndSpace() {
   1444         final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
   1445         // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
   1446         if (lastTwo != null && lastTwo.length() == 2
   1447                 && lastTwo.charAt(0) == Constants.CODE_SPACE) {
   1448             mConnection.deleteSurroundingText(2, 0);
   1449             final String text = lastTwo.charAt(1) + " ";
   1450             mConnection.commitText(text, 1);
   1451             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1452                 ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
   1453             }
   1454             mKeyboardSwitcher.updateShiftState();
   1455         }
   1456     }
   1457 
   1458     private boolean maybeDoubleSpacePeriod() {
   1459         final SettingsValues currentSettingsValues = mSettings.getCurrent();
   1460         if (!currentSettingsValues.mCorrectionEnabled) return false;
   1461         if (!currentSettingsValues.mUseDoubleSpacePeriod) return false;
   1462         if (!mHandler.isAcceptingDoubleSpacePeriod()) return false;
   1463         // We only do this when we see two spaces and an accepted code point before the cursor.
   1464         // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars.
   1465         final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0);
   1466         if (null == lastThree) return false;
   1467         final int length = lastThree.length();
   1468         if (length < 3) return false;
   1469         if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false;
   1470         if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false;
   1471         // We know there are spaces in pos -1 and -2, and we have at least three chars.
   1472         // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space,
   1473         // so this is fine.
   1474         final int firstCodePoint =
   1475                 Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ?
   1476                         Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3);
   1477         if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
   1478             mHandler.cancelDoubleSpacePeriodTimer();
   1479             mConnection.deleteSurroundingText(2, 0);
   1480             final String textToInsert = new String(
   1481                     new int[] { currentSettingsValues.mSentenceSeparator, Constants.CODE_SPACE },
   1482                     0, 2);
   1483             mConnection.commitText(textToInsert, 1);
   1484             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1485                 ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
   1486                         false /* isBatchMode */);
   1487             }
   1488             mKeyboardSwitcher.updateShiftState();
   1489             return true;
   1490         }
   1491         return false;
   1492     }
   1493 
   1494     private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
   1495         // TODO: Check again whether there really ain't a better way to check this.
   1496         // TODO: This should probably be language-dependant...
   1497         return Character.isLetterOrDigit(codePoint)
   1498                 || codePoint == Constants.CODE_SINGLE_QUOTE
   1499                 || codePoint == Constants.CODE_DOUBLE_QUOTE
   1500                 || codePoint == Constants.CODE_CLOSING_PARENTHESIS
   1501                 || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
   1502                 || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
   1503                 || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
   1504                 || codePoint == Constants.CODE_PLUS
   1505                 || Character.getType(codePoint) == Character.OTHER_SYMBOL;
   1506     }
   1507 
   1508     // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
   1509     // pressed.
   1510     @Override
   1511     public void addWordToUserDictionary(final String word) {
   1512         if (TextUtils.isEmpty(word)) {
   1513             // Probably never supposed to happen, but just in case.
   1514             return;
   1515         }
   1516         final String wordToEdit;
   1517         if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) {
   1518             wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
   1519         } else {
   1520             wordToEdit = word;
   1521         }
   1522         mUserDictionary.addWordToUserDictionary(wordToEdit);
   1523     }
   1524 
   1525     private void onSettingsKeyPressed() {
   1526         if (isShowingOptionDialog()) return;
   1527         showSubtypeSelectorAndSettings();
   1528     }
   1529 
   1530     @Override
   1531     public boolean onCustomRequest(final int requestCode) {
   1532         if (isShowingOptionDialog()) return false;
   1533         switch (requestCode) {
   1534         case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
   1535             if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
   1536                 mRichImm.getInputMethodManager().showInputMethodPicker();
   1537                 return true;
   1538             }
   1539             return false;
   1540         }
   1541         return false;
   1542     }
   1543 
   1544     private boolean isShowingOptionDialog() {
   1545         return mOptionsDialog != null && mOptionsDialog.isShowing();
   1546     }
   1547 
   1548     private void performEditorAction(final int actionId) {
   1549         mConnection.performEditorAction(actionId);
   1550     }
   1551 
   1552     // TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
   1553     private void handleLanguageSwitchKey() {
   1554         final IBinder token = getWindow().getWindow().getAttributes().token;
   1555         if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) {
   1556             mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
   1557             return;
   1558         }
   1559         mSubtypeState.switchSubtype(token, mRichImm);
   1560     }
   1561 
   1562     private void sendDownUpKeyEvent(final int code) {
   1563         final long eventTime = SystemClock.uptimeMillis();
   1564         mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
   1565                 KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
   1566                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
   1567         mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
   1568                 KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
   1569                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
   1570     }
   1571 
   1572     private void sendKeyCodePoint(final int code) {
   1573         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1574             ResearchLogger.latinIME_sendKeyCodePoint(code);
   1575         }
   1576         // TODO: Remove this special handling of digit letters.
   1577         // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
   1578         if (code >= '0' && code <= '9') {
   1579             sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0);
   1580             return;
   1581         }
   1582 
   1583         if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) {
   1584             // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
   1585             // a hardware keyboard event on pressing enter or delete. This is bad for many
   1586             // reasons (there are race conditions with commits) but some applications are
   1587             // relying on this behavior so we continue to support it for older apps.
   1588             sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
   1589         } else {
   1590             mConnection.commitText(StringUtils.newSingleCodePointString(code), 1);
   1591         }
   1592     }
   1593 
   1594     // Implementation of {@link KeyboardActionListener}.
   1595     @Override
   1596     public void onCodeInput(final int primaryCode, final int x, final int y) {
   1597         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1598             ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
   1599         }
   1600         final long when = SystemClock.uptimeMillis();
   1601         if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
   1602             mDeleteCount = 0;
   1603         }
   1604         mLastKeyTime = when;
   1605         mConnection.beginBatchEdit();
   1606         final KeyboardSwitcher switcher = mKeyboardSwitcher;
   1607         // The space state depends only on the last character pressed and its own previous
   1608         // state. Here, we revert the space state to neutral if the key is actually modifying
   1609         // the input contents (any non-shift key), which is what we should do for
   1610         // all inputs that do not result in a special state. Each character handling is then
   1611         // free to override the state as they see fit.
   1612         final int spaceState = mSpaceState;
   1613         if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
   1614 
   1615         // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
   1616         if (primaryCode != Constants.CODE_SPACE) {
   1617             mHandler.cancelDoubleSpacePeriodTimer();
   1618         }
   1619 
   1620         boolean didAutoCorrect = false;
   1621         switch (primaryCode) {
   1622         case Constants.CODE_DELETE:
   1623             mSpaceState = SPACE_STATE_NONE;
   1624             handleBackspace(spaceState);
   1625             LatinImeLogger.logOnDelete(x, y);
   1626             break;
   1627         case Constants.CODE_SHIFT:
   1628             // Note: Calling back to the keyboard on Shift key is handled in
   1629             // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
   1630             final Keyboard currentKeyboard = switcher.getKeyboard();
   1631             if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
   1632                 // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
   1633                 // alphabetic shift and shift while in symbol layout.
   1634                 handleRecapitalize();
   1635             }
   1636             break;
   1637         case Constants.CODE_CAPSLOCK:
   1638             // Note: Changing keyboard to shift lock state is handled in
   1639             // {@link KeyboardSwitcher#onCodeInput(int)}.
   1640             break;
   1641         case Constants.CODE_SWITCH_ALPHA_SYMBOL:
   1642             // Note: Calling back to the keyboard on symbol key is handled in
   1643             // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
   1644             break;
   1645         case Constants.CODE_SETTINGS:
   1646             onSettingsKeyPressed();
   1647             break;
   1648         case Constants.CODE_SHORTCUT:
   1649             mSubtypeSwitcher.switchToShortcutIME(this);
   1650             break;
   1651         case Constants.CODE_ACTION_NEXT:
   1652             performEditorAction(EditorInfo.IME_ACTION_NEXT);
   1653             break;
   1654         case Constants.CODE_ACTION_PREVIOUS:
   1655             performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
   1656             break;
   1657         case Constants.CODE_LANGUAGE_SWITCH:
   1658             handleLanguageSwitchKey();
   1659             break;
   1660         case Constants.CODE_EMOJI:
   1661             // Note: Switching emoji keyboard is being handled in
   1662             // {@link KeyboardState#onCodeInput(int,int)}.
   1663             break;
   1664         case Constants.CODE_ENTER:
   1665             final EditorInfo editorInfo = getCurrentInputEditorInfo();
   1666             final int imeOptionsActionId =
   1667                     InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
   1668             if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
   1669                 // Either we have an actionLabel and we should performEditorAction with actionId
   1670                 // regardless of its value.
   1671                 performEditorAction(editorInfo.actionId);
   1672             } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
   1673                 // We didn't have an actionLabel, but we had another action to execute.
   1674                 // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
   1675                 // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
   1676                 // means there should be an action and the app didn't bother to set a specific
   1677                 // code for it - presumably it only handles one. It does not have to be treated
   1678                 // in any specific way: anything that is not IME_ACTION_NONE should be sent to
   1679                 // performEditorAction.
   1680                 performEditorAction(imeOptionsActionId);
   1681             } else {
   1682                 // No action label, and the action from imeOptions is NONE: this is a regular
   1683                 // enter key that should input a carriage return.
   1684                 didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
   1685             }
   1686             break;
   1687         case Constants.CODE_SHIFT_ENTER:
   1688             didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
   1689             break;
   1690         default:
   1691             didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState);
   1692             break;
   1693         }
   1694         switcher.onCodeInput(primaryCode);
   1695         // Reset after any single keystroke, except shift, capslock, and symbol-shift
   1696         if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT
   1697                 && primaryCode != Constants.CODE_CAPSLOCK
   1698                 && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
   1699             mLastComposedWord.deactivate();
   1700         if (Constants.CODE_DELETE != primaryCode) {
   1701             mEnteredText = null;
   1702         }
   1703         mConnection.endBatchEdit();
   1704     }
   1705 
   1706     private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y,
   1707             final int spaceState) {
   1708         mSpaceState = SPACE_STATE_NONE;
   1709         final boolean didAutoCorrect;
   1710         final SettingsValues settingsValues = mSettings.getCurrent();
   1711         if (settingsValues.isWordSeparator(primaryCode)
   1712                 || Character.getType(primaryCode) == Character.OTHER_SYMBOL) {
   1713             didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
   1714         } else {
   1715             didAutoCorrect = false;
   1716             if (SPACE_STATE_PHANTOM == spaceState) {
   1717                 if (settingsValues.mIsInternal) {
   1718                     if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
   1719                         LatinImeLoggerUtils.onAutoCorrection(
   1720                                 "", mWordComposer.getTypedWord(), " ", mWordComposer);
   1721                     }
   1722                 }
   1723                 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   1724                     // If we are in the middle of a recorrection, we need to commit the recorrection
   1725                     // first so that we can insert the character at the current cursor position.
   1726                     resetEntireInputState(mLastSelectionStart);
   1727                 } else {
   1728                     commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   1729                 }
   1730             }
   1731             final int keyX, keyY;
   1732             final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   1733             if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
   1734                 keyX = x;
   1735                 keyY = y;
   1736             } else {
   1737                 keyX = Constants.NOT_A_COORDINATE;
   1738                 keyY = Constants.NOT_A_COORDINATE;
   1739             }
   1740             handleCharacter(primaryCode, keyX, keyY, spaceState);
   1741         }
   1742         mExpectingUpdateSelection = true;
   1743         return didAutoCorrect;
   1744     }
   1745 
   1746     // Called from PointerTracker through the KeyboardActionListener interface
   1747     @Override
   1748     public void onTextInput(final String rawText) {
   1749         mConnection.beginBatchEdit();
   1750         if (mWordComposer.isComposingWord()) {
   1751             commitCurrentAutoCorrection(rawText);
   1752         } else {
   1753             resetComposingState(true /* alsoResetLastComposedWord */);
   1754         }
   1755         mHandler.postUpdateSuggestionStrip();
   1756         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
   1757                 && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) {
   1758             ResearchLogger.getInstance().onResearchKeySelected(this);
   1759             return;
   1760         }
   1761         final String text = specificTldProcessingOnTextInput(rawText);
   1762         if (SPACE_STATE_PHANTOM == mSpaceState) {
   1763             promotePhantomSpace();
   1764         }
   1765         mConnection.commitText(text, 1);
   1766         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   1767             ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */);
   1768         }
   1769         mConnection.endBatchEdit();
   1770         // Space state must be updated before calling updateShiftState
   1771         mSpaceState = SPACE_STATE_NONE;
   1772         mKeyboardSwitcher.updateShiftState();
   1773         mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT);
   1774         mEnteredText = text;
   1775     }
   1776 
   1777     @Override
   1778     public void onStartBatchInput() {
   1779         mInputUpdater.onStartBatchInput();
   1780         mHandler.cancelUpdateSuggestionStrip();
   1781         mConnection.beginBatchEdit();
   1782         final SettingsValues settingsValues = mSettings.getCurrent();
   1783         if (mWordComposer.isComposingWord()) {
   1784             if (settingsValues.mIsInternal) {
   1785                 if (mWordComposer.isBatchMode()) {
   1786                     LatinImeLoggerUtils.onAutoCorrection(
   1787                             "", mWordComposer.getTypedWord(), " ", mWordComposer);
   1788                 }
   1789             }
   1790             final int wordComposerSize = mWordComposer.size();
   1791             // Since isComposingWord() is true, the size is at least 1.
   1792             if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   1793                 // If we are in the middle of a recorrection, we need to commit the recorrection
   1794                 // first so that we can insert the batch input at the current cursor position.
   1795                 resetEntireInputState(mLastSelectionStart);
   1796             } else if (wordComposerSize <= 1) {
   1797                 // We auto-correct the previous (typed, not gestured) string iff it's one character
   1798                 // long. The reason for this is, even in the middle of gesture typing, you'll still
   1799                 // tap one-letter words and you want them auto-corrected (typically, "i" in English
   1800                 // should become "I"). However for any longer word, we assume that the reason for
   1801                 // tapping probably is that the word you intend to type is not in the dictionary,
   1802                 // so we do not attempt to correct, on the assumption that if that was a dictionary
   1803                 // word, the user would probably have gestured instead.
   1804                 commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
   1805             } else {
   1806                 commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   1807             }
   1808             mExpectingUpdateSelection = true;
   1809         }
   1810         final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
   1811         if (Character.isLetterOrDigit(codePointBeforeCursor)
   1812                 || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
   1813             mSpaceState = SPACE_STATE_PHANTOM;
   1814         }
   1815         mConnection.endBatchEdit();
   1816         mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
   1817     }
   1818 
   1819     private static final class InputUpdater implements Handler.Callback {
   1820         private final Handler mHandler;
   1821         private final LatinIME mLatinIme;
   1822         private final Object mLock = new Object();
   1823         private boolean mInBatchInput; // synchronized using {@link #mLock}.
   1824 
   1825         private InputUpdater(final LatinIME latinIme) {
   1826             final HandlerThread handlerThread = new HandlerThread(
   1827                     InputUpdater.class.getSimpleName());
   1828             handlerThread.start();
   1829             mHandler = new Handler(handlerThread.getLooper(), this);
   1830             mLatinIme = latinIme;
   1831         }
   1832 
   1833         private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1;
   1834         private static final int MSG_GET_SUGGESTED_WORDS = 2;
   1835 
   1836         @Override
   1837         public boolean handleMessage(final Message msg) {
   1838             // TODO: straighten message passing - we don't need two kinds of messages calling
   1839             // each other.
   1840             switch (msg.what) {
   1841                 case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
   1842                     updateBatchInput((InputPointers)msg.obj, msg.arg2 /* sequenceNumber */);
   1843                     break;
   1844                 case MSG_GET_SUGGESTED_WORDS:
   1845                     mLatinIme.getSuggestedWords(msg.arg1 /* sessionId */,
   1846                             msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj);
   1847                     break;
   1848             }
   1849             return true;
   1850         }
   1851 
   1852         // Run in the UI thread.
   1853         public void onStartBatchInput() {
   1854             synchronized (mLock) {
   1855                 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
   1856                 mInBatchInput = true;
   1857                 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
   1858                         SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
   1859             }
   1860         }
   1861 
   1862         // Run in the Handler thread.
   1863         private void updateBatchInput(final InputPointers batchPointers, final int sequenceNumber) {
   1864             synchronized (mLock) {
   1865                 if (!mInBatchInput) {
   1866                     // Batch input has ended or canceled while the message was being delivered.
   1867                     return;
   1868                 }
   1869 
   1870                 getSuggestedWordsGestureLocked(batchPointers, sequenceNumber,
   1871                         new OnGetSuggestedWordsCallback() {
   1872                     @Override
   1873                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
   1874                         mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
   1875                                 suggestedWords, false /* dismissGestureFloatingPreviewText */);
   1876                     }
   1877                 });
   1878             }
   1879         }
   1880 
   1881         // Run in the UI thread.
   1882         public void onUpdateBatchInput(final InputPointers batchPointers,
   1883                 final int sequenceNumber) {
   1884             if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
   1885                 return;
   1886             }
   1887             mHandler.obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 0 /* arg1 */,
   1888                     sequenceNumber /* arg2 */, batchPointers /* obj */).sendToTarget();
   1889         }
   1890 
   1891         public void onCancelBatchInput() {
   1892             synchronized (mLock) {
   1893                 mInBatchInput = false;
   1894                 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
   1895                         SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
   1896             }
   1897         }
   1898 
   1899         // Run in the UI thread.
   1900         public void onEndBatchInput(final InputPointers batchPointers) {
   1901             synchronized(mLock) {
   1902                 getSuggestedWordsGestureLocked(batchPointers, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
   1903                         new OnGetSuggestedWordsCallback() {
   1904                     @Override
   1905                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
   1906                         mInBatchInput = false;
   1907                         mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords,
   1908                                 true /* dismissGestureFloatingPreviewText */);
   1909                         mLatinIme.mHandler.onEndBatchInput(suggestedWords);
   1910                     }
   1911                 });
   1912             }
   1913         }
   1914 
   1915         // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
   1916         // be synchronized.
   1917         private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
   1918                 final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
   1919             mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
   1920             mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
   1921                     sequenceNumber, new OnGetSuggestedWordsCallback() {
   1922                 @Override
   1923                 public void onGetSuggestedWords(SuggestedWords suggestedWords) {
   1924                     final int suggestionCount = suggestedWords.size();
   1925                     if (suggestionCount <= 1) {
   1926                         final String mostProbableSuggestion = (suggestionCount == 0) ? null
   1927                                 : suggestedWords.getWord(0);
   1928                         callback.onGetSuggestedWords(
   1929                                 mLatinIme.getOlderSuggestions(mostProbableSuggestion));
   1930                     }
   1931                     callback.onGetSuggestedWords(suggestedWords);
   1932                 }
   1933             });
   1934         }
   1935 
   1936         public void getSuggestedWords(final int sessionId, final int sequenceNumber,
   1937                 final OnGetSuggestedWordsCallback callback) {
   1938             mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback)
   1939                     .sendToTarget();
   1940         }
   1941 
   1942         private void onDestroy() {
   1943             mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS);
   1944             mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
   1945             mHandler.getLooper().quit();
   1946         }
   1947     }
   1948 
   1949     // This method must run in UI Thread.
   1950     private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
   1951             final boolean dismissGestureFloatingPreviewText) {
   1952         showSuggestionStrip(suggestedWords);
   1953         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   1954         mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
   1955         if (dismissGestureFloatingPreviewText) {
   1956             mainKeyboardView.dismissGestureFloatingPreviewText();
   1957         }
   1958     }
   1959 
   1960     /* The sequence number member is only used in onUpdateBatchInput. It is increased each time
   1961      * auto-commit happens. The reason we need this is, when auto-commit happens we trim the
   1962      * input pointers that are held in a singleton, and to know how much to trim we rely on the
   1963      * results of the suggestion process that is held in mSuggestedWords.
   1964      * However, the suggestion process is asynchronous, and sometimes we may enter the
   1965      * onUpdateBatchInput method twice without having recomputed suggestions yet, or having
   1966      * received new suggestions generated from not-yet-trimmed input pointers. In this case, the
   1967      * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we
   1968      * remove an unrelated number of pointers (possibly even more than are left in the input
   1969      * pointers, leading to a crash).
   1970      * To avoid that, we increase the sequence number each time we auto-commit and trim the
   1971      * input pointers, and we do not use any suggested words that have been generated with an
   1972      * earlier sequence number.
   1973      */
   1974     private int mAutoCommitSequenceNumber = 1;
   1975     @Override
   1976     public void onUpdateBatchInput(final InputPointers batchPointers) {
   1977         if (mSettings.getCurrent().mPhraseGestureEnabled) {
   1978             final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
   1979             // If these suggested words have been generated with out of date input pointers, then
   1980             // we skip auto-commit (see comments above on the mSequenceNumber member).
   1981             if (null != candidate && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) {
   1982                 if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
   1983                     final String[] commitParts = candidate.mWord.split(" ", 2);
   1984                     batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
   1985                     promotePhantomSpace();
   1986                     mConnection.commitText(commitParts[0], 0);
   1987                     mSpaceState = SPACE_STATE_PHANTOM;
   1988                     mKeyboardSwitcher.updateShiftState();
   1989                     mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
   1990                     ++mAutoCommitSequenceNumber;
   1991                 }
   1992             }
   1993         }
   1994         mInputUpdater.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
   1995     }
   1996 
   1997     // This method must run in UI Thread.
   1998     public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) {
   1999         final String batchInputText = suggestedWords.isEmpty()
   2000                 ? null : suggestedWords.getWord(0);
   2001         if (TextUtils.isEmpty(batchInputText)) {
   2002             return;
   2003         }
   2004         mConnection.beginBatchEdit();
   2005         if (SPACE_STATE_PHANTOM == mSpaceState) {
   2006             promotePhantomSpace();
   2007         }
   2008         if (mSettings.getCurrent().mPhraseGestureEnabled) {
   2009             // Find the last space
   2010             final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
   2011             if (0 != indexOfLastSpace) {
   2012                 mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
   2013                 showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture());
   2014             }
   2015             final String lastWord = batchInputText.substring(indexOfLastSpace);
   2016             mWordComposer.setBatchInputWord(lastWord);
   2017             mConnection.setComposingText(lastWord, 1);
   2018         } else {
   2019             mWordComposer.setBatchInputWord(batchInputText);
   2020             mConnection.setComposingText(batchInputText, 1);
   2021         }
   2022         mExpectingUpdateSelection = true;
   2023         mConnection.endBatchEdit();
   2024         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2025             ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords);
   2026         }
   2027         // Space state must be updated before calling updateShiftState
   2028         mSpaceState = SPACE_STATE_PHANTOM;
   2029         mKeyboardSwitcher.updateShiftState();
   2030     }
   2031 
   2032     @Override
   2033     public void onEndBatchInput(final InputPointers batchPointers) {
   2034         mInputUpdater.onEndBatchInput(batchPointers);
   2035     }
   2036 
   2037     private String specificTldProcessingOnTextInput(final String text) {
   2038         if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
   2039                 || !Character.isLetter(text.charAt(1))) {
   2040             // Not a tld: do nothing.
   2041             return text;
   2042         }
   2043         // We have a TLD (or something that looks like this): make sure we don't add
   2044         // a space even if currently in phantom mode.
   2045         mSpaceState = SPACE_STATE_NONE;
   2046         // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code
   2047         final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
   2048         if (lastOne != null && lastOne.length() == 1
   2049                 && lastOne.charAt(0) == Constants.CODE_PERIOD) {
   2050             return text.substring(1);
   2051         } else {
   2052             return text;
   2053         }
   2054     }
   2055 
   2056     // Called from PointerTracker through the KeyboardActionListener interface
   2057     @Override
   2058     public void onFinishSlidingInput() {
   2059         // User finished sliding input.
   2060         mKeyboardSwitcher.onFinishSlidingInput();
   2061     }
   2062 
   2063     // Called from PointerTracker through the KeyboardActionListener interface
   2064     @Override
   2065     public void onCancelInput() {
   2066         // User released a finger outside any key
   2067         // Nothing to do so far.
   2068     }
   2069 
   2070     @Override
   2071     public void onCancelBatchInput() {
   2072         mInputUpdater.onCancelBatchInput();
   2073     }
   2074 
   2075     private void handleBackspace(final int spaceState) {
   2076         // We revert these in this method if the deletion doesn't happen.
   2077         mDeleteCount++;
   2078         mExpectingUpdateSelection = true;
   2079 
   2080         // In many cases, we may have to put the keyboard in auto-shift state again. However
   2081         // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
   2082         // during key repeat.
   2083         mHandler.postUpdateShiftState();
   2084 
   2085         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   2086             // If we are in the middle of a recorrection, we need to commit the recorrection
   2087             // first so that we can remove the character at the current cursor position.
   2088             resetEntireInputState(mLastSelectionStart);
   2089             // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
   2090         }
   2091         if (mWordComposer.isComposingWord()) {
   2092             if (mWordComposer.isBatchMode()) {
   2093                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2094                     final String word = mWordComposer.getTypedWord();
   2095                     ResearchLogger.latinIME_handleBackspace_batch(word, 1);
   2096                 }
   2097                 final String rejectedSuggestion = mWordComposer.getTypedWord();
   2098                 mWordComposer.reset();
   2099                 mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
   2100             } else {
   2101                 mWordComposer.deleteLast();
   2102             }
   2103             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
   2104             mHandler.postUpdateSuggestionStrip();
   2105             if (!mWordComposer.isComposingWord()) {
   2106                 // If we just removed the last character, auto-caps mode may have changed so we
   2107                 // need to re-evaluate.
   2108                 mKeyboardSwitcher.updateShiftState();
   2109             }
   2110         } else {
   2111             final SettingsValues currentSettings = mSettings.getCurrent();
   2112             if (mLastComposedWord.canRevertCommit()) {
   2113                 if (currentSettings.mIsInternal) {
   2114                     LatinImeLoggerUtils.onAutoCorrectionCancellation();
   2115                 }
   2116                 revertCommit();
   2117                 return;
   2118             }
   2119             if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
   2120                 // Cancel multi-character input: remove the text we just entered.
   2121                 // This is triggered on backspace after a key that inputs multiple characters,
   2122                 // like the smiley key or the .com key.
   2123                 mConnection.deleteSurroundingText(mEnteredText.length(), 0);
   2124                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2125                     ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
   2126                 }
   2127                 mEnteredText = null;
   2128                 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
   2129                 // In addition we know that spaceState is false, and that we should not be
   2130                 // reverting any autocorrect at this point. So we can safely return.
   2131                 return;
   2132             }
   2133             if (SPACE_STATE_DOUBLE == spaceState) {
   2134                 mHandler.cancelDoubleSpacePeriodTimer();
   2135                 if (mConnection.revertDoubleSpacePeriod()) {
   2136                     // No need to reset mSpaceState, it has already be done (that's why we
   2137                     // receive it as a parameter)
   2138                     return;
   2139                 }
   2140             } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
   2141                 if (mConnection.revertSwapPunctuation()) {
   2142                     // Likewise
   2143                     return;
   2144                 }
   2145             }
   2146 
   2147             // No cancelling of commit/double space/swap: we have a regular backspace.
   2148             // We should backspace one char and restart suggestion if at the end of a word.
   2149             if (mLastSelectionStart != mLastSelectionEnd) {
   2150                 // If there is a selection, remove it.
   2151                 final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
   2152                 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
   2153                 // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
   2154                 // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
   2155                 // but we want to set it right away to avoid it being used with the wrong values
   2156                 // later (typically, in a subsequent press on backspace).
   2157                 mLastSelectionEnd = mLastSelectionStart;
   2158                 mConnection.deleteSurroundingText(numCharsDeleted, 0);
   2159                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2160                     ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
   2161                             false /* shouldUncommitLogUnit */);
   2162                 }
   2163             } else {
   2164                 // There is no selection, just delete one character.
   2165                 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
   2166                     // This should never happen.
   2167                     Log.e(TAG, "Backspace when we don't know the selection position");
   2168                 }
   2169                 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
   2170                 if (codePointBeforeCursor == Constants.NOT_A_CODE) {
   2171                     // Nothing to delete before the cursor. We have to revert the deletion states
   2172                     // that were updated at the beginning of this method.
   2173                     mDeleteCount--;
   2174                     mExpectingUpdateSelection = false;
   2175                     return;
   2176                 }
   2177                 final int lengthToDelete =
   2178                         Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
   2179                 if (mAppWorkAroundsUtils.isBeforeJellyBean() ||
   2180                         currentSettings.mInputAttributes.isTypeNull()) {
   2181                     // There are two possible reasons to send a key event: either the field has
   2182                     // type TYPE_NULL, in which case the keyboard should send events, or we are
   2183                     // running in backward compatibility mode. Before Jelly bean, the keyboard
   2184                     // would simulate a hardware keyboard event on pressing enter or delete. This
   2185                     // is bad for many reasons (there are race conditions with commits) but some
   2186                     // applications are relying on this behavior so we continue to support it for
   2187                     // older apps, so we retain this behavior if the app has target SDK < JellyBean.
   2188                     sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
   2189                 } else {
   2190                     mConnection.deleteSurroundingText(lengthToDelete, 0);
   2191                 }
   2192                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2193                     ResearchLogger.latinIME_handleBackspace(lengthToDelete,
   2194                             true /* shouldUncommitLogUnit */);
   2195                 }
   2196                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
   2197                     final int codePointBeforeCursorToDeleteAgain =
   2198                             mConnection.getCodePointBeforeCursor();
   2199                     if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
   2200                         final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
   2201                                 codePointBeforeCursorToDeleteAgain) ? 2 : 1;
   2202                         mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
   2203                         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2204                             ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
   2205                                     true /* shouldUncommitLogUnit */);
   2206                         }
   2207                     }
   2208                 }
   2209             }
   2210             if (currentSettings.isSuggestionsRequested(mDisplayOrientation)
   2211                     && currentSettings.mCurrentLanguageHasSpaces) {
   2212                 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
   2213             }
   2214             // We just removed a character. We need to update the auto-caps state.
   2215             mKeyboardSwitcher.updateShiftState();
   2216         }
   2217     }
   2218 
   2219     /*
   2220      * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
   2221      */
   2222     private boolean maybeStripSpace(final int code,
   2223             final int spaceState, final boolean isFromSuggestionStrip) {
   2224         if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
   2225             mConnection.removeTrailingSpace();
   2226             return false;
   2227         }
   2228         if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
   2229                 && isFromSuggestionStrip) {
   2230             final SettingsValues currentSettings = mSettings.getCurrent();
   2231             if (currentSettings.isUsuallyPrecededBySpace(code)) return false;
   2232             if (currentSettings.isUsuallyFollowedBySpace(code)) return true;
   2233             mConnection.removeTrailingSpace();
   2234         }
   2235         return false;
   2236     }
   2237 
   2238     private void handleCharacter(final int primaryCode, final int x,
   2239             final int y, final int spaceState) {
   2240         // TODO: refactor this method to stop flipping isComposingWord around all the time, and
   2241         // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
   2242         // which has the same name as other handle* methods but is not the same.
   2243         boolean isComposingWord = mWordComposer.isComposingWord();
   2244 
   2245         // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
   2246         // See onStartBatchInput() to see how to do it.
   2247         final SettingsValues currentSettings = mSettings.getCurrent();
   2248         if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) {
   2249             if (isComposingWord) {
   2250                 // Sanity check
   2251                 throw new RuntimeException("Should not be composing here");
   2252             }
   2253             promotePhantomSpace();
   2254         }
   2255 
   2256         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   2257             // If we are in the middle of a recorrection, we need to commit the recorrection
   2258             // first so that we can insert the character at the current cursor position.
   2259             resetEntireInputState(mLastSelectionStart);
   2260             isComposingWord = false;
   2261         }
   2262         // We want to find out whether to start composing a new word with this character. If so,
   2263         // we need to reset the composing state and switch isComposingWord. The order of the
   2264         // tests is important for good performance.
   2265         // We only start composing if we're not already composing.
   2266         if (!isComposingWord
   2267         // We only start composing if this is a word code point. Essentially that means it's a
   2268         // a letter or a word connector.
   2269                 && currentSettings.isWordCodePoint(primaryCode)
   2270         // We never go into composing state if suggestions are not requested.
   2271                 && currentSettings.isSuggestionsRequested(mDisplayOrientation) &&
   2272         // In languages with spaces, we only start composing a word when we are not already
   2273         // touching a word. In languages without spaces, the above conditions are sufficient.
   2274                 (!mConnection.isCursorTouchingWord(currentSettings)
   2275                         || !currentSettings.mCurrentLanguageHasSpaces)) {
   2276             // Reset entirely the composing state anyway, then start composing a new word unless
   2277             // the character is a single quote or a dash. The idea here is, single quote and dash
   2278             // are not separators and they should be treated as normal characters, except in the
   2279             // first position where they should not start composing a word.
   2280             isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode
   2281                     && Constants.CODE_DASH != primaryCode);
   2282             // Here we don't need to reset the last composed word. It will be reset
   2283             // when we commit this one, if we ever do; if on the other hand we backspace
   2284             // it entirely and resume suggestions on the previous word, we'd like to still
   2285             // have touch coordinates for it.
   2286             resetComposingState(false /* alsoResetLastComposedWord */);
   2287         }
   2288         if (isComposingWord) {
   2289             final int keyX, keyY;
   2290             if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) {
   2291                 final KeyDetector keyDetector =
   2292                         mKeyboardSwitcher.getMainKeyboardView().getKeyDetector();
   2293                 keyX = keyDetector.getTouchX(x);
   2294                 keyY = keyDetector.getTouchY(y);
   2295             } else {
   2296                 keyX = x;
   2297                 keyY = y;
   2298             }
   2299             mWordComposer.add(primaryCode, keyX, keyY);
   2300             // If it's the first letter, make note of auto-caps state
   2301             if (mWordComposer.size() == 1) {
   2302                 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
   2303             }
   2304             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
   2305         } else {
   2306             final boolean swapWeakSpace = maybeStripSpace(primaryCode,
   2307                     spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
   2308 
   2309             sendKeyCodePoint(primaryCode);
   2310 
   2311             if (swapWeakSpace) {
   2312                 swapSwapperAndSpace();
   2313                 mSpaceState = SPACE_STATE_WEAK;
   2314             }
   2315             // In case the "add to dictionary" hint was still displayed.
   2316             if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
   2317         }
   2318         mHandler.postUpdateSuggestionStrip();
   2319         if (currentSettings.mIsInternal) {
   2320             LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y);
   2321         }
   2322     }
   2323 
   2324     private void handleRecapitalize() {
   2325         if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
   2326         // If we have a recapitalize in progress, use it; otherwise, create a new one.
   2327         if (!mRecapitalizeStatus.isActive()
   2328                 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
   2329             final CharSequence selectedText =
   2330                     mConnection.getSelectedText(0 /* flags, 0 for no styles */);
   2331             if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
   2332             final SettingsValues currentSettings = mSettings.getCurrent();
   2333             mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
   2334                     selectedText.toString(), currentSettings.mLocale,
   2335                     currentSettings.mWordSeparators);
   2336             // We trim leading and trailing whitespace.
   2337             mRecapitalizeStatus.trim();
   2338             // Trimming the object may have changed the length of the string, and we need to
   2339             // reposition the selection handles accordingly. As this result in an IPC call,
   2340             // only do it if it's actually necessary, in other words if the recapitalize status
   2341             // is not set at the same place as before.
   2342             if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
   2343                 mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
   2344                 mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
   2345                 mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
   2346             }
   2347         }
   2348         mRecapitalizeStatus.rotate();
   2349         final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
   2350         mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
   2351         mConnection.deleteSurroundingText(numCharsDeleted, 0);
   2352         mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
   2353         mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
   2354         mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
   2355         mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
   2356         // Match the keyboard to the new state.
   2357         mKeyboardSwitcher.updateShiftState();
   2358     }
   2359 
   2360     // Returns true if we do an autocorrection, false otherwise.
   2361     private boolean handleSeparator(final int primaryCode, final int x, final int y,
   2362             final int spaceState) {
   2363         boolean didAutoCorrect = false;
   2364         final SettingsValues currentSettings = mSettings.getCurrent();
   2365         // We avoid sending spaces in languages without spaces if we were composing.
   2366         final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode
   2367                 && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord();
   2368         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   2369             // If we are in the middle of a recorrection, we need to commit the recorrection
   2370             // first so that we can insert the separator at the current cursor position.
   2371             resetEntireInputState(mLastSelectionStart);
   2372         }
   2373         if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
   2374             if (currentSettings.mCorrectionEnabled) {
   2375                 final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
   2376                         : StringUtils.newSingleCodePointString(primaryCode);
   2377                 commitCurrentAutoCorrection(separator);
   2378                 didAutoCorrect = true;
   2379             } else {
   2380                 commitTyped(StringUtils.newSingleCodePointString(primaryCode));
   2381             }
   2382         }
   2383 
   2384         final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
   2385                 Constants.SUGGESTION_STRIP_COORDINATE == x);
   2386 
   2387         if (SPACE_STATE_PHANTOM == spaceState &&
   2388                 currentSettings.isUsuallyPrecededBySpace(primaryCode)) {
   2389             promotePhantomSpace();
   2390         }
   2391         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2392             ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
   2393         }
   2394 
   2395         if (!shouldAvoidSendingCode) {
   2396             sendKeyCodePoint(primaryCode);
   2397         }
   2398 
   2399         if (Constants.CODE_SPACE == primaryCode) {
   2400             if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
   2401                 if (maybeDoubleSpacePeriod()) {
   2402                     mSpaceState = SPACE_STATE_DOUBLE;
   2403                 } else if (!isShowingPunctuationList()) {
   2404                     mSpaceState = SPACE_STATE_WEAK;
   2405                 }
   2406             }
   2407 
   2408             mHandler.startDoubleSpacePeriodTimer();
   2409             mHandler.postUpdateSuggestionStrip();
   2410         } else {
   2411             if (swapWeakSpace) {
   2412                 swapSwapperAndSpace();
   2413                 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
   2414             } else if (SPACE_STATE_PHANTOM == spaceState
   2415                     && currentSettings.isUsuallyFollowedBySpace(primaryCode)) {
   2416                 // If we are in phantom space state, and the user presses a separator, we want to
   2417                 // stay in phantom space state so that the next keypress has a chance to add the
   2418                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
   2419                 // then insert a comma and go on to typing the next word, I want the space to be
   2420                 // inserted automatically before the next word, the same way it is when I don't
   2421                 // input the comma.
   2422                 // The case is a little different if the separator is a space stripper. Such a
   2423                 // separator does not normally need a space on the right (that's the difference
   2424                 // between swappers and strippers), so we should not stay in phantom space state if
   2425                 // the separator is a stripper. Hence the additional test above.
   2426                 mSpaceState = SPACE_STATE_PHANTOM;
   2427             }
   2428 
   2429             // Set punctuation right away. onUpdateSelection will fire but tests whether it is
   2430             // already displayed or not, so it's okay.
   2431             setPunctuationSuggestions();
   2432         }
   2433         if (currentSettings.mIsInternal) {
   2434             LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y);
   2435         }
   2436 
   2437         mKeyboardSwitcher.updateShiftState();
   2438         return didAutoCorrect;
   2439     }
   2440 
   2441     private CharSequence getTextWithUnderline(final String text) {
   2442         return mIsAutoCorrectionIndicatorOn
   2443                 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
   2444                 : text;
   2445     }
   2446 
   2447     private void handleClose() {
   2448         // TODO: Verify that words are logged properly when IME is closed.
   2449         commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   2450         requestHideSelf(0);
   2451         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   2452         if (mainKeyboardView != null) {
   2453             mainKeyboardView.closing();
   2454         }
   2455     }
   2456 
   2457     // TODO: make this private
   2458     // Outside LatinIME, only used by the test suite.
   2459     @UsedForTesting
   2460     boolean isShowingPunctuationList() {
   2461         if (mSuggestedWords == null) return false;
   2462         return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords;
   2463     }
   2464 
   2465     private boolean isSuggestionsStripVisible() {
   2466         final SettingsValues currentSettings = mSettings.getCurrent();
   2467         if (mSuggestionStripView == null)
   2468             return false;
   2469         if (mSuggestionStripView.isShowingAddToDictionaryHint())
   2470             return true;
   2471         if (null == currentSettings)
   2472             return false;
   2473         if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
   2474             return false;
   2475         if (currentSettings.isApplicationSpecifiedCompletionsOn())
   2476             return true;
   2477         return currentSettings.isSuggestionsRequested(mDisplayOrientation);
   2478     }
   2479 
   2480     private void clearSuggestionStrip() {
   2481         setSuggestedWords(SuggestedWords.EMPTY, false);
   2482         setAutoCorrectionIndicator(false);
   2483     }
   2484 
   2485     private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) {
   2486         mSuggestedWords = words;
   2487         if (mSuggestionStripView != null) {
   2488             mSuggestionStripView.setSuggestions(words);
   2489             mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
   2490         }
   2491     }
   2492 
   2493     private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
   2494         // Put a blue underline to a word in TextView which will be auto-corrected.
   2495         if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
   2496                 && mWordComposer.isComposingWord()) {
   2497             mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
   2498             final CharSequence textWithUnderline =
   2499                     getTextWithUnderline(mWordComposer.getTypedWord());
   2500             // TODO: when called from an updateSuggestionStrip() call that results from a posted
   2501             // message, this is called outside any batch edit. Potentially, this may result in some
   2502             // janky flickering of the screen, although the display speed makes it unlikely in
   2503             // the practice.
   2504             mConnection.setComposingText(textWithUnderline, 1);
   2505         }
   2506     }
   2507 
   2508     private void updateSuggestionStrip() {
   2509         mHandler.cancelUpdateSuggestionStrip();
   2510         final SettingsValues currentSettings = mSettings.getCurrent();
   2511 
   2512         // Check if we have a suggestion engine attached.
   2513         if (mSuggest == null
   2514                 || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
   2515             if (mWordComposer.isComposingWord()) {
   2516                 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
   2517                         + "requested!");
   2518             }
   2519             return;
   2520         }
   2521 
   2522         if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) {
   2523             setPunctuationSuggestions();
   2524             return;
   2525         }
   2526 
   2527         final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
   2528         getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
   2529                 SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
   2530                     @Override
   2531                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
   2532                         holder.set(suggestedWords);
   2533                     }
   2534                 }
   2535         );
   2536 
   2537         // This line may cause the current thread to wait.
   2538         final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT);
   2539         if (suggestedWords != null) {
   2540             showSuggestionStrip(suggestedWords);
   2541         }
   2542     }
   2543 
   2544     private void getSuggestedWords(final int sessionId, final int sequenceNumber,
   2545             final OnGetSuggestedWordsCallback callback) {
   2546         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   2547         final Suggest suggest = mSuggest;
   2548         if (keyboard == null || suggest == null) {
   2549             callback.onGetSuggestedWords(SuggestedWords.EMPTY);
   2550             return;
   2551         }
   2552         // Get the word on which we should search the bigrams. If we are composing a word, it's
   2553         // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
   2554         // should just skip whitespace if any, so 1.
   2555         final SettingsValues currentSettings = mSettings.getCurrent();
   2556         final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
   2557         final String prevWord;
   2558         if (currentSettings.mCurrentLanguageHasSpaces) {
   2559             // If we are typing in a language with spaces we can just look up the previous
   2560             // word from textview.
   2561             prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators,
   2562                     mWordComposer.isComposingWord() ? 2 : 1);
   2563         } else {
   2564             prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
   2565                     : mLastComposedWord.mCommittedWord;
   2566         }
   2567         suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
   2568                 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
   2569                 additionalFeaturesOptions, sessionId, sequenceNumber, callback);
   2570     }
   2571 
   2572     private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId,
   2573             final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
   2574         mInputUpdater.getSuggestedWords(sessionId, sequenceNumber,
   2575                 new OnGetSuggestedWordsCallback() {
   2576                     @Override
   2577                     public void onGetSuggestedWords(SuggestedWords suggestedWords) {
   2578                         callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
   2579                                 mWordComposer.getTypedWord(), suggestedWords));
   2580                     }
   2581                 });
   2582     }
   2583 
   2584     private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
   2585             final SuggestedWords suggestedWords) {
   2586         // TODO: consolidate this into getSuggestedWords
   2587         // We update the suggestion strip only when we have some suggestions to show, i.e. when
   2588         // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
   2589         // replaced with the new one. However, when the word is a dictionary word, or when the
   2590         // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
   2591         // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
   2592         // revert to suggestions - although it is unclear how we can come here if it's displayed.
   2593         if (suggestedWords.size() > 1 || typedWord.length() <= 1
   2594                 || suggestedWords.mTypedWordValid || null == mSuggestionStripView
   2595                 || mSuggestionStripView.isShowingAddToDictionaryHint()) {
   2596             return suggestedWords;
   2597         } else {
   2598             return getOlderSuggestions(typedWord);
   2599         }
   2600     }
   2601 
   2602     private SuggestedWords getOlderSuggestions(final String typedWord) {
   2603         SuggestedWords previousSuggestedWords = mSuggestedWords;
   2604         if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) {
   2605             previousSuggestedWords = SuggestedWords.EMPTY;
   2606         }
   2607         if (typedWord == null) {
   2608             return previousSuggestedWords;
   2609         }
   2610         final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
   2611                 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord,
   2612                         previousSuggestedWords);
   2613         return new SuggestedWords(typedWordAndPreviousSuggestions,
   2614                 false /* typedWordValid */,
   2615                 false /* hasAutoCorrectionCandidate */,
   2616                 false /* isPunctuationSuggestions */,
   2617                 true /* isObsoleteSuggestions */,
   2618                 false /* isPrediction */);
   2619     }
   2620 
   2621     private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) {
   2622         if (suggestedWords.isEmpty()) return;
   2623         final String autoCorrection;
   2624         if (suggestedWords.mWillAutoCorrect) {
   2625             autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
   2626         } else {
   2627             // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)
   2628             // because it may differ from mWordComposer.mTypedWord.
   2629             autoCorrection = typedWord;
   2630         }
   2631         mWordComposer.setAutoCorrection(autoCorrection);
   2632     }
   2633 
   2634     private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
   2635             final String typedWord) {
   2636       if (suggestedWords.isEmpty()) {
   2637           // No auto-correction is available, clear the cached values.
   2638           AccessibilityUtils.getInstance().setAutoCorrection(null, null);
   2639           clearSuggestionStrip();
   2640           return;
   2641       }
   2642       setAutoCorrection(suggestedWords, typedWord);
   2643       final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
   2644       setSuggestedWords(suggestedWords, isAutoCorrection);
   2645       setAutoCorrectionIndicator(isAutoCorrection);
   2646       setSuggestionStripShown(isSuggestionsStripVisible());
   2647       // An auto-correction is available, cache it in accessibility code so
   2648       // we can be speak it if the user touches a key that will insert it.
   2649       AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord);
   2650     }
   2651 
   2652     private void showSuggestionStrip(final SuggestedWords suggestedWords) {
   2653         if (suggestedWords.isEmpty()) {
   2654             clearSuggestionStrip();
   2655             return;
   2656         }
   2657         showSuggestionStripWithTypedWord(suggestedWords,
   2658             suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
   2659     }
   2660 
   2661     private void commitCurrentAutoCorrection(final String separator) {
   2662         // Complete any pending suggestions query first
   2663         if (mHandler.hasPendingUpdateSuggestions()) {
   2664             updateSuggestionStrip();
   2665         }
   2666         final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
   2667         final String typedWord = mWordComposer.getTypedWord();
   2668         final String autoCorrection = (typedAutoCorrection != null)
   2669                 ? typedAutoCorrection : typedWord;
   2670         if (autoCorrection != null) {
   2671             if (TextUtils.isEmpty(typedWord)) {
   2672                 throw new RuntimeException("We have an auto-correction but the typed word "
   2673                         + "is empty? Impossible! I must commit suicide.");
   2674             }
   2675             if (mSettings.isInternal()) {
   2676                 LatinImeLoggerUtils.onAutoCorrection(
   2677                         typedWord, autoCorrection, separator, mWordComposer);
   2678             }
   2679             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2680                 final SuggestedWords suggestedWords = mSuggestedWords;
   2681                 ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
   2682                         separator, mWordComposer.isBatchMode(), suggestedWords);
   2683             }
   2684             mExpectingUpdateSelection = true;
   2685             commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
   2686                     separator);
   2687             if (!typedWord.equals(autoCorrection)) {
   2688                 // This will make the correction flash for a short while as a visual clue
   2689                 // to the user that auto-correction happened. It has no other effect; in particular
   2690                 // note that this won't affect the text inside the text field AT ALL: it only makes
   2691                 // the segment of text starting at the supplied index and running for the length
   2692                 // of the auto-correction flash. At this moment, the "typedWord" argument is
   2693                 // ignored by TextView.
   2694                 mConnection.commitCorrection(
   2695                         new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
   2696                         typedWord, autoCorrection));
   2697             }
   2698         }
   2699     }
   2700 
   2701     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
   2702     // interface
   2703     @Override
   2704     public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) {
   2705         final SuggestedWords suggestedWords = mSuggestedWords;
   2706         final String suggestion = suggestionInfo.mWord;
   2707         // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
   2708         if (suggestion.length() == 1 && isShowingPunctuationList()) {
   2709             // Word separators are suggested before the user inputs something.
   2710             // So, LatinImeLogger logs "" as a user's input.
   2711             LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords);
   2712             // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
   2713             final int primaryCode = suggestion.charAt(0);
   2714             onCodeInput(primaryCode,
   2715                     Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
   2716             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2717                 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion,
   2718                         false /* isBatchMode */, suggestedWords.mIsPrediction);
   2719             }
   2720             return;
   2721         }
   2722 
   2723         mConnection.beginBatchEdit();
   2724         final SettingsValues currentSettings = mSettings.getCurrent();
   2725         if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
   2726                 // In the batch input mode, a manually picked suggested word should just replace
   2727                 // the current batch input text and there is no need for a phantom space.
   2728                 && !mWordComposer.isBatchMode()) {
   2729             final int firstChar = Character.codePointAt(suggestion, 0);
   2730             if (!currentSettings.isWordSeparator(firstChar)
   2731                     || currentSettings.isUsuallyPrecededBySpace(firstChar)) {
   2732                 promotePhantomSpace();
   2733             }
   2734         }
   2735 
   2736         if (currentSettings.isApplicationSpecifiedCompletionsOn()
   2737                 && mApplicationSpecifiedCompletions != null
   2738                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
   2739             mSuggestedWords = SuggestedWords.EMPTY;
   2740             if (mSuggestionStripView != null) {
   2741                 mSuggestionStripView.clear();
   2742             }
   2743             mKeyboardSwitcher.updateShiftState();
   2744             resetComposingState(true /* alsoResetLastComposedWord */);
   2745             final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
   2746             mConnection.commitCompletion(completionInfo);
   2747             mConnection.endBatchEdit();
   2748             return;
   2749         }
   2750 
   2751         // We need to log before we commit, because the word composer will store away the user
   2752         // typed word.
   2753         final String replacedWord = mWordComposer.getTypedWord();
   2754         LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords);
   2755         mExpectingUpdateSelection = true;
   2756         commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
   2757                 LastComposedWord.NOT_A_SEPARATOR);
   2758         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2759             ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
   2760                     mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind,
   2761                     suggestionInfo.mSourceDict.mDictType);
   2762         }
   2763         mConnection.endBatchEdit();
   2764         // Don't allow cancellation of manual pick
   2765         mLastComposedWord.deactivate();
   2766         // Space state must be updated before calling updateShiftState
   2767         mSpaceState = SPACE_STATE_PHANTOM;
   2768         mKeyboardSwitcher.updateShiftState();
   2769 
   2770         // We should show the "Touch again to save" hint if the user pressed the first entry
   2771         // AND it's in none of our current dictionaries (main, user or otherwise).
   2772         // Please note that if mSuggest is null, it means that everything is off: suggestion
   2773         // and correction, so we shouldn't try to show the hint
   2774         final Suggest suggest = mSuggest;
   2775         final boolean showingAddToDictionaryHint =
   2776                 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
   2777                         || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
   2778                         && suggest != null
   2779                         // If the suggestion is not in the dictionary, the hint should be shown.
   2780                         && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true);
   2781 
   2782         if (currentSettings.mIsInternal) {
   2783             LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
   2784                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
   2785         }
   2786         if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
   2787             mSuggestionStripView.showAddToDictionaryHint(
   2788                     suggestion, currentSettings.mHintToSaveText);
   2789         } else {
   2790             // If we're not showing the "Touch again to save", then update the suggestion strip.
   2791             mHandler.postUpdateSuggestionStrip();
   2792         }
   2793     }
   2794 
   2795     /**
   2796      * Commits the chosen word to the text field and saves it for later retrieval.
   2797      */
   2798     private void commitChosenWord(final String chosenWord, final int commitType,
   2799             final String separatorString) {
   2800         final SuggestedWords suggestedWords = mSuggestedWords;
   2801         mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
   2802                 this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
   2803         // Add the word to the user history dictionary
   2804         final String prevWord = addToUserHistoryDictionary(chosenWord);
   2805         // TODO: figure out here if this is an auto-correct or if the best word is actually
   2806         // what user typed. Note: currently this is done much later in
   2807         // LastComposedWord#didCommitTypedWord by string equality of the remembered
   2808         // strings.
   2809         mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString,
   2810                 prevWord);
   2811     }
   2812 
   2813     private void setPunctuationSuggestions() {
   2814         final SettingsValues currentSettings = mSettings.getCurrent();
   2815         if (currentSettings.mBigramPredictionEnabled) {
   2816             clearSuggestionStrip();
   2817         } else {
   2818             setSuggestedWords(currentSettings.mSuggestPuncList, false);
   2819         }
   2820         setAutoCorrectionIndicator(false);
   2821         setSuggestionStripShown(isSuggestionsStripVisible());
   2822     }
   2823 
   2824     private String addToUserHistoryDictionary(final String suggestion) {
   2825         if (TextUtils.isEmpty(suggestion)) return null;
   2826         final Suggest suggest = mSuggest;
   2827         if (suggest == null) return null;
   2828 
   2829         // If correction is not enabled, we don't add words to the user history dictionary.
   2830         // That's to avoid unintended additions in some sensitive fields, or fields that
   2831         // expect to receive non-words.
   2832         final SettingsValues currentSettings = mSettings.getCurrent();
   2833         if (!currentSettings.mCorrectionEnabled) return null;
   2834 
   2835         final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
   2836         if (userHistoryDictionary == null) return null;
   2837 
   2838         final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2);
   2839         final String secondWord;
   2840         if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
   2841             secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
   2842         } else {
   2843             secondWord = suggestion;
   2844         }
   2845         // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
   2846         // We don't add words with 0-frequency (assuming they would be profanity etc.).
   2847         final int maxFreq = AutoCorrectionUtils.getMaxFrequency(
   2848                 suggest.getUnigramDictionaries(), suggestion);
   2849         if (maxFreq == 0) return null;
   2850         userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0);
   2851         return prevWord;
   2852     }
   2853 
   2854     private boolean isResumableWord(final String word, final SettingsValues settings) {
   2855         final int firstCodePoint = word.codePointAt(0);
   2856         return settings.isWordCodePoint(firstCodePoint)
   2857                 && Constants.CODE_SINGLE_QUOTE != firstCodePoint
   2858                 && Constants.CODE_DASH != firstCodePoint;
   2859     }
   2860 
   2861     /**
   2862      * Check if the cursor is touching a word. If so, restart suggestions on this word, else
   2863      * do nothing.
   2864      */
   2865     private void restartSuggestionsOnWordTouchedByCursor() {
   2866         // HACK: We may want to special-case some apps that exhibit bad behavior in case of
   2867         // recorrection. This is a temporary, stopgap measure that will be removed later.
   2868         // TODO: remove this.
   2869         if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
   2870         // A simple way to test for support from the TextView.
   2871         if (!isSuggestionsStripVisible()) return;
   2872         // Recorrection is not supported in languages without spaces because we don't know
   2873         // how to segment them yet.
   2874         if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
   2875         // If the cursor is not touching a word, or if there is a selection, return right away.
   2876         if (mLastSelectionStart != mLastSelectionEnd) return;
   2877         // If we don't know the cursor location, return.
   2878         if (mLastSelectionStart < 0) return;
   2879         final SettingsValues currentSettings = mSettings.getCurrent();
   2880         if (!mConnection.isCursorTouchingWord(currentSettings)) return;
   2881         final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators,
   2882                 0 /* additionalPrecedingWordsCount */);
   2883         if (null == range) return; // Happens if we don't have an input connection at all
   2884         if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out.
   2885         // If for some strange reason (editor bug or so) we measure the text before the cursor as
   2886         // longer than what the entire text is supposed to be, the safe thing to do is bail out.
   2887         final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
   2888         if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return;
   2889         final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
   2890         final String typedWord = range.mWord.toString();
   2891         if (!isResumableWord(typedWord, currentSettings)) return;
   2892         int i = 0;
   2893         for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) {
   2894             for (final String s : span.getSuggestions()) {
   2895                 ++i;
   2896                 if (!TextUtils.equals(s, typedWord)) {
   2897                     suggestions.add(new SuggestedWordInfo(s,
   2898                             SuggestionStripView.MAX_SUGGESTIONS - i,
   2899                             SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
   2900                             SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
   2901                             SuggestedWordInfo.NOT_A_CONFIDENCE
   2902                                     /* autoCommitFirstWordConfidence */));
   2903                 }
   2904             }
   2905         }
   2906         mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
   2907         mWordComposer.setCursorPositionWithinWord(
   2908                 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
   2909         mConnection.setComposingRegion(
   2910                 mLastSelectionStart - numberOfCharsInWordBeforeCursor,
   2911                 mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
   2912         if (suggestions.isEmpty()) {
   2913             // We come here if there weren't any suggestion spans on this word. We will try to
   2914             // compute suggestions for it instead.
   2915             mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
   2916                     SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
   2917                         @Override
   2918                         public void onGetSuggestedWords(
   2919                                 final SuggestedWords suggestedWordsIncludingTypedWord) {
   2920                             final SuggestedWords suggestedWords;
   2921                             if (suggestedWordsIncludingTypedWord.size() > 1) {
   2922                                 // We were able to compute new suggestions for this word.
   2923                                 // Remove the typed word, since we don't want to display it in this
   2924                                 // case. The #getSuggestedWordsExcludingTypedWord() method sets
   2925                                 // willAutoCorrect to false.
   2926                                 suggestedWords = suggestedWordsIncludingTypedWord
   2927                                         .getSuggestedWordsExcludingTypedWord();
   2928                             } else {
   2929                                 // No saved suggestions, and we were unable to compute any good one
   2930                                 // either. Rather than displaying an empty suggestion strip, we'll
   2931                                 // display the original word alone in the middle.
   2932                                 // Since there is only one word, willAutoCorrect is false.
   2933                                 suggestedWords = suggestedWordsIncludingTypedWord;
   2934                             }
   2935                             // We need to pass typedWord because mWordComposer.mTypedWord may
   2936                             // differ from typedWord.
   2937                             unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
   2938                                     suggestedWords, typedWord);
   2939                         }});
   2940         } else {
   2941             // We found suggestion spans in the word. We'll create the SuggestedWords out of
   2942             // them, and make willAutoCorrect false.
   2943             final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
   2944                     true /* typedWordValid */, false /* willAutoCorrect */,
   2945                     false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
   2946                     false /* isPrediction */);
   2947             // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord.
   2948             unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, typedWord);
   2949         }
   2950     }
   2951 
   2952     public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
   2953             final SuggestedWords suggestedWords, final String typedWord) {
   2954         // Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
   2955         // We never want to auto-correct on a resumed suggestion. Please refer to the three places
   2956         // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected.
   2957         // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching
   2958         // the text to adapt it.
   2959         // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition)
   2960         mIsAutoCorrectionIndicatorOn = false;
   2961         mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord);
   2962     }
   2963 
   2964     /**
   2965      * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
   2966      * word, else do nothing.
   2967      */
   2968     private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
   2969         final CharSequence word =
   2970                 mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
   2971         if (null != word) {
   2972             final String wordString = word.toString();
   2973             restartSuggestionsOnWordBeforeCursor(wordString);
   2974             // TODO: Handle the case where the user manually moves the cursor and then backs up over
   2975             // a separator.  In that case, the current log unit should not be uncommitted.
   2976             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2977                 ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
   2978                         true /* dumpCurrentLogUnit */);
   2979             }
   2980         }
   2981     }
   2982 
   2983     private void restartSuggestionsOnWordBeforeCursor(final String word) {
   2984         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
   2985         final int length = word.length();
   2986         mConnection.deleteSurroundingText(length, 0);
   2987         mConnection.setComposingText(word, 1);
   2988         mHandler.postUpdateSuggestionStrip();
   2989     }
   2990 
   2991     /**
   2992      * Retry resetting caches in the rich input connection.
   2993      *
   2994      * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
   2995      * This method handles the retry, and re-schedules a new retry if we still can't access.
   2996      * We only retry up to 5 times before giving up.
   2997      *
   2998      * @param tryResumeSuggestions Whether we should resume suggestions or not.
   2999      * @param remainingTries How many times we may try again before giving up.
   3000      */
   3001     private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
   3002         if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart, false)) {
   3003             if (0 < remainingTries) {
   3004                 mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
   3005                 return;
   3006             }
   3007             // If remainingTries is 0, we should stop waiting for new tries, but it's still
   3008             // better to load the keyboard (less things will be broken).
   3009         }
   3010         tryFixLyingCursorPosition();
   3011         mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
   3012         if (tryResumeSuggestions) mHandler.postResumeSuggestions();
   3013     }
   3014 
   3015     private void revertCommit() {
   3016         final String previousWord = mLastComposedWord.mPrevWord;
   3017         final String originallyTypedWord = mLastComposedWord.mTypedWord;
   3018         final String committedWord = mLastComposedWord.mCommittedWord;
   3019         final int cancelLength = committedWord.length();
   3020         // We want java chars, not codepoints for the following.
   3021         final int separatorLength = mLastComposedWord.mSeparatorString.length();
   3022         // TODO: should we check our saved separator against the actual contents of the text view?
   3023         final int deleteLength = cancelLength + separatorLength;
   3024         if (DEBUG) {
   3025             if (mWordComposer.isComposingWord()) {
   3026                 throw new RuntimeException("revertCommit, but we are composing a word");
   3027             }
   3028             final CharSequence wordBeforeCursor =
   3029                     mConnection.getTextBeforeCursor(deleteLength, 0)
   3030                             .subSequence(0, cancelLength);
   3031             if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
   3032                 throw new RuntimeException("revertCommit check failed: we thought we were "
   3033                         + "reverting \"" + committedWord
   3034                         + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
   3035             }
   3036         }
   3037         mConnection.deleteSurroundingText(deleteLength, 0);
   3038         if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
   3039             mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
   3040         }
   3041         final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
   3042         if (mSettings.getCurrent().mCurrentLanguageHasSpaces) {
   3043             // For languages with spaces, we revert to the typed string, but the cursor is still
   3044             // after the separator so we don't resume suggestions. If the user wants to correct
   3045             // the word, they have to press backspace again.
   3046             mConnection.commitText(stringToCommit, 1);
   3047         } else {
   3048             // For languages without spaces, we revert the typed string but the cursor is flush
   3049             // with the typed word, so we need to resume suggestions right away.
   3050             mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard());
   3051             mConnection.setComposingText(stringToCommit, 1);
   3052         }
   3053         if (mSettings.isInternal()) {
   3054             LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
   3055                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
   3056         }
   3057         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   3058             ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
   3059                     mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
   3060         }
   3061         // Don't restart suggestion yet. We'll restart if the user deletes the
   3062         // separator.
   3063         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
   3064         // We have a separator between the word and the cursor: we should show predictions.
   3065         mHandler.postUpdateSuggestionStrip();
   3066     }
   3067 
   3068     // This essentially inserts a space, and that's it.
   3069     public void promotePhantomSpace() {
   3070         final SettingsValues currentSettings = mSettings.getCurrent();
   3071         if (currentSettings.shouldInsertSpacesAutomatically()
   3072                 && currentSettings.mCurrentLanguageHasSpaces
   3073                 && !mConnection.textBeforeCursorLooksLikeURL()) {
   3074             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   3075                 ResearchLogger.latinIME_promotePhantomSpace();
   3076             }
   3077             sendKeyCodePoint(Constants.CODE_SPACE);
   3078         }
   3079     }
   3080 
   3081     // TODO: Make this private
   3082     // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
   3083     @UsedForTesting
   3084     void loadKeyboard() {
   3085         // Since we are switching languages, the most urgent thing is to let the keyboard graphics
   3086         // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on
   3087         // the screen. Anything we do right now will delay this, so wait until the next frame
   3088         // before we do the rest, like reopening dictionaries and updating suggestions. So we
   3089         // post a message.
   3090         mHandler.postReopenDictionaries();
   3091         loadSettings();
   3092         if (mKeyboardSwitcher.getMainKeyboardView() != null) {
   3093             // Reload keyboard because the current language has been changed.
   3094             mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
   3095         }
   3096     }
   3097 
   3098     private void hapticAndAudioFeedback(final int code, final int repeatCount) {
   3099         final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView();
   3100         if (keyboardView != null && keyboardView.isInSlidingKeyInput()) {
   3101             // No need to feedback while sliding input.
   3102             return;
   3103         }
   3104         if (repeatCount > 0) {
   3105             if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
   3106                 // No need to feedback when repeat delete key will have no effect.
   3107                 return;
   3108             }
   3109             // TODO: Use event time that the last feedback has been generated instead of relying on
   3110             // a repeat count to thin out feedback.
   3111             if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) {
   3112                 return;
   3113             }
   3114         }
   3115         final AudioAndHapticFeedbackManager feedbackManager =
   3116                 AudioAndHapticFeedbackManager.getInstance();
   3117         if (repeatCount == 0) {
   3118             // TODO: Reconsider how to perform haptic feedback when repeating key.
   3119             feedbackManager.performHapticFeedback(keyboardView);
   3120         }
   3121         feedbackManager.performAudioFeedback(code);
   3122     }
   3123 
   3124     // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
   3125     // release matching call is {@link #onReleaseKey(int,boolean)} below.
   3126     @Override
   3127     public void onPressKey(final int primaryCode, final int repeatCount,
   3128             final boolean isSinglePointer) {
   3129         mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
   3130         hapticAndAudioFeedback(primaryCode, repeatCount);
   3131     }
   3132 
   3133     // Callback of the {@link KeyboardActionListener}. This is called when a key is released;
   3134     // press matching call is {@link #onPressKey(int,int,boolean)} above.
   3135     @Override
   3136     public void onReleaseKey(final int primaryCode, final boolean withSliding) {
   3137         mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
   3138 
   3139         // If accessibility is on, ensure the user receives keyboard state updates.
   3140         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
   3141             switch (primaryCode) {
   3142             case Constants.CODE_SHIFT:
   3143                 AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
   3144                 break;
   3145             case Constants.CODE_SWITCH_ALPHA_SYMBOL:
   3146                 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
   3147                 break;
   3148             }
   3149         }
   3150     }
   3151 
   3152     // Hooks for hardware keyboard
   3153     @Override
   3154     public boolean onKeyDown(final int keyCode, final KeyEvent event) {
   3155         if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event);
   3156         // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if
   3157         // it doesn't know what to do with it and leave it to the application. For example,
   3158         // hardware key events for adjusting the screen's brightness are passed as is.
   3159         if (mEventInterpreter.onHardwareKeyEvent(event)) {
   3160             final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
   3161             mCurrentlyPressedHardwareKeys.add(keyIdentifier);
   3162             return true;
   3163         }
   3164         return super.onKeyDown(keyCode, event);
   3165     }
   3166 
   3167     @Override
   3168     public boolean onKeyUp(final int keyCode, final KeyEvent event) {
   3169         final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
   3170         if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
   3171             return true;
   3172         }
   3173         return super.onKeyUp(keyCode, event);
   3174     }
   3175 
   3176     // onKeyDown and onKeyUp are the main events we are interested in. There are two more events
   3177     // related to handling of hardware key events that we may want to implement in the future:
   3178     // boolean onKeyLongPress(final int keyCode, final KeyEvent event);
   3179     // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event);
   3180 
   3181     // receive ringer mode change and network state change.
   3182     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
   3183         @Override
   3184         public void onReceive(final Context context, final Intent intent) {
   3185             final String action = intent.getAction();
   3186             if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
   3187                 mSubtypeSwitcher.onNetworkStateChanged(intent);
   3188             } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
   3189                 AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
   3190             }
   3191         }
   3192     };
   3193 
   3194     private void launchSettings() {
   3195         handleClose();
   3196         launchSubActivity(SettingsActivity.class);
   3197     }
   3198 
   3199     public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
   3200         // Put the text in the attached EditText into a safe, saved state before switching to a
   3201         // new activity that will also use the soft keyboard.
   3202         commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   3203         launchSubActivity(activityClass);
   3204     }
   3205 
   3206     private void launchSubActivity(final Class<? extends Activity> activityClass) {
   3207         Intent intent = new Intent();
   3208         intent.setClass(LatinIME.this, activityClass);
   3209         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
   3210                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
   3211                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
   3212         startActivity(intent);
   3213     }
   3214 
   3215     private void showSubtypeSelectorAndSettings() {
   3216         final CharSequence title = getString(R.string.english_ime_input_options);
   3217         final CharSequence[] items = new CharSequence[] {
   3218                 // TODO: Should use new string "Select active input modes".
   3219                 getString(R.string.language_selection_title),
   3220                 getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)),
   3221         };
   3222         final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
   3223             @Override
   3224             public void onClick(DialogInterface di, int position) {
   3225                 di.dismiss();
   3226                 switch (position) {
   3227                 case 0:
   3228                     final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
   3229                             mRichImm.getInputMethodIdOfThisIme(),
   3230                             Intent.FLAG_ACTIVITY_NEW_TASK
   3231                             | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
   3232                             | Intent.FLAG_ACTIVITY_CLEAR_TOP);
   3233                     startActivity(intent);
   3234                     break;
   3235                 case 1:
   3236                     launchSettings();
   3237                     break;
   3238                 }
   3239             }
   3240         };
   3241         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
   3242                 .setItems(items, listener)
   3243                 .setTitle(title);
   3244         showOptionDialog(builder.create());
   3245     }
   3246 
   3247     public void showOptionDialog(final AlertDialog dialog) {
   3248         final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
   3249         if (windowToken == null) {
   3250             return;
   3251         }
   3252 
   3253         dialog.setCancelable(true);
   3254         dialog.setCanceledOnTouchOutside(true);
   3255 
   3256         final Window window = dialog.getWindow();
   3257         final WindowManager.LayoutParams lp = window.getAttributes();
   3258         lp.token = windowToken;
   3259         lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
   3260         window.setAttributes(lp);
   3261         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
   3262 
   3263         mOptionsDialog = dialog;
   3264         dialog.show();
   3265     }
   3266 
   3267     // TODO: can this be removed somehow without breaking the tests?
   3268     @UsedForTesting
   3269     /* package for test */ String getFirstSuggestedWord() {
   3270         return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null;
   3271     }
   3272 
   3273     // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
   3274     @UsedForTesting
   3275     /* package for test */ boolean isCurrentlyWaitingForMainDictionary() {
   3276         return mSuggest.isCurrentlyWaitingForMainDictionary();
   3277     }
   3278 
   3279     // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
   3280     @UsedForTesting
   3281     /* package for test */ boolean hasMainDictionary() {
   3282         return mSuggest.hasMainDictionary();
   3283     }
   3284 
   3285     // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
   3286     @UsedForTesting
   3287     /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
   3288         mSuggest.resetMainDict(this, locale, null);
   3289     }
   3290 
   3291     public void debugDumpStateAndCrashWithException(final String context) {
   3292         final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
   3293         s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
   3294                 .append("\nContext : ").append(context);
   3295         throw new RuntimeException(s.toString());
   3296     }
   3297 
   3298     @Override
   3299     protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) {
   3300         super.dump(fd, fout, args);
   3301 
   3302         final Printer p = new PrintWriterPrinter(fout);
   3303         p.println("LatinIME state :");
   3304         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   3305         final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
   3306         p.println("  Keyboard mode = " + keyboardMode);
   3307         final SettingsValues settingsValues = mSettings.getCurrent();
   3308         p.println("  mIsSuggestionsSuggestionsRequested = "
   3309                 + settingsValues.isSuggestionsRequested(mDisplayOrientation));
   3310         p.println("  mCorrectionEnabled=" + settingsValues.mCorrectionEnabled);
   3311         p.println("  isComposingWord=" + mWordComposer.isComposingWord());
   3312         p.println("  mSoundOn=" + settingsValues.mSoundOn);
   3313         p.println("  mVibrateOn=" + settingsValues.mVibrateOn);
   3314         p.println("  mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn);
   3315         p.println("  inputAttributes=" + settingsValues.mInputAttributes);
   3316     }
   3317 }
   3318