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