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