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