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             switch (msg.what) {
   1839                 case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
   1840                     updateBatchInput((InputPointers)msg.obj);
   1841                     break;
   1842                 case MSG_GET_SUGGESTED_WORDS:
   1843                     mLatinIme.getSuggestedWords(msg.arg1, (OnGetSuggestedWordsCallback) msg.obj);
   1844                     break;
   1845             }
   1846             return true;
   1847         }
   1848 
   1849         // Run in the UI thread.
   1850         public void onStartBatchInput() {
   1851             synchronized (mLock) {
   1852                 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
   1853                 mInBatchInput = true;
   1854                 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
   1855                         SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
   1856             }
   1857         }
   1858 
   1859         // Run in the Handler thread.
   1860         private void updateBatchInput(final InputPointers batchPointers) {
   1861             synchronized (mLock) {
   1862                 if (!mInBatchInput) {
   1863                     // Batch input has ended or canceled while the message was being delivered.
   1864                     return;
   1865                 }
   1866 
   1867                 getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() {
   1868                     @Override
   1869                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
   1870                         mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
   1871                                 suggestedWords, false /* dismissGestureFloatingPreviewText */);
   1872                     }
   1873                 });
   1874             }
   1875         }
   1876 
   1877         // Run in the UI thread.
   1878         public void onUpdateBatchInput(final InputPointers batchPointers) {
   1879             if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
   1880                 return;
   1881             }
   1882             mHandler.obtainMessage(
   1883                     MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers)
   1884                     .sendToTarget();
   1885         }
   1886 
   1887         public void onCancelBatchInput() {
   1888             synchronized (mLock) {
   1889                 mInBatchInput = false;
   1890                 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
   1891                         SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
   1892             }
   1893         }
   1894 
   1895         // Run in the UI thread.
   1896         public void onEndBatchInput(final InputPointers batchPointers) {
   1897             synchronized(mLock) {
   1898                 getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() {
   1899                     @Override
   1900                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
   1901                         mInBatchInput = false;
   1902                         mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords,
   1903                                 true /* dismissGestureFloatingPreviewText */);
   1904                         mLatinIme.mHandler.onEndBatchInput(suggestedWords);
   1905                     }
   1906                 });
   1907             }
   1908         }
   1909 
   1910         // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
   1911         // be synchronized.
   1912         private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
   1913                 final OnGetSuggestedWordsCallback callback) {
   1914             mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
   1915             mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
   1916                     new OnGetSuggestedWordsCallback() {
   1917                 @Override
   1918                 public void onGetSuggestedWords(SuggestedWords suggestedWords) {
   1919                     final int suggestionCount = suggestedWords.size();
   1920                     if (suggestionCount <= 1) {
   1921                         final String mostProbableSuggestion = (suggestionCount == 0) ? null
   1922                                 : suggestedWords.getWord(0);
   1923                         callback.onGetSuggestedWords(
   1924                                 mLatinIme.getOlderSuggestions(mostProbableSuggestion));
   1925                     }
   1926                     callback.onGetSuggestedWords(suggestedWords);
   1927                 }
   1928             });
   1929         }
   1930 
   1931         public void getSuggestedWords(final int sessionId,
   1932                 final OnGetSuggestedWordsCallback callback) {
   1933             mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, 0, callback).sendToTarget();
   1934         }
   1935 
   1936         private void onDestroy() {
   1937             mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS);
   1938             mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
   1939             mHandler.getLooper().quit();
   1940         }
   1941     }
   1942 
   1943     // This method must run in UI Thread.
   1944     private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
   1945             final boolean dismissGestureFloatingPreviewText) {
   1946         showSuggestionStrip(suggestedWords);
   1947         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   1948         mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
   1949         if (dismissGestureFloatingPreviewText) {
   1950             mainKeyboardView.dismissGestureFloatingPreviewText();
   1951         }
   1952     }
   1953 
   1954     @Override
   1955     public void onUpdateBatchInput(final InputPointers batchPointers) {
   1956         if (mSettings.getCurrent().mPhraseGestureEnabled) {
   1957             final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
   1958             if (null != candidate) {
   1959                 if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
   1960                     final String[] commitParts = candidate.mWord.split(" ", 2);
   1961                     batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
   1962                     promotePhantomSpace();
   1963                     mConnection.commitText(commitParts[0], 0);
   1964                     mSpaceState = SPACE_STATE_PHANTOM;
   1965                     mKeyboardSwitcher.updateShiftState();
   1966                     mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
   1967                 }
   1968             }
   1969         }
   1970         mInputUpdater.onUpdateBatchInput(batchPointers);
   1971     }
   1972 
   1973     // This method must run in UI Thread.
   1974     public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) {
   1975         final String batchInputText = suggestedWords.isEmpty()
   1976                 ? null : suggestedWords.getWord(0);
   1977         if (TextUtils.isEmpty(batchInputText)) {
   1978             return;
   1979         }
   1980         mConnection.beginBatchEdit();
   1981         if (SPACE_STATE_PHANTOM == mSpaceState) {
   1982             promotePhantomSpace();
   1983         }
   1984         if (mSettings.getCurrent().mPhraseGestureEnabled) {
   1985             // Find the last space
   1986             final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
   1987             if (0 != indexOfLastSpace) {
   1988                 mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
   1989                 showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture());
   1990             }
   1991             final String lastWord = batchInputText.substring(indexOfLastSpace);
   1992             mWordComposer.setBatchInputWord(lastWord);
   1993             mConnection.setComposingText(lastWord, 1);
   1994         } else {
   1995             mWordComposer.setBatchInputWord(batchInputText);
   1996             mConnection.setComposingText(batchInputText, 1);
   1997         }
   1998         mExpectingUpdateSelection = true;
   1999         mConnection.endBatchEdit();
   2000         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2001             ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords);
   2002         }
   2003         // Space state must be updated before calling updateShiftState
   2004         mSpaceState = SPACE_STATE_PHANTOM;
   2005         mKeyboardSwitcher.updateShiftState();
   2006     }
   2007 
   2008     @Override
   2009     public void onEndBatchInput(final InputPointers batchPointers) {
   2010         mInputUpdater.onEndBatchInput(batchPointers);
   2011     }
   2012 
   2013     private String specificTldProcessingOnTextInput(final String text) {
   2014         if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
   2015                 || !Character.isLetter(text.charAt(1))) {
   2016             // Not a tld: do nothing.
   2017             return text;
   2018         }
   2019         // We have a TLD (or something that looks like this): make sure we don't add
   2020         // a space even if currently in phantom mode.
   2021         mSpaceState = SPACE_STATE_NONE;
   2022         // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code
   2023         final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
   2024         if (lastOne != null && lastOne.length() == 1
   2025                 && lastOne.charAt(0) == Constants.CODE_PERIOD) {
   2026             return text.substring(1);
   2027         } else {
   2028             return text;
   2029         }
   2030     }
   2031 
   2032     // Called from PointerTracker through the KeyboardActionListener interface
   2033     @Override
   2034     public void onFinishSlidingInput() {
   2035         // User finished sliding input.
   2036         mKeyboardSwitcher.onFinishSlidingInput();
   2037     }
   2038 
   2039     // Called from PointerTracker through the KeyboardActionListener interface
   2040     @Override
   2041     public void onCancelInput() {
   2042         // User released a finger outside any key
   2043         // Nothing to do so far.
   2044     }
   2045 
   2046     @Override
   2047     public void onCancelBatchInput() {
   2048         mInputUpdater.onCancelBatchInput();
   2049     }
   2050 
   2051     private void handleBackspace(final int spaceState) {
   2052         // We revert these in this method if the deletion doesn't happen.
   2053         mDeleteCount++;
   2054         mExpectingUpdateSelection = true;
   2055 
   2056         // In many cases, we may have to put the keyboard in auto-shift state again. However
   2057         // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
   2058         // during key repeat.
   2059         mHandler.postUpdateShiftState();
   2060 
   2061         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   2062             // If we are in the middle of a recorrection, we need to commit the recorrection
   2063             // first so that we can remove the character at the current cursor position.
   2064             resetEntireInputState(mLastSelectionStart);
   2065             // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
   2066         }
   2067         if (mWordComposer.isComposingWord()) {
   2068             if (mWordComposer.isBatchMode()) {
   2069                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2070                     final String word = mWordComposer.getTypedWord();
   2071                     ResearchLogger.latinIME_handleBackspace_batch(word, 1);
   2072                 }
   2073                 final String rejectedSuggestion = mWordComposer.getTypedWord();
   2074                 mWordComposer.reset();
   2075                 mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
   2076             } else {
   2077                 mWordComposer.deleteLast();
   2078             }
   2079             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
   2080             mHandler.postUpdateSuggestionStrip();
   2081             if (!mWordComposer.isComposingWord()) {
   2082                 // If we just removed the last character, auto-caps mode may have changed so we
   2083                 // need to re-evaluate.
   2084                 mKeyboardSwitcher.updateShiftState();
   2085             }
   2086         } else {
   2087             final SettingsValues currentSettings = mSettings.getCurrent();
   2088             if (mLastComposedWord.canRevertCommit()) {
   2089                 if (currentSettings.mIsInternal) {
   2090                     LatinImeLoggerUtils.onAutoCorrectionCancellation();
   2091                 }
   2092                 revertCommit();
   2093                 return;
   2094             }
   2095             if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
   2096                 // Cancel multi-character input: remove the text we just entered.
   2097                 // This is triggered on backspace after a key that inputs multiple characters,
   2098                 // like the smiley key or the .com key.
   2099                 mConnection.deleteSurroundingText(mEnteredText.length(), 0);
   2100                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2101                     ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
   2102                 }
   2103                 mEnteredText = null;
   2104                 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
   2105                 // In addition we know that spaceState is false, and that we should not be
   2106                 // reverting any autocorrect at this point. So we can safely return.
   2107                 return;
   2108             }
   2109             if (SPACE_STATE_DOUBLE == spaceState) {
   2110                 mHandler.cancelDoubleSpacePeriodTimer();
   2111                 if (mConnection.revertDoubleSpacePeriod()) {
   2112                     // No need to reset mSpaceState, it has already be done (that's why we
   2113                     // receive it as a parameter)
   2114                     return;
   2115                 }
   2116             } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
   2117                 if (mConnection.revertSwapPunctuation()) {
   2118                     // Likewise
   2119                     return;
   2120                 }
   2121             }
   2122 
   2123             // No cancelling of commit/double space/swap: we have a regular backspace.
   2124             // We should backspace one char and restart suggestion if at the end of a word.
   2125             if (mLastSelectionStart != mLastSelectionEnd) {
   2126                 // If there is a selection, remove it.
   2127                 final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
   2128                 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
   2129                 // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
   2130                 // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
   2131                 // but we want to set it right away to avoid it being used with the wrong values
   2132                 // later (typically, in a subsequent press on backspace).
   2133                 mLastSelectionEnd = mLastSelectionStart;
   2134                 mConnection.deleteSurroundingText(numCharsDeleted, 0);
   2135                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2136                     ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
   2137                             false /* shouldUncommitLogUnit */);
   2138                 }
   2139             } else {
   2140                 // There is no selection, just delete one character.
   2141                 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
   2142                     // This should never happen.
   2143                     Log.e(TAG, "Backspace when we don't know the selection position");
   2144                 }
   2145                 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
   2146                 if (codePointBeforeCursor == Constants.NOT_A_CODE) {
   2147                     // Nothing to delete before the cursor. We have to revert the deletion states
   2148                     // that were updated at the beginning of this method.
   2149                     mDeleteCount--;
   2150                     mExpectingUpdateSelection = false;
   2151                     return;
   2152                 }
   2153                 final int lengthToDelete =
   2154                         Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
   2155                 if (mAppWorkAroundsUtils.isBeforeJellyBean() ||
   2156                         currentSettings.mInputAttributes.isTypeNull()) {
   2157                     // There are two possible reasons to send a key event: either the field has
   2158                     // type TYPE_NULL, in which case the keyboard should send events, or we are
   2159                     // running in backward compatibility mode. Before Jelly bean, the keyboard
   2160                     // would simulate a hardware keyboard event on pressing enter or delete. This
   2161                     // is bad for many reasons (there are race conditions with commits) but some
   2162                     // applications are relying on this behavior so we continue to support it for
   2163                     // older apps, so we retain this behavior if the app has target SDK < JellyBean.
   2164                     sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
   2165                 } else {
   2166                     mConnection.deleteSurroundingText(lengthToDelete, 0);
   2167                 }
   2168                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2169                     ResearchLogger.latinIME_handleBackspace(lengthToDelete,
   2170                             true /* shouldUncommitLogUnit */);
   2171                 }
   2172                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
   2173                     final int codePointBeforeCursorToDeleteAgain =
   2174                             mConnection.getCodePointBeforeCursor();
   2175                     if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
   2176                         final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
   2177                                 codePointBeforeCursorToDeleteAgain) ? 2 : 1;
   2178                         mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
   2179                         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2180                             ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
   2181                                     true /* shouldUncommitLogUnit */);
   2182                         }
   2183                     }
   2184                 }
   2185             }
   2186             if (currentSettings.isSuggestionsRequested(mDisplayOrientation)
   2187                     && currentSettings.mCurrentLanguageHasSpaces) {
   2188                 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
   2189             }
   2190             // We just removed a character. We need to update the auto-caps state.
   2191             mKeyboardSwitcher.updateShiftState();
   2192         }
   2193     }
   2194 
   2195     /*
   2196      * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
   2197      */
   2198     private boolean maybeStripSpace(final int code,
   2199             final int spaceState, final boolean isFromSuggestionStrip) {
   2200         if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
   2201             mConnection.removeTrailingSpace();
   2202             return false;
   2203         }
   2204         if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
   2205                 && isFromSuggestionStrip) {
   2206             final SettingsValues currentSettings = mSettings.getCurrent();
   2207             if (currentSettings.isUsuallyPrecededBySpace(code)) return false;
   2208             if (currentSettings.isUsuallyFollowedBySpace(code)) return true;
   2209             mConnection.removeTrailingSpace();
   2210         }
   2211         return false;
   2212     }
   2213 
   2214     private void handleCharacter(final int primaryCode, final int x,
   2215             final int y, final int spaceState) {
   2216         // TODO: refactor this method to stop flipping isComposingWord around all the time, and
   2217         // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
   2218         // which has the same name as other handle* methods but is not the same.
   2219         boolean isComposingWord = mWordComposer.isComposingWord();
   2220 
   2221         // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
   2222         // See onStartBatchInput() to see how to do it.
   2223         final SettingsValues currentSettings = mSettings.getCurrent();
   2224         if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) {
   2225             if (isComposingWord) {
   2226                 // Sanity check
   2227                 throw new RuntimeException("Should not be composing here");
   2228             }
   2229             promotePhantomSpace();
   2230         }
   2231 
   2232         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   2233             // If we are in the middle of a recorrection, we need to commit the recorrection
   2234             // first so that we can insert the character at the current cursor position.
   2235             resetEntireInputState(mLastSelectionStart);
   2236             isComposingWord = false;
   2237         }
   2238         // We want to find out whether to start composing a new word with this character. If so,
   2239         // we need to reset the composing state and switch isComposingWord. The order of the
   2240         // tests is important for good performance.
   2241         // We only start composing if we're not already composing.
   2242         if (!isComposingWord
   2243         // We only start composing if this is a word code point. Essentially that means it's a
   2244         // a letter or a word connector.
   2245                 && currentSettings.isWordCodePoint(primaryCode)
   2246         // We never go into composing state if suggestions are not requested.
   2247                 && currentSettings.isSuggestionsRequested(mDisplayOrientation) &&
   2248         // In languages with spaces, we only start composing a word when we are not already
   2249         // touching a word. In languages without spaces, the above conditions are sufficient.
   2250                 (!mConnection.isCursorTouchingWord(currentSettings)
   2251                         || !currentSettings.mCurrentLanguageHasSpaces)) {
   2252             // Reset entirely the composing state anyway, then start composing a new word unless
   2253             // the character is a single quote or a dash. The idea here is, single quote and dash
   2254             // are not separators and they should be treated as normal characters, except in the
   2255             // first position where they should not start composing a word.
   2256             isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode
   2257                     && Constants.CODE_DASH != primaryCode);
   2258             // Here we don't need to reset the last composed word. It will be reset
   2259             // when we commit this one, if we ever do; if on the other hand we backspace
   2260             // it entirely and resume suggestions on the previous word, we'd like to still
   2261             // have touch coordinates for it.
   2262             resetComposingState(false /* alsoResetLastComposedWord */);
   2263         }
   2264         if (isComposingWord) {
   2265             final int keyX, keyY;
   2266             if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) {
   2267                 final KeyDetector keyDetector =
   2268                         mKeyboardSwitcher.getMainKeyboardView().getKeyDetector();
   2269                 keyX = keyDetector.getTouchX(x);
   2270                 keyY = keyDetector.getTouchY(y);
   2271             } else {
   2272                 keyX = x;
   2273                 keyY = y;
   2274             }
   2275             mWordComposer.add(primaryCode, keyX, keyY);
   2276             // If it's the first letter, make note of auto-caps state
   2277             if (mWordComposer.size() == 1) {
   2278                 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
   2279             }
   2280             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
   2281         } else {
   2282             final boolean swapWeakSpace = maybeStripSpace(primaryCode,
   2283                     spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
   2284 
   2285             sendKeyCodePoint(primaryCode);
   2286 
   2287             if (swapWeakSpace) {
   2288                 swapSwapperAndSpace();
   2289                 mSpaceState = SPACE_STATE_WEAK;
   2290             }
   2291             // In case the "add to dictionary" hint was still displayed.
   2292             if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
   2293         }
   2294         mHandler.postUpdateSuggestionStrip();
   2295         if (currentSettings.mIsInternal) {
   2296             LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y);
   2297         }
   2298     }
   2299 
   2300     private void handleRecapitalize() {
   2301         if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
   2302         // If we have a recapitalize in progress, use it; otherwise, create a new one.
   2303         if (!mRecapitalizeStatus.isActive()
   2304                 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
   2305             final CharSequence selectedText =
   2306                     mConnection.getSelectedText(0 /* flags, 0 for no styles */);
   2307             if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
   2308             final SettingsValues currentSettings = mSettings.getCurrent();
   2309             mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
   2310                     selectedText.toString(), currentSettings.mLocale,
   2311                     currentSettings.mWordSeparators);
   2312             // We trim leading and trailing whitespace.
   2313             mRecapitalizeStatus.trim();
   2314             // Trimming the object may have changed the length of the string, and we need to
   2315             // reposition the selection handles accordingly. As this result in an IPC call,
   2316             // only do it if it's actually necessary, in other words if the recapitalize status
   2317             // is not set at the same place as before.
   2318             if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
   2319                 mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
   2320                 mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
   2321                 mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
   2322             }
   2323         }
   2324         mRecapitalizeStatus.rotate();
   2325         final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
   2326         mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
   2327         mConnection.deleteSurroundingText(numCharsDeleted, 0);
   2328         mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
   2329         mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
   2330         mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
   2331         mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
   2332         // Match the keyboard to the new state.
   2333         mKeyboardSwitcher.updateShiftState();
   2334     }
   2335 
   2336     // Returns true if we do an autocorrection, false otherwise.
   2337     private boolean handleSeparator(final int primaryCode, final int x, final int y,
   2338             final int spaceState) {
   2339         boolean didAutoCorrect = false;
   2340         final SettingsValues currentSettings = mSettings.getCurrent();
   2341         // We avoid sending spaces in languages without spaces if we were composing.
   2342         final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode
   2343                 && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord();
   2344         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
   2345             // If we are in the middle of a recorrection, we need to commit the recorrection
   2346             // first so that we can insert the separator at the current cursor position.
   2347             resetEntireInputState(mLastSelectionStart);
   2348         }
   2349         if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
   2350             if (currentSettings.mCorrectionEnabled) {
   2351                 final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
   2352                         : StringUtils.newSingleCodePointString(primaryCode);
   2353                 commitCurrentAutoCorrection(separator);
   2354                 didAutoCorrect = true;
   2355             } else {
   2356                 commitTyped(StringUtils.newSingleCodePointString(primaryCode));
   2357             }
   2358         }
   2359 
   2360         final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
   2361                 Constants.SUGGESTION_STRIP_COORDINATE == x);
   2362 
   2363         if (SPACE_STATE_PHANTOM == spaceState &&
   2364                 currentSettings.isUsuallyPrecededBySpace(primaryCode)) {
   2365             promotePhantomSpace();
   2366         }
   2367         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2368             ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
   2369         }
   2370 
   2371         if (!shouldAvoidSendingCode) {
   2372             sendKeyCodePoint(primaryCode);
   2373         }
   2374 
   2375         if (Constants.CODE_SPACE == primaryCode) {
   2376             if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
   2377                 if (maybeDoubleSpacePeriod()) {
   2378                     mSpaceState = SPACE_STATE_DOUBLE;
   2379                 } else if (!isShowingPunctuationList()) {
   2380                     mSpaceState = SPACE_STATE_WEAK;
   2381                 }
   2382             }
   2383 
   2384             mHandler.startDoubleSpacePeriodTimer();
   2385             mHandler.postUpdateSuggestionStrip();
   2386         } else {
   2387             if (swapWeakSpace) {
   2388                 swapSwapperAndSpace();
   2389                 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
   2390             } else if (SPACE_STATE_PHANTOM == spaceState
   2391                     && currentSettings.isUsuallyFollowedBySpace(primaryCode)) {
   2392                 // If we are in phantom space state, and the user presses a separator, we want to
   2393                 // stay in phantom space state so that the next keypress has a chance to add the
   2394                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
   2395                 // then insert a comma and go on to typing the next word, I want the space to be
   2396                 // inserted automatically before the next word, the same way it is when I don't
   2397                 // input the comma.
   2398                 // The case is a little different if the separator is a space stripper. Such a
   2399                 // separator does not normally need a space on the right (that's the difference
   2400                 // between swappers and strippers), so we should not stay in phantom space state if
   2401                 // the separator is a stripper. Hence the additional test above.
   2402                 mSpaceState = SPACE_STATE_PHANTOM;
   2403             }
   2404 
   2405             // Set punctuation right away. onUpdateSelection will fire but tests whether it is
   2406             // already displayed or not, so it's okay.
   2407             setPunctuationSuggestions();
   2408         }
   2409         if (currentSettings.mIsInternal) {
   2410             LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y);
   2411         }
   2412 
   2413         mKeyboardSwitcher.updateShiftState();
   2414         return didAutoCorrect;
   2415     }
   2416 
   2417     private CharSequence getTextWithUnderline(final String text) {
   2418         return mIsAutoCorrectionIndicatorOn
   2419                 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
   2420                 : text;
   2421     }
   2422 
   2423     private void handleClose() {
   2424         // TODO: Verify that words are logged properly when IME is closed.
   2425         commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   2426         requestHideSelf(0);
   2427         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
   2428         if (mainKeyboardView != null) {
   2429             mainKeyboardView.closing();
   2430         }
   2431     }
   2432 
   2433     // TODO: make this private
   2434     // Outside LatinIME, only used by the test suite.
   2435     @UsedForTesting
   2436     boolean isShowingPunctuationList() {
   2437         if (mSuggestedWords == null) return false;
   2438         return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords;
   2439     }
   2440 
   2441     private boolean isSuggestionsStripVisible() {
   2442         final SettingsValues currentSettings = mSettings.getCurrent();
   2443         if (mSuggestionStripView == null)
   2444             return false;
   2445         if (mSuggestionStripView.isShowingAddToDictionaryHint())
   2446             return true;
   2447         if (null == currentSettings)
   2448             return false;
   2449         if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
   2450             return false;
   2451         if (currentSettings.isApplicationSpecifiedCompletionsOn())
   2452             return true;
   2453         return currentSettings.isSuggestionsRequested(mDisplayOrientation);
   2454     }
   2455 
   2456     private void clearSuggestionStrip() {
   2457         setSuggestedWords(SuggestedWords.EMPTY, false);
   2458         setAutoCorrectionIndicator(false);
   2459     }
   2460 
   2461     private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) {
   2462         mSuggestedWords = words;
   2463         if (mSuggestionStripView != null) {
   2464             mSuggestionStripView.setSuggestions(words);
   2465             mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
   2466         }
   2467     }
   2468 
   2469     private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
   2470         // Put a blue underline to a word in TextView which will be auto-corrected.
   2471         if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
   2472                 && mWordComposer.isComposingWord()) {
   2473             mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
   2474             final CharSequence textWithUnderline =
   2475                     getTextWithUnderline(mWordComposer.getTypedWord());
   2476             // TODO: when called from an updateSuggestionStrip() call that results from a posted
   2477             // message, this is called outside any batch edit. Potentially, this may result in some
   2478             // janky flickering of the screen, although the display speed makes it unlikely in
   2479             // the practice.
   2480             mConnection.setComposingText(textWithUnderline, 1);
   2481         }
   2482     }
   2483 
   2484     private void updateSuggestionStrip() {
   2485         mHandler.cancelUpdateSuggestionStrip();
   2486         final SettingsValues currentSettings = mSettings.getCurrent();
   2487 
   2488         // Check if we have a suggestion engine attached.
   2489         if (mSuggest == null
   2490                 || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
   2491             if (mWordComposer.isComposingWord()) {
   2492                 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
   2493                         + "requested!");
   2494             }
   2495             return;
   2496         }
   2497 
   2498         if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) {
   2499             setPunctuationSuggestions();
   2500             return;
   2501         }
   2502 
   2503         final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
   2504         getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
   2505                 new OnGetSuggestedWordsCallback() {
   2506                     @Override
   2507                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
   2508                         holder.set(suggestedWords);
   2509                     }
   2510                 }
   2511         );
   2512 
   2513         // This line may cause the current thread to wait.
   2514         final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT);
   2515         if (suggestedWords != null) {
   2516             showSuggestionStrip(suggestedWords);
   2517         }
   2518     }
   2519 
   2520     private void getSuggestedWords(final int sessionId,
   2521             final OnGetSuggestedWordsCallback callback) {
   2522         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   2523         final Suggest suggest = mSuggest;
   2524         if (keyboard == null || suggest == null) {
   2525             callback.onGetSuggestedWords(SuggestedWords.EMPTY);
   2526             return;
   2527         }
   2528         // Get the word on which we should search the bigrams. If we are composing a word, it's
   2529         // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
   2530         // should just skip whitespace if any, so 1.
   2531         final SettingsValues currentSettings = mSettings.getCurrent();
   2532         final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
   2533         final String prevWord;
   2534         if (currentSettings.mCurrentLanguageHasSpaces) {
   2535             // If we are typing in a language with spaces we can just look up the previous
   2536             // word from textview.
   2537             prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators,
   2538                     mWordComposer.isComposingWord() ? 2 : 1);
   2539         } else {
   2540             prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
   2541                     : mLastComposedWord.mCommittedWord;
   2542         }
   2543         suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
   2544                 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
   2545                 additionalFeaturesOptions, sessionId, callback);
   2546     }
   2547 
   2548     private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId,
   2549             final OnGetSuggestedWordsCallback callback) {
   2550         mInputUpdater.getSuggestedWords(sessionId, new OnGetSuggestedWordsCallback() {
   2551             @Override
   2552             public void onGetSuggestedWords(SuggestedWords suggestedWords) {
   2553                 callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
   2554                         mWordComposer.getTypedWord(), suggestedWords));
   2555             }
   2556         });
   2557     }
   2558 
   2559     private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
   2560             final SuggestedWords suggestedWords) {
   2561         // TODO: consolidate this into getSuggestedWords
   2562         // We update the suggestion strip only when we have some suggestions to show, i.e. when
   2563         // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
   2564         // replaced with the new one. However, when the word is a dictionary word, or when the
   2565         // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
   2566         // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
   2567         // revert to suggestions - although it is unclear how we can come here if it's displayed.
   2568         if (suggestedWords.size() > 1 || typedWord.length() <= 1
   2569                 || suggestedWords.mTypedWordValid || null == mSuggestionStripView
   2570                 || mSuggestionStripView.isShowingAddToDictionaryHint()) {
   2571             return suggestedWords;
   2572         } else {
   2573             return getOlderSuggestions(typedWord);
   2574         }
   2575     }
   2576 
   2577     private SuggestedWords getOlderSuggestions(final String typedWord) {
   2578         SuggestedWords previousSuggestedWords = mSuggestedWords;
   2579         if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) {
   2580             previousSuggestedWords = SuggestedWords.EMPTY;
   2581         }
   2582         if (typedWord == null) {
   2583             return previousSuggestedWords;
   2584         }
   2585         final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
   2586                 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord,
   2587                         previousSuggestedWords);
   2588         return new SuggestedWords(typedWordAndPreviousSuggestions,
   2589                 false /* typedWordValid */,
   2590                 false /* hasAutoCorrectionCandidate */,
   2591                 false /* isPunctuationSuggestions */,
   2592                 true /* isObsoleteSuggestions */,
   2593                 false /* isPrediction */);
   2594     }
   2595 
   2596     private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) {
   2597         if (suggestedWords.isEmpty()) return;
   2598         final String autoCorrection;
   2599         if (suggestedWords.mWillAutoCorrect) {
   2600             autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
   2601         } else {
   2602             // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)
   2603             // because it may differ from mWordComposer.mTypedWord.
   2604             autoCorrection = typedWord;
   2605         }
   2606         mWordComposer.setAutoCorrection(autoCorrection);
   2607     }
   2608 
   2609     private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
   2610             final String typedWord) {
   2611       if (suggestedWords.isEmpty()) {
   2612           // No auto-correction is available, clear the cached values.
   2613           AccessibilityUtils.getInstance().setAutoCorrection(null, null);
   2614           clearSuggestionStrip();
   2615           return;
   2616       }
   2617       setAutoCorrection(suggestedWords, typedWord);
   2618       final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
   2619       setSuggestedWords(suggestedWords, isAutoCorrection);
   2620       setAutoCorrectionIndicator(isAutoCorrection);
   2621       setSuggestionStripShown(isSuggestionsStripVisible());
   2622       // An auto-correction is available, cache it in accessibility code so
   2623       // we can be speak it if the user touches a key that will insert it.
   2624       AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord);
   2625     }
   2626 
   2627     private void showSuggestionStrip(final SuggestedWords suggestedWords) {
   2628         if (suggestedWords.isEmpty()) {
   2629             clearSuggestionStrip();
   2630             return;
   2631         }
   2632         showSuggestionStripWithTypedWord(suggestedWords,
   2633             suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
   2634     }
   2635 
   2636     private void commitCurrentAutoCorrection(final String separator) {
   2637         // Complete any pending suggestions query first
   2638         if (mHandler.hasPendingUpdateSuggestions()) {
   2639             updateSuggestionStrip();
   2640         }
   2641         final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
   2642         final String typedWord = mWordComposer.getTypedWord();
   2643         final String autoCorrection = (typedAutoCorrection != null)
   2644                 ? typedAutoCorrection : typedWord;
   2645         if (autoCorrection != null) {
   2646             if (TextUtils.isEmpty(typedWord)) {
   2647                 throw new RuntimeException("We have an auto-correction but the typed word "
   2648                         + "is empty? Impossible! I must commit suicide.");
   2649             }
   2650             if (mSettings.isInternal()) {
   2651                 LatinImeLoggerUtils.onAutoCorrection(
   2652                         typedWord, autoCorrection, separator, mWordComposer);
   2653             }
   2654             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2655                 final SuggestedWords suggestedWords = mSuggestedWords;
   2656                 ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
   2657                         separator, mWordComposer.isBatchMode(), suggestedWords);
   2658             }
   2659             mExpectingUpdateSelection = true;
   2660             commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
   2661                     separator);
   2662             if (!typedWord.equals(autoCorrection)) {
   2663                 // This will make the correction flash for a short while as a visual clue
   2664                 // to the user that auto-correction happened. It has no other effect; in particular
   2665                 // note that this won't affect the text inside the text field AT ALL: it only makes
   2666                 // the segment of text starting at the supplied index and running for the length
   2667                 // of the auto-correction flash. At this moment, the "typedWord" argument is
   2668                 // ignored by TextView.
   2669                 mConnection.commitCorrection(
   2670                         new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
   2671                         typedWord, autoCorrection));
   2672             }
   2673         }
   2674     }
   2675 
   2676     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
   2677     // interface
   2678     @Override
   2679     public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) {
   2680         final SuggestedWords suggestedWords = mSuggestedWords;
   2681         final String suggestion = suggestionInfo.mWord;
   2682         // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
   2683         if (suggestion.length() == 1 && isShowingPunctuationList()) {
   2684             // Word separators are suggested before the user inputs something.
   2685             // So, LatinImeLogger logs "" as a user's input.
   2686             LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords);
   2687             // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
   2688             final int primaryCode = suggestion.charAt(0);
   2689             onCodeInput(primaryCode,
   2690                     Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
   2691             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2692                 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion,
   2693                         false /* isBatchMode */, suggestedWords.mIsPrediction);
   2694             }
   2695             return;
   2696         }
   2697 
   2698         mConnection.beginBatchEdit();
   2699         final SettingsValues currentSettings = mSettings.getCurrent();
   2700         if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
   2701                 // In the batch input mode, a manually picked suggested word should just replace
   2702                 // the current batch input text and there is no need for a phantom space.
   2703                 && !mWordComposer.isBatchMode()) {
   2704             final int firstChar = Character.codePointAt(suggestion, 0);
   2705             if (!currentSettings.isWordSeparator(firstChar)
   2706                     || currentSettings.isUsuallyPrecededBySpace(firstChar)) {
   2707                 promotePhantomSpace();
   2708             }
   2709         }
   2710 
   2711         if (currentSettings.isApplicationSpecifiedCompletionsOn()
   2712                 && mApplicationSpecifiedCompletions != null
   2713                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
   2714             mSuggestedWords = SuggestedWords.EMPTY;
   2715             if (mSuggestionStripView != null) {
   2716                 mSuggestionStripView.clear();
   2717             }
   2718             mKeyboardSwitcher.updateShiftState();
   2719             resetComposingState(true /* alsoResetLastComposedWord */);
   2720             final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
   2721             mConnection.commitCompletion(completionInfo);
   2722             mConnection.endBatchEdit();
   2723             return;
   2724         }
   2725 
   2726         // We need to log before we commit, because the word composer will store away the user
   2727         // typed word.
   2728         final String replacedWord = mWordComposer.getTypedWord();
   2729         LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords);
   2730         mExpectingUpdateSelection = true;
   2731         commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
   2732                 LastComposedWord.NOT_A_SEPARATOR);
   2733         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2734             ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
   2735                     mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind,
   2736                     suggestionInfo.mSourceDict.mDictType);
   2737         }
   2738         mConnection.endBatchEdit();
   2739         // Don't allow cancellation of manual pick
   2740         mLastComposedWord.deactivate();
   2741         // Space state must be updated before calling updateShiftState
   2742         mSpaceState = SPACE_STATE_PHANTOM;
   2743         mKeyboardSwitcher.updateShiftState();
   2744 
   2745         // We should show the "Touch again to save" hint if the user pressed the first entry
   2746         // AND it's in none of our current dictionaries (main, user or otherwise).
   2747         // Please note that if mSuggest is null, it means that everything is off: suggestion
   2748         // and correction, so we shouldn't try to show the hint
   2749         final Suggest suggest = mSuggest;
   2750         final boolean showingAddToDictionaryHint =
   2751                 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
   2752                         || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
   2753                         && suggest != null
   2754                         // If the suggestion is not in the dictionary, the hint should be shown.
   2755                         && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true);
   2756 
   2757         if (currentSettings.mIsInternal) {
   2758             LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
   2759                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
   2760         }
   2761         if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
   2762             mSuggestionStripView.showAddToDictionaryHint(
   2763                     suggestion, currentSettings.mHintToSaveText);
   2764         } else {
   2765             // If we're not showing the "Touch again to save", then update the suggestion strip.
   2766             mHandler.postUpdateSuggestionStrip();
   2767         }
   2768     }
   2769 
   2770     /**
   2771      * Commits the chosen word to the text field and saves it for later retrieval.
   2772      */
   2773     private void commitChosenWord(final String chosenWord, final int commitType,
   2774             final String separatorString) {
   2775         final SuggestedWords suggestedWords = mSuggestedWords;
   2776         mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
   2777                 this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
   2778         // Add the word to the user history dictionary
   2779         final String prevWord = addToUserHistoryDictionary(chosenWord);
   2780         // TODO: figure out here if this is an auto-correct or if the best word is actually
   2781         // what user typed. Note: currently this is done much later in
   2782         // LastComposedWord#didCommitTypedWord by string equality of the remembered
   2783         // strings.
   2784         mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString,
   2785                 prevWord);
   2786     }
   2787 
   2788     private void setPunctuationSuggestions() {
   2789         final SettingsValues currentSettings = mSettings.getCurrent();
   2790         if (currentSettings.mBigramPredictionEnabled) {
   2791             clearSuggestionStrip();
   2792         } else {
   2793             setSuggestedWords(currentSettings.mSuggestPuncList, false);
   2794         }
   2795         setAutoCorrectionIndicator(false);
   2796         setSuggestionStripShown(isSuggestionsStripVisible());
   2797     }
   2798 
   2799     private String addToUserHistoryDictionary(final String suggestion) {
   2800         if (TextUtils.isEmpty(suggestion)) return null;
   2801         final Suggest suggest = mSuggest;
   2802         if (suggest == null) return null;
   2803 
   2804         // If correction is not enabled, we don't add words to the user history dictionary.
   2805         // That's to avoid unintended additions in some sensitive fields, or fields that
   2806         // expect to receive non-words.
   2807         final SettingsValues currentSettings = mSettings.getCurrent();
   2808         if (!currentSettings.mCorrectionEnabled) return null;
   2809 
   2810         final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
   2811         if (userHistoryDictionary == null) return null;
   2812 
   2813         final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2);
   2814         final String secondWord;
   2815         if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
   2816             secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
   2817         } else {
   2818             secondWord = suggestion;
   2819         }
   2820         // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
   2821         // We don't add words with 0-frequency (assuming they would be profanity etc.).
   2822         final int maxFreq = AutoCorrectionUtils.getMaxFrequency(
   2823                 suggest.getUnigramDictionaries(), suggestion);
   2824         if (maxFreq == 0) return null;
   2825         userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0);
   2826         return prevWord;
   2827     }
   2828 
   2829     private boolean isResumableWord(final String word, final SettingsValues settings) {
   2830         final int firstCodePoint = word.codePointAt(0);
   2831         return settings.isWordCodePoint(firstCodePoint)
   2832                 && Constants.CODE_SINGLE_QUOTE != firstCodePoint
   2833                 && Constants.CODE_DASH != firstCodePoint;
   2834     }
   2835 
   2836     /**
   2837      * Check if the cursor is touching a word. If so, restart suggestions on this word, else
   2838      * do nothing.
   2839      */
   2840     private void restartSuggestionsOnWordTouchedByCursor() {
   2841         // HACK: We may want to special-case some apps that exhibit bad behavior in case of
   2842         // recorrection. This is a temporary, stopgap measure that will be removed later.
   2843         // TODO: remove this.
   2844         if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
   2845         // A simple way to test for support from the TextView.
   2846         if (!isSuggestionsStripVisible()) return;
   2847         // Recorrection is not supported in languages without spaces because we don't know
   2848         // how to segment them yet.
   2849         if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
   2850         // If the cursor is not touching a word, or if there is a selection, return right away.
   2851         if (mLastSelectionStart != mLastSelectionEnd) return;
   2852         // If we don't know the cursor location, return.
   2853         if (mLastSelectionStart < 0) return;
   2854         final SettingsValues currentSettings = mSettings.getCurrent();
   2855         if (!mConnection.isCursorTouchingWord(currentSettings)) return;
   2856         final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators,
   2857                 0 /* additionalPrecedingWordsCount */);
   2858         if (null == range) return; // Happens if we don't have an input connection at all
   2859         if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out.
   2860         // If for some strange reason (editor bug or so) we measure the text before the cursor as
   2861         // longer than what the entire text is supposed to be, the safe thing to do is bail out.
   2862         final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
   2863         if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return;
   2864         final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
   2865         final String typedWord = range.mWord.toString();
   2866         if (!isResumableWord(typedWord, currentSettings)) return;
   2867         int i = 0;
   2868         for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) {
   2869             for (final String s : span.getSuggestions()) {
   2870                 ++i;
   2871                 if (!TextUtils.equals(s, typedWord)) {
   2872                     suggestions.add(new SuggestedWordInfo(s,
   2873                             SuggestionStripView.MAX_SUGGESTIONS - i,
   2874                             SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
   2875                             SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
   2876                             SuggestedWordInfo.NOT_A_CONFIDENCE
   2877                                     /* autoCommitFirstWordConfidence */));
   2878                 }
   2879             }
   2880         }
   2881         mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
   2882         mWordComposer.setCursorPositionWithinWord(
   2883                 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
   2884         mConnection.setComposingRegion(
   2885                 mLastSelectionStart - numberOfCharsInWordBeforeCursor,
   2886                 mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
   2887         if (suggestions.isEmpty()) {
   2888             // We come here if there weren't any suggestion spans on this word. We will try to
   2889             // compute suggestions for it instead.
   2890             mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
   2891                     new OnGetSuggestedWordsCallback() {
   2892                 @Override
   2893                 public void onGetSuggestedWords(
   2894                         final SuggestedWords suggestedWordsIncludingTypedWord) {
   2895                     final SuggestedWords suggestedWords;
   2896                     if (suggestedWordsIncludingTypedWord.size() > 1) {
   2897                         // We were able to compute new suggestions for this word.
   2898                         // Remove the typed word, since we don't want to display it in this case.
   2899                         // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to
   2900                         // false.
   2901                         suggestedWords = suggestedWordsIncludingTypedWord
   2902                                 .getSuggestedWordsExcludingTypedWord();
   2903                     } else {
   2904                         // No saved suggestions, and we were unable to compute any good one either.
   2905                         // Rather than displaying an empty suggestion strip, we'll display the
   2906                         // original word alone in the middle.
   2907                         // Since there is only one word, willAutoCorrect is false.
   2908                         suggestedWords = suggestedWordsIncludingTypedWord;
   2909                     }
   2910                     // We need to pass typedWord because mWordComposer.mTypedWord may differ from
   2911                     // typedWord.
   2912                     unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords,
   2913                         typedWord);
   2914                 }});
   2915         } else {
   2916             // We found suggestion spans in the word. We'll create the SuggestedWords out of
   2917             // them, and make willAutoCorrect false.
   2918             final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
   2919                     true /* typedWordValid */, false /* willAutoCorrect */,
   2920                     false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
   2921                     false /* isPrediction */);
   2922             // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord.
   2923             unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, typedWord);
   2924         }
   2925     }
   2926 
   2927     public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
   2928             final SuggestedWords suggestedWords, final String typedWord) {
   2929         // Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
   2930         // We never want to auto-correct on a resumed suggestion. Please refer to the three places
   2931         // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected.
   2932         // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching
   2933         // the text to adapt it.
   2934         // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition)
   2935         mIsAutoCorrectionIndicatorOn = false;
   2936         mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord);
   2937     }
   2938 
   2939     /**
   2940      * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
   2941      * word, else do nothing.
   2942      */
   2943     private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
   2944         final CharSequence word =
   2945                 mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
   2946         if (null != word) {
   2947             final String wordString = word.toString();
   2948             restartSuggestionsOnWordBeforeCursor(wordString);
   2949             // TODO: Handle the case where the user manually moves the cursor and then backs up over
   2950             // a separator.  In that case, the current log unit should not be uncommitted.
   2951             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   2952                 ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
   2953                         true /* dumpCurrentLogUnit */);
   2954             }
   2955         }
   2956     }
   2957 
   2958     private void restartSuggestionsOnWordBeforeCursor(final String word) {
   2959         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
   2960         final int length = word.length();
   2961         mConnection.deleteSurroundingText(length, 0);
   2962         mConnection.setComposingText(word, 1);
   2963         mHandler.postUpdateSuggestionStrip();
   2964     }
   2965 
   2966     /**
   2967      * Retry resetting caches in the rich input connection.
   2968      *
   2969      * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
   2970      * This method handles the retry, and re-schedules a new retry if we still can't access.
   2971      * We only retry up to 5 times before giving up.
   2972      *
   2973      * @param tryResumeSuggestions Whether we should resume suggestions or not.
   2974      * @param remainingTries How many times we may try again before giving up.
   2975      */
   2976     private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
   2977         if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart, false)) {
   2978             if (0 < remainingTries) {
   2979                 mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
   2980                 return;
   2981             }
   2982             // If remainingTries is 0, we should stop waiting for new tries, but it's still
   2983             // better to load the keyboard (less things will be broken).
   2984         }
   2985         tryFixLyingCursorPosition();
   2986         mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
   2987         if (tryResumeSuggestions) mHandler.postResumeSuggestions();
   2988     }
   2989 
   2990     private void revertCommit() {
   2991         final String previousWord = mLastComposedWord.mPrevWord;
   2992         final String originallyTypedWord = mLastComposedWord.mTypedWord;
   2993         final String committedWord = mLastComposedWord.mCommittedWord;
   2994         final int cancelLength = committedWord.length();
   2995         // We want java chars, not codepoints for the following.
   2996         final int separatorLength = mLastComposedWord.mSeparatorString.length();
   2997         // TODO: should we check our saved separator against the actual contents of the text view?
   2998         final int deleteLength = cancelLength + separatorLength;
   2999         if (DEBUG) {
   3000             if (mWordComposer.isComposingWord()) {
   3001                 throw new RuntimeException("revertCommit, but we are composing a word");
   3002             }
   3003             final CharSequence wordBeforeCursor =
   3004                     mConnection.getTextBeforeCursor(deleteLength, 0)
   3005                             .subSequence(0, cancelLength);
   3006             if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
   3007                 throw new RuntimeException("revertCommit check failed: we thought we were "
   3008                         + "reverting \"" + committedWord
   3009                         + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
   3010             }
   3011         }
   3012         mConnection.deleteSurroundingText(deleteLength, 0);
   3013         if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
   3014             mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
   3015         }
   3016         final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
   3017         if (mSettings.getCurrent().mCurrentLanguageHasSpaces) {
   3018             // For languages with spaces, we revert to the typed string, but the cursor is still
   3019             // after the separator so we don't resume suggestions. If the user wants to correct
   3020             // the word, they have to press backspace again.
   3021             mConnection.commitText(stringToCommit, 1);
   3022         } else {
   3023             // For languages without spaces, we revert the typed string but the cursor is flush
   3024             // with the typed word, so we need to resume suggestions right away.
   3025             mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard());
   3026             mConnection.setComposingText(stringToCommit, 1);
   3027         }
   3028         if (mSettings.isInternal()) {
   3029             LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
   3030                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
   3031         }
   3032         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   3033             ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
   3034                     mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
   3035         }
   3036         // Don't restart suggestion yet. We'll restart if the user deletes the
   3037         // separator.
   3038         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
   3039         // We have a separator between the word and the cursor: we should show predictions.
   3040         mHandler.postUpdateSuggestionStrip();
   3041     }
   3042 
   3043     // This essentially inserts a space, and that's it.
   3044     public void promotePhantomSpace() {
   3045         final SettingsValues currentSettings = mSettings.getCurrent();
   3046         if (currentSettings.shouldInsertSpacesAutomatically()
   3047                 && currentSettings.mCurrentLanguageHasSpaces
   3048                 && !mConnection.textBeforeCursorLooksLikeURL()) {
   3049             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
   3050                 ResearchLogger.latinIME_promotePhantomSpace();
   3051             }
   3052             sendKeyCodePoint(Constants.CODE_SPACE);
   3053         }
   3054     }
   3055 
   3056     // TODO: Make this private
   3057     // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
   3058     @UsedForTesting
   3059     void loadKeyboard() {
   3060         // Since we are switching languages, the most urgent thing is to let the keyboard graphics
   3061         // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on
   3062         // the screen. Anything we do right now will delay this, so wait until the next frame
   3063         // before we do the rest, like reopening dictionaries and updating suggestions. So we
   3064         // post a message.
   3065         mHandler.postReopenDictionaries();
   3066         loadSettings();
   3067         if (mKeyboardSwitcher.getMainKeyboardView() != null) {
   3068             // Reload keyboard because the current language has been changed.
   3069             mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
   3070         }
   3071     }
   3072 
   3073     private void hapticAndAudioFeedback(final int code, final int repeatCount) {
   3074         final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView();
   3075         if (keyboardView != null && keyboardView.isInSlidingKeyInput()) {
   3076             // No need to feedback while sliding input.
   3077             return;
   3078         }
   3079         if (repeatCount > 0) {
   3080             if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
   3081                 // No need to feedback when repeat delete key will have no effect.
   3082                 return;
   3083             }
   3084             // TODO: Use event time that the last feedback has been generated instead of relying on
   3085             // a repeat count to thin out feedback.
   3086             if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) {
   3087                 return;
   3088             }
   3089         }
   3090         final AudioAndHapticFeedbackManager feedbackManager =
   3091                 AudioAndHapticFeedbackManager.getInstance();
   3092         if (repeatCount == 0) {
   3093             // TODO: Reconsider how to perform haptic feedback when repeating key.
   3094             feedbackManager.performHapticFeedback(keyboardView);
   3095         }
   3096         feedbackManager.performAudioFeedback(code);
   3097     }
   3098 
   3099     // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
   3100     // release matching call is {@link #onReleaseKey(int,boolean)} below.
   3101     @Override
   3102     public void onPressKey(final int primaryCode, final int repeatCount,
   3103             final boolean isSinglePointer) {
   3104         mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
   3105         hapticAndAudioFeedback(primaryCode, repeatCount);
   3106     }
   3107 
   3108     // Callback of the {@link KeyboardActionListener}. This is called when a key is released;
   3109     // press matching call is {@link #onPressKey(int,int,boolean)} above.
   3110     @Override
   3111     public void onReleaseKey(final int primaryCode, final boolean withSliding) {
   3112         mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
   3113 
   3114         // If accessibility is on, ensure the user receives keyboard state updates.
   3115         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
   3116             switch (primaryCode) {
   3117             case Constants.CODE_SHIFT:
   3118                 AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
   3119                 break;
   3120             case Constants.CODE_SWITCH_ALPHA_SYMBOL:
   3121                 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
   3122                 break;
   3123             }
   3124         }
   3125     }
   3126 
   3127     // Hooks for hardware keyboard
   3128     @Override
   3129     public boolean onKeyDown(final int keyCode, final KeyEvent event) {
   3130         if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event);
   3131         // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if
   3132         // it doesn't know what to do with it and leave it to the application. For example,
   3133         // hardware key events for adjusting the screen's brightness are passed as is.
   3134         if (mEventInterpreter.onHardwareKeyEvent(event)) {
   3135             final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
   3136             mCurrentlyPressedHardwareKeys.add(keyIdentifier);
   3137             return true;
   3138         }
   3139         return super.onKeyDown(keyCode, event);
   3140     }
   3141 
   3142     @Override
   3143     public boolean onKeyUp(final int keyCode, final KeyEvent event) {
   3144         final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
   3145         if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
   3146             return true;
   3147         }
   3148         return super.onKeyUp(keyCode, event);
   3149     }
   3150 
   3151     // onKeyDown and onKeyUp are the main events we are interested in. There are two more events
   3152     // related to handling of hardware key events that we may want to implement in the future:
   3153     // boolean onKeyLongPress(final int keyCode, final KeyEvent event);
   3154     // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event);
   3155 
   3156     // receive ringer mode change and network state change.
   3157     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
   3158         @Override
   3159         public void onReceive(final Context context, final Intent intent) {
   3160             final String action = intent.getAction();
   3161             if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
   3162                 mSubtypeSwitcher.onNetworkStateChanged(intent);
   3163             } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
   3164                 AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
   3165             }
   3166         }
   3167     };
   3168 
   3169     private void launchSettings() {
   3170         handleClose();
   3171         launchSubActivity(SettingsActivity.class);
   3172     }
   3173 
   3174     public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
   3175         // Put the text in the attached EditText into a safe, saved state before switching to a
   3176         // new activity that will also use the soft keyboard.
   3177         commitTyped(LastComposedWord.NOT_A_SEPARATOR);
   3178         launchSubActivity(activityClass);
   3179     }
   3180 
   3181     private void launchSubActivity(final Class<? extends Activity> activityClass) {
   3182         Intent intent = new Intent();
   3183         intent.setClass(LatinIME.this, activityClass);
   3184         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
   3185                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
   3186                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
   3187         startActivity(intent);
   3188     }
   3189 
   3190     private void showSubtypeSelectorAndSettings() {
   3191         final CharSequence title = getString(R.string.english_ime_input_options);
   3192         final CharSequence[] items = new CharSequence[] {
   3193                 // TODO: Should use new string "Select active input modes".
   3194                 getString(R.string.language_selection_title),
   3195                 getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)),
   3196         };
   3197         final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
   3198             @Override
   3199             public void onClick(DialogInterface di, int position) {
   3200                 di.dismiss();
   3201                 switch (position) {
   3202                 case 0:
   3203                     final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
   3204                             mRichImm.getInputMethodIdOfThisIme(),
   3205                             Intent.FLAG_ACTIVITY_NEW_TASK
   3206                             | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
   3207                             | Intent.FLAG_ACTIVITY_CLEAR_TOP);
   3208                     startActivity(intent);
   3209                     break;
   3210                 case 1:
   3211                     launchSettings();
   3212                     break;
   3213                 }
   3214             }
   3215         };
   3216         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
   3217                 .setItems(items, listener)
   3218                 .setTitle(title);
   3219         showOptionDialog(builder.create());
   3220     }
   3221 
   3222     public void showOptionDialog(final AlertDialog dialog) {
   3223         final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
   3224         if (windowToken == null) {
   3225             return;
   3226         }
   3227 
   3228         dialog.setCancelable(true);
   3229         dialog.setCanceledOnTouchOutside(true);
   3230 
   3231         final Window window = dialog.getWindow();
   3232         final WindowManager.LayoutParams lp = window.getAttributes();
   3233         lp.token = windowToken;
   3234         lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
   3235         window.setAttributes(lp);
   3236         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
   3237 
   3238         mOptionsDialog = dialog;
   3239         dialog.show();
   3240     }
   3241 
   3242     // TODO: can this be removed somehow without breaking the tests?
   3243     @UsedForTesting
   3244     /* package for test */ String getFirstSuggestedWord() {
   3245         return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null;
   3246     }
   3247 
   3248     // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
   3249     @UsedForTesting
   3250     /* package for test */ boolean isCurrentlyWaitingForMainDictionary() {
   3251         return mSuggest.isCurrentlyWaitingForMainDictionary();
   3252     }
   3253 
   3254     // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
   3255     @UsedForTesting
   3256     /* package for test */ boolean hasMainDictionary() {
   3257         return mSuggest.hasMainDictionary();
   3258     }
   3259 
   3260     // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
   3261     @UsedForTesting
   3262     /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
   3263         mSuggest.resetMainDict(this, locale, null);
   3264     }
   3265 
   3266     public void debugDumpStateAndCrashWithException(final String context) {
   3267         final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
   3268         s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
   3269                 .append("\nContext : ").append(context);
   3270         throw new RuntimeException(s.toString());
   3271     }
   3272 
   3273     @Override
   3274     protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) {
   3275         super.dump(fd, fout, args);
   3276 
   3277         final Printer p = new PrintWriterPrinter(fout);
   3278         p.println("LatinIME state :");
   3279         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
   3280         final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
   3281         p.println("  Keyboard mode = " + keyboardMode);
   3282         final SettingsValues settingsValues = mSettings.getCurrent();
   3283         p.println("  mIsSuggestionsSuggestionsRequested = "
   3284                 + settingsValues.isSuggestionsRequested(mDisplayOrientation));
   3285         p.println("  mCorrectionEnabled=" + settingsValues.mCorrectionEnabled);
   3286         p.println("  isComposingWord=" + mWordComposer.isComposingWord());
   3287         p.println("  mSoundOn=" + settingsValues.mSoundOn);
   3288         p.println("  mVibrateOn=" + settingsValues.mVibrateOn);
   3289         p.println("  mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn);
   3290         p.println("  inputAttributes=" + settingsValues.mInputAttributes);
   3291     }
   3292 }
   3293