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