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