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