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