LatinIME.java revision cd11905022306c9b95f8781f0f8b23a3570f30e9
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.AlertDialog;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.DialogInterface.OnClickListener;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.res.Configuration;
31import android.content.res.Resources;
32import android.inputmethodservice.InputMethodService;
33import android.media.AudioManager;
34import android.net.ConnectivityManager;
35import android.os.Debug;
36import android.os.IBinder;
37import android.os.Message;
38import android.preference.PreferenceManager;
39import android.text.InputType;
40import android.text.TextUtils;
41import android.util.Log;
42import android.util.PrintWriterPrinter;
43import android.util.Printer;
44import android.util.SparseArray;
45import android.view.Gravity;
46import android.view.KeyEvent;
47import android.view.View;
48import android.view.ViewGroup.LayoutParams;
49import android.view.ViewTreeObserver;
50import android.view.Window;
51import android.view.WindowManager;
52import android.view.inputmethod.CompletionInfo;
53import android.view.inputmethod.CursorAnchorInfo;
54import android.view.inputmethod.EditorInfo;
55import android.view.inputmethod.InputMethod;
56import android.view.inputmethod.InputMethodSubtype;
57import android.widget.TextView;
58
59import com.android.inputmethod.accessibility.AccessibilityUtils;
60import com.android.inputmethod.annotations.UsedForTesting;
61import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper;
62import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
63import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
64import com.android.inputmethod.event.Event;
65import com.android.inputmethod.event.HardwareEventDecoder;
66import com.android.inputmethod.event.HardwareKeyboardEventDecoder;
67import com.android.inputmethod.event.InputTransaction;
68import com.android.inputmethod.keyboard.Keyboard;
69import com.android.inputmethod.keyboard.KeyboardActionListener;
70import com.android.inputmethod.keyboard.KeyboardId;
71import com.android.inputmethod.keyboard.KeyboardSwitcher;
72import com.android.inputmethod.keyboard.MainKeyboardView;
73import com.android.inputmethod.keyboard.TextDecoratorUi;
74import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
75import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
76import com.android.inputmethod.latin.define.DebugFlags;
77import com.android.inputmethod.latin.define.ProductionFlags;
78import com.android.inputmethod.latin.inputlogic.InputLogic;
79import com.android.inputmethod.latin.personalization.ContextualDictionaryUpdater;
80import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever;
81import com.android.inputmethod.latin.personalization.PersonalizationDictionaryUpdater;
82import com.android.inputmethod.latin.personalization.PersonalizationHelper;
83import com.android.inputmethod.latin.settings.Settings;
84import com.android.inputmethod.latin.settings.SettingsActivity;
85import com.android.inputmethod.latin.settings.SettingsValues;
86import com.android.inputmethod.latin.suggestions.SuggestionStripView;
87import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
88import com.android.inputmethod.latin.utils.ApplicationUtils;
89import com.android.inputmethod.latin.utils.CapsModeUtils;
90import com.android.inputmethod.latin.utils.CursorAnchorInfoUtils;
91import com.android.inputmethod.latin.utils.CoordinateUtils;
92import com.android.inputmethod.latin.utils.DialogUtils;
93import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions;
94import com.android.inputmethod.latin.utils.ImportantNoticeUtils;
95import com.android.inputmethod.latin.utils.IntentUtils;
96import com.android.inputmethod.latin.utils.JniUtils;
97import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper;
98import com.android.inputmethod.latin.utils.StatsUtils;
99import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
100import com.android.inputmethod.latin.utils.ViewLayoutUtils;
101
102import java.io.FileDescriptor;
103import java.io.PrintWriter;
104import java.util.ArrayList;
105import java.util.List;
106import java.util.Locale;
107import java.util.concurrent.TimeUnit;
108
109/**
110 * Input method implementation for Qwerty'ish keyboard.
111 */
112public class LatinIME extends InputMethodService implements KeyboardActionListener,
113        SuggestionStripView.Listener, SuggestionStripViewAccessor,
114        DictionaryFacilitator.DictionaryInitializationListener,
115        ImportantNoticeDialog.ImportantNoticeDialogListener {
116    private static final String TAG = LatinIME.class.getSimpleName();
117    private static final boolean TRACE = false;
118    private static boolean DEBUG = false;
119
120    private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
121
122    private static final int PENDING_IMS_CALLBACK_DURATION = 800;
123
124    private static final int DELAY_WAIT_FOR_DICTIONARY_LOAD = 2000; // 2s
125
126    private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2;
127
128    /**
129     * The name of the scheme used by the Package Manager to warn of a new package installation,
130     * replacement or removal.
131     */
132    private static final String SCHEME_PACKAGE = "package";
133
134    private final Settings mSettings;
135    private final DictionaryFacilitator mDictionaryFacilitator =
136            new DictionaryFacilitator(
137                    new DistracterFilterCheckingExactMatchesAndSuggestions(this /* context */));
138    // TODO: Move from LatinIME.
139    private final PersonalizationDictionaryUpdater mPersonalizationDictionaryUpdater =
140            new PersonalizationDictionaryUpdater(this /* context */, mDictionaryFacilitator);
141    private final ContextualDictionaryUpdater mContextualDictionaryUpdater =
142            new ContextualDictionaryUpdater(this /* context */, mDictionaryFacilitator,
143                    new Runnable() {
144                        @Override
145                        public void run() {
146                            mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE);
147                        }
148                    });
149    private final InputLogic mInputLogic = new InputLogic(this /* LatinIME */,
150            this /* SuggestionStripViewAccessor */, mDictionaryFacilitator);
151    // We expect to have only one decoder in almost all cases, hence the default capacity of 1.
152    // If it turns out we need several, it will get grown seamlessly.
153    final SparseArray<HardwareEventDecoder> mHardwareEventDecoders = new SparseArray<>(1);
154
155    // TODO: Move these {@link View}s to {@link KeyboardSwitcher}.
156    private View mInputView;
157    private SuggestionStripView mSuggestionStripView;
158    private TextView mExtractEditText;
159
160    private RichInputMethodManager mRichImm;
161    @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
162    private final SubtypeSwitcher mSubtypeSwitcher;
163    private final SubtypeState mSubtypeState = new SubtypeState();
164    private final SpecialKeyDetector mSpecialKeyDetector;
165
166    // Object for reacting to adding/removing a dictionary pack.
167    private final BroadcastReceiver mDictionaryPackInstallReceiver =
168            new DictionaryPackInstallBroadcastReceiver(this);
169
170    private final BroadcastReceiver mDictionaryDumpBroadcastReceiver =
171            new DictionaryDumpBroadcastReceiver(this);
172
173    private AlertDialog mOptionsDialog;
174
175    private final boolean mIsHardwareAcceleratedDrawingEnabled;
176
177    public final UIHandler mHandler = new UIHandler(this);
178
179    public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> {
180        private static final int MSG_UPDATE_SHIFT_STATE = 0;
181        private static final int MSG_PENDING_IMS_CALLBACK = 1;
182        private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
183        private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
184        private static final int MSG_RESUME_SUGGESTIONS = 4;
185        private static final int MSG_REOPEN_DICTIONARIES = 5;
186        private static final int MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED = 6;
187        private static final int MSG_RESET_CACHES = 7;
188        private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8;
189        private static final int MSG_SHOW_COMMIT_INDICATOR = 9;
190        // Update this when adding new messages
191        private static final int MSG_LAST = MSG_SHOW_COMMIT_INDICATOR;
192
193        private static final int ARG1_NOT_GESTURE_INPUT = 0;
194        private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
195        private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2;
196        private static final int ARG2_UNUSED = 0;
197        private static final int ARG1_FALSE = 0;
198        private static final int ARG1_TRUE = 1;
199
200        private int mDelayInMillisecondsToUpdateSuggestions;
201        private int mDelayInMillisecondsToUpdateShiftState;
202        private int mDelayInMillisecondsToShowCommitIndicator;
203
204        public UIHandler(final LatinIME ownerInstance) {
205            super(ownerInstance);
206        }
207
208        public void onCreate() {
209            final LatinIME latinIme = getOwnerInstance();
210            if (latinIme == null) {
211                return;
212            }
213            final Resources res = latinIme.getResources();
214            mDelayInMillisecondsToUpdateSuggestions = res.getInteger(
215                    R.integer.config_delay_in_milliseconds_to_update_suggestions);
216            mDelayInMillisecondsToUpdateShiftState = res.getInteger(
217                    R.integer.config_delay_in_milliseconds_to_update_shift_state);
218            mDelayInMillisecondsToShowCommitIndicator = res.getInteger(
219                    R.integer.text_decorator_delay_in_milliseconds_to_show_commit_indicator);
220        }
221
222        @Override
223        public void handleMessage(final Message msg) {
224            final LatinIME latinIme = getOwnerInstance();
225            if (latinIme == null) {
226                return;
227            }
228            final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
229            switch (msg.what) {
230            case MSG_UPDATE_SUGGESTION_STRIP:
231                cancelUpdateSuggestionStrip();
232                latinIme.mInputLogic.performUpdateSuggestionStripSync(
233                        latinIme.mSettings.getCurrent(), msg.arg1 /* inputStyle */);
234                break;
235            case MSG_UPDATE_SHIFT_STATE:
236                switcher.requestUpdatingShiftState(latinIme.getCurrentAutoCapsState(),
237                        latinIme.getCurrentRecapitalizeState());
238                break;
239            case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
240                if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) {
241                    final SuggestedWords suggestedWords = (SuggestedWords) msg.obj;
242                    latinIme.showSuggestionStrip(suggestedWords);
243                } else {
244                    latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj,
245                            msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
246                }
247                break;
248            case MSG_RESUME_SUGGESTIONS:
249                latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor(
250                        latinIme.mSettings.getCurrent(),
251                        msg.arg1 == ARG1_TRUE /* shouldIncludeResumedWordInSuggestions */,
252                        latinIme.mKeyboardSwitcher.getCurrentKeyboardScriptId());
253                break;
254            case MSG_REOPEN_DICTIONARIES:
255                // We need to re-evaluate the currently composing word in case the script has
256                // changed.
257                postWaitForDictionaryLoad();
258                latinIme.resetSuggest();
259                break;
260            case MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED:
261                latinIme.mInputLogic.onUpdateTailBatchInputCompleted(
262                        latinIme.mSettings.getCurrent(),
263                        (SuggestedWords) msg.obj, latinIme.mKeyboardSwitcher);
264                break;
265            case MSG_RESET_CACHES:
266                final SettingsValues settingsValues = latinIme.mSettings.getCurrent();
267                if (latinIme.mInputLogic.retryResetCachesAndReturnSuccess(
268                        msg.arg1 == ARG1_TRUE /* tryResumeSuggestions */,
269                        msg.arg2 /* remainingTries */, this /* handler */)) {
270                    // If we were able to reset the caches, then we can reload the keyboard.
271                    // Otherwise, we'll do it when we can.
272                    latinIme.mKeyboardSwitcher.loadKeyboard(latinIme.getCurrentInputEditorInfo(),
273                            settingsValues, latinIme.getCurrentAutoCapsState(),
274                            latinIme.getCurrentRecapitalizeState());
275                }
276                break;
277            case MSG_SHOW_COMMIT_INDICATOR:
278                // Protocol of MSG_SET_COMMIT_INDICATOR_ENABLED:
279                // - what: MSG_SHOW_COMMIT_INDICATOR
280                // - arg1: not used.
281                // - arg2: not used.
282                // - obj:  the Runnable object to be called back.
283                ((Runnable) msg.obj).run();
284                break;
285            case MSG_WAIT_FOR_DICTIONARY_LOAD:
286                Log.i(TAG, "Timeout waiting for dictionary load");
287                break;
288            }
289        }
290
291        public void postUpdateSuggestionStrip(final int inputStyle) {
292            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP, inputStyle,
293                    0 /* ignored */), mDelayInMillisecondsToUpdateSuggestions);
294        }
295
296        public void postReopenDictionaries() {
297            sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES));
298        }
299
300        public void postResumeSuggestions(final boolean shouldIncludeResumedWordInSuggestions,
301                final boolean shouldDelay) {
302            final LatinIME latinIme = getOwnerInstance();
303            if (latinIme == null) {
304                return;
305            }
306            if (!latinIme.mSettings.getCurrent().isSuggestionsEnabledPerUserSettings()) {
307                return;
308            }
309            removeMessages(MSG_RESUME_SUGGESTIONS);
310            if (shouldDelay) {
311                sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS,
312                        shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE,
313                        0 /* ignored */), mDelayInMillisecondsToUpdateSuggestions);
314            } else {
315                sendMessage(obtainMessage(MSG_RESUME_SUGGESTIONS,
316                        shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE,
317                        0 /* ignored */));
318            }
319        }
320
321        public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
322            removeMessages(MSG_RESET_CACHES);
323            sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0,
324                    remainingTries, null));
325        }
326
327        public void postWaitForDictionaryLoad() {
328            sendMessageDelayed(obtainMessage(MSG_WAIT_FOR_DICTIONARY_LOAD),
329                    DELAY_WAIT_FOR_DICTIONARY_LOAD);
330        }
331
332        public void cancelWaitForDictionaryLoad() {
333            removeMessages(MSG_WAIT_FOR_DICTIONARY_LOAD);
334        }
335
336        public boolean hasPendingWaitForDictionaryLoad() {
337            return hasMessages(MSG_WAIT_FOR_DICTIONARY_LOAD);
338        }
339
340        public void cancelUpdateSuggestionStrip() {
341            removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
342        }
343
344        public boolean hasPendingUpdateSuggestions() {
345            return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
346        }
347
348        public boolean hasPendingReopenDictionaries() {
349            return hasMessages(MSG_REOPEN_DICTIONARIES);
350        }
351
352        public void postUpdateShiftState() {
353            removeMessages(MSG_UPDATE_SHIFT_STATE);
354            sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE),
355                    mDelayInMillisecondsToUpdateShiftState);
356        }
357
358        @UsedForTesting
359        public void removeAllMessages() {
360            for (int i = 0; i <= MSG_LAST; ++i) {
361                removeMessages(i);
362            }
363        }
364
365        public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
366                final boolean dismissGestureFloatingPreviewText) {
367            removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
368            final int arg1 = dismissGestureFloatingPreviewText
369                    ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT
370                    : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT;
371            obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1,
372                    ARG2_UNUSED, suggestedWords).sendToTarget();
373        }
374
375        public void showSuggestionStrip(final SuggestedWords suggestedWords) {
376            removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
377            obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP,
378                    ARG1_NOT_GESTURE_INPUT, ARG2_UNUSED, suggestedWords).sendToTarget();
379        }
380
381        public void showTailBatchInputResult(final SuggestedWords suggestedWords) {
382            obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget();
383        }
384
385        /**
386         * Posts a delayed task to show the commit indicator.
387         *
388         * <p>Only one task can exist in the queue. When this method is called, any prior task that
389         * has not yet fired will be canceled.</p>
390         * @param task the runnable object that will be fired when the delayed task is dispatched.
391         */
392        public void postShowCommitIndicatorTask(final Runnable task) {
393            removeMessages(MSG_SHOW_COMMIT_INDICATOR);
394            sendMessageDelayed(obtainMessage(MSG_SHOW_COMMIT_INDICATOR, task),
395                    mDelayInMillisecondsToShowCommitIndicator);
396        }
397
398        // Working variables for the following methods.
399        private boolean mIsOrientationChanging;
400        private boolean mPendingSuccessiveImsCallback;
401        private boolean mHasPendingStartInput;
402        private boolean mHasPendingFinishInputView;
403        private boolean mHasPendingFinishInput;
404        private EditorInfo mAppliedEditorInfo;
405
406        public void startOrientationChanging() {
407            removeMessages(MSG_PENDING_IMS_CALLBACK);
408            resetPendingImsCallback();
409            mIsOrientationChanging = true;
410            final LatinIME latinIme = getOwnerInstance();
411            if (latinIme == null) {
412                return;
413            }
414            if (latinIme.isInputViewShown()) {
415                latinIme.mKeyboardSwitcher.saveKeyboardState();
416            }
417        }
418
419        private void resetPendingImsCallback() {
420            mHasPendingFinishInputView = false;
421            mHasPendingFinishInput = false;
422            mHasPendingStartInput = false;
423        }
424
425        private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo,
426                boolean restarting) {
427            if (mHasPendingFinishInputView) {
428                latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
429            }
430            if (mHasPendingFinishInput) {
431                latinIme.onFinishInputInternal();
432            }
433            if (mHasPendingStartInput) {
434                latinIme.onStartInputInternal(editorInfo, restarting);
435            }
436            resetPendingImsCallback();
437        }
438
439        public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
440            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
441                // Typically this is the second onStartInput after orientation changed.
442                mHasPendingStartInput = true;
443            } else {
444                if (mIsOrientationChanging && restarting) {
445                    // This is the first onStartInput after orientation changed.
446                    mIsOrientationChanging = false;
447                    mPendingSuccessiveImsCallback = true;
448                }
449                final LatinIME latinIme = getOwnerInstance();
450                if (latinIme != null) {
451                    executePendingImsCallback(latinIme, editorInfo, restarting);
452                    latinIme.onStartInputInternal(editorInfo, restarting);
453                }
454            }
455        }
456
457        public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
458            if (hasMessages(MSG_PENDING_IMS_CALLBACK)
459                    && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
460                // Typically this is the second onStartInputView after orientation changed.
461                resetPendingImsCallback();
462            } else {
463                if (mPendingSuccessiveImsCallback) {
464                    // This is the first onStartInputView after orientation changed.
465                    mPendingSuccessiveImsCallback = false;
466                    resetPendingImsCallback();
467                    sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
468                            PENDING_IMS_CALLBACK_DURATION);
469                }
470                final LatinIME latinIme = getOwnerInstance();
471                if (latinIme != null) {
472                    executePendingImsCallback(latinIme, editorInfo, restarting);
473                    latinIme.onStartInputViewInternal(editorInfo, restarting);
474                    mAppliedEditorInfo = editorInfo;
475                }
476            }
477        }
478
479        public void onFinishInputView(final boolean finishingInput) {
480            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
481                // Typically this is the first onFinishInputView after orientation changed.
482                mHasPendingFinishInputView = true;
483            } else {
484                final LatinIME latinIme = getOwnerInstance();
485                if (latinIme != null) {
486                    latinIme.onFinishInputViewInternal(finishingInput);
487                    mAppliedEditorInfo = null;
488                }
489            }
490        }
491
492        public void onFinishInput() {
493            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
494                // Typically this is the first onFinishInput after orientation changed.
495                mHasPendingFinishInput = true;
496            } else {
497                final LatinIME latinIme = getOwnerInstance();
498                if (latinIme != null) {
499                    executePendingImsCallback(latinIme, null, false);
500                    latinIme.onFinishInputInternal();
501                }
502            }
503        }
504    }
505
506    static final class SubtypeState {
507        private InputMethodSubtype mLastActiveSubtype;
508        private boolean mCurrentSubtypeHasBeenUsed;
509
510        public void setCurrentSubtypeHasBeenUsed() {
511            mCurrentSubtypeHasBeenUsed = true;
512        }
513
514        public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) {
515            final InputMethodSubtype currentSubtype = richImm.getInputMethodManager()
516                    .getCurrentInputMethodSubtype();
517            final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype;
518            final boolean currentSubtypeHasBeenUsed = mCurrentSubtypeHasBeenUsed;
519            if (currentSubtypeHasBeenUsed) {
520                mLastActiveSubtype = currentSubtype;
521                mCurrentSubtypeHasBeenUsed = false;
522            }
523            if (currentSubtypeHasBeenUsed
524                    && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
525                    && !currentSubtype.equals(lastActiveSubtype)) {
526                richImm.setInputMethodAndSubtype(token, lastActiveSubtype);
527                return;
528            }
529            richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */);
530        }
531    }
532
533    // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial
534    // JNI call as much as possible.
535    static {
536        JniUtils.loadNativeLibrary();
537    }
538
539    public LatinIME() {
540        super();
541        mSettings = Settings.getInstance();
542        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
543        mKeyboardSwitcher = KeyboardSwitcher.getInstance();
544        mSpecialKeyDetector = new SpecialKeyDetector(this);
545        mIsHardwareAcceleratedDrawingEnabled =
546                InputMethodServiceCompatUtils.enableHardwareAcceleration(this);
547        Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled);
548    }
549
550    @Override
551    public void onCreate() {
552        Settings.init(this);
553        DebugFlags.init(PreferenceManager.getDefaultSharedPreferences(this));
554        RichInputMethodManager.init(this);
555        mRichImm = RichInputMethodManager.getInstance();
556        SubtypeSwitcher.init(this);
557        KeyboardSwitcher.init(this);
558        AudioAndHapticFeedbackManager.init(this);
559        AccessibilityUtils.init(this);
560        StatsUtils.init(this);
561
562        super.onCreate();
563
564        mHandler.onCreate();
565        DEBUG = DebugFlags.DEBUG_ENABLED;
566
567        // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}.
568        loadSettings();
569        resetSuggest();
570
571        // Register to receive ringer mode change and network state change.
572        // Also receive installation and removal of a dictionary pack.
573        final IntentFilter filter = new IntentFilter();
574        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
575        filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
576        registerReceiver(mConnectivityAndRingerModeChangeReceiver, filter);
577
578        final IntentFilter packageFilter = new IntentFilter();
579        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
580        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
581        packageFilter.addDataScheme(SCHEME_PACKAGE);
582        registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
583
584        final IntentFilter newDictFilter = new IntentFilter();
585        newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
586        registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
587
588        final IntentFilter dictDumpFilter = new IntentFilter();
589        dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION);
590        registerReceiver(mDictionaryDumpBroadcastReceiver, dictDumpFilter);
591
592        DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this);
593
594        StatsUtils.onCreate(mSettings.getCurrent());
595    }
596
597    // Has to be package-visible for unit tests
598    @UsedForTesting
599    void loadSettings() {
600        final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
601        final EditorInfo editorInfo = getCurrentInputEditorInfo();
602        final InputAttributes inputAttributes = new InputAttributes(
603                editorInfo, isFullscreenMode(), getPackageName());
604        mSettings.loadSettings(this, locale, inputAttributes);
605        final SettingsValues currentSettingsValues = mSettings.getCurrent();
606        AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues);
607        // This method is called on startup and language switch, before the new layout has
608        // been displayed. Opening dictionaries never affects responsivity as dictionaries are
609        // asynchronously loaded.
610        if (!mHandler.hasPendingReopenDictionaries()) {
611            resetSuggestForLocale(locale);
612        }
613        mDictionaryFacilitator.updateEnabledSubtypes(mRichImm.getMyEnabledInputMethodSubtypeList(
614                true /* allowsImplicitlySelectedSubtypes */));
615        refreshPersonalizationDictionarySession(currentSettingsValues);
616        StatsUtils.onLoadSettings(currentSettingsValues);
617    }
618
619    private void refreshPersonalizationDictionarySession(
620            final SettingsValues currentSettingsValues) {
621        mPersonalizationDictionaryUpdater.onLoadSettings(
622                currentSettingsValues.mUsePersonalizedDicts,
623                mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes());
624        mContextualDictionaryUpdater.onLoadSettings(currentSettingsValues.mUsePersonalizedDicts);
625        final boolean shouldKeepUserHistoryDictionaries;
626        if (currentSettingsValues.mUsePersonalizedDicts) {
627            shouldKeepUserHistoryDictionaries = true;
628        } else {
629            shouldKeepUserHistoryDictionaries = false;
630        }
631        if (!shouldKeepUserHistoryDictionaries) {
632            // Remove user history dictionaries.
633            PersonalizationHelper.removeAllUserHistoryDictionaries(this);
634            mDictionaryFacilitator.clearUserHistoryDictionary();
635        }
636    }
637
638    // Note that this method is called from a non-UI thread.
639    @Override
640    public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) {
641        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
642        if (mainKeyboardView != null) {
643            mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
644        }
645        if (mHandler.hasPendingWaitForDictionaryLoad()) {
646            mHandler.cancelWaitForDictionaryLoad();
647            mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */,
648                    false /* shouldDelay */);
649        }
650    }
651
652    private void resetSuggest() {
653        final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
654        final String switcherLocaleStr = switcherSubtypeLocale.toString();
655        final Locale subtypeLocale;
656        if (TextUtils.isEmpty(switcherLocaleStr)) {
657            // This happens in very rare corner cases - for example, immediately after a switch
658            // to LatinIME has been requested, about a frame later another switch happens. In this
659            // case, we are about to go down but we still don't know it, however the system tells
660            // us there is no current subtype so the locale is the empty string. Take the best
661            // possible guess instead -- it's bound to have no consequences, and we have no way
662            // of knowing anyway.
663            Log.e(TAG, "System is reporting no current subtype.");
664            subtypeLocale = getResources().getConfiguration().locale;
665        } else {
666            subtypeLocale = switcherSubtypeLocale;
667        }
668        resetSuggestForLocale(subtypeLocale);
669    }
670
671    /**
672     * Reset suggest by loading dictionaries for the locale and the current settings values.
673     *
674     * @param locale the locale
675     */
676    private void resetSuggestForLocale(final Locale locale) {
677        final SettingsValues settingsValues = mSettings.getCurrent();
678        mDictionaryFacilitator.resetDictionaries(this /* context */, locale,
679                settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts,
680                false /* forceReloadMainDictionary */, this);
681        if (settingsValues.mAutoCorrectionEnabledPerUserSettings) {
682            mInputLogic.mSuggest.setAutoCorrectionThreshold(
683                    settingsValues.mAutoCorrectionThreshold);
684        }
685    }
686
687    /**
688     * Reset suggest by loading the main dictionary of the current locale.
689     */
690    /* package private */ void resetSuggestMainDict() {
691        final SettingsValues settingsValues = mSettings.getCurrent();
692        mDictionaryFacilitator.resetDictionaries(this /* context */,
693                mDictionaryFacilitator.getLocale(), settingsValues.mUseContactsDict,
694                settingsValues.mUsePersonalizedDicts, true /* forceReloadMainDictionary */, this);
695    }
696
697    @Override
698    public void onDestroy() {
699        mDictionaryFacilitator.closeDictionaries();
700        mPersonalizationDictionaryUpdater.onDestroy();
701        mContextualDictionaryUpdater.onDestroy();
702        mSettings.onDestroy();
703        unregisterReceiver(mConnectivityAndRingerModeChangeReceiver);
704        unregisterReceiver(mDictionaryPackInstallReceiver);
705        unregisterReceiver(mDictionaryDumpBroadcastReceiver);
706        StatsUtils.onDestroy();
707        super.onDestroy();
708    }
709
710    @UsedForTesting
711    public void recycle() {
712        unregisterReceiver(mDictionaryPackInstallReceiver);
713        unregisterReceiver(mDictionaryDumpBroadcastReceiver);
714        unregisterReceiver(mConnectivityAndRingerModeChangeReceiver);
715        mInputLogic.recycle();
716    }
717
718    @Override
719    public void onConfigurationChanged(final Configuration conf) {
720        SettingsValues settingsValues = mSettings.getCurrent();
721        if (settingsValues.mDisplayOrientation != conf.orientation) {
722            mHandler.startOrientationChanging();
723            mInputLogic.onOrientationChange(mSettings.getCurrent());
724        }
725        if (settingsValues.mHasHardwareKeyboard != Settings.readHasHardwareKeyboard(conf)) {
726            // If the state of having a hardware keyboard changed, then we want to reload the
727            // settings to adjust for that.
728            // TODO: we should probably do this unconditionally here, rather than only when we
729            // have a change in hardware keyboard configuration.
730            loadSettings();
731            settingsValues = mSettings.getCurrent();
732            if (settingsValues.mHasHardwareKeyboard) {
733                // We call cleanupInternalStateForFinishInput() because it's the right thing to do;
734                // however, it seems at the moment the framework is passing us a seemingly valid
735                // but actually non-functional InputConnection object. So if this bug ever gets
736                // fixed we'll be able to remove the composition, but until it is this code is
737                // actually not doing much.
738                cleanupInternalStateForFinishInput();
739            }
740        }
741        // TODO: Remove this test.
742        if (!conf.locale.equals(mPersonalizationDictionaryUpdater.getLocale())) {
743            refreshPersonalizationDictionarySession(settingsValues);
744        }
745        super.onConfigurationChanged(conf);
746    }
747
748    @Override
749    public View onCreateInputView() {
750        return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
751    }
752
753    @Override
754    public void setInputView(final View view) {
755        super.setInputView(view);
756        mInputView = view;
757        mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
758        if (hasSuggestionStripView()) {
759            mSuggestionStripView.setListener(this, view);
760        }
761        mInputLogic.setTextDecoratorUi(new TextDecoratorUi(this, view));
762    }
763
764    @Override
765    public void setExtractView(final View view) {
766        final TextView prevExtractEditText = mExtractEditText;
767        super.setExtractView(view);
768        TextView nextExtractEditText = null;
769        if (view != null) {
770            final View extractEditText = view.findViewById(android.R.id.inputExtractEditText);
771            if (extractEditText instanceof TextView) {
772                nextExtractEditText = (TextView)extractEditText;
773            }
774        }
775        if (prevExtractEditText == nextExtractEditText) {
776            return;
777        }
778        if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && prevExtractEditText != null) {
779            prevExtractEditText.getViewTreeObserver().removeOnPreDrawListener(
780                    mExtractTextViewPreDrawListener);
781        }
782        mExtractEditText = nextExtractEditText;
783        if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && mExtractEditText != null) {
784            mExtractEditText.getViewTreeObserver().addOnPreDrawListener(
785                    mExtractTextViewPreDrawListener);
786        }
787    }
788
789    private final ViewTreeObserver.OnPreDrawListener mExtractTextViewPreDrawListener =
790            new ViewTreeObserver.OnPreDrawListener() {
791                @Override
792                public boolean onPreDraw() {
793                    onExtractTextViewPreDraw();
794                    return true;
795                }
796            };
797
798    private void onExtractTextViewPreDraw() {
799        if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || !isFullscreenMode()
800                || mExtractEditText == null) {
801            return;
802        }
803        final CursorAnchorInfo info = CursorAnchorInfoUtils.getCursorAnchorInfo(mExtractEditText);
804        mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info));
805    }
806
807    @Override
808    public void setCandidatesView(final View view) {
809        // To ensure that CandidatesView will never be set.
810        return;
811    }
812
813    @Override
814    public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
815        mHandler.onStartInput(editorInfo, restarting);
816    }
817
818    @Override
819    public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
820        mHandler.onStartInputView(editorInfo, restarting);
821    }
822
823    @Override
824    public void onFinishInputView(final boolean finishingInput) {
825        mHandler.onFinishInputView(finishingInput);
826    }
827
828    @Override
829    public void onFinishInput() {
830        mHandler.onFinishInput();
831    }
832
833    @Override
834    public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) {
835        // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
836        // is not guaranteed. It may even be called at the same time on a different thread.
837        mSubtypeSwitcher.onSubtypeChanged(subtype);
838        mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype),
839                mSettings.getCurrent());
840        loadKeyboard();
841    }
842
843    private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
844        super.onStartInput(editorInfo, restarting);
845    }
846
847    @SuppressWarnings("deprecation")
848    private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
849        super.onStartInputView(editorInfo, restarting);
850        mRichImm.clearSubtypeCaches();
851        final KeyboardSwitcher switcher = mKeyboardSwitcher;
852        switcher.updateKeyboardTheme();
853        final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
854        // If we are starting input in a different text field from before, we'll have to reload
855        // settings, so currentSettingsValues can't be final.
856        SettingsValues currentSettingsValues = mSettings.getCurrent();
857
858        if (editorInfo == null) {
859            Log.e(TAG, "Null EditorInfo in onStartInputView()");
860            if (DebugFlags.DEBUG_ENABLED) {
861                throw new NullPointerException("Null EditorInfo in onStartInputView()");
862            }
863            return;
864        }
865        if (DEBUG) {
866            Log.d(TAG, "onStartInputView: editorInfo:"
867                    + String.format("inputType=0x%08x imeOptions=0x%08x",
868                            editorInfo.inputType, editorInfo.imeOptions));
869            Log.d(TAG, "All caps = "
870                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0)
871                    + ", sentence caps = "
872                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0)
873                    + ", word caps = "
874                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
875        }
876        Log.i(TAG, "Starting input. Cursor position = "
877                + editorInfo.initialSelStart + "," + editorInfo.initialSelEnd);
878        // TODO: Consolidate these checks with {@link InputAttributes}.
879        if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
880            Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions);
881            Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead");
882        }
883        if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) {
884            Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions);
885            Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
886        }
887
888        // In landscape mode, this method gets called without the input view being created.
889        if (mainKeyboardView == null) {
890            return;
891        }
892
893        // Forward this event to the accessibility utilities, if enabled.
894        final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
895        if (accessUtils.isTouchExplorationEnabled()) {
896            accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
897        }
898
899        final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo);
900        final boolean isDifferentTextField = !restarting || inputTypeChanged;
901        if (isDifferentTextField) {
902            mSubtypeSwitcher.updateParametersOnStartInputView();
903        }
904
905        // The EditorInfo might have a flag that affects fullscreen mode.
906        // Note: This call should be done by InputMethodService?
907        updateFullscreenMode();
908
909        // ALERT: settings have not been reloaded and there is a chance they may be stale.
910        // In the practice, if it is, we should have gotten onConfigurationChanged so it should
911        // be fine, but this is horribly confusing and must be fixed AS SOON AS POSSIBLE.
912
913        // In some cases the input connection has not been reset yet and we can't access it. In
914        // this case we will need to call loadKeyboard() later, when it's accessible, so that we
915        // can go into the correct mode, so we need to do some housekeeping here.
916        final boolean needToCallLoadKeyboardLater;
917        final Suggest suggest = mInputLogic.mSuggest;
918        if (!currentSettingsValues.mHasHardwareKeyboard) {
919            // The app calling setText() has the effect of clearing the composing
920            // span, so we should reset our state unconditionally, even if restarting is true.
921            // We also tell the input logic about the combining rules for the current subtype, so
922            // it can adjust its combiners if needed.
923            mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype(),
924                    currentSettingsValues);
925
926            // Note: the following does a round-trip IPC on the main thread: be careful
927            final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
928            if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) {
929                // TODO: Do this automatically.
930                resetSuggest();
931            }
932
933            // TODO[IL]: Can the following be moved to InputLogic#startInput?
934            if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
935                    editorInfo.initialSelStart, editorInfo.initialSelEnd,
936                    false /* shouldFinishComposition */)) {
937                // Sometimes, while rotating, for some reason the framework tells the app we are not
938                // connected to it and that means we can't refresh the cache. In this case, schedule
939                // a refresh later.
940                // We try resetting the caches up to 5 times before giving up.
941                mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
942                // mLastSelection{Start,End} are reset later in this method, no need to do it here
943                needToCallLoadKeyboardLater = true;
944            } else {
945                // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best
946                // effort to work around this bug.
947                mInputLogic.mConnection.tryFixLyingCursorPosition();
948                mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */,
949                        true /* shouldDelay */);
950                needToCallLoadKeyboardLater = false;
951            }
952        } else {
953            // If we have a hardware keyboard we don't need to call loadKeyboard later anyway.
954            needToCallLoadKeyboardLater = false;
955        }
956
957        if (isDifferentTextField ||
958                !currentSettingsValues.hasSameOrientation(getResources().getConfiguration())) {
959            loadSettings();
960        }
961        if (isDifferentTextField) {
962            mainKeyboardView.closing();
963            currentSettingsValues = mSettings.getCurrent();
964
965            if (currentSettingsValues.mAutoCorrectionEnabledPerUserSettings) {
966                suggest.setAutoCorrectionThreshold(
967                        currentSettingsValues.mAutoCorrectionThreshold);
968            }
969
970            switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(),
971                    getCurrentRecapitalizeState());
972            if (needToCallLoadKeyboardLater) {
973                // If we need to call loadKeyboard again later, we need to save its state now. The
974                // later call will be done in #retryResetCaches.
975                switcher.saveKeyboardState();
976            }
977        } else if (restarting) {
978            // TODO: Come up with a more comprehensive way to reset the keyboard layout when
979            // a keyboard layout set doesn't get reloaded in this method.
980            switcher.resetKeyboardStateToAlphabet(getCurrentAutoCapsState(),
981                    getCurrentRecapitalizeState());
982            // In apps like Talk, we come here when the text is sent and the field gets emptied and
983            // we need to re-evaluate the shift state, but not the whole layout which would be
984            // disruptive.
985            // Space state must be updated before calling updateShiftState
986            switcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
987                    getCurrentRecapitalizeState());
988        }
989        // This will set the punctuation suggestions if next word suggestion is off;
990        // otherwise it will clear the suggestion strip.
991        setNeutralSuggestionStrip();
992
993        mHandler.cancelUpdateSuggestionStrip();
994
995        mainKeyboardView.setMainDictionaryAvailability(
996                mDictionaryFacilitator.hasInitializedMainDictionary());
997        mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
998                currentSettingsValues.mKeyPreviewPopupDismissDelay);
999        mainKeyboardView.setSlidingKeyInputPreviewEnabled(
1000                currentSettingsValues.mSlidingKeyInputPreviewEnabled);
1001        mainKeyboardView.setGestureHandlingEnabledByUser(
1002                currentSettingsValues.mGestureInputEnabled,
1003                currentSettingsValues.mGestureTrailEnabled,
1004                currentSettingsValues.mGestureFloatingPreviewTextEnabled);
1005
1006        // Contextual dictionary should be updated for the current application.
1007        mContextualDictionaryUpdater.onStartInputView(editorInfo.packageName);
1008        if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
1009    }
1010
1011    @Override
1012    public void onWindowHidden() {
1013        super.onWindowHidden();
1014        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1015        if (mainKeyboardView != null) {
1016            mainKeyboardView.closing();
1017        }
1018    }
1019
1020    private void onFinishInputInternal() {
1021        super.onFinishInput();
1022
1023        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1024        if (mainKeyboardView != null) {
1025            mainKeyboardView.closing();
1026        }
1027    }
1028
1029    private void onFinishInputViewInternal(final boolean finishingInput) {
1030        super.onFinishInputView(finishingInput);
1031        cleanupInternalStateForFinishInput();
1032    }
1033
1034    private void cleanupInternalStateForFinishInput() {
1035        mKeyboardSwitcher.deallocateMemory();
1036        // Remove pending messages related to update suggestions
1037        mHandler.cancelUpdateSuggestionStrip();
1038        // Should do the following in onFinishInputInternal but until JB MR2 it's not called :(
1039        mInputLogic.finishInput();
1040    }
1041
1042    @Override
1043    public void onUpdateSelection(final int oldSelStart, final int oldSelEnd,
1044            final int newSelStart, final int newSelEnd,
1045            final int composingSpanStart, final int composingSpanEnd) {
1046        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
1047                composingSpanStart, composingSpanEnd);
1048        if (DEBUG) {
1049            Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd
1050                    + ", nss=" + newSelStart + ", nse=" + newSelEnd
1051                    + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd);
1052        }
1053
1054        // This call happens when we have a hardware keyboard as well as when we don't. While we
1055        // don't support hardware keyboards yet we should avoid doing the processing associated
1056        // with cursor movement when we have a hardware keyboard since we are not in charge.
1057        final SettingsValues settingsValues = mSettings.getCurrent();
1058        if ((!settingsValues.mHasHardwareKeyboard || ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED)
1059                && mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd)) {
1060            mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
1061                    getCurrentRecapitalizeState());
1062        }
1063    }
1064
1065    // We cannot mark this method as @Override until new SDK becomes publicly available.
1066    // @Override
1067    public void onUpdateCursorAnchorInfo(final CursorAnchorInfo info) {
1068        if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || isFullscreenMode()) {
1069            return;
1070        }
1071        mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info));
1072    }
1073
1074    /**
1075     * This is called when the user has clicked on the extracted text view,
1076     * when running in fullscreen mode.  The default implementation hides
1077     * the suggestions view when this happens, but only if the extracted text
1078     * editor has a vertical scroll bar because its text doesn't fit.
1079     * Here we override the behavior due to the possibility that a re-correction could
1080     * cause the suggestions strip to disappear and re-appear.
1081     */
1082    @Override
1083    public void onExtractedTextClicked() {
1084        if (mSettings.getCurrent().needsToLookupSuggestions()) {
1085            return;
1086        }
1087
1088        super.onExtractedTextClicked();
1089    }
1090
1091    /**
1092     * This is called when the user has performed a cursor movement in the
1093     * extracted text view, when it is running in fullscreen mode.  The default
1094     * implementation hides the suggestions view when a vertical movement
1095     * happens, but only if the extracted text editor has a vertical scroll bar
1096     * because its text doesn't fit.
1097     * Here we override the behavior due to the possibility that a re-correction could
1098     * cause the suggestions strip to disappear and re-appear.
1099     */
1100    @Override
1101    public void onExtractedCursorMovement(final int dx, final int dy) {
1102        if (mSettings.getCurrent().needsToLookupSuggestions()) {
1103            return;
1104        }
1105
1106        super.onExtractedCursorMovement(dx, dy);
1107    }
1108
1109    @Override
1110    public void hideWindow() {
1111        mKeyboardSwitcher.onHideWindow();
1112
1113        if (TRACE) Debug.stopMethodTracing();
1114        if (isShowingOptionDialog()) {
1115            mOptionsDialog.dismiss();
1116            mOptionsDialog = null;
1117        }
1118        super.hideWindow();
1119    }
1120
1121    @Override
1122    public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
1123        if (DEBUG) {
1124            Log.i(TAG, "Received completions:");
1125            if (applicationSpecifiedCompletions != null) {
1126                for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
1127                    Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
1128                }
1129            }
1130        }
1131        if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) {
1132            return;
1133        }
1134        // If we have an update request in flight, we need to cancel it so it does not override
1135        // these completions.
1136        mHandler.cancelUpdateSuggestionStrip();
1137        if (applicationSpecifiedCompletions == null) {
1138            setNeutralSuggestionStrip();
1139            return;
1140        }
1141
1142        final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
1143                SuggestedWords.getFromApplicationSpecifiedCompletions(
1144                        applicationSpecifiedCompletions);
1145        final SuggestedWords suggestedWords = new SuggestedWords(applicationSuggestedWords,
1146                null /* rawSuggestions */, false /* typedWordValid */, false /* willAutoCorrect */,
1147                false /* isObsoleteSuggestions */,
1148                SuggestedWords.INPUT_STYLE_APPLICATION_SPECIFIED /* inputStyle */);
1149        // When in fullscreen mode, show completions generated by the application forcibly
1150        setSuggestedWords(suggestedWords);
1151    }
1152
1153    @Override
1154    public void onComputeInsets(final InputMethodService.Insets outInsets) {
1155        super.onComputeInsets(outInsets);
1156        final SettingsValues settingsValues = mSettings.getCurrent();
1157        final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
1158        if (visibleKeyboardView == null || !hasSuggestionStripView()) {
1159            return;
1160        }
1161        final int inputHeight = mInputView.getHeight();
1162        final boolean hasHardwareKeyboard = settingsValues.mHasHardwareKeyboard;
1163        if (hasHardwareKeyboard && visibleKeyboardView.getVisibility() == View.GONE) {
1164            // If there is a hardware keyboard and a visible software keyboard view has been hidden,
1165            // no visual element will be shown on the screen.
1166            outInsets.touchableInsets = inputHeight;
1167            outInsets.visibleTopInsets = inputHeight;
1168            return;
1169        }
1170        final int suggestionsHeight = (!mKeyboardSwitcher.isShowingEmojiPalettes()
1171                && mSuggestionStripView.getVisibility() == View.VISIBLE)
1172                ? mSuggestionStripView.getHeight() : 0;
1173        final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - suggestionsHeight;
1174        mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY);
1175        // Need to set touchable region only if a keyboard view is being shown.
1176        if (visibleKeyboardView.isShown()) {
1177            final int touchLeft = 0;
1178            final int touchTop = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
1179            final int touchRight = visibleKeyboardView.getWidth();
1180            final int touchBottom = inputHeight
1181                    // Extend touchable region below the keyboard.
1182                    + EXTENDED_TOUCHABLE_REGION_HEIGHT;
1183            outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
1184            outInsets.touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom);
1185        }
1186        outInsets.contentTopInsets = visibleTopY;
1187        outInsets.visibleTopInsets = visibleTopY;
1188    }
1189
1190    @Override
1191    public boolean onEvaluateInputViewShown() {
1192        // Always show {@link InputView}.
1193        return true;
1194    }
1195
1196    @Override
1197    public boolean onShowInputRequested(final int flags, final boolean configChange) {
1198        final SettingsValues settingsValues = mSettings.getCurrent();
1199        if ((flags & InputMethod.SHOW_EXPLICIT) == 0 && settingsValues.mHasHardwareKeyboard) {
1200            // Even when IME is implicitly shown and physical keyboard is connected, we should
1201            // show {@link InputView}.
1202            // See {@link InputMethodService#onShowInputRequested(int,boolean)}.
1203            return true;
1204        }
1205        return super.onShowInputRequested(flags, configChange);
1206    }
1207
1208    @Override
1209    public boolean onEvaluateFullscreenMode() {
1210        final SettingsValues settingsValues = mSettings.getCurrent();
1211        if (settingsValues.mHasHardwareKeyboard) {
1212            // If there is a hardware keyboard, disable full screen mode.
1213            return false;
1214        }
1215        // Reread resource value here, because this method is called by the framework as needed.
1216        final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources());
1217        if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) {
1218            // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI
1219            // implies NO_FULLSCREEN. However, the framework mistakenly does.  i.e. NO_EXTRACT_UI
1220            // without NO_FULLSCREEN doesn't work as expected. Because of this we need this
1221            // hack for now.  Let's get rid of this once the framework gets fixed.
1222            final EditorInfo ei = getCurrentInputEditorInfo();
1223            return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0));
1224        } else {
1225            return false;
1226        }
1227    }
1228
1229    @Override
1230    public void updateFullscreenMode() {
1231        // Override layout parameters to expand {@link SoftInputWindow} to the entire screen.
1232        // See {@link InputMethodService#setinputView(View) and
1233        // {@link SoftInputWindow#updateWidthHeight(WindowManager.LayoutParams)}.
1234        final Window window = getWindow().getWindow();
1235        ViewLayoutUtils.updateLayoutHeightOf(window, LayoutParams.MATCH_PARENT);
1236        // This method may be called before {@link #setInputView(View)}.
1237        if (mInputView != null) {
1238            // In non-fullscreen mode, {@link InputView} and its parent inputArea should expand to
1239            // the entire screen and be placed at the bottom of {@link SoftInputWindow}.
1240            // In fullscreen mode, these shouldn't expand to the entire screen and should be
1241            // coexistent with {@link #mExtractedArea} above.
1242            // See {@link InputMethodService#setInputView(View) and
1243            // com.android.internal.R.layout.input_method.xml.
1244            final int layoutHeight = isFullscreenMode()
1245                    ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT;
1246            final View inputArea = window.findViewById(android.R.id.inputArea);
1247            ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight);
1248            ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM);
1249            ViewLayoutUtils.updateLayoutHeightOf(mInputView, layoutHeight);
1250        }
1251        super.updateFullscreenMode();
1252        mInputLogic.onUpdateFullscreenMode(isFullscreenMode());
1253    }
1254
1255    private int getCurrentAutoCapsState() {
1256        return mInputLogic.getCurrentAutoCapsState(mSettings.getCurrent());
1257    }
1258
1259    private int getCurrentRecapitalizeState() {
1260        return mInputLogic.getCurrentRecapitalizeState();
1261    }
1262
1263    public Locale getCurrentSubtypeLocale() {
1264        return mSubtypeSwitcher.getCurrentSubtypeLocale();
1265    }
1266
1267    /**
1268     * @param codePoints code points to get coordinates for.
1269     * @return x,y coordinates for this keyboard, as a flattened array.
1270     */
1271    public int[] getCoordinatesForCurrentKeyboard(final int[] codePoints) {
1272        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1273        if (null == keyboard) {
1274            return CoordinateUtils.newCoordinateArray(codePoints.length,
1275                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
1276        } else {
1277            return keyboard.getCoordinates(codePoints);
1278        }
1279    }
1280
1281    // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
1282    // pressed.
1283    @Override
1284    public void addWordToUserDictionary(final String word) {
1285        if (TextUtils.isEmpty(word)) {
1286            // Probably never supposed to happen, but just in case.
1287            return;
1288        }
1289        final String wordToEdit;
1290        if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) {
1291            wordToEdit = word.toLowerCase(getCurrentSubtypeLocale());
1292        } else {
1293            wordToEdit = word;
1294        }
1295        mDictionaryFacilitator.addWordToUserDictionary(this /* context */, wordToEdit);
1296        mInputLogic.onAddWordToUserDictionary();
1297    }
1298
1299    // Callback for the {@link SuggestionStripView}, to call when the important notice strip is
1300    // pressed.
1301    @Override
1302    public void showImportantNoticeContents() {
1303        showOptionDialog(new ImportantNoticeDialog(this /* context */, this /* listener */));
1304    }
1305
1306    // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener}
1307    @Override
1308    public void onClickSettingsOfImportantNoticeDialog(final int nextVersion) {
1309        launchSettings();
1310    }
1311
1312    // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener}
1313    @Override
1314    public void onUserAcknowledgmentOfImportantNoticeDialog(final int nextVersion) {
1315        setNeutralSuggestionStrip();
1316    }
1317
1318    public void displaySettingsDialog() {
1319        if (isShowingOptionDialog()) {
1320            return;
1321        }
1322        showSubtypeSelectorAndSettings();
1323    }
1324
1325    @Override
1326    public boolean onCustomRequest(final int requestCode) {
1327        if (isShowingOptionDialog()) return false;
1328        switch (requestCode) {
1329        case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
1330            if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
1331                mRichImm.getInputMethodManager().showInputMethodPicker();
1332                return true;
1333            }
1334            return false;
1335        }
1336        return false;
1337    }
1338
1339    private boolean isShowingOptionDialog() {
1340        return mOptionsDialog != null && mOptionsDialog.isShowing();
1341    }
1342
1343    // TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
1344    public void switchToNextSubtype() {
1345        final IBinder token = getWindow().getWindow().getAttributes().token;
1346        if (shouldSwitchToOtherInputMethods()) {
1347            mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
1348            return;
1349        }
1350        mSubtypeState.switchSubtype(token, mRichImm);
1351    }
1352
1353    // Implementation of {@link KeyboardActionListener}.
1354    @Override
1355    public void onCodeInput(final int codePoint, final int x, final int y,
1356            final boolean isKeyRepeat) {
1357        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1358        // x and y include some padding, but everything down the line (especially native
1359        // code) needs the coordinates in the keyboard frame.
1360        // TODO: We should reconsider which coordinate system should be used to represent
1361        // keyboard event. Also we should pull this up -- LatinIME has no business doing
1362        // this transformation, it should be done already before calling onCodeInput.
1363        final int keyX = mainKeyboardView.getKeyX(x);
1364        final int keyY = mainKeyboardView.getKeyY(y);
1365        final int codeToSend;
1366        if (Constants.CODE_SHIFT == codePoint) {
1367            // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
1368            // alphabetic shift and shift while in symbol layout.
1369            final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard();
1370            if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
1371                codeToSend = codePoint;
1372            } else {
1373                codeToSend = Constants.CODE_SYMBOL_SHIFT;
1374            }
1375        } else {
1376            codeToSend = codePoint;
1377        }
1378        if (Constants.CODE_SHORTCUT == codePoint) {
1379            mSubtypeSwitcher.switchToShortcutIME(this);
1380            // Still call the *#onCodeInput methods for readability.
1381        }
1382        final Event event = createSoftwareKeypressEvent(codeToSend, keyX, keyY, isKeyRepeat);
1383        final InputTransaction completeInputTransaction =
1384                mInputLogic.onCodeInput(mSettings.getCurrent(), event,
1385                        mKeyboardSwitcher.getKeyboardShiftMode(),
1386                        mKeyboardSwitcher.getCurrentKeyboardScriptId(), mHandler);
1387        updateStateAfterInputTransaction(completeInputTransaction);
1388        mKeyboardSwitcher.onCodeInput(codePoint, getCurrentAutoCapsState(),
1389                getCurrentRecapitalizeState());
1390    }
1391
1392    // A helper method to split the code point and the key code. Ultimately, they should not be
1393    // squashed into the same variable, and this method should be removed.
1394    private static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int keyX,
1395             final int keyY, final boolean isKeyRepeat) {
1396        final int keyCode;
1397        final int codePoint;
1398        if (keyCodeOrCodePoint <= 0) {
1399            keyCode = keyCodeOrCodePoint;
1400            codePoint = Event.NOT_A_CODE_POINT;
1401        } else {
1402            keyCode = Event.NOT_A_KEY_CODE;
1403            codePoint = keyCodeOrCodePoint;
1404        }
1405        return Event.createSoftwareKeypressEvent(codePoint, keyCode, keyX, keyY, isKeyRepeat);
1406    }
1407
1408    // Called from PointerTracker through the KeyboardActionListener interface
1409    @Override
1410    public void onTextInput(final String rawText) {
1411        // TODO: have the keyboard pass the correct key code when we need it.
1412        final Event event = Event.createSoftwareTextEvent(rawText, Event.NOT_A_KEY_CODE);
1413        final InputTransaction completeInputTransaction =
1414                mInputLogic.onTextInput(mSettings.getCurrent(), event,
1415                        mKeyboardSwitcher.getKeyboardShiftMode(), mHandler);
1416        updateStateAfterInputTransaction(completeInputTransaction);
1417        mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT, getCurrentAutoCapsState(),
1418                getCurrentRecapitalizeState());
1419    }
1420
1421    @Override
1422    public void onStartBatchInput() {
1423        mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler);
1424    }
1425
1426    @Override
1427    public void onUpdateBatchInput(final InputPointers batchPointers) {
1428        mInputLogic.onUpdateBatchInput(mSettings.getCurrent(), batchPointers, mKeyboardSwitcher);
1429    }
1430
1431    @Override
1432    public void onEndBatchInput(final InputPointers batchPointers) {
1433        mInputLogic.onEndBatchInput(batchPointers);
1434    }
1435
1436    @Override
1437    public void onCancelBatchInput() {
1438        mInputLogic.onCancelBatchInput(mHandler);
1439    }
1440
1441    // This method must run on the UI Thread.
1442    private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
1443            final boolean dismissGestureFloatingPreviewText) {
1444        showSuggestionStrip(suggestedWords);
1445        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1446        mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
1447        if (dismissGestureFloatingPreviewText) {
1448            mainKeyboardView.dismissGestureFloatingPreviewText();
1449        }
1450    }
1451
1452    // Called from PointerTracker through the KeyboardActionListener interface
1453    @Override
1454    public void onFinishSlidingInput() {
1455        // User finished sliding input.
1456        mKeyboardSwitcher.onFinishSlidingInput(getCurrentAutoCapsState(),
1457                getCurrentRecapitalizeState());
1458    }
1459
1460    // Called from PointerTracker through the KeyboardActionListener interface
1461    @Override
1462    public void onCancelInput() {
1463        // User released a finger outside any key
1464        // Nothing to do so far.
1465    }
1466
1467    public boolean hasSuggestionStripView() {
1468        return null != mSuggestionStripView;
1469    }
1470
1471    @Override
1472    public boolean isShowingAddToDictionaryHint() {
1473        return hasSuggestionStripView() && mSuggestionStripView.isShowingAddToDictionaryHint();
1474    }
1475
1476    @Override
1477    public void dismissAddToDictionaryHint() {
1478        if (!hasSuggestionStripView()) {
1479            return;
1480        }
1481        mSuggestionStripView.dismissAddToDictionaryHint();
1482    }
1483
1484    private void setSuggestedWords(final SuggestedWords suggestedWords) {
1485        final SettingsValues currentSettingsValues = mSettings.getCurrent();
1486        mInputLogic.setSuggestedWords(suggestedWords, currentSettingsValues, mHandler);
1487        // TODO: Modify this when we support suggestions with hard keyboard
1488        if (!hasSuggestionStripView()) {
1489            return;
1490        }
1491        if (!onEvaluateInputViewShown()) {
1492            return;
1493        }
1494
1495        final boolean shouldShowImportantNotice =
1496                ImportantNoticeUtils.shouldShowImportantNotice(this);
1497        final boolean shouldShowSuggestionCandidates =
1498                currentSettingsValues.mInputAttributes.mShouldShowSuggestions
1499                && currentSettingsValues.isSuggestionsEnabledPerUserSettings();
1500        final boolean shouldShowSuggestionsStripUnlessPassword = shouldShowImportantNotice
1501                || currentSettingsValues.mShowsVoiceInputKey
1502                || shouldShowSuggestionCandidates
1503                || currentSettingsValues.isApplicationSpecifiedCompletionsOn();
1504        final boolean shouldShowSuggestionsStrip = shouldShowSuggestionsStripUnlessPassword
1505                && !currentSettingsValues.mInputAttributes.mIsPasswordField;
1506        mSuggestionStripView.updateVisibility(shouldShowSuggestionsStrip, isFullscreenMode());
1507        if (!shouldShowSuggestionsStrip) {
1508            return;
1509        }
1510
1511        final boolean isEmptyApplicationSpecifiedCompletions =
1512                currentSettingsValues.isApplicationSpecifiedCompletionsOn()
1513                && suggestedWords.isEmpty();
1514        final boolean noSuggestionsToShow = (SuggestedWords.EMPTY == suggestedWords)
1515                || suggestedWords.isPunctuationSuggestions()
1516                || isEmptyApplicationSpecifiedCompletions;
1517        if (shouldShowImportantNotice && noSuggestionsToShow) {
1518            if (mSuggestionStripView.maybeShowImportantNoticeTitle()) {
1519                return;
1520            }
1521        }
1522
1523        if (currentSettingsValues.isSuggestionsEnabledPerUserSettings()
1524                // We should clear suggestions if there is no suggestion to show.
1525                || noSuggestionsToShow
1526                || currentSettingsValues.isApplicationSpecifiedCompletionsOn()) {
1527            mSuggestionStripView.setSuggestions(suggestedWords,
1528                    SubtypeLocaleUtils.isRtlLanguage(mSubtypeSwitcher.getCurrentSubtype()));
1529        }
1530    }
1531
1532    // TODO[IL]: Move this out of LatinIME.
1533    public void getSuggestedWords(final int inputStyle, final int sequenceNumber,
1534            final OnGetSuggestedWordsCallback callback) {
1535        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1536        if (keyboard == null) {
1537            callback.onGetSuggestedWords(SuggestedWords.EMPTY);
1538            return;
1539        }
1540        mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard.getProximityInfo(),
1541                mKeyboardSwitcher.getKeyboardShiftMode(), inputStyle, sequenceNumber, callback);
1542    }
1543
1544    @Override
1545    public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords) {
1546        final SuggestedWords suggestedWords =
1547                sourceSuggestedWords.isEmpty() ? SuggestedWords.EMPTY : sourceSuggestedWords;
1548        if (SuggestedWords.EMPTY == suggestedWords) {
1549            setNeutralSuggestionStrip();
1550        } else {
1551            setSuggestedWords(suggestedWords);
1552        }
1553        // Cache the auto-correction in accessibility code so we can speak it if the user
1554        // touches a key that will insert it.
1555        AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords,
1556                sourceSuggestedWords.mTypedWord);
1557    }
1558
1559    // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
1560    // interface
1561    @Override
1562    public void pickSuggestionManually(final SuggestedWordInfo suggestionInfo) {
1563        final InputTransaction completeInputTransaction = mInputLogic.onPickSuggestionManually(
1564                mSettings.getCurrent(), suggestionInfo,
1565                mKeyboardSwitcher.getKeyboardShiftMode(),
1566                mKeyboardSwitcher.getCurrentKeyboardScriptId(),
1567                mHandler);
1568        updateStateAfterInputTransaction(completeInputTransaction);
1569    }
1570
1571    @Override
1572    public void showAddToDictionaryHint(final String word) {
1573        if (!hasSuggestionStripView()) {
1574            return;
1575        }
1576        mSuggestionStripView.showAddToDictionaryHint(word);
1577    }
1578
1579    // This will show either an empty suggestion strip (if prediction is enabled) or
1580    // punctuation suggestions (if it's disabled).
1581    @Override
1582    public void setNeutralSuggestionStrip() {
1583        final SettingsValues currentSettings = mSettings.getCurrent();
1584        final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled
1585                ? SuggestedWords.EMPTY : currentSettings.mSpacingAndPunctuations.mSuggestPuncList;
1586        setSuggestedWords(neutralSuggestions);
1587    }
1588
1589    // TODO: Make this private
1590    // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
1591    @UsedForTesting
1592    void loadKeyboard() {
1593        // Since we are switching languages, the most urgent thing is to let the keyboard graphics
1594        // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on
1595        // the screen. Anything we do right now will delay this, so wait until the next frame
1596        // before we do the rest, like reopening dictionaries and updating suggestions. So we
1597        // post a message.
1598        mHandler.postReopenDictionaries();
1599        loadSettings();
1600        if (mKeyboardSwitcher.getMainKeyboardView() != null) {
1601            // Reload keyboard because the current language has been changed.
1602            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent(),
1603                    getCurrentAutoCapsState(), getCurrentRecapitalizeState());
1604        }
1605    }
1606
1607    /**
1608     * After an input transaction has been executed, some state must be updated. This includes
1609     * the shift state of the keyboard and suggestions. This method looks at the finished
1610     * inputTransaction to find out what is necessary and updates the state accordingly.
1611     * @param inputTransaction The transaction that has been executed.
1612     */
1613    private void updateStateAfterInputTransaction(final InputTransaction inputTransaction) {
1614        switch (inputTransaction.getRequiredShiftUpdate()) {
1615        case InputTransaction.SHIFT_UPDATE_LATER:
1616            mHandler.postUpdateShiftState();
1617            break;
1618        case InputTransaction.SHIFT_UPDATE_NOW:
1619            mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
1620                    getCurrentRecapitalizeState());
1621            break;
1622        default: // SHIFT_NO_UPDATE
1623        }
1624        if (inputTransaction.requiresUpdateSuggestions()) {
1625            final int inputStyle;
1626            if (inputTransaction.mEvent.isSuggestionStripPress()) {
1627                // Suggestion strip press: no input.
1628                inputStyle = SuggestedWords.INPUT_STYLE_NONE;
1629            } else if (inputTransaction.mEvent.isGesture()) {
1630                inputStyle = SuggestedWords.INPUT_STYLE_TAIL_BATCH;
1631            } else {
1632                inputStyle = SuggestedWords.INPUT_STYLE_TYPING;
1633            }
1634            mHandler.postUpdateSuggestionStrip(inputStyle);
1635        }
1636        if (inputTransaction.didAffectContents()) {
1637            mSubtypeState.setCurrentSubtypeHasBeenUsed();
1638        }
1639    }
1640
1641    private void hapticAndAudioFeedback(final int code, final int repeatCount) {
1642        final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView();
1643        if (keyboardView != null && keyboardView.isInDraggingFinger()) {
1644            // No need to feedback while finger is dragging.
1645            return;
1646        }
1647        if (repeatCount > 0) {
1648            if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) {
1649                // No need to feedback when repeat delete key will have no effect.
1650                return;
1651            }
1652            // TODO: Use event time that the last feedback has been generated instead of relying on
1653            // a repeat count to thin out feedback.
1654            if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) {
1655                return;
1656            }
1657        }
1658        final AudioAndHapticFeedbackManager feedbackManager =
1659                AudioAndHapticFeedbackManager.getInstance();
1660        if (repeatCount == 0) {
1661            // TODO: Reconsider how to perform haptic feedback when repeating key.
1662            feedbackManager.performHapticFeedback(keyboardView);
1663        }
1664        feedbackManager.performAudioFeedback(code);
1665    }
1666
1667    // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
1668    // release matching call is {@link #onReleaseKey(int,boolean)} below.
1669    @Override
1670    public void onPressKey(final int primaryCode, final int repeatCount,
1671            final boolean isSinglePointer) {
1672        mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer, getCurrentAutoCapsState(),
1673                getCurrentRecapitalizeState());
1674        hapticAndAudioFeedback(primaryCode, repeatCount);
1675    }
1676
1677    // Callback of the {@link KeyboardActionListener}. This is called when a key is released;
1678    // press matching call is {@link #onPressKey(int,int,boolean)} above.
1679    @Override
1680    public void onReleaseKey(final int primaryCode, final boolean withSliding) {
1681        mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding, getCurrentAutoCapsState(),
1682                getCurrentRecapitalizeState());
1683    }
1684
1685    private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) {
1686        final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId);
1687        if (null != decoder) return decoder;
1688        // TODO: create the decoder according to the specification
1689        final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId);
1690        mHardwareEventDecoders.put(deviceId, newDecoder);
1691        return newDecoder;
1692    }
1693
1694    // Hooks for hardware keyboard
1695    @Override
1696    public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
1697        mSpecialKeyDetector.onKeyDown(keyEvent);
1698        if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
1699            return super.onKeyDown(keyCode, keyEvent);
1700        }
1701        final Event event = getHardwareKeyEventDecoder(
1702                keyEvent.getDeviceId()).decodeHardwareKey(keyEvent);
1703        // If the event is not handled by LatinIME, we just pass it to the parent implementation.
1704        // If it's handled, we return true because we did handle it.
1705        if (event.isHandled()) {
1706            mInputLogic.onCodeInput(mSettings.getCurrent(), event,
1707                    mKeyboardSwitcher.getKeyboardShiftMode(),
1708                    // TODO: this is not necessarily correct for a hardware keyboard right now
1709                    mKeyboardSwitcher.getCurrentKeyboardScriptId(),
1710                    mHandler);
1711            return true;
1712        }
1713        return super.onKeyDown(keyCode, keyEvent);
1714    }
1715
1716    @Override
1717    public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) {
1718        mSpecialKeyDetector.onKeyUp(keyEvent);
1719        if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
1720            return super.onKeyUp(keyCode, keyEvent);
1721        }
1722        final long keyIdentifier = keyEvent.getDeviceId() << 32 + keyEvent.getKeyCode();
1723        if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
1724            return true;
1725        }
1726        return super.onKeyUp(keyCode, keyEvent);
1727    }
1728
1729    // onKeyDown and onKeyUp are the main events we are interested in. There are two more events
1730    // related to handling of hardware key events that we may want to implement in the future:
1731    // boolean onKeyLongPress(final int keyCode, final KeyEvent event);
1732    // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event);
1733
1734    // receive ringer mode change and network state change.
1735    private final BroadcastReceiver mConnectivityAndRingerModeChangeReceiver =
1736            new BroadcastReceiver() {
1737        @Override
1738        public void onReceive(final Context context, final Intent intent) {
1739            final String action = intent.getAction();
1740            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
1741                mSubtypeSwitcher.onNetworkStateChanged(intent);
1742            } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
1743                AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
1744            }
1745        }
1746    };
1747
1748    private void launchSettings() {
1749        mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR);
1750        requestHideSelf(0);
1751        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1752        if (mainKeyboardView != null) {
1753            mainKeyboardView.closing();
1754        }
1755        final Intent intent = new Intent();
1756        intent.setClass(LatinIME.this, SettingsActivity.class);
1757        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
1758                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
1759                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
1760        intent.putExtra(SettingsActivity.EXTRA_SHOW_HOME_AS_UP, false);
1761        startActivity(intent);
1762    }
1763
1764    private void showSubtypeSelectorAndSettings() {
1765        final CharSequence title = getString(R.string.english_ime_input_options);
1766        // TODO: Should use new string "Select active input modes".
1767        final CharSequence languageSelectionTitle = getString(R.string.language_selection_title);
1768        final CharSequence[] items = new CharSequence[] {
1769                languageSelectionTitle,
1770                getString(ApplicationUtils.getActivityTitleResId(this, SettingsActivity.class))
1771        };
1772        final OnClickListener listener = new OnClickListener() {
1773            @Override
1774            public void onClick(DialogInterface di, int position) {
1775                di.dismiss();
1776                switch (position) {
1777                case 0:
1778                    final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
1779                            mRichImm.getInputMethodIdOfThisIme(),
1780                            Intent.FLAG_ACTIVITY_NEW_TASK
1781                                    | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
1782                                    | Intent.FLAG_ACTIVITY_CLEAR_TOP);
1783                    intent.putExtra(Intent.EXTRA_TITLE, languageSelectionTitle);
1784                    startActivity(intent);
1785                    break;
1786                case 1:
1787                    launchSettings();
1788                    break;
1789                }
1790            }
1791        };
1792        final AlertDialog.Builder builder = new AlertDialog.Builder(
1793                DialogUtils.getPlatformDialogThemeContext(this));
1794        builder.setItems(items, listener).setTitle(title);
1795        final AlertDialog dialog = builder.create();
1796        dialog.setCancelable(true /* cancelable */);
1797        dialog.setCanceledOnTouchOutside(true /* cancelable */);
1798        showOptionDialog(dialog);
1799    }
1800
1801    // TODO: Move this method out of {@link LatinIME}.
1802    private void showOptionDialog(final AlertDialog dialog) {
1803        final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
1804        if (windowToken == null) {
1805            return;
1806        }
1807
1808        final Window window = dialog.getWindow();
1809        final WindowManager.LayoutParams lp = window.getAttributes();
1810        lp.token = windowToken;
1811        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
1812        window.setAttributes(lp);
1813        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
1814
1815        mOptionsDialog = dialog;
1816        dialog.show();
1817    }
1818
1819    // TODO: can this be removed somehow without breaking the tests?
1820    @UsedForTesting
1821    /* package for test */ SuggestedWords getSuggestedWordsForTest() {
1822        // You may not use this method for anything else than debug
1823        return DEBUG ? mInputLogic.mSuggestedWords : null;
1824    }
1825
1826    // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
1827    @UsedForTesting
1828    /* package for test */ void waitForLoadingDictionaries(final long timeout, final TimeUnit unit)
1829            throws InterruptedException {
1830        mDictionaryFacilitator.waitForLoadingDictionariesForTesting(timeout, unit);
1831    }
1832
1833    // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
1834    @UsedForTesting
1835    /* package for test */ void replaceDictionariesForTest(final Locale locale) {
1836        final SettingsValues settingsValues = mSettings.getCurrent();
1837        mDictionaryFacilitator.resetDictionaries(this, locale,
1838            settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts,
1839            false /* forceReloadMainDictionary */, this /* listener */);
1840    }
1841
1842    // DO NOT USE THIS for any other purpose than testing.
1843    @UsedForTesting
1844    /* package for test */ void clearPersonalizedDictionariesForTest() {
1845        mDictionaryFacilitator.clearUserHistoryDictionary();
1846        mDictionaryFacilitator.clearPersonalizationDictionary();
1847    }
1848
1849    @UsedForTesting
1850    /* package for test */ List<InputMethodSubtype> getEnabledSubtypesForTest() {
1851        return (mRichImm != null) ? mRichImm.getMyEnabledInputMethodSubtypeList(
1852                true /* allowsImplicitlySelectedSubtypes */) : new ArrayList<InputMethodSubtype>();
1853    }
1854
1855    public void dumpDictionaryForDebug(final String dictName) {
1856        if (mDictionaryFacilitator.getLocale() == null) {
1857            resetSuggest();
1858        }
1859        mDictionaryFacilitator.dumpDictionaryForDebug(dictName);
1860    }
1861
1862    public void debugDumpStateAndCrashWithException(final String context) {
1863        final SettingsValues settingsValues = mSettings.getCurrent();
1864        final StringBuilder s = new StringBuilder(settingsValues.toString());
1865        s.append("\nAttributes : ").append(settingsValues.mInputAttributes)
1866                .append("\nContext : ").append(context);
1867        throw new RuntimeException(s.toString());
1868    }
1869
1870    @Override
1871    protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) {
1872        super.dump(fd, fout, args);
1873
1874        final Printer p = new PrintWriterPrinter(fout);
1875        p.println("LatinIME state :");
1876        p.println("  VersionCode = " + ApplicationUtils.getVersionCode(this));
1877        p.println("  VersionName = " + ApplicationUtils.getVersionName(this));
1878        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1879        final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
1880        p.println("  Keyboard mode = " + keyboardMode);
1881        final SettingsValues settingsValues = mSettings.getCurrent();
1882        p.println(settingsValues.dump());
1883        // TODO: Dump all settings values
1884    }
1885
1886    public boolean shouldSwitchToOtherInputMethods() {
1887        // TODO: Revisit here to reorganize the settings. Probably we can/should use different
1888        // strategy once the implementation of
1889        // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} is defined well.
1890        final boolean fallbackValue = mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList;
1891        final IBinder token = getWindow().getWindow().getAttributes().token;
1892        if (token == null) {
1893            return fallbackValue;
1894        }
1895        return mRichImm.shouldOfferSwitchingToNextInputMethod(token, fallbackValue);
1896    }
1897
1898    public boolean shouldShowLanguageSwitchKey() {
1899        // TODO: Revisit here to reorganize the settings. Probably we can/should use different
1900        // strategy once the implementation of
1901        // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} is defined well.
1902        final boolean fallbackValue = mSettings.getCurrent().isLanguageSwitchKeyEnabled();
1903        final IBinder token = getWindow().getWindow().getAttributes().token;
1904        if (token == null) {
1905            return fallbackValue;
1906        }
1907        return mRichImm.shouldOfferSwitchingToNextInputMethod(token, fallbackValue);
1908    }
1909}
1910