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