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