LatinIME.java revision a905fcec00f78e828c1fe9109f27cc9f149941b5
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
20import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
21import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
22
23import android.app.Activity;
24import android.app.AlertDialog;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.SharedPreferences;
31import android.content.pm.PackageInfo;
32import android.content.res.Configuration;
33import android.content.res.Resources;
34import android.graphics.Rect;
35import android.inputmethodservice.InputMethodService;
36import android.media.AudioManager;
37import android.net.ConnectivityManager;
38import android.os.Debug;
39import android.os.Handler;
40import android.os.HandlerThread;
41import android.os.IBinder;
42import android.os.Message;
43import android.os.SystemClock;
44import android.preference.PreferenceManager;
45import android.text.InputType;
46import android.text.TextUtils;
47import android.text.style.SuggestionSpan;
48import android.util.Log;
49import android.util.Pair;
50import android.util.PrintWriterPrinter;
51import android.util.Printer;
52import android.view.KeyCharacterMap;
53import android.view.KeyEvent;
54import android.view.View;
55import android.view.ViewGroup.LayoutParams;
56import android.view.Window;
57import android.view.WindowManager;
58import android.view.inputmethod.CompletionInfo;
59import android.view.inputmethod.CorrectionInfo;
60import android.view.inputmethod.EditorInfo;
61import android.view.inputmethod.InputMethodSubtype;
62
63import com.android.inputmethod.accessibility.AccessibilityUtils;
64import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
65import com.android.inputmethod.annotations.UsedForTesting;
66import com.android.inputmethod.compat.AppWorkaroundsUtils;
67import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
68import com.android.inputmethod.compat.SuggestionSpanUtils;
69import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
70import com.android.inputmethod.event.EventInterpreter;
71import com.android.inputmethod.keyboard.Keyboard;
72import com.android.inputmethod.keyboard.KeyboardActionListener;
73import com.android.inputmethod.keyboard.KeyboardId;
74import com.android.inputmethod.keyboard.KeyboardSwitcher;
75import com.android.inputmethod.keyboard.MainKeyboardView;
76import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
77import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
78import com.android.inputmethod.latin.define.ProductionFlag;
79import com.android.inputmethod.latin.inputlogic.InputLogic;
80import com.android.inputmethod.latin.inputlogic.SpaceState;
81import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever;
82import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister;
83import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
84import com.android.inputmethod.latin.settings.Settings;
85import com.android.inputmethod.latin.settings.SettingsActivity;
86import com.android.inputmethod.latin.settings.SettingsValues;
87import com.android.inputmethod.latin.suggestions.SuggestionStripView;
88import com.android.inputmethod.latin.utils.ApplicationUtils;
89import com.android.inputmethod.latin.utils.AsyncResultHolder;
90import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
91import com.android.inputmethod.latin.utils.CapsModeUtils;
92import com.android.inputmethod.latin.utils.CollectionUtils;
93import com.android.inputmethod.latin.utils.CompletionInfoUtils;
94import com.android.inputmethod.latin.utils.InputTypeUtils;
95import com.android.inputmethod.latin.utils.IntentUtils;
96import com.android.inputmethod.latin.utils.JniUtils;
97import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
98import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper;
99import com.android.inputmethod.latin.utils.RecapitalizeStatus;
100import com.android.inputmethod.latin.utils.StringUtils;
101import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
102import com.android.inputmethod.latin.utils.TextRange;
103import com.android.inputmethod.research.ResearchLogger;
104
105import java.io.FileDescriptor;
106import java.io.PrintWriter;
107import java.util.ArrayList;
108import java.util.Locale;
109import java.util.TreeSet;
110import java.util.concurrent.TimeUnit;
111
112/**
113 * Input method implementation for Qwerty'ish keyboard.
114 */
115public class LatinIME extends InputMethodService implements KeyboardActionListener,
116        SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener,
117        Suggest.SuggestInitializationListener {
118    private static final String TAG = LatinIME.class.getSimpleName();
119    private static final boolean TRACE = false;
120    // TODO[IL]: Make this private
121    public static boolean DEBUG;
122
123    private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
124
125    private static final int PENDING_IMS_CALLBACK_DURATION = 800;
126
127    private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2;
128
129    // TODO: Set this value appropriately.
130    private static final int GET_SUGGESTED_WORDS_TIMEOUT = 200;
131
132    /**
133     * The name of the scheme used by the Package Manager to warn of a new package installation,
134     * replacement or removal.
135     */
136    private static final String SCHEME_PACKAGE = "package";
137
138    private final Settings mSettings;
139    private final InputLogic mInputLogic = new InputLogic(this);
140
141    private View mExtractArea;
142    private View mKeyPreviewBackingView;
143    private SuggestionStripView mSuggestionStripView;
144
145    private CompletionInfo[] mApplicationSpecifiedCompletions;
146    // TODO[IL]: Make this an AsyncResultHolder or a Future in SettingsValues
147    public AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils();
148
149    private RichInputMethodManager mRichImm;
150    @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
151    private final SubtypeSwitcher mSubtypeSwitcher;
152    private final SubtypeState mSubtypeState = new SubtypeState();
153
154    private boolean mIsMainDictionaryAvailable;
155    private UserBinaryDictionary mUserDictionary;
156    private boolean mIsUserDictionaryAvailable;
157
158    // Personalization debugging params
159    private boolean mUseOnlyPersonalizationDictionaryForDebug = false;
160    private boolean mBoostPersonalizationDictionaryForDebug = false;
161
162    // Member variable for remembering the current device orientation.
163    // TODO[IL]: Move this to SettingsValues.
164    public int mDisplayOrientation;
165
166    // Object for reacting to adding/removing a dictionary pack.
167    private BroadcastReceiver mDictionaryPackInstallReceiver =
168            new DictionaryPackInstallBroadcastReceiver(this);
169
170    private AlertDialog mOptionsDialog;
171
172    private final boolean mIsHardwareAcceleratedDrawingEnabled;
173
174    public final UIHandler mHandler = new UIHandler(this);
175    private InputUpdater mInputUpdater;
176
177    public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> {
178        private static final int MSG_UPDATE_SHIFT_STATE = 0;
179        private static final int MSG_PENDING_IMS_CALLBACK = 1;
180        private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
181        private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
182        private static final int MSG_RESUME_SUGGESTIONS = 4;
183        private static final int MSG_REOPEN_DICTIONARIES = 5;
184        private static final int MSG_ON_END_BATCH_INPUT = 6;
185        private static final int MSG_RESET_CACHES = 7;
186        // Update this when adding new messages
187        private static final int MSG_LAST = MSG_RESET_CACHES;
188
189        private static final int ARG1_NOT_GESTURE_INPUT = 0;
190        private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
191        private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2;
192        private static final int ARG2_WITHOUT_TYPED_WORD = 0;
193        private static final int ARG2_WITH_TYPED_WORD = 1;
194
195        private int mDelayUpdateSuggestions;
196        private int mDelayUpdateShiftState;
197        private long mDoubleSpacePeriodTimeout;
198        private long mDoubleSpacePeriodTimerStart;
199
200        public UIHandler(final LatinIME ownerInstance) {
201            super(ownerInstance);
202        }
203
204        public void onCreate() {
205            final Resources res = getOwnerInstance().getResources();
206            mDelayUpdateSuggestions =
207                    res.getInteger(R.integer.config_delay_update_suggestions);
208            mDelayUpdateShiftState =
209                    res.getInteger(R.integer.config_delay_update_shift_state);
210            mDoubleSpacePeriodTimeout =
211                    res.getInteger(R.integer.config_double_space_period_timeout);
212        }
213
214        @Override
215        public void handleMessage(final Message msg) {
216            final LatinIME latinIme = getOwnerInstance();
217            final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
218            switch (msg.what) {
219            case MSG_UPDATE_SUGGESTION_STRIP:
220                latinIme.updateSuggestionStrip();
221                break;
222            case MSG_UPDATE_SHIFT_STATE:
223                switcher.updateShiftState();
224                break;
225            case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
226                if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) {
227                    if (msg.arg2 == ARG2_WITH_TYPED_WORD) {
228                        final Pair<SuggestedWords, String> p =
229                                (Pair<SuggestedWords, String>) msg.obj;
230                        latinIme.showSuggestionStripWithTypedWord(p.first, p.second);
231                    } else {
232                        latinIme.showSuggestionStrip((SuggestedWords) msg.obj);
233                    }
234                } else {
235                    latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj,
236                            msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
237                }
238                break;
239            case MSG_RESUME_SUGGESTIONS:
240                latinIme.restartSuggestionsOnWordTouchedByCursor();
241                break;
242            case MSG_REOPEN_DICTIONARIES:
243                latinIme.initSuggest();
244                // In theory we could call latinIme.updateSuggestionStrip() right away, but
245                // in the practice, the dictionary is not finished opening yet so we wouldn't
246                // get any suggestions. Wait one frame.
247                postUpdateSuggestionStrip();
248                break;
249            case MSG_ON_END_BATCH_INPUT:
250                latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj);
251                break;
252            case MSG_RESET_CACHES:
253                latinIme.retryResetCaches(msg.arg1 == 1 /* tryResumeSuggestions */,
254                        msg.arg2 /* remainingTries */);
255                break;
256            }
257        }
258
259        public void postUpdateSuggestionStrip() {
260            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
261        }
262
263        public void postReopenDictionaries() {
264            sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES));
265        }
266
267        public void postResumeSuggestions() {
268            removeMessages(MSG_RESUME_SUGGESTIONS);
269            sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
270        }
271
272        public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
273            removeMessages(MSG_RESET_CACHES);
274            sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0,
275                    remainingTries, null));
276        }
277
278        public void cancelUpdateSuggestionStrip() {
279            removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
280        }
281
282        public boolean hasPendingUpdateSuggestions() {
283            return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
284        }
285
286        public boolean hasPendingReopenDictionaries() {
287            return hasMessages(MSG_REOPEN_DICTIONARIES);
288        }
289
290        public void postUpdateShiftState() {
291            removeMessages(MSG_UPDATE_SHIFT_STATE);
292            sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
293        }
294
295        public void cancelUpdateShiftState() {
296            removeMessages(MSG_UPDATE_SHIFT_STATE);
297        }
298
299        @UsedForTesting
300        public void removeAllMessages() {
301            for (int i = 0; i <= MSG_LAST; ++i) {
302                removeMessages(i);
303            }
304        }
305
306        public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
307                final boolean dismissGestureFloatingPreviewText) {
308            removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
309            final int arg1 = dismissGestureFloatingPreviewText
310                    ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT
311                    : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT;
312            obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1,
313                    ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget();
314        }
315
316        public void showSuggestionStrip(final SuggestedWords suggestedWords) {
317            removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
318            obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP,
319                    ARG1_NOT_GESTURE_INPUT, ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget();
320        }
321
322        // TODO: Remove this method.
323        public void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
324                final String typedWord) {
325            removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
326            obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, ARG1_NOT_GESTURE_INPUT,
327                    ARG2_WITH_TYPED_WORD,
328                    new Pair<SuggestedWords, String>(suggestedWords, typedWord)).sendToTarget();
329        }
330
331        public void onEndBatchInput(final SuggestedWords suggestedWords) {
332            obtainMessage(MSG_ON_END_BATCH_INPUT, suggestedWords).sendToTarget();
333        }
334
335        public void startDoubleSpacePeriodTimer() {
336            mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis();
337        }
338
339        public void cancelDoubleSpacePeriodTimer() {
340            mDoubleSpacePeriodTimerStart = 0;
341        }
342
343        public boolean isAcceptingDoubleSpacePeriod() {
344            return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart
345                    < mDoubleSpacePeriodTimeout;
346        }
347
348        // Working variables for the following methods.
349        private boolean mIsOrientationChanging;
350        private boolean mPendingSuccessiveImsCallback;
351        private boolean mHasPendingStartInput;
352        private boolean mHasPendingFinishInputView;
353        private boolean mHasPendingFinishInput;
354        private EditorInfo mAppliedEditorInfo;
355
356        public void startOrientationChanging() {
357            removeMessages(MSG_PENDING_IMS_CALLBACK);
358            resetPendingImsCallback();
359            mIsOrientationChanging = true;
360            final LatinIME latinIme = getOwnerInstance();
361            if (latinIme.isInputViewShown()) {
362                latinIme.mKeyboardSwitcher.saveKeyboardState();
363            }
364        }
365
366        private void resetPendingImsCallback() {
367            mHasPendingFinishInputView = false;
368            mHasPendingFinishInput = false;
369            mHasPendingStartInput = false;
370        }
371
372        private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo,
373                boolean restarting) {
374            if (mHasPendingFinishInputView)
375                latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
376            if (mHasPendingFinishInput)
377                latinIme.onFinishInputInternal();
378            if (mHasPendingStartInput)
379                latinIme.onStartInputInternal(editorInfo, restarting);
380            resetPendingImsCallback();
381        }
382
383        public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
384            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
385                // Typically this is the second onStartInput after orientation changed.
386                mHasPendingStartInput = true;
387            } else {
388                if (mIsOrientationChanging && restarting) {
389                    // This is the first onStartInput after orientation changed.
390                    mIsOrientationChanging = false;
391                    mPendingSuccessiveImsCallback = true;
392                }
393                final LatinIME latinIme = getOwnerInstance();
394                executePendingImsCallback(latinIme, editorInfo, restarting);
395                latinIme.onStartInputInternal(editorInfo, restarting);
396            }
397        }
398
399        public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
400            if (hasMessages(MSG_PENDING_IMS_CALLBACK)
401                    && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
402                // Typically this is the second onStartInputView after orientation changed.
403                resetPendingImsCallback();
404            } else {
405                if (mPendingSuccessiveImsCallback) {
406                    // This is the first onStartInputView after orientation changed.
407                    mPendingSuccessiveImsCallback = false;
408                    resetPendingImsCallback();
409                    sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
410                            PENDING_IMS_CALLBACK_DURATION);
411                }
412                final LatinIME latinIme = getOwnerInstance();
413                executePendingImsCallback(latinIme, editorInfo, restarting);
414                latinIme.onStartInputViewInternal(editorInfo, restarting);
415                mAppliedEditorInfo = editorInfo;
416            }
417        }
418
419        public void onFinishInputView(final boolean finishingInput) {
420            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
421                // Typically this is the first onFinishInputView after orientation changed.
422                mHasPendingFinishInputView = true;
423            } else {
424                final LatinIME latinIme = getOwnerInstance();
425                latinIme.onFinishInputViewInternal(finishingInput);
426                mAppliedEditorInfo = null;
427            }
428        }
429
430        public void onFinishInput() {
431            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
432                // Typically this is the first onFinishInput after orientation changed.
433                mHasPendingFinishInput = true;
434            } else {
435                final LatinIME latinIme = getOwnerInstance();
436                executePendingImsCallback(latinIme, null, false);
437                latinIme.onFinishInputInternal();
438            }
439        }
440    }
441
442    static final class SubtypeState {
443        private InputMethodSubtype mLastActiveSubtype;
444        private boolean mCurrentSubtypeUsed;
445
446        public void currentSubtypeUsed() {
447            mCurrentSubtypeUsed = true;
448        }
449
450        public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) {
451            final InputMethodSubtype currentSubtype = richImm.getInputMethodManager()
452                    .getCurrentInputMethodSubtype();
453            final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype;
454            final boolean currentSubtypeUsed = mCurrentSubtypeUsed;
455            if (currentSubtypeUsed) {
456                mLastActiveSubtype = currentSubtype;
457                mCurrentSubtypeUsed = false;
458            }
459            if (currentSubtypeUsed
460                    && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
461                    && !currentSubtype.equals(lastActiveSubtype)) {
462                richImm.setInputMethodAndSubtype(token, lastActiveSubtype);
463                return;
464            }
465            richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */);
466        }
467    }
468
469    // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial
470    // JNI call as much as possible.
471    static {
472        JniUtils.loadNativeLibrary();
473    }
474
475    public LatinIME() {
476        super();
477        mSettings = Settings.getInstance();
478        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
479        mKeyboardSwitcher = KeyboardSwitcher.getInstance();
480        mIsHardwareAcceleratedDrawingEnabled =
481                InputMethodServiceCompatUtils.enableHardwareAcceleration(this);
482        Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled);
483    }
484
485    @Override
486    public void onCreate() {
487        Settings.init(this);
488        LatinImeLogger.init(this);
489        RichInputMethodManager.init(this);
490        mRichImm = RichInputMethodManager.getInstance();
491        SubtypeSwitcher.init(this);
492        KeyboardSwitcher.init(this);
493        AudioAndHapticFeedbackManager.init(this);
494        AccessibilityUtils.init(this);
495        PersonalizationDictionarySessionRegister.init(this);
496
497        super.onCreate();
498
499        mHandler.onCreate();
500        DEBUG = LatinImeLogger.sDBG;
501
502        // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}.
503        loadSettings();
504        initSuggest();
505
506        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
507            ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mInputLogic.mSuggest);
508        }
509        mDisplayOrientation = getResources().getConfiguration().orientation;
510
511        // Register to receive ringer mode change and network state change.
512        // Also receive installation and removal of a dictionary pack.
513        final IntentFilter filter = new IntentFilter();
514        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
515        filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
516        registerReceiver(mReceiver, filter);
517
518        final IntentFilter packageFilter = new IntentFilter();
519        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
520        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
521        packageFilter.addDataScheme(SCHEME_PACKAGE);
522        registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
523
524        final IntentFilter newDictFilter = new IntentFilter();
525        newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
526        registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
527
528        DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this);
529
530        mInputUpdater = new InputUpdater(this);
531    }
532
533    // Has to be package-visible for unit tests
534    @UsedForTesting
535    void loadSettings() {
536        final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
537        final InputAttributes inputAttributes =
538                new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
539        mSettings.loadSettings(locale, inputAttributes);
540        AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent());
541        // To load the keyboard we need to load all the settings once, but resetting the
542        // contacts dictionary should be deferred until after the new layout has been displayed
543        // to improve responsivity. In the language switching process, we post a reopenDictionaries
544        // message, then come here to read the settings for the new language before we change
545        // the layout; at this time, we need to skip resetting the contacts dictionary. It will
546        // be done later inside {@see #initSuggest()} when the reopenDictionaries message is
547        // processed.
548        if (!mHandler.hasPendingReopenDictionaries() && mInputLogic.mSuggest != null) {
549            // May need to reset dictionaries depending on the user settings.
550            mInputLogic.mSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */,
551                    mSettings.getCurrent());
552        }
553    }
554
555    // Note that this method is called from a non-UI thread.
556    @Override
557    public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) {
558        mIsMainDictionaryAvailable = isMainDictionaryAvailable;
559        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
560        if (mainKeyboardView != null) {
561            mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
562        }
563    }
564
565    private void initSuggest() {
566        final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
567        final String switcherLocaleStr = switcherSubtypeLocale.toString();
568        final Locale subtypeLocale;
569        if (TextUtils.isEmpty(switcherLocaleStr)) {
570            // This happens in very rare corner cases - for example, immediately after a switch
571            // to LatinIME has been requested, about a frame later another switch happens. In this
572            // case, we are about to go down but we still don't know it, however the system tells
573            // us there is no current subtype so the locale is the empty string. Take the best
574            // possible guess instead -- it's bound to have no consequences, and we have no way
575            // of knowing anyway.
576            Log.e(TAG, "System is reporting no current subtype.");
577            subtypeLocale = getResources().getConfiguration().locale;
578        } else {
579            subtypeLocale = switcherSubtypeLocale;
580        }
581
582        final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale,
583                this /* SuggestInitializationListener */);
584        final SettingsValues settingsValues = mSettings.getCurrent();
585        if (settingsValues.mCorrectionEnabled) {
586            newSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
587        }
588
589        mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
590        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
591            ResearchLogger.getInstance().initSuggest(newSuggest);
592        }
593
594        mUserDictionary = new UserBinaryDictionary(this, subtypeLocale);
595        mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
596        newSuggest.setUserDictionary(mUserDictionary);
597        newSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */,
598                mSettings.getCurrent());
599        final Suggest oldSuggest = mInputLogic.mSuggest;
600        mInputLogic.mSuggest = newSuggest;
601        if (oldSuggest != null) oldSuggest.close();
602    }
603
604    /* package private */ void resetSuggestMainDict() {
605        final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
606        mInputLogic.mSuggest.resetMainDict(this, subtypeLocale,
607                this /* SuggestInitializationListener */);
608        mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
609    }
610
611    @Override
612    public void onDestroy() {
613        final Suggest suggest = mInputLogic.mSuggest;
614        if (suggest != null) {
615            suggest.close();
616            mInputLogic.mSuggest = null;
617        }
618        mSettings.onDestroy();
619        unregisterReceiver(mReceiver);
620        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
621            ResearchLogger.getInstance().onDestroy();
622        }
623        unregisterReceiver(mDictionaryPackInstallReceiver);
624        PersonalizationDictionarySessionRegister.onDestroy(this);
625        LatinImeLogger.commit();
626        LatinImeLogger.onDestroy();
627        if (mInputUpdater != null) {
628            mInputUpdater.quitLooper();
629        }
630        super.onDestroy();
631    }
632
633    @Override
634    public void onConfigurationChanged(final Configuration conf) {
635        // If orientation changed while predicting, commit the change
636        if (mDisplayOrientation != conf.orientation) {
637            mDisplayOrientation = conf.orientation;
638            mHandler.startOrientationChanging();
639            mInputLogic.mConnection.beginBatchEdit();
640            mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR);
641            mInputLogic.mConnection.finishComposingText();
642            mInputLogic.mConnection.endBatchEdit();
643            if (isShowingOptionDialog()) {
644                mOptionsDialog.dismiss();
645            }
646        }
647        PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf);
648        super.onConfigurationChanged(conf);
649    }
650
651    @Override
652    public View onCreateInputView() {
653        return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
654    }
655
656    @Override
657    public void setInputView(final View view) {
658        super.setInputView(view);
659        mExtractArea = getWindow().getWindow().getDecorView()
660                .findViewById(android.R.id.extractArea);
661        mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
662        mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
663        if (mSuggestionStripView != null) {
664            mSuggestionStripView.setListener(this, view);
665        }
666        if (LatinImeLogger.sVISUALDEBUG) {
667            mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
668        }
669    }
670
671    @Override
672    public void setCandidatesView(final View view) {
673        // To ensure that CandidatesView will never be set.
674        return;
675    }
676
677    @Override
678    public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
679        mHandler.onStartInput(editorInfo, restarting);
680    }
681
682    @Override
683    public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
684        mHandler.onStartInputView(editorInfo, restarting);
685    }
686
687    @Override
688    public void onFinishInputView(final boolean finishingInput) {
689        mHandler.onFinishInputView(finishingInput);
690    }
691
692    @Override
693    public void onFinishInput() {
694        mHandler.onFinishInput();
695    }
696
697    @Override
698    public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) {
699        // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
700        // is not guaranteed. It may even be called at the same time on a different thread.
701        mSubtypeSwitcher.onSubtypeChanged(subtype);
702        loadKeyboard();
703    }
704
705    private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
706        super.onStartInput(editorInfo, restarting);
707    }
708
709    @SuppressWarnings("deprecation")
710    private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
711        super.onStartInputView(editorInfo, restarting);
712        mRichImm.clearSubtypeCaches();
713        final KeyboardSwitcher switcher = mKeyboardSwitcher;
714        switcher.updateKeyboardTheme();
715        final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
716        // If we are starting input in a different text field from before, we'll have to reload
717        // settings, so currentSettingsValues can't be final.
718        SettingsValues currentSettingsValues = mSettings.getCurrent();
719
720        if (editorInfo == null) {
721            Log.e(TAG, "Null EditorInfo in onStartInputView()");
722            if (LatinImeLogger.sDBG) {
723                throw new NullPointerException("Null EditorInfo in onStartInputView()");
724            }
725            return;
726        }
727        if (DEBUG) {
728            Log.d(TAG, "onStartInputView: editorInfo:"
729                    + String.format("inputType=0x%08x imeOptions=0x%08x",
730                            editorInfo.inputType, editorInfo.imeOptions));
731            Log.d(TAG, "All caps = "
732                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0)
733                    + ", sentence caps = "
734                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0)
735                    + ", word caps = "
736                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
737        }
738        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
739            final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
740            ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs);
741        }
742        if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
743            Log.w(TAG, "Deprecated private IME option specified: "
744                    + editorInfo.privateImeOptions);
745            Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead");
746        }
747        if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) {
748            Log.w(TAG, "Deprecated private IME option specified: "
749                    + editorInfo.privateImeOptions);
750            Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
751        }
752
753        final PackageInfo packageInfo =
754                TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName);
755        mAppWorkAroundsUtils.setPackageInfo(packageInfo);
756        if (null == packageInfo) {
757            new TargetPackageInfoGetterTask(this /* context */, this /* listener */)
758                    .execute(editorInfo.packageName);
759        }
760
761        LatinImeLogger.onStartInputView(editorInfo);
762        // In landscape mode, this method gets called without the input view being created.
763        if (mainKeyboardView == null) {
764            return;
765        }
766
767        // Forward this event to the accessibility utilities, if enabled.
768        final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
769        if (accessUtils.isTouchExplorationEnabled()) {
770            accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
771        }
772
773        final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo);
774        final boolean isDifferentTextField = !restarting || inputTypeChanged;
775        if (isDifferentTextField) {
776            mSubtypeSwitcher.updateParametersOnStartInputView();
777        }
778
779        // The EditorInfo might have a flag that affects fullscreen mode.
780        // Note: This call should be done by InputMethodService?
781        updateFullscreenMode();
782        mApplicationSpecifiedCompletions = null;
783
784        // The app calling setText() has the effect of clearing the composing
785        // span, so we should reset our state unconditionally, even if restarting is true.
786        mInputLogic.mEnteredText = null;
787        mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */);
788        mInputLogic.mDeleteCount = 0;
789        mInputLogic.mSpaceState = SpaceState.NONE;
790        mInputLogic.mRecapitalizeStatus.deactivate();
791        mInputLogic.mCurrentlyPressedHardwareKeys.clear();
792
793        // Note: the following does a round-trip IPC on the main thread: be careful
794        final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
795        final Suggest suggest = mInputLogic.mSuggest;
796        if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) {
797            initSuggest();
798        }
799        if (mSuggestionStripView != null) {
800            // This will set the punctuation suggestions if next word suggestion is off;
801            // otherwise it will clear the suggestion strip.
802            setPunctuationSuggestions();
803        }
804        mInputLogic.mSuggestedWords = SuggestedWords.EMPTY;
805
806        // Sometimes, while rotating, for some reason the framework tells the app we are not
807        // connected to it and that means we can't refresh the cache. In this case, schedule a
808        // refresh later.
809        final boolean canReachInputConnection;
810        if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
811                editorInfo.initialSelStart, editorInfo.initialSelEnd,
812                false /* shouldFinishComposition */)) {
813            // We try resetting the caches up to 5 times before giving up.
814            mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
815            // mLastSelection{Start,End} are reset later in this method, don't need to do it here
816            canReachInputConnection = false;
817        } else {
818            if (isDifferentTextField) {
819                mHandler.postResumeSuggestions();
820            }
821            canReachInputConnection = true;
822        }
823
824        if (isDifferentTextField) {
825            mainKeyboardView.closing();
826            loadSettings();
827            currentSettingsValues = mSettings.getCurrent();
828
829            if (suggest != null && currentSettingsValues.mCorrectionEnabled) {
830                suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
831            }
832
833            switcher.loadKeyboard(editorInfo, currentSettingsValues);
834            if (!canReachInputConnection) {
835                // If we can't reach the input connection, we will call loadKeyboard again later,
836                // so we need to save its state now. The call will be done in #retryResetCaches.
837                switcher.saveKeyboardState();
838            }
839        } else if (restarting) {
840            // TODO: Come up with a more comprehensive way to reset the keyboard layout when
841            // a keyboard layout set doesn't get reloaded in this method.
842            switcher.resetKeyboardStateToAlphabet();
843            // In apps like Talk, we come here when the text is sent and the field gets emptied and
844            // we need to re-evaluate the shift state, but not the whole layout which would be
845            // disruptive.
846            // Space state must be updated before calling updateShiftState
847            switcher.updateShiftState();
848        }
849        setSuggestionStripShownInternal(
850                isSuggestionsStripVisible(), /* needsInputViewShown */ false);
851
852        mInputLogic.mLastSelectionStart = editorInfo.initialSelStart;
853        mInputLogic.mLastSelectionEnd = editorInfo.initialSelEnd;
854        // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying
855        // so we try using some heuristics to find out about these and fix them.
856        tryFixLyingCursorPosition();
857
858        mHandler.cancelUpdateSuggestionStrip();
859        mHandler.cancelDoubleSpacePeriodTimer();
860
861        mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
862        mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
863                currentSettingsValues.mKeyPreviewPopupDismissDelay);
864        mainKeyboardView.setSlidingKeyInputPreviewEnabled(
865                currentSettingsValues.mSlidingKeyInputPreviewEnabled);
866        mainKeyboardView.setGestureHandlingEnabledByUser(
867                currentSettingsValues.mGestureInputEnabled,
868                currentSettingsValues.mGestureTrailEnabled,
869                currentSettingsValues.mGestureFloatingPreviewTextEnabled);
870
871        initPersonalizationDebugSettings(currentSettingsValues);
872
873        if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
874    }
875
876    /**
877     * Try to get the text from the editor to expose lies the framework may have been
878     * telling us. Concretely, when the device rotates, the frameworks tells us about where the
879     * cursor used to be initially in the editor at the time it first received the focus; this
880     * may be completely different from the place it is upon rotation. Since we don't have any
881     * means to get the real value, try at least to ask the text view for some characters and
882     * detect the most damaging cases: when the cursor position is declared to be much smaller
883     * than it really is.
884     */
885    private void tryFixLyingCursorPosition() {
886        final CharSequence textBeforeCursor = mInputLogic.mConnection.getTextBeforeCursor(
887                Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
888        if (null == textBeforeCursor) {
889            mInputLogic.mLastSelectionStart = mInputLogic.mLastSelectionEnd =
890                    Constants.NOT_A_CURSOR_POSITION;
891        } else {
892            final int textLength = textBeforeCursor.length();
893            if (textLength > mInputLogic.mLastSelectionStart
894                    || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
895                            && mInputLogic.mLastSelectionStart <
896                                    Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
897                // It should not be possible to have only one of those variables be
898                // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
899                // (simple cursor, no selection) or there is no cursor/we don't know its pos
900                final boolean wasEqual =
901                        mInputLogic.mLastSelectionStart == mInputLogic.mLastSelectionEnd;
902                mInputLogic.mLastSelectionStart = textLength;
903                // We can't figure out the value of mLastSelectionEnd :(
904                // But at least if it's smaller than mLastSelectionStart something is wrong,
905                // and if they used to be equal we also don't want to make it look like there is a
906                // selection.
907                if (wasEqual || mInputLogic.mLastSelectionStart > mInputLogic.mLastSelectionEnd) {
908                    mInputLogic.mLastSelectionEnd = mInputLogic.mLastSelectionStart;
909                }
910            }
911        }
912    }
913
914    // Initialization of personalization debug settings. This must be called inside
915    // onStartInputView.
916    private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) {
917        if (mUseOnlyPersonalizationDictionaryForDebug
918                != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
919            // Only for debug
920            initSuggest();
921            mUseOnlyPersonalizationDictionaryForDebug =
922                    currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug;
923        }
924
925        if (mBoostPersonalizationDictionaryForDebug !=
926                currentSettingsValues.mBoostPersonalizationDictionaryForDebug) {
927            // Only for debug
928            mBoostPersonalizationDictionaryForDebug =
929                    currentSettingsValues.mBoostPersonalizationDictionaryForDebug;
930        }
931    }
932
933    // Callback for the TargetPackageInfoGetterTask
934    @Override
935    public void onTargetPackageInfoKnown(final PackageInfo info) {
936        mAppWorkAroundsUtils.setPackageInfo(info);
937    }
938
939    @Override
940    public void onWindowHidden() {
941        super.onWindowHidden();
942        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
943        if (mainKeyboardView != null) {
944            mainKeyboardView.closing();
945        }
946    }
947
948    private void onFinishInputInternal() {
949        super.onFinishInput();
950
951        LatinImeLogger.commit();
952        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
953        if (mainKeyboardView != null) {
954            mainKeyboardView.closing();
955        }
956    }
957
958    private void onFinishInputViewInternal(final boolean finishingInput) {
959        super.onFinishInputView(finishingInput);
960        mKeyboardSwitcher.onFinishInputView();
961        mKeyboardSwitcher.deallocateMemory();
962        // Remove pending messages related to update suggestions
963        mHandler.cancelUpdateSuggestionStrip();
964        // Should do the following in onFinishInputInternal but until JB MR2 it's not called :(
965        if (mInputLogic.mWordComposer.isComposingWord()) {
966            mInputLogic.mConnection.finishComposingText();
967        }
968        mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */);
969        // Notify ResearchLogger
970        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
971            ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput,
972                    mInputLogic.mLastSelectionStart,
973                    mInputLogic.mLastSelectionEnd, getCurrentInputConnection());
974        }
975    }
976
977    @Override
978    public void onUpdateSelection(final int oldSelStart, final int oldSelEnd,
979            final int newSelStart, final int newSelEnd,
980            final int composingSpanStart, final int composingSpanEnd) {
981        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
982                composingSpanStart, composingSpanEnd);
983        if (DEBUG) {
984            Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
985                    + ", ose=" + oldSelEnd
986                    + ", lss=" + mInputLogic.mLastSelectionStart
987                    + ", lse=" + mInputLogic.mLastSelectionEnd
988                    + ", nss=" + newSelStart
989                    + ", nse=" + newSelEnd
990                    + ", cs=" + composingSpanStart
991                    + ", ce=" + composingSpanEnd);
992        }
993        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
994            ResearchLogger.latinIME_onUpdateSelection(mInputLogic.mLastSelectionStart,
995                    mInputLogic.mLastSelectionEnd,
996                    oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
997                    composingSpanEnd, mInputLogic.mConnection);
998        }
999
1000        final boolean selectionChanged = mInputLogic.mLastSelectionStart != newSelStart
1001                || mInputLogic.mLastSelectionEnd != newSelEnd;
1002
1003        // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
1004        // span in the view - we can use that to narrow down whether the cursor was moved
1005        // by us or not. If we are composing a word but there is no composing span, then
1006        // we know for sure the cursor moved while we were composing and we should reset
1007        // the state. TODO: rescind this policy: the framework never removes the composing
1008        // span on its own accord while editing. This test is useless.
1009        final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
1010
1011        // If the keyboard is not visible, we don't need to do all the housekeeping work, as it
1012        // will be reset when the keyboard shows up anyway.
1013        // TODO: revisit this when LatinIME supports hardware keyboards.
1014        // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
1015        // TODO: find a better way to simulate actual execution.
1016        if (isInputViewShown() && !mInputLogic.mConnection.isBelatedExpectedUpdate(oldSelStart,
1017                newSelStart, oldSelEnd, newSelEnd)) {
1018            // TODO: the following is probably better done in resetEntireInputState().
1019            // it should only happen when the cursor moved, and the very purpose of the
1020            // test below is to narrow down whether this happened or not. Likewise with
1021            // the call to updateShiftState.
1022            // We set this to NONE because after a cursor move, we don't want the space
1023            // state-related special processing to kick in.
1024            mInputLogic.mSpaceState = SpaceState.NONE;
1025
1026            // TODO: is it still necessary to test for composingSpan related stuff?
1027            final boolean selectionChangedOrSafeToReset = selectionChanged
1028                    || (!mInputLogic.mWordComposer.isComposingWord()) || noComposingSpan;
1029            final boolean hasOrHadSelection = (oldSelStart != oldSelEnd
1030                    || newSelStart != newSelEnd);
1031            final int moveAmount = newSelStart - oldSelStart;
1032            if (selectionChangedOrSafeToReset && (hasOrHadSelection
1033                    || !mInputLogic.mWordComposer.moveCursorByAndReturnIfInsideComposingWord(
1034                            moveAmount))) {
1035                // If we are composing a word and moving the cursor, we would want to set a
1036                // suggestion span for recorrection to work correctly. Unfortunately, that
1037                // would involve the keyboard committing some new text, which would move the
1038                // cursor back to where it was. Latin IME could then fix the position of the cursor
1039                // again, but the asynchronous nature of the calls results in this wreaking havoc
1040                // with selection on double tap and the like.
1041                // Another option would be to send suggestions each time we set the composing
1042                // text, but that is probably too expensive to do, so we decided to leave things
1043                // as is.
1044                mInputLogic.resetEntireInputState(mSettings.getCurrent(), newSelStart, newSelEnd);
1045            } else {
1046                // resetEntireInputState calls resetCachesUponCursorMove, but forcing the
1047                // composition to end.  But in all cases where we don't reset the entire input
1048                // state, we still want to tell the rich input connection about the new cursor
1049                // position so that it can update its caches.
1050                mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
1051                        newSelStart, newSelEnd, false /* shouldFinishComposition */);
1052            }
1053
1054            // We moved the cursor. If we are touching a word, we need to resume suggestion,
1055            // unless suggestions are off.
1056            if (isSuggestionsStripVisible()) {
1057                mHandler.postResumeSuggestions();
1058            }
1059            // Reset the last recapitalization.
1060            mInputLogic.mRecapitalizeStatus.deactivate();
1061            mKeyboardSwitcher.updateShiftState();
1062        }
1063
1064        // Make a note of the cursor position
1065        mInputLogic.mLastSelectionStart = newSelStart;
1066        mInputLogic.mLastSelectionEnd = newSelEnd;
1067        mSubtypeState.currentSubtypeUsed();
1068    }
1069
1070    /**
1071     * This is called when the user has clicked on the extracted text view,
1072     * when running in fullscreen mode.  The default implementation hides
1073     * the suggestions view when this happens, but only if the extracted text
1074     * editor has a vertical scroll bar because its text doesn't fit.
1075     * Here we override the behavior due to the possibility that a re-correction could
1076     * cause the suggestions strip to disappear and re-appear.
1077     */
1078    @Override
1079    public void onExtractedTextClicked() {
1080        if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
1081
1082        super.onExtractedTextClicked();
1083    }
1084
1085    /**
1086     * This is called when the user has performed a cursor movement in the
1087     * extracted text view, when it is running in fullscreen mode.  The default
1088     * implementation hides the suggestions view when a vertical movement
1089     * happens, but only if the extracted text editor has a vertical scroll bar
1090     * because its text doesn't fit.
1091     * Here we override the behavior due to the possibility that a re-correction could
1092     * cause the suggestions strip to disappear and re-appear.
1093     */
1094    @Override
1095    public void onExtractedCursorMovement(final int dx, final int dy) {
1096        if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
1097
1098        super.onExtractedCursorMovement(dx, dy);
1099    }
1100
1101    @Override
1102    public void hideWindow() {
1103        LatinImeLogger.commit();
1104        mKeyboardSwitcher.onHideWindow();
1105
1106        if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
1107            AccessibleKeyboardViewProxy.getInstance().onHideWindow();
1108        }
1109
1110        if (TRACE) Debug.stopMethodTracing();
1111        if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
1112            mOptionsDialog.dismiss();
1113            mOptionsDialog = null;
1114        }
1115        super.hideWindow();
1116    }
1117
1118    @Override
1119    public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
1120        if (DEBUG) {
1121            Log.i(TAG, "Received completions:");
1122            if (applicationSpecifiedCompletions != null) {
1123                for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
1124                    Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
1125                }
1126            }
1127        }
1128        if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return;
1129        if (applicationSpecifiedCompletions == null) {
1130            clearSuggestionStrip();
1131            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1132                ResearchLogger.latinIME_onDisplayCompletions(null);
1133            }
1134            return;
1135        }
1136        mApplicationSpecifiedCompletions =
1137                CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions);
1138
1139        final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
1140                SuggestedWords.getFromApplicationSpecifiedCompletions(
1141                        applicationSpecifiedCompletions);
1142        final SuggestedWords suggestedWords = new SuggestedWords(
1143                applicationSuggestedWords,
1144                false /* typedWordValid */,
1145                false /* hasAutoCorrectionCandidate */,
1146                false /* isPunctuationSuggestions */,
1147                false /* isObsoleteSuggestions */,
1148                false /* isPrediction */);
1149        // When in fullscreen mode, show completions generated by the application
1150        final boolean isAutoCorrection = false;
1151        setSuggestedWords(suggestedWords, isAutoCorrection);
1152        setAutoCorrectionIndicator(isAutoCorrection);
1153        setSuggestionStripShown(true);
1154        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1155            ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
1156        }
1157    }
1158
1159    private void setSuggestionStripShownInternal(final boolean shown,
1160            final boolean needsInputViewShown) {
1161        // TODO: Modify this if we support suggestions with hard keyboard
1162        if (onEvaluateInputViewShown() && mSuggestionStripView != null) {
1163            final boolean inputViewShown = mKeyboardSwitcher.isShowingMainKeyboardOrEmojiPalettes();
1164            final boolean shouldShowSuggestions = shown
1165                    && (needsInputViewShown ? inputViewShown : true);
1166            if (isFullscreenMode()) {
1167                mSuggestionStripView.setVisibility(
1168                        shouldShowSuggestions ? View.VISIBLE : View.GONE);
1169            } else {
1170                mSuggestionStripView.setVisibility(
1171                        shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
1172            }
1173        }
1174    }
1175
1176    private void setSuggestionStripShown(final boolean shown) {
1177        setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
1178    }
1179
1180    private int getAdjustedBackingViewHeight() {
1181        final int currentHeight = mKeyPreviewBackingView.getHeight();
1182        if (currentHeight > 0) {
1183            return currentHeight;
1184        }
1185
1186        final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
1187        if (visibleKeyboardView == null) {
1188            return 0;
1189        }
1190        // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main   !!!
1191        // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1192        final int keyboardHeight = visibleKeyboardView.getHeight();
1193        final int suggestionsHeight = mSuggestionStripView.getHeight();
1194        final int displayHeight = getResources().getDisplayMetrics().heightPixels;
1195        final Rect rect = new Rect();
1196        mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
1197        final int notificationBarHeight = rect.top;
1198        final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
1199                - keyboardHeight;
1200
1201        final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
1202        params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight);
1203        mKeyPreviewBackingView.setLayoutParams(params);
1204        return params.height;
1205    }
1206
1207    @Override
1208    public void onComputeInsets(final InputMethodService.Insets outInsets) {
1209        super.onComputeInsets(outInsets);
1210        final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
1211        if (visibleKeyboardView == null || mSuggestionStripView == null) {
1212            return;
1213        }
1214        final int adjustedBackingHeight = getAdjustedBackingViewHeight();
1215        final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
1216        final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
1217        // In fullscreen mode, the height of the extract area managed by InputMethodService should
1218        // be considered.
1219        // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
1220        final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
1221        final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0
1222                : mSuggestionStripView.getHeight();
1223        final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
1224        int visibleTopY = extraHeight;
1225        // Need to set touchable region only if input view is being shown
1226        if (visibleKeyboardView.isShown()) {
1227            // Note that the height of Emoji layout is the same as the height of the main keyboard
1228            // and the suggestion strip
1229            if (mKeyboardSwitcher.isShowingEmojiPalettes()
1230                    || mSuggestionStripView.getVisibility() == View.VISIBLE) {
1231                visibleTopY -= suggestionsHeight;
1232            }
1233            final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
1234            final int touchWidth = visibleKeyboardView.getWidth();
1235            final int touchHeight = visibleKeyboardView.getHeight() + extraHeight
1236                    // Extend touchable region below the keyboard.
1237                    + EXTENDED_TOUCHABLE_REGION_HEIGHT;
1238            outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
1239            outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight);
1240        }
1241        outInsets.contentTopInsets = visibleTopY;
1242        outInsets.visibleTopInsets = visibleTopY;
1243    }
1244
1245    @Override
1246    public boolean onEvaluateFullscreenMode() {
1247        // Reread resource value here, because this method is called by framework anytime as needed.
1248        final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources());
1249        if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) {
1250            // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI
1251            // implies NO_FULLSCREEN. However, the framework mistakenly does.  i.e. NO_EXTRACT_UI
1252            // without NO_FULLSCREEN doesn't work as expected. Because of this we need this
1253            // hack for now.  Let's get rid of this once the framework gets fixed.
1254            final EditorInfo ei = getCurrentInputEditorInfo();
1255            return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0));
1256        } else {
1257            return false;
1258        }
1259    }
1260
1261    @Override
1262    public void updateFullscreenMode() {
1263        super.updateFullscreenMode();
1264
1265        if (mKeyPreviewBackingView == null) return;
1266        // In fullscreen mode, no need to have extra space to show the key preview.
1267        // If not, we should have extra space above the keyboard to show the key preview.
1268        mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
1269    }
1270
1271    // Called from the KeyboardSwitcher which needs to know auto caps state to display
1272    // the right layout.
1273    // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead?
1274    public int getCurrentAutoCapsState() {
1275        return mInputLogic.getCurrentAutoCapsState(null /* optionalSettingsValues */);
1276    }
1277
1278    // Called from the KeyboardSwitcher which needs to know recaps state to display
1279    // the right layout.
1280    // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead?
1281    public int getCurrentRecapitalizeState() {
1282        return mInputLogic.getCurrentRecapitalizeState();
1283    }
1284
1285    // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
1286    // pressed.
1287    @Override
1288    public void addWordToUserDictionary(final String word) {
1289        if (TextUtils.isEmpty(word)) {
1290            // Probably never supposed to happen, but just in case.
1291            return;
1292        }
1293        final String wordToEdit;
1294        if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) {
1295            wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
1296        } else {
1297            wordToEdit = word;
1298        }
1299        mUserDictionary.addWordToUserDictionary(wordToEdit);
1300    }
1301
1302    public void displaySettingsDialog() {
1303        if (isShowingOptionDialog()) return;
1304        showSubtypeSelectorAndSettings();
1305    }
1306
1307    @Override
1308    public boolean onCustomRequest(final int requestCode) {
1309        if (isShowingOptionDialog()) return false;
1310        switch (requestCode) {
1311        case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
1312            if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
1313                mRichImm.getInputMethodManager().showInputMethodPicker();
1314                return true;
1315            }
1316            return false;
1317        }
1318        return false;
1319    }
1320
1321    private boolean isShowingOptionDialog() {
1322        return mOptionsDialog != null && mOptionsDialog.isShowing();
1323    }
1324
1325    // TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
1326    public void switchToNextSubtype() {
1327        final IBinder token = getWindow().getWindow().getAttributes().token;
1328        if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) {
1329            mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
1330            return;
1331        }
1332        mSubtypeState.switchSubtype(token, mRichImm);
1333    }
1334
1335    // Implementation of {@link KeyboardActionListener}.
1336    @Override
1337    public void onCodeInput(final int primaryCode, final int x, final int y) {
1338        mInputLogic.onCodeInput(primaryCode, x, y, mHandler, mKeyboardSwitcher, mSubtypeSwitcher);
1339    }
1340
1341    // Called from PointerTracker through the KeyboardActionListener interface
1342    // TODO[IL]: Move this to InputLogic
1343    @Override
1344    public void onTextInput(final String rawText) {
1345        mInputLogic.mConnection.beginBatchEdit();
1346        if (mInputLogic.mWordComposer.isComposingWord()) {
1347            mInputLogic.commitCurrentAutoCorrection(mSettings.getCurrent(), rawText, mHandler);
1348        } else {
1349            mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */);
1350        }
1351        mHandler.postUpdateSuggestionStrip();
1352        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
1353                && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) {
1354            ResearchLogger.getInstance().onResearchKeySelected(this);
1355            return;
1356        }
1357        final String text = specificTldProcessingOnTextInput(rawText);
1358        if (SpaceState.PHANTOM == mInputLogic.mSpaceState) {
1359            mInputLogic.promotePhantomSpace(mSettings.getCurrent());
1360        }
1361        mInputLogic.mConnection.commitText(text, 1);
1362        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1363            ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */);
1364        }
1365        mInputLogic.mConnection.endBatchEdit();
1366        // Space state must be updated before calling updateShiftState
1367        mInputLogic.mSpaceState = SpaceState.NONE;
1368        mKeyboardSwitcher.updateShiftState();
1369        mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT);
1370        mInputLogic.mEnteredText = text;
1371    }
1372
1373    @Override
1374    public void onStartBatchInput() {
1375        mInputUpdater.onStartBatchInput();
1376        mHandler.cancelUpdateSuggestionStrip();
1377        mInputLogic.mConnection.beginBatchEdit();
1378        final SettingsValues currentSettingsValues = mSettings.getCurrent();
1379        if (mInputLogic.mWordComposer.isComposingWord()) {
1380            if (currentSettingsValues.mIsInternal) {
1381                if (mInputLogic.mWordComposer.isBatchMode()) {
1382                    LatinImeLoggerUtils.onAutoCorrection("",
1383                            mInputLogic.mWordComposer.getTypedWord(), " ",
1384                            mInputLogic.mWordComposer);
1385                }
1386            }
1387            final int wordComposerSize = mInputLogic.mWordComposer.size();
1388            // Since isComposingWord() is true, the size is at least 1.
1389            if (mInputLogic.mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
1390                // If we are in the middle of a recorrection, we need to commit the recorrection
1391                // first so that we can insert the batch input at the current cursor position.
1392                mInputLogic.resetEntireInputState(currentSettingsValues,
1393                        mInputLogic.mLastSelectionStart, mInputLogic.mLastSelectionEnd);
1394            } else if (wordComposerSize <= 1) {
1395                // We auto-correct the previous (typed, not gestured) string iff it's one character
1396                // long. The reason for this is, even in the middle of gesture typing, you'll still
1397                // tap one-letter words and you want them auto-corrected (typically, "i" in English
1398                // should become "I"). However for any longer word, we assume that the reason for
1399                // tapping probably is that the word you intend to type is not in the dictionary,
1400                // so we do not attempt to correct, on the assumption that if that was a dictionary
1401                // word, the user would probably have gestured instead.
1402                mInputLogic.commitCurrentAutoCorrection(currentSettingsValues,
1403                        LastComposedWord.NOT_A_SEPARATOR, mHandler);
1404            } else {
1405                mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR);
1406            }
1407        }
1408        final int codePointBeforeCursor = mInputLogic.mConnection.getCodePointBeforeCursor();
1409        if (Character.isLetterOrDigit(codePointBeforeCursor)
1410                || currentSettingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
1411            final boolean autoShiftHasBeenOverriden = mKeyboardSwitcher.getKeyboardShiftMode() !=
1412                    getCurrentAutoCapsState();
1413            mInputLogic.mSpaceState = SpaceState.PHANTOM;
1414            if (!autoShiftHasBeenOverriden) {
1415                // When we change the space state, we need to update the shift state of the
1416                // keyboard unless it has been overridden manually. This is happening for example
1417                // after typing some letters and a period, then gesturing; the keyboard is not in
1418                // caps mode yet, but since a gesture is starting, it should go in caps mode,
1419                // unless the user explictly said it should not.
1420                mKeyboardSwitcher.updateShiftState();
1421            }
1422        }
1423        mInputLogic.mConnection.endBatchEdit();
1424        mInputLogic.mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
1425                mInputLogic.getActualCapsMode(currentSettingsValues, mKeyboardSwitcher),
1426                // Prev word is 1st word before cursor
1427                mInputLogic.getNthPreviousWordForSuggestion(currentSettingsValues,
1428                        1 /* nthPreviousWord */));
1429    }
1430
1431    static final class InputUpdater implements Handler.Callback {
1432        private final Handler mHandler;
1433        private final LatinIME mLatinIme;
1434        private final Object mLock = new Object();
1435        private boolean mInBatchInput; // synchronized using {@link #mLock}.
1436
1437        InputUpdater(final LatinIME latinIme) {
1438            final HandlerThread handlerThread = new HandlerThread(
1439                    InputUpdater.class.getSimpleName());
1440            handlerThread.start();
1441            mHandler = new Handler(handlerThread.getLooper(), this);
1442            mLatinIme = latinIme;
1443        }
1444
1445        private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1;
1446        private static final int MSG_GET_SUGGESTED_WORDS = 2;
1447
1448        @Override
1449        public boolean handleMessage(final Message msg) {
1450            // TODO: straighten message passing - we don't need two kinds of messages calling
1451            // each other.
1452            switch (msg.what) {
1453                case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
1454                    updateBatchInput((InputPointers)msg.obj, msg.arg2 /* sequenceNumber */);
1455                    break;
1456                case MSG_GET_SUGGESTED_WORDS:
1457                    mLatinIme.getSuggestedWords(msg.arg1 /* sessionId */,
1458                            msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj);
1459                    break;
1460            }
1461            return true;
1462        }
1463
1464        // Run in the UI thread.
1465        public void onStartBatchInput() {
1466            synchronized (mLock) {
1467                mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
1468                mInBatchInput = true;
1469                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
1470                        SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
1471            }
1472        }
1473
1474        // Run in the Handler thread.
1475        private void updateBatchInput(final InputPointers batchPointers, final int sequenceNumber) {
1476            synchronized (mLock) {
1477                if (!mInBatchInput) {
1478                    // Batch input has ended or canceled while the message was being delivered.
1479                    return;
1480                }
1481
1482                getSuggestedWordsGestureLocked(batchPointers, sequenceNumber,
1483                        new OnGetSuggestedWordsCallback() {
1484                    @Override
1485                    public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
1486                        mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
1487                                suggestedWords, false /* dismissGestureFloatingPreviewText */);
1488                    }
1489                });
1490            }
1491        }
1492
1493        // Run in the UI thread.
1494        public void onUpdateBatchInput(final InputPointers batchPointers,
1495                final int sequenceNumber) {
1496            if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
1497                return;
1498            }
1499            mHandler.obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 0 /* arg1 */,
1500                    sequenceNumber /* arg2 */, batchPointers /* obj */).sendToTarget();
1501        }
1502
1503        public void onCancelBatchInput() {
1504            synchronized (mLock) {
1505                mInBatchInput = false;
1506                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
1507                        SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
1508            }
1509        }
1510
1511        // Run in the UI thread.
1512        public void onEndBatchInput(final InputPointers batchPointers) {
1513            synchronized(mLock) {
1514                getSuggestedWordsGestureLocked(batchPointers, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
1515                        new OnGetSuggestedWordsCallback() {
1516                    @Override
1517                    public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
1518                        mInBatchInput = false;
1519                        mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords,
1520                                true /* dismissGestureFloatingPreviewText */);
1521                        mLatinIme.mHandler.onEndBatchInput(suggestedWords);
1522                    }
1523                });
1524            }
1525        }
1526
1527        // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
1528        // be synchronized.
1529        private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
1530                final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
1531            mLatinIme.mInputLogic.mWordComposer.setBatchInputPointers(batchPointers);
1532            mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
1533                    sequenceNumber, new OnGetSuggestedWordsCallback() {
1534                @Override
1535                public void onGetSuggestedWords(SuggestedWords suggestedWords) {
1536                    final int suggestionCount = suggestedWords.size();
1537                    if (suggestionCount <= 1) {
1538                        final String mostProbableSuggestion = (suggestionCount == 0) ? null
1539                                : suggestedWords.getWord(0);
1540                        callback.onGetSuggestedWords(
1541                                mLatinIme.getOlderSuggestions(mostProbableSuggestion));
1542                    }
1543                    callback.onGetSuggestedWords(suggestedWords);
1544                }
1545            });
1546        }
1547
1548        public void getSuggestedWords(final int sessionId, final int sequenceNumber,
1549                final OnGetSuggestedWordsCallback callback) {
1550            mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback)
1551                    .sendToTarget();
1552        }
1553
1554        void quitLooper() {
1555            mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS);
1556            mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
1557            mHandler.getLooper().quit();
1558        }
1559    }
1560
1561    // This method must run in UI Thread.
1562    private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
1563            final boolean dismissGestureFloatingPreviewText) {
1564        showSuggestionStrip(suggestedWords);
1565        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1566        mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
1567        if (dismissGestureFloatingPreviewText) {
1568            mainKeyboardView.dismissGestureFloatingPreviewText();
1569        }
1570    }
1571
1572    /* The sequence number member is only used in onUpdateBatchInput. It is increased each time
1573     * auto-commit happens. The reason we need this is, when auto-commit happens we trim the
1574     * input pointers that are held in a singleton, and to know how much to trim we rely on the
1575     * results of the suggestion process that is held in mSuggestedWords.
1576     * However, the suggestion process is asynchronous, and sometimes we may enter the
1577     * onUpdateBatchInput method twice without having recomputed suggestions yet, or having
1578     * received new suggestions generated from not-yet-trimmed input pointers. In this case, the
1579     * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we
1580     * remove an unrelated number of pointers (possibly even more than are left in the input
1581     * pointers, leading to a crash).
1582     * To avoid that, we increase the sequence number each time we auto-commit and trim the
1583     * input pointers, and we do not use any suggested words that have been generated with an
1584     * earlier sequence number.
1585     */
1586    private int mAutoCommitSequenceNumber = 1;
1587    @Override
1588    public void onUpdateBatchInput(final InputPointers batchPointers) {
1589        final SettingsValues settingsValues = mSettings.getCurrent();
1590        if (settingsValues.mPhraseGestureEnabled) {
1591            final SuggestedWordInfo candidate =
1592                    mInputLogic.mSuggestedWords.getAutoCommitCandidate();
1593            // If these suggested words have been generated with out of date input pointers, then
1594            // we skip auto-commit (see comments above on the mSequenceNumber member).
1595            if (null != candidate
1596                    && mInputLogic.mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) {
1597                if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
1598                    final String[] commitParts = candidate.mWord.split(" ", 2);
1599                    batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
1600                    mInputLogic.promotePhantomSpace(mSettings.getCurrent());
1601                    mInputLogic.mConnection.commitText(commitParts[0], 0);
1602                    mInputLogic.mSpaceState = SpaceState.PHANTOM;
1603                    mKeyboardSwitcher.updateShiftState();
1604                    mInputLogic.mWordComposer.
1605                            setCapitalizedModeAndPreviousWordAtStartComposingTime(
1606                            mInputLogic.getActualCapsMode(settingsValues, mKeyboardSwitcher),
1607                            commitParts[0]);
1608                    ++mAutoCommitSequenceNumber;
1609                }
1610            }
1611        }
1612        mInputUpdater.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
1613    }
1614
1615    // This method must run in UI Thread.
1616    public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) {
1617        final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0);
1618        if (TextUtils.isEmpty(batchInputText)) {
1619            return;
1620        }
1621        mInputLogic.mConnection.beginBatchEdit();
1622        if (SpaceState.PHANTOM == mInputLogic.mSpaceState) {
1623            mInputLogic.promotePhantomSpace(mSettings.getCurrent());
1624        }
1625        if (mSettings.getCurrent().mPhraseGestureEnabled) {
1626            // Find the last space
1627            final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
1628            if (0 != indexOfLastSpace) {
1629                mInputLogic.mConnection.commitText(batchInputText.substring(0, indexOfLastSpace),
1630                        1);
1631                showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture());
1632            }
1633            final String lastWord = batchInputText.substring(indexOfLastSpace);
1634            mInputLogic.mWordComposer.setBatchInputWord(lastWord);
1635            mInputLogic.mConnection.setComposingText(lastWord, 1);
1636        } else {
1637            mInputLogic.mWordComposer.setBatchInputWord(batchInputText);
1638            mInputLogic.mConnection.setComposingText(batchInputText, 1);
1639        }
1640        mInputLogic.mConnection.endBatchEdit();
1641        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1642            ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords);
1643        }
1644        // Space state must be updated before calling updateShiftState
1645        mInputLogic.mSpaceState = SpaceState.PHANTOM;
1646        mKeyboardSwitcher.updateShiftState();
1647    }
1648
1649    @Override
1650    public void onEndBatchInput(final InputPointers batchPointers) {
1651        mInputUpdater.onEndBatchInput(batchPointers);
1652    }
1653
1654    private String specificTldProcessingOnTextInput(final String text) {
1655        if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
1656                || !Character.isLetter(text.charAt(1))) {
1657            // Not a tld: do nothing.
1658            return text;
1659        }
1660        // We have a TLD (or something that looks like this): make sure we don't add
1661        // a space even if currently in phantom mode.
1662        mInputLogic.mSpaceState = SpaceState.NONE;
1663        // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code
1664        final CharSequence lastOne = mInputLogic.mConnection.getTextBeforeCursor(1, 0);
1665        if (lastOne != null && lastOne.length() == 1
1666                && lastOne.charAt(0) == Constants.CODE_PERIOD) {
1667            return text.substring(1);
1668        } else {
1669            return text;
1670        }
1671    }
1672
1673    // Called from PointerTracker through the KeyboardActionListener interface
1674    @Override
1675    public void onFinishSlidingInput() {
1676        // User finished sliding input.
1677        mKeyboardSwitcher.onFinishSlidingInput();
1678    }
1679
1680    // Called from PointerTracker through the KeyboardActionListener interface
1681    @Override
1682    public void onCancelInput() {
1683        // User released a finger outside any key
1684        // Nothing to do so far.
1685    }
1686
1687    @Override
1688    public void onCancelBatchInput() {
1689        mInputUpdater.onCancelBatchInput();
1690    }
1691
1692    // TODO[IL]: Rename this to avoid using handle*
1693    private void handleClose() {
1694        // TODO: Verify that words are logged properly when IME is closed.
1695        mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR);
1696        requestHideSelf(0);
1697        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1698        if (mainKeyboardView != null) {
1699            mainKeyboardView.closing();
1700        }
1701    }
1702
1703    // TODO[IL]: Move this to InputLogic and make it private
1704    // Outside LatinIME, only used by the test suite.
1705    @UsedForTesting
1706    public boolean isShowingPunctuationList() {
1707        if (mInputLogic.mSuggestedWords == null) return false;
1708        return mSettings.getCurrent().mSuggestPuncList == mInputLogic.mSuggestedWords;
1709    }
1710
1711    private boolean isSuggestionsStripVisible() {
1712        final SettingsValues currentSettings = mSettings.getCurrent();
1713        if (mSuggestionStripView == null)
1714            return false;
1715        if (mSuggestionStripView.isShowingAddToDictionaryHint())
1716            return true;
1717        if (null == currentSettings)
1718            return false;
1719        if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
1720            return false;
1721        if (currentSettings.isApplicationSpecifiedCompletionsOn())
1722            return true;
1723        return currentSettings.isSuggestionsRequested(mDisplayOrientation);
1724    }
1725
1726    public void dismissAddToDictionaryHint() {
1727        if (null != mSuggestionStripView) {
1728            mSuggestionStripView.dismissAddToDictionaryHint();
1729        }
1730    }
1731
1732    // TODO[IL]: Define a clear interface for this
1733    public void clearSuggestionStrip() {
1734        setSuggestedWords(SuggestedWords.EMPTY, false);
1735        setAutoCorrectionIndicator(false);
1736    }
1737
1738    // TODO[IL]: Define a clear interface for this
1739    public void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) {
1740        mInputLogic.mSuggestedWords = words;
1741        if (mSuggestionStripView != null) {
1742            mSuggestionStripView.setSuggestions(words);
1743            mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
1744        }
1745    }
1746
1747    private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
1748        // Put a blue underline to a word in TextView which will be auto-corrected.
1749        if (mInputLogic.mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
1750                && mInputLogic.mWordComposer.isComposingWord()) {
1751            mInputLogic.mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
1752            final CharSequence textWithUnderline =
1753                    mInputLogic.getTextWithUnderline(mInputLogic.mWordComposer.getTypedWord());
1754            // TODO: when called from an updateSuggestionStrip() call that results from a posted
1755            // message, this is called outside any batch edit. Potentially, this may result in some
1756            // janky flickering of the screen, although the display speed makes it unlikely in
1757            // the practice.
1758            mInputLogic.mConnection.setComposingText(textWithUnderline, 1);
1759        }
1760    }
1761
1762    // TODO[IL]: Move this to InputLogic and make private again
1763    public void updateSuggestionStrip() {
1764        mHandler.cancelUpdateSuggestionStrip();
1765        final SettingsValues currentSettings = mSettings.getCurrent();
1766
1767        // Check if we have a suggestion engine attached.
1768        if (mInputLogic.mSuggest == null
1769                || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
1770            if (mInputLogic.mWordComposer.isComposingWord()) {
1771                Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
1772                        + "requested!");
1773            }
1774            return;
1775        }
1776
1777        if (!mInputLogic.mWordComposer.isComposingWord()
1778                && !currentSettings.mBigramPredictionEnabled) {
1779            setPunctuationSuggestions();
1780            return;
1781        }
1782
1783        final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
1784        getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
1785                SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
1786                    @Override
1787                    public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
1788                        holder.set(suggestedWords);
1789                    }
1790                }
1791        );
1792
1793        // This line may cause the current thread to wait.
1794        final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT);
1795        if (suggestedWords != null) {
1796            showSuggestionStrip(suggestedWords);
1797        }
1798    }
1799
1800    private void getSuggestedWords(final int sessionId, final int sequenceNumber,
1801            final OnGetSuggestedWordsCallback callback) {
1802        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1803        final Suggest suggest = mInputLogic.mSuggest;
1804        if (keyboard == null || suggest == null) {
1805            callback.onGetSuggestedWords(SuggestedWords.EMPTY);
1806            return;
1807        }
1808        // Get the word on which we should search the bigrams. If we are composing a word, it's
1809        // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
1810        // should just skip whitespace if any, so 1.
1811        final SettingsValues currentSettings = mSettings.getCurrent();
1812        final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
1813
1814        if (DEBUG) {
1815            if (mInputLogic.mWordComposer.isComposingWord()
1816                    || mInputLogic.mWordComposer.isBatchMode()) {
1817                final String previousWord
1818                        = mInputLogic.mWordComposer.getPreviousWordForSuggestion();
1819                // TODO: this is for checking consistency with older versions. Remove this when
1820                // we are confident this is stable.
1821                // We're checking the previous word in the text field against the memorized previous
1822                // word. If we are composing a word we should have the second word before the cursor
1823                // memorized, otherwise we should have the first.
1824                final String rereadPrevWord = mInputLogic.getNthPreviousWordForSuggestion(
1825                        currentSettings, mInputLogic.mWordComposer.isComposingWord() ? 2 : 1);
1826                if (!TextUtils.equals(previousWord, rereadPrevWord)) {
1827                    throw new RuntimeException("Unexpected previous word: "
1828                            + previousWord + " <> " + rereadPrevWord);
1829                }
1830            }
1831        }
1832        suggest.getSuggestedWords(mInputLogic.mWordComposer,
1833                mInputLogic.mWordComposer.getPreviousWordForSuggestion(),
1834                keyboard.getProximityInfo(),
1835                currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
1836                additionalFeaturesOptions, sessionId, sequenceNumber, callback);
1837    }
1838
1839    private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId,
1840            final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
1841        mInputUpdater.getSuggestedWords(sessionId, sequenceNumber,
1842                new OnGetSuggestedWordsCallback() {
1843                    @Override
1844                    public void onGetSuggestedWords(SuggestedWords suggestedWords) {
1845                        callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
1846                                mInputLogic.mWordComposer.getTypedWord(), suggestedWords));
1847                    }
1848                });
1849    }
1850
1851    private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
1852            final SuggestedWords suggestedWords) {
1853        // TODO: consolidate this into getSuggestedWords
1854        // We update the suggestion strip only when we have some suggestions to show, i.e. when
1855        // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
1856        // replaced with the new one. However, when the word is a dictionary word, or when the
1857        // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
1858        // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
1859        // revert to suggestions - although it is unclear how we can come here if it's displayed.
1860        if (suggestedWords.size() > 1 || typedWord.length() <= 1
1861                || suggestedWords.mTypedWordValid || null == mSuggestionStripView
1862                || mSuggestionStripView.isShowingAddToDictionaryHint()) {
1863            return suggestedWords;
1864        } else {
1865            return getOlderSuggestions(typedWord);
1866        }
1867    }
1868
1869    private SuggestedWords getOlderSuggestions(final String typedWord) {
1870        SuggestedWords previousSuggestedWords = mInputLogic.mSuggestedWords;
1871        if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) {
1872            previousSuggestedWords = SuggestedWords.EMPTY;
1873        }
1874        if (typedWord == null) {
1875            return previousSuggestedWords;
1876        }
1877        final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
1878                SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord,
1879                        previousSuggestedWords);
1880        return new SuggestedWords(typedWordAndPreviousSuggestions,
1881                false /* typedWordValid */,
1882                false /* hasAutoCorrectionCandidate */,
1883                false /* isPunctuationSuggestions */,
1884                true /* isObsoleteSuggestions */,
1885                false /* isPrediction */);
1886    }
1887
1888    private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) {
1889        if (suggestedWords.isEmpty()) return;
1890        final String autoCorrection;
1891        if (suggestedWords.mWillAutoCorrect) {
1892            autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
1893        } else {
1894            // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)
1895            // because it may differ from mWordComposer.mTypedWord.
1896            autoCorrection = typedWord;
1897        }
1898        mInputLogic.mWordComposer.setAutoCorrection(autoCorrection);
1899    }
1900
1901    private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
1902            final String typedWord) {
1903      if (suggestedWords.isEmpty()) {
1904          // No auto-correction is available, clear the cached values.
1905          AccessibilityUtils.getInstance().setAutoCorrection(null, null);
1906          clearSuggestionStrip();
1907          return;
1908      }
1909      setAutoCorrection(suggestedWords, typedWord);
1910      final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
1911      setSuggestedWords(suggestedWords, isAutoCorrection);
1912      setAutoCorrectionIndicator(isAutoCorrection);
1913      setSuggestionStripShown(isSuggestionsStripVisible());
1914      // An auto-correction is available, cache it in accessibility code so
1915      // we can be speak it if the user touches a key that will insert it.
1916      AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord);
1917    }
1918
1919    private void showSuggestionStrip(final SuggestedWords suggestedWords) {
1920        if (suggestedWords.isEmpty()) {
1921            clearSuggestionStrip();
1922            return;
1923        }
1924        showSuggestionStripWithTypedWord(suggestedWords,
1925            suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
1926    }
1927
1928    // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
1929    // interface
1930    @Override
1931    public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) {
1932        final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords;
1933        final String suggestion = suggestionInfo.mWord;
1934        // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
1935        if (suggestion.length() == 1 && isShowingPunctuationList()) {
1936            // Word separators are suggested before the user inputs something.
1937            // So, LatinImeLogger logs "" as a user's input.
1938            LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords);
1939            // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
1940            final int primaryCode = suggestion.charAt(0);
1941            onCodeInput(primaryCode,
1942                    Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
1943            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1944                ResearchLogger.latinIME_punctuationSuggestion(index, suggestion,
1945                        false /* isBatchMode */, suggestedWords.mIsPrediction);
1946            }
1947            return;
1948        }
1949
1950        mInputLogic.mConnection.beginBatchEdit();
1951        final SettingsValues currentSettings = mSettings.getCurrent();
1952        if (SpaceState.PHANTOM == mInputLogic.mSpaceState && suggestion.length() > 0
1953                // In the batch input mode, a manually picked suggested word should just replace
1954                // the current batch input text and there is no need for a phantom space.
1955                && !mInputLogic.mWordComposer.isBatchMode()) {
1956            final int firstChar = Character.codePointAt(suggestion, 0);
1957            if (!currentSettings.isWordSeparator(firstChar)
1958                    || currentSettings.isUsuallyPrecededBySpace(firstChar)) {
1959                mInputLogic.promotePhantomSpace(currentSettings);
1960            }
1961        }
1962
1963        if (currentSettings.isApplicationSpecifiedCompletionsOn()
1964                && mApplicationSpecifiedCompletions != null
1965                && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
1966            mInputLogic.mSuggestedWords = SuggestedWords.EMPTY;
1967            if (mSuggestionStripView != null) {
1968                mSuggestionStripView.clear();
1969            }
1970            mKeyboardSwitcher.updateShiftState();
1971            mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */);
1972            final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
1973            mInputLogic.mConnection.commitCompletion(completionInfo);
1974            mInputLogic.mConnection.endBatchEdit();
1975            return;
1976        }
1977
1978        // We need to log before we commit, because the word composer will store away the user
1979        // typed word.
1980        final String replacedWord = mInputLogic.mWordComposer.getTypedWord();
1981        LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords);
1982        commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
1983                LastComposedWord.NOT_A_SEPARATOR);
1984        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1985            ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
1986                    mInputLogic.mWordComposer.isBatchMode(), suggestionInfo.mScore,
1987                    suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType);
1988        }
1989        mInputLogic.mConnection.endBatchEdit();
1990        // Don't allow cancellation of manual pick
1991        mInputLogic.mLastComposedWord.deactivate();
1992        // Space state must be updated before calling updateShiftState
1993        mInputLogic.mSpaceState = SpaceState.PHANTOM;
1994        mKeyboardSwitcher.updateShiftState();
1995
1996        // We should show the "Touch again to save" hint if the user pressed the first entry
1997        // AND it's in none of our current dictionaries (main, user or otherwise).
1998        // Please note that if mSuggest is null, it means that everything is off: suggestion
1999        // and correction, so we shouldn't try to show the hint
2000        final Suggest suggest = mInputLogic.mSuggest;
2001        final boolean showingAddToDictionaryHint =
2002                (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
2003                        || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
2004                        && suggest != null
2005                        // If the suggestion is not in the dictionary, the hint should be shown.
2006                        && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true);
2007
2008        if (currentSettings.mIsInternal) {
2009            LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
2010                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
2011        }
2012        if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
2013            mSuggestionStripView.showAddToDictionaryHint(
2014                    suggestion, currentSettings.mHintToSaveText);
2015        } else {
2016            // If we're not showing the "Touch again to save", then update the suggestion strip.
2017            mHandler.postUpdateSuggestionStrip();
2018        }
2019    }
2020
2021    /**
2022     * Commits the chosen word to the text field and saves it for later retrieval.
2023     */
2024    // TODO[IL]: Move to InputLogic and make public again
2025    public void commitChosenWord(final String chosenWord, final int commitType,
2026            final String separatorString) {
2027        final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords;
2028        mInputLogic.mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
2029                this, chosenWord, suggestedWords), 1);
2030        // Add the word to the user history dictionary
2031        final String prevWord = addToUserHistoryDictionary(chosenWord);
2032        // TODO: figure out here if this is an auto-correct or if the best word is actually
2033        // what user typed. Note: currently this is done much later in
2034        // LastComposedWord#didCommitTypedWord by string equality of the remembered
2035        // strings.
2036        mInputLogic.mLastComposedWord = mInputLogic.mWordComposer.commitWord(commitType,
2037                chosenWord, separatorString, prevWord);
2038        final boolean shouldDiscardPreviousWordForSuggestion;
2039        if (0 == StringUtils.codePointCount(separatorString)) {
2040            // Separator is 0-length. Discard the word only if the current language has spaces.
2041            shouldDiscardPreviousWordForSuggestion =
2042                    mSettings.getCurrent().mCurrentLanguageHasSpaces;
2043        } else {
2044            // Otherwise, we discard if the separator contains any non-whitespace.
2045            shouldDiscardPreviousWordForSuggestion =
2046                    !StringUtils.containsOnlyWhitespace(separatorString);
2047        }
2048        if (shouldDiscardPreviousWordForSuggestion) {
2049            mInputLogic.mWordComposer.discardPreviousWordForSuggestion();
2050        }
2051    }
2052
2053    // TODO[IL]: Define a clean interface for this
2054    public void setPunctuationSuggestions() {
2055        final SettingsValues currentSettings = mSettings.getCurrent();
2056        if (currentSettings.mBigramPredictionEnabled) {
2057            clearSuggestionStrip();
2058        } else {
2059            setSuggestedWords(currentSettings.mSuggestPuncList, false);
2060        }
2061        setAutoCorrectionIndicator(false);
2062        setSuggestionStripShown(isSuggestionsStripVisible());
2063    }
2064
2065    private String addToUserHistoryDictionary(final String suggestion) {
2066        if (TextUtils.isEmpty(suggestion)) return null;
2067        final Suggest suggest = mInputLogic.mSuggest;
2068        if (suggest == null) return null;
2069
2070        // If correction is not enabled, we don't add words to the user history dictionary.
2071        // That's to avoid unintended additions in some sensitive fields, or fields that
2072        // expect to receive non-words.
2073        final SettingsValues currentSettings = mSettings.getCurrent();
2074        if (!currentSettings.mCorrectionEnabled) return null;
2075
2076        final UserHistoryDictionary userHistoryDictionary = suggest.getUserHistoryDictionary();
2077        if (userHistoryDictionary == null) return null;
2078
2079        final String prevWord = mInputLogic.mConnection.getNthPreviousWord(currentSettings, 2);
2080        final String secondWord;
2081        if (mInputLogic.mWordComposer.wasAutoCapitalized()
2082                && !mInputLogic.mWordComposer.isMostlyCaps()) {
2083            secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
2084        } else {
2085            secondWord = suggestion;
2086        }
2087        // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
2088        // We don't add words with 0-frequency (assuming they would be profanity etc.).
2089        final int maxFreq = AutoCorrectionUtils.getMaxFrequency(
2090                suggest.getUnigramDictionaries(), suggestion);
2091        if (maxFreq == 0) return null;
2092        userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0,
2093                (int)TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis())));
2094        return prevWord;
2095    }
2096
2097    private boolean isResumableWord(final String word, final SettingsValues settings) {
2098        final int firstCodePoint = word.codePointAt(0);
2099        return settings.isWordCodePoint(firstCodePoint)
2100                && Constants.CODE_SINGLE_QUOTE != firstCodePoint
2101                && Constants.CODE_DASH != firstCodePoint;
2102    }
2103
2104    /**
2105     * Check if the cursor is touching a word. If so, restart suggestions on this word, else
2106     * do nothing.
2107     */
2108    private void restartSuggestionsOnWordTouchedByCursor() {
2109        // HACK: We may want to special-case some apps that exhibit bad behavior in case of
2110        // recorrection. This is a temporary, stopgap measure that will be removed later.
2111        // TODO: remove this.
2112        if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
2113        // A simple way to test for support from the TextView.
2114        if (!isSuggestionsStripVisible()) return;
2115        // Recorrection is not supported in languages without spaces because we don't know
2116        // how to segment them yet.
2117        if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
2118        // If the cursor is not touching a word, or if there is a selection, return right away.
2119        if (mInputLogic.mLastSelectionStart != mInputLogic.mLastSelectionEnd) return;
2120        // If we don't know the cursor location, return.
2121        if (mInputLogic.mLastSelectionStart < 0) return;
2122        final SettingsValues currentSettings = mSettings.getCurrent();
2123        if (!mInputLogic.mConnection.isCursorTouchingWord(currentSettings)) return;
2124        final TextRange range = mInputLogic.mConnection.getWordRangeAtCursor(
2125                currentSettings.mWordSeparators, 0 /* additionalPrecedingWordsCount */);
2126        if (null == range) return; // Happens if we don't have an input connection at all
2127        if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out.
2128        // If for some strange reason (editor bug or so) we measure the text before the cursor as
2129        // longer than what the entire text is supposed to be, the safe thing to do is bail out.
2130        final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
2131        if (numberOfCharsInWordBeforeCursor > mInputLogic.mLastSelectionStart) return;
2132        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
2133        final String typedWord = range.mWord.toString();
2134        if (!isResumableWord(typedWord, currentSettings)) return;
2135        int i = 0;
2136        for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) {
2137            for (final String s : span.getSuggestions()) {
2138                ++i;
2139                if (!TextUtils.equals(s, typedWord)) {
2140                    suggestions.add(new SuggestedWordInfo(s,
2141                            SuggestionStripView.MAX_SUGGESTIONS - i,
2142                            SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
2143                            SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
2144                            SuggestedWordInfo.NOT_A_CONFIDENCE
2145                                    /* autoCommitFirstWordConfidence */));
2146                }
2147            }
2148        }
2149        mInputLogic.mWordComposer.setComposingWord(typedWord,
2150                mInputLogic.getNthPreviousWordForSuggestion(currentSettings,
2151                        // We want the previous word for suggestion. If we have chars in the word
2152                        // before the cursor, then we want the word before that, hence 2; otherwise,
2153                        // we want the word immediately before the cursor, hence 1.
2154                        0 == numberOfCharsInWordBeforeCursor ? 1 : 2),
2155                mKeyboardSwitcher.getKeyboard());
2156        mInputLogic.mWordComposer.setCursorPositionWithinWord(
2157                typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
2158        mInputLogic.mConnection.setComposingRegion(
2159                mInputLogic.mLastSelectionStart - numberOfCharsInWordBeforeCursor,
2160                mInputLogic.mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
2161        if (suggestions.isEmpty()) {
2162            // We come here if there weren't any suggestion spans on this word. We will try to
2163            // compute suggestions for it instead.
2164            mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
2165                    SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
2166                        @Override
2167                        public void onGetSuggestedWords(
2168                                final SuggestedWords suggestedWordsIncludingTypedWord) {
2169                            final SuggestedWords suggestedWords;
2170                            if (suggestedWordsIncludingTypedWord.size() > 1) {
2171                                // We were able to compute new suggestions for this word.
2172                                // Remove the typed word, since we don't want to display it in this
2173                                // case. The #getSuggestedWordsExcludingTypedWord() method sets
2174                                // willAutoCorrect to false.
2175                                suggestedWords = suggestedWordsIncludingTypedWord
2176                                        .getSuggestedWordsExcludingTypedWord();
2177                            } else {
2178                                // No saved suggestions, and we were unable to compute any good one
2179                                // either. Rather than displaying an empty suggestion strip, we'll
2180                                // display the original word alone in the middle.
2181                                // Since there is only one word, willAutoCorrect is false.
2182                                suggestedWords = suggestedWordsIncludingTypedWord;
2183                            }
2184                            // We need to pass typedWord because mWordComposer.mTypedWord may
2185                            // differ from typedWord.
2186                            unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
2187                                    suggestedWords, typedWord);
2188                        }});
2189        } else {
2190            // We found suggestion spans in the word. We'll create the SuggestedWords out of
2191            // them, and make willAutoCorrect false.
2192            final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
2193                    true /* typedWordValid */, false /* willAutoCorrect */,
2194                    false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
2195                    false /* isPrediction */);
2196            // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord.
2197            unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, typedWord);
2198        }
2199    }
2200
2201    public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
2202            final SuggestedWords suggestedWords, final String typedWord) {
2203        // Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
2204        // We never want to auto-correct on a resumed suggestion. Please refer to the three places
2205        // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected.
2206        // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching
2207        // the text to adapt it.
2208        // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition)
2209        mInputLogic.mIsAutoCorrectionIndicatorOn = false;
2210        mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord);
2211    }
2212
2213    /**
2214     * Retry resetting caches in the rich input connection.
2215     *
2216     * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
2217     * This method handles the retry, and re-schedules a new retry if we still can't access.
2218     * We only retry up to 5 times before giving up.
2219     *
2220     * @param tryResumeSuggestions Whether we should resume suggestions or not.
2221     * @param remainingTries How many times we may try again before giving up.
2222     */
2223    private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
2224        if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
2225                mInputLogic.mLastSelectionStart, mInputLogic.mLastSelectionEnd, false)) {
2226            if (0 < remainingTries) {
2227                mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
2228                return;
2229            }
2230            // If remainingTries is 0, we should stop waiting for new tries, but it's still
2231            // better to load the keyboard (less things will be broken).
2232        }
2233        tryFixLyingCursorPosition();
2234        mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
2235        if (tryResumeSuggestions) mHandler.postResumeSuggestions();
2236    }
2237
2238    // TODO: Make this private
2239    // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
2240    @UsedForTesting
2241    void loadKeyboard() {
2242        // Since we are switching languages, the most urgent thing is to let the keyboard graphics
2243        // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on
2244        // the screen. Anything we do right now will delay this, so wait until the next frame
2245        // before we do the rest, like reopening dictionaries and updating suggestions. So we
2246        // post a message.
2247        mHandler.postReopenDictionaries();
2248        loadSettings();
2249        if (mKeyboardSwitcher.getMainKeyboardView() != null) {
2250            // Reload keyboard because the current language has been changed.
2251            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
2252        }
2253    }
2254
2255    private void hapticAndAudioFeedback(final int code, final int repeatCount) {
2256        final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView();
2257        if (keyboardView != null && keyboardView.isInDraggingFinger()) {
2258            // No need to feedback while finger is dragging.
2259            return;
2260        }
2261        if (repeatCount > 0) {
2262            if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) {
2263                // No need to feedback when repeat delete key will have no effect.
2264                return;
2265            }
2266            // TODO: Use event time that the last feedback has been generated instead of relying on
2267            // a repeat count to thin out feedback.
2268            if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) {
2269                return;
2270            }
2271        }
2272        final AudioAndHapticFeedbackManager feedbackManager =
2273                AudioAndHapticFeedbackManager.getInstance();
2274        if (repeatCount == 0) {
2275            // TODO: Reconsider how to perform haptic feedback when repeating key.
2276            feedbackManager.performHapticFeedback(keyboardView);
2277        }
2278        feedbackManager.performAudioFeedback(code);
2279    }
2280
2281    // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
2282    // release matching call is {@link #onReleaseKey(int,boolean)} below.
2283    @Override
2284    public void onPressKey(final int primaryCode, final int repeatCount,
2285            final boolean isSinglePointer) {
2286        mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
2287        hapticAndAudioFeedback(primaryCode, repeatCount);
2288    }
2289
2290    // Callback of the {@link KeyboardActionListener}. This is called when a key is released;
2291    // press matching call is {@link #onPressKey(int,int,boolean)} above.
2292    @Override
2293    public void onReleaseKey(final int primaryCode, final boolean withSliding) {
2294        mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
2295
2296        // If accessibility is on, ensure the user receives keyboard state updates.
2297        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
2298            switch (primaryCode) {
2299            case Constants.CODE_SHIFT:
2300                AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
2301                break;
2302            case Constants.CODE_SWITCH_ALPHA_SYMBOL:
2303                AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
2304                break;
2305            }
2306        }
2307    }
2308
2309    // Hooks for hardware keyboard
2310    @Override
2311    public boolean onKeyDown(final int keyCode, final KeyEvent event) {
2312        if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event);
2313        // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if
2314        // it doesn't know what to do with it and leave it to the application. For example,
2315        // hardware key events for adjusting the screen's brightness are passed as is.
2316        if (mInputLogic.mEventInterpreter.onHardwareKeyEvent(event)) {
2317            final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
2318            mInputLogic.mCurrentlyPressedHardwareKeys.add(keyIdentifier);
2319            return true;
2320        }
2321        return super.onKeyDown(keyCode, event);
2322    }
2323
2324    @Override
2325    public boolean onKeyUp(final int keyCode, final KeyEvent event) {
2326        final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
2327        if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
2328            return true;
2329        }
2330        return super.onKeyUp(keyCode, event);
2331    }
2332
2333    // onKeyDown and onKeyUp are the main events we are interested in. There are two more events
2334    // related to handling of hardware key events that we may want to implement in the future:
2335    // boolean onKeyLongPress(final int keyCode, final KeyEvent event);
2336    // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event);
2337
2338    // receive ringer mode change and network state change.
2339    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2340        @Override
2341        public void onReceive(final Context context, final Intent intent) {
2342            final String action = intent.getAction();
2343            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2344                mSubtypeSwitcher.onNetworkStateChanged(intent);
2345            } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2346                AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
2347            }
2348        }
2349    };
2350
2351    private void launchSettings() {
2352        handleClose();
2353        launchSubActivity(SettingsActivity.class);
2354    }
2355
2356    public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
2357        // Put the text in the attached EditText into a safe, saved state before switching to a
2358        // new activity that will also use the soft keyboard.
2359        mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR);
2360        launchSubActivity(activityClass);
2361    }
2362
2363    private void launchSubActivity(final Class<? extends Activity> activityClass) {
2364        Intent intent = new Intent();
2365        intent.setClass(LatinIME.this, activityClass);
2366        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
2367                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2368                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2369        startActivity(intent);
2370    }
2371
2372    private void showSubtypeSelectorAndSettings() {
2373        final CharSequence title = getString(R.string.english_ime_input_options);
2374        final CharSequence[] items = new CharSequence[] {
2375                // TODO: Should use new string "Select active input modes".
2376                getString(R.string.language_selection_title),
2377                getString(ApplicationUtils.getActivityTitleResId(this, SettingsActivity.class)),
2378        };
2379        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2380            @Override
2381            public void onClick(DialogInterface di, int position) {
2382                di.dismiss();
2383                switch (position) {
2384                case 0:
2385                    final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
2386                            mRichImm.getInputMethodIdOfThisIme(),
2387                            Intent.FLAG_ACTIVITY_NEW_TASK
2388                                    | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2389                                    | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2390                    startActivity(intent);
2391                    break;
2392                case 1:
2393                    launchSettings();
2394                    break;
2395                }
2396            }
2397        };
2398        final AlertDialog.Builder builder =
2399                new AlertDialog.Builder(this).setItems(items, listener).setTitle(title);
2400        showOptionDialog(builder.create());
2401    }
2402
2403    public void showOptionDialog(final AlertDialog dialog) {
2404        final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
2405        if (windowToken == null) {
2406            return;
2407        }
2408
2409        dialog.setCancelable(true);
2410        dialog.setCanceledOnTouchOutside(true);
2411
2412        final Window window = dialog.getWindow();
2413        final WindowManager.LayoutParams lp = window.getAttributes();
2414        lp.token = windowToken;
2415        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
2416        window.setAttributes(lp);
2417        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
2418
2419        mOptionsDialog = dialog;
2420        dialog.show();
2421    }
2422
2423    // TODO: can this be removed somehow without breaking the tests?
2424    @UsedForTesting
2425    /* package for test */ SuggestedWords getSuggestedWords() {
2426        // You may not use this method for anything else than debug
2427        return DEBUG ? mInputLogic.mSuggestedWords : null;
2428    }
2429
2430    // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
2431    @UsedForTesting
2432    /* package for test */ boolean isCurrentlyWaitingForMainDictionary() {
2433        return mInputLogic.mSuggest.isCurrentlyWaitingForMainDictionary();
2434    }
2435
2436    // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
2437    @UsedForTesting
2438    /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
2439        mInputLogic.mSuggest.resetMainDict(this, locale, null);
2440    }
2441
2442    public void debugDumpStateAndCrashWithException(final String context) {
2443        final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
2444        s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
2445                .append("\nContext : ").append(context);
2446        throw new RuntimeException(s.toString());
2447    }
2448
2449    @Override
2450    protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) {
2451        super.dump(fd, fout, args);
2452
2453        final Printer p = new PrintWriterPrinter(fout);
2454        p.println("LatinIME state :");
2455        p.println("  VersionCode = " + ApplicationUtils.getVersionCode(this));
2456        p.println("  VersionName = " + ApplicationUtils.getVersionName(this));
2457        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
2458        final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
2459        p.println("  Keyboard mode = " + keyboardMode);
2460        final SettingsValues settingsValues = mSettings.getCurrent();
2461        p.println("  mIsSuggestionsRequested = "
2462                + settingsValues.isSuggestionsRequested(mDisplayOrientation));
2463        p.println("  mCorrectionEnabled=" + settingsValues.mCorrectionEnabled);
2464        p.println("  isComposingWord=" + mInputLogic.mWordComposer.isComposingWord());
2465        p.println("  mSoundOn=" + settingsValues.mSoundOn);
2466        p.println("  mVibrateOn=" + settingsValues.mVibrateOn);
2467        p.println("  mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn);
2468        p.println("  inputAttributes=" + settingsValues.mInputAttributes);
2469    }
2470}
2471