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