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