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