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