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