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