LatinIME.java revision 305326e789c3a89517855cc5a023ed1aa3074dc0
1f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org/*
2f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * Copyright (C) 2008 The Android Open Source Project
3f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org *
4f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * use this file except in compliance with the License. You may obtain a copy of
6f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * the License at
7f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org *
8f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * http://www.apache.org/licenses/LICENSE-2.0
9f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org *
10f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * Unless required by applicable law or agreed to in writing, software
11f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * License for the specific language governing permissions and limitations under
14f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * the License.
15f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org */
16f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
17f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgpackage com.android.inputmethod.latin;
18f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
19f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
20f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
21f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
22f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
23f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.app.AlertDialog;
24f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.content.BroadcastReceiver;
25f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.content.Context;
26f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.content.DialogInterface;
27f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.content.Intent;
28f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.content.IntentFilter;
29f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.content.SharedPreferences;
30f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.content.pm.ApplicationInfo;
31f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.content.res.Configuration;
32f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.content.res.Resources;
33f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.graphics.Rect;
34f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.inputmethodservice.InputMethodService;
35f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.media.AudioManager;
36f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.net.ConnectivityManager;
37f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.os.Debug;
38f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.os.IBinder;
39f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.os.Message;
40f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.os.SystemClock;
41f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.preference.PreferenceActivity;
42f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.preference.PreferenceManager;
43f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.text.InputType;
44f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.text.TextUtils;
45f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.util.Log;
46f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.util.PrintWriterPrinter;
47f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.util.Printer;
48f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.KeyCharacterMap;
49f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.KeyEvent;
50f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.View;
51f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.ViewGroup;
52f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.ViewGroup.LayoutParams;
53f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.ViewParent;
54f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.Window;
55f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.WindowManager;
56f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.inputmethod.CompletionInfo;
57f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.inputmethod.CorrectionInfo;
58f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.inputmethod.EditorInfo;
59f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport android.view.inputmethod.InputMethodSubtype;
60f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
61f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.accessibility.AccessibilityUtils;
62f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
63f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.compat.CompatUtils;
64f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
65f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.compat.SuggestionSpanUtils;
66f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.keyboard.Keyboard;
67f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.keyboard.KeyboardActionListener;
68f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.keyboard.KeyboardId;
69f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.keyboard.KeyboardSwitcher;
70f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.keyboard.KeyboardView;
71f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.keyboard.LatinKeyboardView;
72f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.latin.LocaleUtils.RunInLocale;
73f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.latin.define.ProductionFlag;
74f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport com.android.inputmethod.latin.suggestions.SuggestionsView;
75f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
76f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport java.io.FileDescriptor;
77f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport java.io.PrintWriter;
78f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport java.util.ArrayList;
79f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgimport java.util.Locale;
80f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
81f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org/**
82f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org * Input method implementation for Qwerty'ish keyboard.
83f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org */
84f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.orgpublic class LatinIME extends InputMethodService implements KeyboardActionListener,
85f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        SuggestionsView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener {
86f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final String TAG = LatinIME.class.getSimpleName();
87f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final boolean TRACE = false;
88f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static boolean DEBUG;
89f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
90f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
91f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
92f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // How many continuous deletes at which to start deleting at a higher speed.
93f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int DELETE_ACCELERATE_AT = 20;
94f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Key events coming any faster than this are long-presses.
95f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int QUICK_PRESS = 200;
96f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
97f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int PENDING_IMS_CALLBACK_DURATION = 800;
98f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
99f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    /**
100f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org     * The name of the scheme used by the Package Manager to warn of a new package installation,
101f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org     * replacement or removal.
102f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org     */
103f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final String SCHEME_PACKAGE = "package";
104f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
105f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int SPACE_STATE_NONE = 0;
106f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Double space: the state where the user pressed space twice quickly, which LatinIME
107f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // resolved as period-space. Undoing this converts the period to a space.
108f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int SPACE_STATE_DOUBLE = 1;
109f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
110f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // have just been swapped. Undoing this swaps them back; the space is still considered weak.
111f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int SPACE_STATE_SWAP_PUNCTUATION = 2;
112f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
113f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // spaces happen when the user presses space, accepting the current suggestion (whether
114f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // it's an auto-correction or not).
115f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int SPACE_STATE_WEAK = 3;
116f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Phantom space: a not-yet-inserted space that should get inserted on the next input,
117f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // character provided it's not a separator. If it's a separator, the phantom space is dropped.
118f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Phantom spaces happen when a user chooses a word from the suggestion strip.
119f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int SPACE_STATE_PHANTOM = 4;
120f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
121f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Current space state of the input method. This can be any of the above constants.
122f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private int mSpaceState;
123f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
124f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private SettingsValues mCurrentSettings;
125f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
126f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private View mExtractArea;
127f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private View mKeyPreviewBackingView;
128f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private View mSuggestionsContainer;
129f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private SuggestionsView mSuggestionsView;
130f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    /* package for tests */ Suggest mSuggest;
131f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private CompletionInfo[] mApplicationSpecifiedCompletions;
132f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private ApplicationInfo mTargetApplicationInfo;
133f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
134f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private InputMethodManagerCompatWrapper mImm;
135f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private Resources mResources;
136f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private SharedPreferences mPrefs;
137f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    /* package for tests */ final KeyboardSwitcher mKeyboardSwitcher;
138f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private final SubtypeSwitcher mSubtypeSwitcher;
139f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private boolean mShouldSwitchToLastSubtype = true;
140f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
141f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private boolean mIsMainDictionaryAvailable;
142f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private UserBinaryDictionary mUserDictionary;
143f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private UserHistoryDictionary mUserHistoryDictionary;
144f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private boolean mIsUserDictionaryAvailable;
145f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
146f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
147f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private WordComposer mWordComposer = new WordComposer();
148f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private RichInputConnection mConnection = new RichInputConnection();
149f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
150f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Keep track of the last selection range to decide if we need to show word alternatives
151f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private static final int NOT_A_CURSOR_POSITION = -1;
152f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private int mLastSelectionStart = NOT_A_CURSOR_POSITION;
153f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
154f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
155f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
156f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // "expect" it, it means the user actually moved the cursor.
157f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private boolean mExpectingUpdateSelection;
158f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private int mDeleteCount;
159f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private long mLastKeyTime;
160f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
161f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private AudioAndHapticFeedbackManager mFeedbackManager;
162f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
163f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Member variables for remembering the current device orientation.
164f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private int mDisplayOrientation;
165f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
166f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Object for reacting to adding/removing a dictionary pack.
167f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private BroadcastReceiver mDictionaryPackInstallReceiver =
168f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            new DictionaryPackInstallBroadcastReceiver(this);
169f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
170f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    // Keeps track of most recently inserted text (multi-character key) for reverting
171f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private CharSequence mEnteredText;
172f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
173f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private boolean mIsAutoCorrectionIndicatorOn;
174f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
175f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    private AlertDialog mOptionsDialog;
176f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
177f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    public final UIHandler mHandler = new UIHandler(this);
178f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
179f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
180f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private static final int MSG_UPDATE_SHIFT_STATE = 1;
181f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private static final int MSG_SET_BIGRAM_PREDICTIONS = 5;
182f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private static final int MSG_PENDING_IMS_CALLBACK = 6;
183f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private static final int MSG_UPDATE_SUGGESTIONS = 7;
184f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
185f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private int mDelayUpdateSuggestions;
186f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private int mDelayUpdateShiftState;
187f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private long mDoubleSpacesTurnIntoPeriodTimeout;
188f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private long mDoubleSpaceTimerStart;
189f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
190f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public UIHandler(LatinIME outerInstance) {
191f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            super(outerInstance);
192f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
193f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
194f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void onCreate() {
195f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            final Resources res = getOuterInstance().getResources();
196f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            mDelayUpdateSuggestions =
197f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    res.getInteger(R.integer.config_delay_update_suggestions);
198f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            mDelayUpdateShiftState =
199f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    res.getInteger(R.integer.config_delay_update_shift_state);
200f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger(
201f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    R.integer.config_double_spaces_turn_into_period_timeout);
202f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
203f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
204f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        @Override
205f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void handleMessage(Message msg) {
206f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            final LatinIME latinIme = getOuterInstance();
207f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
208f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            switch (msg.what) {
209f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            case MSG_UPDATE_SUGGESTIONS:
210f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.updateSuggestions();
211f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                break;
212f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            case MSG_UPDATE_SHIFT_STATE:
213f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                switcher.updateShiftState();
214f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                break;
215f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            case MSG_SET_BIGRAM_PREDICTIONS:
216f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.updateBigramPredictions();
217f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                break;
218f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            }
219f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
220f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
221f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void postUpdateSuggestions() {
222f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            removeMessages(MSG_UPDATE_SUGGESTIONS);
223f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions);
224f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
225f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
226f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void cancelUpdateSuggestions() {
227f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            removeMessages(MSG_UPDATE_SUGGESTIONS);
228f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
229f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
230f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public boolean hasPendingUpdateSuggestions() {
231f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            return hasMessages(MSG_UPDATE_SUGGESTIONS);
232f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
233f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
234f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void postUpdateShiftState() {
235f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            removeMessages(MSG_UPDATE_SHIFT_STATE);
236f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
237f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
238f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
239f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void cancelUpdateShiftState() {
240f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            removeMessages(MSG_UPDATE_SHIFT_STATE);
241f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
242f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
243f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void postUpdateBigramPredictions() {
244f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
245f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions);
246f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
247f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
248f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void cancelUpdateBigramPredictions() {
249f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
250f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
251f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
252f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void startDoubleSpacesTimer() {
253f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            mDoubleSpaceTimerStart = SystemClock.uptimeMillis();
254f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
255f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
256f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void cancelDoubleSpacesTimer() {
257f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            mDoubleSpaceTimerStart = 0;
258f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
259f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
260f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public boolean isAcceptingDoubleSpaces() {
261f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            return SystemClock.uptimeMillis() - mDoubleSpaceTimerStart
262f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    < mDoubleSpacesTurnIntoPeriodTimeout;
263f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
264f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
265f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        // Working variables for the following methods.
266f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private boolean mIsOrientationChanging;
267f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private boolean mPendingSuccessiveImsCallback;
268f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private boolean mHasPendingStartInput;
269f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private boolean mHasPendingFinishInputView;
270f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private boolean mHasPendingFinishInput;
271f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private EditorInfo mAppliedEditorInfo;
272f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
273f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void startOrientationChanging() {
274f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            removeMessages(MSG_PENDING_IMS_CALLBACK);
275f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            resetPendingImsCallback();
276f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            mIsOrientationChanging = true;
277f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            final LatinIME latinIme = getOuterInstance();
278f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            if (latinIme.isInputViewShown()) {
279f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.mKeyboardSwitcher.saveKeyboardState();
280f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            }
281f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
282f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
283f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private void resetPendingImsCallback() {
284f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            mHasPendingFinishInputView = false;
285f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            mHasPendingFinishInput = false;
286f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            mHasPendingStartInput = false;
287f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
288f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
289f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo,
290f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                boolean restarting) {
291f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            if (mHasPendingFinishInputView)
292f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
293f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            if (mHasPendingFinishInput)
294f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.onFinishInputInternal();
295f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            if (mHasPendingStartInput)
296f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.onStartInputInternal(editorInfo, restarting);
297f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            resetPendingImsCallback();
298f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
299f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
300f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void onStartInput(EditorInfo editorInfo, boolean restarting) {
301f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
302f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                // Typically this is the second onStartInput after orientation changed.
303f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                mHasPendingStartInput = true;
304f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            } else {
305f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                if (mIsOrientationChanging && restarting) {
306f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    // This is the first onStartInput after orientation changed.
307f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    mIsOrientationChanging = false;
308f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    mPendingSuccessiveImsCallback = true;
309f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                }
310f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                final LatinIME latinIme = getOuterInstance();
311f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                executePendingImsCallback(latinIme, editorInfo, restarting);
312f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.onStartInputInternal(editorInfo, restarting);
313f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            }
314f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
315f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
316f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
317f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            if (hasMessages(MSG_PENDING_IMS_CALLBACK)
318f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
319f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                // Typically this is the second onStartInputView after orientation changed.
320f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                resetPendingImsCallback();
321f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            } else {
322f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                if (mPendingSuccessiveImsCallback) {
323f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    // This is the first onStartInputView after orientation changed.
324f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    mPendingSuccessiveImsCallback = false;
325f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    resetPendingImsCallback();
326f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                    sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
327f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                            PENDING_IMS_CALLBACK_DURATION);
328f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                }
329f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                final LatinIME latinIme = getOuterInstance();
330f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                executePendingImsCallback(latinIme, editorInfo, restarting);
331f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.onStartInputViewInternal(editorInfo, restarting);
332f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                mAppliedEditorInfo = editorInfo;
333f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            }
334f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
335f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
336f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void onFinishInputView(boolean finishingInput) {
337f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
338f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                // Typically this is the first onFinishInputView after orientation changed.
339f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                mHasPendingFinishInputView = true;
340f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            } else {
341f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                final LatinIME latinIme = getOuterInstance();
342f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.onFinishInputViewInternal(finishingInput);
343f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                mAppliedEditorInfo = null;
344f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            }
345f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
346f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
347f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        public void onFinishInput() {
348f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
349f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                // Typically this is the first onFinishInput after orientation changed.
350f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                mHasPendingFinishInput = true;
351f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            } else {
352f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                final LatinIME latinIme = getOuterInstance();
353f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                executePendingImsCallback(latinIme, null, false);
354f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                latinIme.onFinishInputInternal();
355f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            }
356f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
357f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    }
358f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
359f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    public LatinIME() {
360f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        super();
361f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
362f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        mKeyboardSwitcher = KeyboardSwitcher.getInstance();
363f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    }
364f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
365f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    @Override
366f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org    public void onCreate() {
367f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
368f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        mPrefs = prefs;
369f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        LatinImeLogger.init(this, prefs);
370f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        if (ProductionFlag.IS_EXPERIMENTAL) {
371f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            ResearchLogger.getInstance().init(this, prefs);
372f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        }
373f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        InputMethodManagerCompatWrapper.init(this);
374f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        SubtypeSwitcher.init(this);
375f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        KeyboardSwitcher.init(this, prefs);
376f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        AccessibilityUtils.init(this);
377f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
378f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        super.onCreate();
379f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
380f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        mImm = InputMethodManagerCompatWrapper.getInstance();
381f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        mHandler.onCreate();
382f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        DEBUG = LatinImeLogger.sDBG;
383f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
384f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        final Resources res = getResources();
385f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        mResources = res;
386f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
387f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        loadSettings();
388f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
389f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        ImfUtils.setAdditionalInputMethodSubtypes(this, mCurrentSettings.getAdditionalSubtypes());
390f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org
391f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        Utils.GCUtils.getInstance().reset();
392f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        boolean tryGC = true;
393f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        // Shouldn't this be removed? I think that from Honeycomb on, the GC is now actually working
394f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        // as expected and this code is useless.
395f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
396f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            try {
397f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                initSuggest();
398f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                tryGC = false;
399f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org            } catch (OutOfMemoryError e) {
400f2ba7591b1407a7ee9209f842c50696914dc2dedkbr@chromium.org                tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
401            }
402        }
403
404        mDisplayOrientation = res.getConfiguration().orientation;
405
406        // Register to receive ringer mode change and network state change.
407        // Also receive installation and removal of a dictionary pack.
408        final IntentFilter filter = new IntentFilter();
409        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
410        filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
411        registerReceiver(mReceiver, filter);
412
413        final IntentFilter packageFilter = new IntentFilter();
414        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
415        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
416        packageFilter.addDataScheme(SCHEME_PACKAGE);
417        registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
418
419        final IntentFilter newDictFilter = new IntentFilter();
420        newDictFilter.addAction(
421                DictionaryPackInstallBroadcastReceiver.NEW_DICTIONARY_INTENT_ACTION);
422        registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
423    }
424
425    // Has to be package-visible for unit tests
426    /* package */ void loadSettings() {
427        // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
428        // is not guaranteed. It may even be called at the same time on a different thread.
429        if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
430        final InputAttributes inputAttributes =
431                new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
432        final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
433            @Override
434            protected SettingsValues job(Resources res) {
435                return new SettingsValues(mPrefs, inputAttributes, LatinIME.this);
436            }
437        };
438        mCurrentSettings = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
439        mFeedbackManager = new AudioAndHapticFeedbackManager(this, mCurrentSettings);
440        resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
441    }
442
443    private void initSuggest() {
444        final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
445        final String localeStr = subtypeLocale.toString();
446
447        final ContactsBinaryDictionary oldContactsDictionary;
448        if (mSuggest != null) {
449            oldContactsDictionary = mSuggest.getContactsDictionary();
450            mSuggest.close();
451        } else {
452            oldContactsDictionary = null;
453        }
454        mSuggest = new Suggest(this, subtypeLocale);
455        if (mCurrentSettings.mCorrectionEnabled) {
456            mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
457        }
458
459        mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
460        if (ProductionFlag.IS_EXPERIMENTAL) {
461            ResearchLogger.getInstance().initSuggest(mSuggest);
462        }
463
464        mUserDictionary = new UserBinaryDictionary(this, localeStr);
465        mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
466        mSuggest.setUserDictionary(mUserDictionary);
467
468        resetContactsDictionary(oldContactsDictionary);
469
470        // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
471        // is not guaranteed. It may even be called at the same time on a different thread.
472        if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
473        mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, mPrefs);
474        mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
475    }
476
477    /**
478     * Resets the contacts dictionary in mSuggest according to the user settings.
479     *
480     * This method takes an optional contacts dictionary to use when the locale hasn't changed
481     * since the contacts dictionary can be opened or closed as necessary depending on the settings.
482     *
483     * @param oldContactsDictionary an optional dictionary to use, or null
484     */
485    private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
486        final boolean shouldSetDictionary = (null != mSuggest && mCurrentSettings.mUseContactsDict);
487
488        final ContactsBinaryDictionary dictionaryToUse;
489        if (!shouldSetDictionary) {
490            // Make sure the dictionary is closed. If it is already closed, this is a no-op,
491            // so it's safe to call it anyways.
492            if (null != oldContactsDictionary) oldContactsDictionary.close();
493            dictionaryToUse = null;
494        } else {
495            final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
496            if (null != oldContactsDictionary) {
497                if (!oldContactsDictionary.mLocale.equals(locale)) {
498                    // If the locale has changed then recreate the contacts dictionary. This
499                    // allows locale dependent rules for handling bigram name predictions.
500                    oldContactsDictionary.close();
501                    dictionaryToUse = new ContactsBinaryDictionary(this, locale);
502                } else {
503                    // Make sure the old contacts dictionary is opened. If it is already open,
504                    // this is a no-op, so it's safe to call it anyways.
505                    oldContactsDictionary.reopen(this);
506                    dictionaryToUse = oldContactsDictionary;
507                }
508            } else {
509                dictionaryToUse = new ContactsBinaryDictionary(this, locale);
510            }
511        }
512
513        if (null != mSuggest) {
514            mSuggest.setContactsDictionary(dictionaryToUse);
515        }
516    }
517
518    /* package private */ void resetSuggestMainDict() {
519        final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
520        mSuggest.resetMainDict(this, subtypeLocale);
521        mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
522    }
523
524    @Override
525    public void onDestroy() {
526        if (mSuggest != null) {
527            mSuggest.close();
528            mSuggest = null;
529        }
530        unregisterReceiver(mReceiver);
531        unregisterReceiver(mDictionaryPackInstallReceiver);
532        LatinImeLogger.commit();
533        LatinImeLogger.onDestroy();
534        super.onDestroy();
535    }
536
537    @Override
538    public void onConfigurationChanged(Configuration conf) {
539        mSubtypeSwitcher.onConfigurationChanged(conf);
540        // If orientation changed while predicting, commit the change
541        if (mDisplayOrientation != conf.orientation) {
542            mDisplayOrientation = conf.orientation;
543            mHandler.startOrientationChanging();
544            mConnection.beginBatchEdit(getCurrentInputConnection());
545            commitTyped(LastComposedWord.NOT_A_SEPARATOR);
546            mConnection.finishComposingText();
547            mConnection.endBatchEdit();
548            if (isShowingOptionDialog())
549                mOptionsDialog.dismiss();
550        }
551        super.onConfigurationChanged(conf);
552    }
553
554    @Override
555    public View onCreateInputView() {
556        return mKeyboardSwitcher.onCreateInputView();
557    }
558
559    @Override
560    public void setInputView(View view) {
561        super.setInputView(view);
562        mExtractArea = getWindow().getWindow().getDecorView()
563                .findViewById(android.R.id.extractArea);
564        mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
565        mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
566        mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view);
567        if (mSuggestionsView != null)
568            mSuggestionsView.setListener(this, view);
569        if (LatinImeLogger.sVISUALDEBUG) {
570            mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
571        }
572    }
573
574    @Override
575    public void setCandidatesView(View view) {
576        // To ensure that CandidatesView will never be set.
577        return;
578    }
579
580    @Override
581    public void onStartInput(EditorInfo editorInfo, boolean restarting) {
582        mHandler.onStartInput(editorInfo, restarting);
583    }
584
585    @Override
586    public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
587        mHandler.onStartInputView(editorInfo, restarting);
588    }
589
590    @Override
591    public void onFinishInputView(boolean finishingInput) {
592        mHandler.onFinishInputView(finishingInput);
593    }
594
595    @Override
596    public void onFinishInput() {
597        mHandler.onFinishInput();
598    }
599
600    @Override
601    public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) {
602        // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
603        // is not guaranteed. It may even be called at the same time on a different thread.
604        mSubtypeSwitcher.updateSubtype(subtype);
605    }
606
607    private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) {
608        super.onStartInput(editorInfo, restarting);
609    }
610
611    @SuppressWarnings("deprecation")
612    private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
613        super.onStartInputView(editorInfo, restarting);
614        final KeyboardSwitcher switcher = mKeyboardSwitcher;
615        LatinKeyboardView inputView = switcher.getKeyboardView();
616
617        if (editorInfo == null) {
618            Log.e(TAG, "Null EditorInfo in onStartInputView()");
619            if (LatinImeLogger.sDBG) {
620                throw new NullPointerException("Null EditorInfo in onStartInputView()");
621            }
622            return;
623        }
624        if (DEBUG) {
625            Log.d(TAG, "onStartInputView: editorInfo:"
626                    + String.format("inputType=0x%08x imeOptions=0x%08x",
627                            editorInfo.inputType, editorInfo.imeOptions));
628            Log.d(TAG, "All caps = "
629                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0)
630                    + ", sentence caps = "
631                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0)
632                    + ", word caps = "
633                    + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
634        }
635        if (ProductionFlag.IS_EXPERIMENTAL) {
636            ResearchLogger.getInstance().start();
637            ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, mPrefs);
638        }
639        if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
640            Log.w(TAG, "Deprecated private IME option specified: "
641                    + editorInfo.privateImeOptions);
642            Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead");
643        }
644        if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) {
645            Log.w(TAG, "Deprecated private IME option specified: "
646                    + editorInfo.privateImeOptions);
647            Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
648        }
649
650        mTargetApplicationInfo =
651                TargetApplicationGetter.getCachedApplicationInfo(editorInfo.packageName);
652        if (null == mTargetApplicationInfo) {
653            new TargetApplicationGetter(this /* context */, this /* listener */)
654                    .execute(editorInfo.packageName);
655        }
656
657        LatinImeLogger.onStartInputView(editorInfo);
658        // In landscape mode, this method gets called without the input view being created.
659        if (inputView == null) {
660            return;
661        }
662
663        // Forward this event to the accessibility utilities, if enabled.
664        final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
665        if (accessUtils.isTouchExplorationEnabled()) {
666            accessUtils.onStartInputViewInternal(editorInfo, restarting);
667        }
668
669        mSubtypeSwitcher.updateParametersOnStartInputView();
670
671        // The EditorInfo might have a flag that affects fullscreen mode.
672        // Note: This call should be done by InputMethodService?
673        updateFullscreenMode();
674        mLastSelectionStart = editorInfo.initialSelStart;
675        mLastSelectionEnd = editorInfo.initialSelEnd;
676        mApplicationSpecifiedCompletions = null;
677
678        inputView.closing();
679        mEnteredText = null;
680        resetComposingState(true /* alsoResetLastComposedWord */);
681        mDeleteCount = 0;
682        mSpaceState = SPACE_STATE_NONE;
683
684        loadSettings();
685
686        if (mSuggest != null && mCurrentSettings.mCorrectionEnabled) {
687            mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
688        }
689
690        switcher.loadKeyboard(editorInfo, mCurrentSettings);
691
692        if (mSuggestionsView != null)
693            mSuggestionsView.clear();
694        setSuggestionStripShownInternal(
695                isSuggestionsStripVisible(), /* needsInputViewShown */ false);
696        // Delay updating suggestions because keyboard input view may not be shown at this point.
697        mHandler.postUpdateSuggestions();
698        mHandler.cancelDoubleSpacesTimer();
699
700        inputView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn,
701                mCurrentSettings.mKeyPreviewPopupDismissDelay);
702        inputView.setProximityCorrectionEnabled(true);
703
704        if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
705    }
706
707    @Override
708    public void onTargetApplicationKnown(final ApplicationInfo info) {
709        mTargetApplicationInfo = info;
710    }
711
712    @Override
713    public void onWindowHidden() {
714        if (ProductionFlag.IS_EXPERIMENTAL) {
715            ResearchLogger.latinIME_onWindowHidden(mLastSelectionStart, mLastSelectionEnd,
716                    getCurrentInputConnection());
717        }
718        super.onWindowHidden();
719        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
720        if (inputView != null) inputView.closing();
721    }
722
723    private void onFinishInputInternal() {
724        super.onFinishInput();
725
726        LatinImeLogger.commit();
727        if (ProductionFlag.IS_EXPERIMENTAL) {
728            ResearchLogger.getInstance().stop();
729        }
730
731        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
732        if (inputView != null) inputView.closing();
733    }
734
735    private void onFinishInputViewInternal(boolean finishingInput) {
736        super.onFinishInputView(finishingInput);
737        mKeyboardSwitcher.onFinishInputView();
738        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
739        if (inputView != null) inputView.cancelAllMessages();
740        // Remove pending messages related to update suggestions
741        mHandler.cancelUpdateSuggestions();
742    }
743
744    @Override
745    public void onUpdateSelection(int oldSelStart, int oldSelEnd,
746            int newSelStart, int newSelEnd,
747            int composingSpanStart, int composingSpanEnd) {
748        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
749                composingSpanStart, composingSpanEnd);
750        if (DEBUG) {
751            Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
752                    + ", ose=" + oldSelEnd
753                    + ", lss=" + mLastSelectionStart
754                    + ", lse=" + mLastSelectionEnd
755                    + ", nss=" + newSelStart
756                    + ", nse=" + newSelEnd
757                    + ", cs=" + composingSpanStart
758                    + ", ce=" + composingSpanEnd);
759        }
760        if (ProductionFlag.IS_EXPERIMENTAL) {
761            final boolean expectingUpdateSelectionFromLogger =
762                    ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
763            ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
764                    oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
765                    composingSpanEnd, mExpectingUpdateSelection,
766                    expectingUpdateSelectionFromLogger, mConnection);
767            if (expectingUpdateSelectionFromLogger) {
768                // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
769                return;
770            }
771        }
772
773        // TODO: refactor the following code to be less contrived.
774        // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means
775        // that the cursor is not at the end of the composing span, or there is a selection.
776        // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place
777        // as last time we were called (if there is a selection, it means the start hasn't
778        // changed, so it's the end that did).
779        final boolean selectionChanged = (newSelStart != composingSpanEnd
780                || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart;
781        // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
782        // span in the view - we can use that to narrow down whether the cursor was moved
783        // by us or not. If we are composing a word but there is no composing span, then
784        // we know for sure the cursor moved while we were composing and we should reset
785        // the state.
786        final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
787        if (!mExpectingUpdateSelection) {
788            // TAKE CARE: there is a race condition when we enter this test even when the user
789            // did not explicitly move the cursor. This happens when typing fast, where two keys
790            // turn this flag on in succession and both onUpdateSelection() calls arrive after
791            // the second one - the first call successfully avoids this test, but the second one
792            // enters. For the moment we rely on noComposingSpan to further reduce the impact.
793
794            // TODO: the following is probably better done in resetEntireInputState().
795            // it should only happen when the cursor moved, and the very purpose of the
796            // test below is to narrow down whether this happened or not. Likewise with
797            // the call to postUpdateShiftState.
798            // We set this to NONE because after a cursor move, we don't want the space
799            // state-related special processing to kick in.
800            mSpaceState = SPACE_STATE_NONE;
801
802            if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
803                resetEntireInputState();
804            }
805
806            mHandler.postUpdateShiftState();
807        }
808        mExpectingUpdateSelection = false;
809        // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not
810        // here. It would probably be too expensive to call directly here but we may want to post a
811        // message to delay it. The point would be to unify behavior between backspace to the
812        // end of a word and manually put the pointer at the end of the word.
813
814        // Make a note of the cursor position
815        mLastSelectionStart = newSelStart;
816        mLastSelectionEnd = newSelEnd;
817    }
818
819    /**
820     * This is called when the user has clicked on the extracted text view,
821     * when running in fullscreen mode.  The default implementation hides
822     * the suggestions view when this happens, but only if the extracted text
823     * editor has a vertical scroll bar because its text doesn't fit.
824     * Here we override the behavior due to the possibility that a re-correction could
825     * cause the suggestions strip to disappear and re-appear.
826     */
827    @Override
828    public void onExtractedTextClicked() {
829        if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
830
831        super.onExtractedTextClicked();
832    }
833
834    /**
835     * This is called when the user has performed a cursor movement in the
836     * extracted text view, when it is running in fullscreen mode.  The default
837     * implementation hides the suggestions view when a vertical movement
838     * happens, but only if the extracted text editor has a vertical scroll bar
839     * because its text doesn't fit.
840     * Here we override the behavior due to the possibility that a re-correction could
841     * cause the suggestions strip to disappear and re-appear.
842     */
843    @Override
844    public void onExtractedCursorMovement(int dx, int dy) {
845        if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
846
847        super.onExtractedCursorMovement(dx, dy);
848    }
849
850    @Override
851    public void hideWindow() {
852        LatinImeLogger.commit();
853        mKeyboardSwitcher.onHideWindow();
854
855        if (TRACE) Debug.stopMethodTracing();
856        if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
857            mOptionsDialog.dismiss();
858            mOptionsDialog = null;
859        }
860        super.hideWindow();
861    }
862
863    @Override
864    public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
865        if (DEBUG) {
866            Log.i(TAG, "Received completions:");
867            if (applicationSpecifiedCompletions != null) {
868                for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
869                    Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
870                }
871            }
872        }
873        if (ProductionFlag.IS_EXPERIMENTAL) {
874            ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
875        }
876        if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
877        mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
878        if (applicationSpecifiedCompletions == null) {
879            clearSuggestions();
880            return;
881        }
882
883        final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
884                SuggestedWords.getFromApplicationSpecifiedCompletions(
885                        applicationSpecifiedCompletions);
886        final SuggestedWords suggestedWords = new SuggestedWords(
887                applicationSuggestedWords,
888                false /* typedWordValid */,
889                false /* hasAutoCorrectionCandidate */,
890                false /* allowsToBeAutoCorrected */,
891                false /* isPunctuationSuggestions */,
892                false /* isObsoleteSuggestions */,
893                false /* isPrediction */);
894        // When in fullscreen mode, show completions generated by the application
895        final boolean isAutoCorrection = false;
896        setSuggestions(suggestedWords, isAutoCorrection);
897        setAutoCorrectionIndicator(isAutoCorrection);
898        // TODO: is this the right thing to do? What should we auto-correct to in
899        // this case? This says to keep whatever the user typed.
900        mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
901        setSuggestionStripShown(true);
902    }
903
904    private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
905        // TODO: Modify this if we support suggestions with hard keyboard
906        if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
907            final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
908            final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false;
909            final boolean shouldShowSuggestions = shown
910                    && (needsInputViewShown ? inputViewShown : true);
911            if (isFullscreenMode()) {
912                mSuggestionsContainer.setVisibility(
913                        shouldShowSuggestions ? View.VISIBLE : View.GONE);
914            } else {
915                mSuggestionsContainer.setVisibility(
916                        shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
917            }
918        }
919    }
920
921    private void setSuggestionStripShown(boolean shown) {
922        setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
923    }
924
925    private int getAdjustedBackingViewHeight() {
926        final int currentHeight = mKeyPreviewBackingView.getHeight();
927        if (currentHeight > 0) {
928            return currentHeight;
929        }
930
931        final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
932        if (keyboardView == null) {
933            return 0;
934        }
935        final int keyboardHeight = keyboardView.getHeight();
936        final int suggestionsHeight = mSuggestionsContainer.getHeight();
937        final int displayHeight = mResources.getDisplayMetrics().heightPixels;
938        final Rect rect = new Rect();
939        mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
940        final int notificationBarHeight = rect.top;
941        final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
942                - keyboardHeight;
943
944        final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
945        params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight);
946        mKeyPreviewBackingView.setLayoutParams(params);
947        return params.height;
948    }
949
950    @Override
951    public void onComputeInsets(InputMethodService.Insets outInsets) {
952        super.onComputeInsets(outInsets);
953        final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
954        if (inputView == null || mSuggestionsContainer == null)
955            return;
956        final int adjustedBackingHeight = getAdjustedBackingViewHeight();
957        final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
958        final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
959        // In fullscreen mode, the height of the extract area managed by InputMethodService should
960        // be considered.
961        // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
962        final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
963        final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0
964                : mSuggestionsContainer.getHeight();
965        final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
966        int touchY = extraHeight;
967        // Need to set touchable region only if input view is being shown
968        final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
969        if (keyboardView != null && keyboardView.isShown()) {
970            if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
971                touchY -= suggestionsHeight;
972            }
973            final int touchWidth = inputView.getWidth();
974            final int touchHeight = inputView.getHeight() + extraHeight
975                    // Extend touchable region below the keyboard.
976                    + EXTENDED_TOUCHABLE_REGION_HEIGHT;
977            outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
978            outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight);
979        }
980        outInsets.contentTopInsets = touchY;
981        outInsets.visibleTopInsets = touchY;
982    }
983
984    @Override
985    public boolean onEvaluateFullscreenMode() {
986        // Reread resource value here, because this method is called by framework anytime as needed.
987        final boolean isFullscreenModeAllowed =
988                mCurrentSettings.isFullscreenModeAllowed(getResources());
989        return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed;
990    }
991
992    @Override
993    public void updateFullscreenMode() {
994        super.updateFullscreenMode();
995
996        if (mKeyPreviewBackingView == null) return;
997        // In fullscreen mode, no need to have extra space to show the key preview.
998        // If not, we should have extra space above the keyboard to show the key preview.
999        mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
1000    }
1001
1002    // This will reset the whole input state to the starting state. It will clear
1003    // the composing word, reset the last composed word, tell the inputconnection about it.
1004    private void resetEntireInputState() {
1005        resetComposingState(true /* alsoResetLastComposedWord */);
1006        updateSuggestions();
1007        mConnection.finishComposingText();
1008    }
1009
1010    private void resetComposingState(final boolean alsoResetLastComposedWord) {
1011        mWordComposer.reset();
1012        if (alsoResetLastComposedWord)
1013            mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
1014    }
1015
1016    public void commitTyped(final int separatorCode) {
1017        if (!mWordComposer.isComposingWord()) return;
1018        final CharSequence typedWord = mWordComposer.getTypedWord();
1019        if (typedWord.length() > 0) {
1020            mConnection.commitText(typedWord, 1);
1021            if (ProductionFlag.IS_EXPERIMENTAL) {
1022                ResearchLogger.latinIME_commitText(typedWord);
1023            }
1024            final CharSequence prevWord = addToUserHistoryDictionary(typedWord);
1025            mLastComposedWord = mWordComposer.commitWord(
1026                    LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
1027                    separatorCode, prevWord);
1028        }
1029        updateSuggestions();
1030    }
1031
1032    public int getCurrentAutoCapsState() {
1033        if (!mCurrentSettings.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
1034
1035        final EditorInfo ei = getCurrentInputEditorInfo();
1036        if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
1037
1038        final int inputType = ei.inputType;
1039        if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
1040            return TextUtils.CAP_MODE_CHARACTERS;
1041        }
1042
1043        final boolean noNeedToCheckCapsMode = (inputType & (InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
1044                | InputType.TYPE_TEXT_FLAG_CAP_WORDS)) == 0;
1045        if (noNeedToCheckCapsMode) return Constants.TextUtils.CAP_MODE_OFF;
1046
1047        // Avoid making heavy round-trip IPC calls of {@link InputConnection#getCursorCapsMode}
1048        // unless needed.
1049        if (mWordComposer.isComposingWord()) return Constants.TextUtils.CAP_MODE_OFF;
1050
1051        // TODO: This blocking IPC call is heavy. Consider doing this without using IPC calls.
1052        // Note: getCursorCapsMode() returns the current capitalization mode that is any
1053        // combination of CAP_MODE_CHARACTERS, CAP_MODE_WORDS, and CAP_MODE_SENTENCES. 0 means none
1054        // of them.
1055        return mConnection.getCursorCapsMode(inputType);
1056    }
1057
1058    private void swapSwapperAndSpace() {
1059        CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
1060        // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
1061        if (lastTwo != null && lastTwo.length() == 2
1062                && lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
1063            mConnection.deleteSurroundingText(2, 0);
1064            if (ProductionFlag.IS_EXPERIMENTAL) {
1065                ResearchLogger.latinIME_deleteSurroundingText(2);
1066            }
1067            mConnection.commitText(lastTwo.charAt(1) + " ", 1);
1068            if (ProductionFlag.IS_EXPERIMENTAL) {
1069                ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit();
1070            }
1071            mKeyboardSwitcher.updateShiftState();
1072        }
1073    }
1074
1075    private boolean maybeDoubleSpace() {
1076        if (!mCurrentSettings.mCorrectionEnabled) return false;
1077        if (!mHandler.isAcceptingDoubleSpaces()) return false;
1078        final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
1079        if (lastThree != null && lastThree.length() == 3
1080                && canBeFollowedByPeriod(lastThree.charAt(0))
1081                && lastThree.charAt(1) == Keyboard.CODE_SPACE
1082                && lastThree.charAt(2) == Keyboard.CODE_SPACE) {
1083            mHandler.cancelDoubleSpacesTimer();
1084            mConnection.deleteSurroundingText(2, 0);
1085            mConnection.commitText(". ", 1);
1086            if (ProductionFlag.IS_EXPERIMENTAL) {
1087                ResearchLogger.latinIME_doubleSpaceAutoPeriod();
1088            }
1089            mKeyboardSwitcher.updateShiftState();
1090            return true;
1091        }
1092        return false;
1093    }
1094
1095    private static boolean canBeFollowedByPeriod(final int codePoint) {
1096        // TODO: Check again whether there really ain't a better way to check this.
1097        // TODO: This should probably be language-dependant...
1098        return Character.isLetterOrDigit(codePoint)
1099                || codePoint == Keyboard.CODE_SINGLE_QUOTE
1100                || codePoint == Keyboard.CODE_DOUBLE_QUOTE
1101                || codePoint == Keyboard.CODE_CLOSING_PARENTHESIS
1102                || codePoint == Keyboard.CODE_CLOSING_SQUARE_BRACKET
1103                || codePoint == Keyboard.CODE_CLOSING_CURLY_BRACKET
1104                || codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET;
1105    }
1106
1107    @Override
1108    public boolean addWordToDictionary(String word) {
1109        mUserDictionary.addWordToUserDictionary(word, 128);
1110        // Suggestion strip should be updated after the operation of adding word to the
1111        // user dictionary
1112        mHandler.postUpdateSuggestions();
1113        return true;
1114    }
1115
1116    private static boolean isAlphabet(int code) {
1117        return Character.isLetter(code);
1118    }
1119
1120    private void onSettingsKeyPressed() {
1121        if (isShowingOptionDialog()) return;
1122        showSubtypeSelectorAndSettings();
1123    }
1124
1125    // Virtual codes representing custom requests.  These are used in onCustomRequest() below.
1126    public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
1127
1128    @Override
1129    public boolean onCustomRequest(int requestCode) {
1130        if (isShowingOptionDialog()) return false;
1131        switch (requestCode) {
1132        case CODE_SHOW_INPUT_METHOD_PICKER:
1133            if (ImfUtils.hasMultipleEnabledIMEsOrSubtypes(
1134                    this, true /* include aux subtypes */)) {
1135                mImm.showInputMethodPicker();
1136                return true;
1137            }
1138            return false;
1139        }
1140        return false;
1141    }
1142
1143    private boolean isShowingOptionDialog() {
1144        return mOptionsDialog != null && mOptionsDialog.isShowing();
1145    }
1146
1147    private static int getActionId(Keyboard keyboard) {
1148        return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE;
1149    }
1150
1151    private void performEditorAction(int actionId) {
1152        mConnection.performEditorAction(actionId);
1153        if (ProductionFlag.IS_EXPERIMENTAL) {
1154            ResearchLogger.latinIME_performEditorAction(actionId);
1155        }
1156    }
1157
1158    private void handleLanguageSwitchKey() {
1159        final boolean includesOtherImes = mCurrentSettings.mIncludesOtherImesInLanguageSwitchList;
1160        final IBinder token = getWindow().getWindow().getAttributes().token;
1161        if (mShouldSwitchToLastSubtype) {
1162            final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype();
1163            final boolean lastSubtypeBelongsToThisIme =
1164                    ImfUtils.checkIfSubtypeBelongsToThisImeAndEnabled(this, lastSubtype);
1165            if ((includesOtherImes || lastSubtypeBelongsToThisIme)
1166                    && mImm.switchToLastInputMethod(token)) {
1167                mShouldSwitchToLastSubtype = false;
1168            } else {
1169                mImm.switchToNextInputMethod(token, !includesOtherImes);
1170                mShouldSwitchToLastSubtype = true;
1171            }
1172        } else {
1173            mImm.switchToNextInputMethod(token, !includesOtherImes);
1174        }
1175    }
1176
1177    private void sendUpDownEnterOrBackspace(final int code) {
1178        final long eventTime = SystemClock.uptimeMillis();
1179        mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
1180                KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1181                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1182        mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
1183                KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1184                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1185    }
1186
1187    private void sendKeyCodePoint(int code) {
1188        // TODO: Remove this special handling of digit letters.
1189        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
1190        if (code >= '0' && code <= '9') {
1191            super.sendKeyChar((char)code);
1192            return;
1193        }
1194
1195        // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
1196        // we want to be able to compile against the Ice Cream Sandwich SDK.
1197        if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
1198                && mTargetApplicationInfo.targetSdkVersion < 16) {
1199            // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
1200            // a hardware keyboard event on pressing enter or delete. This is bad for many
1201            // reasons (there are race conditions with commits) but some applications are
1202            // relying on this behavior so we continue to support it for older apps.
1203            sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER);
1204        } else {
1205            final String text = new String(new int[] { code }, 0, 1);
1206            mConnection.commitText(text, text.length());
1207        }
1208        if (ProductionFlag.IS_EXPERIMENTAL) {
1209            ResearchLogger.latinIME_sendKeyCodePoint(code);
1210        }
1211    }
1212
1213    // Implementation of {@link KeyboardActionListener}.
1214    @Override
1215    public void onCodeInput(int primaryCode, int x, int y) {
1216        final long when = SystemClock.uptimeMillis();
1217        if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
1218            mDeleteCount = 0;
1219        }
1220        mLastKeyTime = when;
1221        mConnection.beginBatchEdit(getCurrentInputConnection());
1222
1223        if (ProductionFlag.IS_EXPERIMENTAL) {
1224            ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
1225        }
1226
1227        final KeyboardSwitcher switcher = mKeyboardSwitcher;
1228        // The space state depends only on the last character pressed and its own previous
1229        // state. Here, we revert the space state to neutral if the key is actually modifying
1230        // the input contents (any non-shift key), which is what we should do for
1231        // all inputs that do not result in a special state. Each character handling is then
1232        // free to override the state as they see fit.
1233        final int spaceState = mSpaceState;
1234        if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
1235
1236        // TODO: Consolidate the double space timer, mLastKeyTime, and the space state.
1237        if (primaryCode != Keyboard.CODE_SPACE) {
1238            mHandler.cancelDoubleSpacesTimer();
1239        }
1240
1241        boolean didAutoCorrect = false;
1242        switch (primaryCode) {
1243        case Keyboard.CODE_DELETE:
1244            mSpaceState = SPACE_STATE_NONE;
1245            handleBackspace(spaceState);
1246            mDeleteCount++;
1247            mExpectingUpdateSelection = true;
1248            mShouldSwitchToLastSubtype = true;
1249            LatinImeLogger.logOnDelete(x, y);
1250            break;
1251        case Keyboard.CODE_SHIFT:
1252        case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
1253            // Shift and symbol key is handled in onPressKey() and onReleaseKey().
1254            break;
1255        case Keyboard.CODE_SETTINGS:
1256            onSettingsKeyPressed();
1257            break;
1258        case Keyboard.CODE_SHORTCUT:
1259            mSubtypeSwitcher.switchToShortcutIME();
1260            break;
1261        case Keyboard.CODE_ACTION_ENTER:
1262            performEditorAction(getActionId(switcher.getKeyboard()));
1263            break;
1264        case Keyboard.CODE_ACTION_NEXT:
1265            performEditorAction(EditorInfo.IME_ACTION_NEXT);
1266            break;
1267        case Keyboard.CODE_ACTION_PREVIOUS:
1268            performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
1269            break;
1270        case Keyboard.CODE_LANGUAGE_SWITCH:
1271            handleLanguageSwitchKey();
1272            break;
1273        case Keyboard.CODE_RESEARCH:
1274            if (ProductionFlag.IS_EXPERIMENTAL) {
1275                ResearchLogger.getInstance().presentResearchDialog(this);
1276            }
1277            break;
1278        default:
1279            if (primaryCode == Keyboard.CODE_TAB && mCurrentSettings.isEditorActionNext()) {
1280                performEditorAction(EditorInfo.IME_ACTION_NEXT);
1281                break;
1282            }
1283            mSpaceState = SPACE_STATE_NONE;
1284            if (mCurrentSettings.isWordSeparator(primaryCode)) {
1285                didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
1286            } else {
1287                final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1288                if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
1289                    handleCharacter(primaryCode, x, y, spaceState);
1290                } else {
1291                    handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE,
1292                            spaceState);
1293                }
1294            }
1295            mExpectingUpdateSelection = true;
1296            mShouldSwitchToLastSubtype = true;
1297            break;
1298        }
1299        switcher.onCodeInput(primaryCode);
1300        // Reset after any single keystroke, except shift and symbol-shift
1301        if (!didAutoCorrect && primaryCode != Keyboard.CODE_SHIFT
1302                && primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL)
1303            mLastComposedWord.deactivate();
1304        mEnteredText = null;
1305        mConnection.endBatchEdit();
1306    }
1307
1308    @Override
1309    public void onTextInput(CharSequence text) {
1310        mConnection.beginBatchEdit(getCurrentInputConnection());
1311        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
1312        text = specificTldProcessingOnTextInput(text);
1313        if (SPACE_STATE_PHANTOM == mSpaceState) {
1314            sendKeyCodePoint(Keyboard.CODE_SPACE);
1315        }
1316        mConnection.commitText(text, 1);
1317        if (ProductionFlag.IS_EXPERIMENTAL) {
1318            ResearchLogger.latinIME_commitText(text);
1319        }
1320        mConnection.endBatchEdit();
1321        mKeyboardSwitcher.updateShiftState();
1322        mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
1323        mSpaceState = SPACE_STATE_NONE;
1324        mEnteredText = text;
1325        resetComposingState(true /* alsoResetLastComposedWord */);
1326    }
1327
1328    private CharSequence specificTldProcessingOnTextInput(final CharSequence text) {
1329        if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
1330                || !Character.isLetter(text.charAt(1))) {
1331            // Not a tld: do nothing.
1332            return text;
1333        }
1334        // We have a TLD (or something that looks like this): make sure we don't add
1335        // a space even if currently in phantom mode.
1336        mSpaceState = SPACE_STATE_NONE;
1337        final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
1338        if (lastOne != null && lastOne.length() == 1
1339                && lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
1340            return text.subSequence(1, text.length());
1341        } else {
1342            return text;
1343        }
1344    }
1345
1346    @Override
1347    public void onCancelInput() {
1348        // User released a finger outside any key
1349        mKeyboardSwitcher.onCancelInput();
1350    }
1351
1352    private void handleBackspace(final int spaceState) {
1353        // In many cases, we may have to put the keyboard in auto-shift state again.
1354        mHandler.postUpdateShiftState();
1355
1356        if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
1357            // Cancel multi-character input: remove the text we just entered.
1358            // This is triggered on backspace after a key that inputs multiple characters,
1359            // like the smiley key or the .com key.
1360            final int length = mEnteredText.length();
1361            mConnection.deleteSurroundingText(length, 0);
1362            if (ProductionFlag.IS_EXPERIMENTAL) {
1363                ResearchLogger.latinIME_deleteSurroundingText(length);
1364            }
1365            // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
1366            // In addition we know that spaceState is false, and that we should not be
1367            // reverting any autocorrect at this point. So we can safely return.
1368            return;
1369        }
1370
1371        if (mWordComposer.isComposingWord()) {
1372            final int length = mWordComposer.size();
1373            if (length > 0) {
1374                mWordComposer.deleteLast();
1375                mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1376                // If we have deleted the last remaining character of a word, then we are not
1377                // isComposingWord() any more.
1378                if (!mWordComposer.isComposingWord()) {
1379                    // Not composing word any more, so we can show bigrams.
1380                    mHandler.postUpdateBigramPredictions();
1381                } else {
1382                    // Still composing a word, so we still have letters to deduce a suggestion from.
1383                    mHandler.postUpdateSuggestions();
1384                }
1385            } else {
1386                mConnection.deleteSurroundingText(1, 0);
1387                if (ProductionFlag.IS_EXPERIMENTAL) {
1388                    ResearchLogger.latinIME_deleteSurroundingText(1);
1389                }
1390            }
1391        } else {
1392            if (mLastComposedWord.canRevertCommit()) {
1393                Utils.Stats.onAutoCorrectionCancellation();
1394                revertCommit();
1395                return;
1396            }
1397            if (SPACE_STATE_DOUBLE == spaceState) {
1398                mHandler.cancelDoubleSpacesTimer();
1399                if (mConnection.revertDoubleSpace()) {
1400                    // No need to reset mSpaceState, it has already be done (that's why we
1401                    // receive it as a parameter)
1402                    return;
1403                }
1404            } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1405                if (mConnection.revertSwapPunctuation()) {
1406                    // Likewise
1407                    return;
1408                }
1409            }
1410
1411            // No cancelling of commit/double space/swap: we have a regular backspace.
1412            // We should backspace one char and restart suggestion if at the end of a word.
1413            if (mLastSelectionStart != mLastSelectionEnd) {
1414                // If there is a selection, remove it.
1415                final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
1416                mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
1417                mConnection.deleteSurroundingText(lengthToDelete, 0);
1418                if (ProductionFlag.IS_EXPERIMENTAL) {
1419                    ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
1420                }
1421            } else {
1422                // There is no selection, just delete one character.
1423                if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
1424                    // This should never happen.
1425                    Log.e(TAG, "Backspace when we don't know the selection position");
1426                }
1427                // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
1428                // we want to be able to compile against the Ice Cream Sandwich SDK.
1429                if (mTargetApplicationInfo != null
1430                        && mTargetApplicationInfo.targetSdkVersion < 16) {
1431                    // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
1432                    // a hardware keyboard event on pressing enter or delete. This is bad for many
1433                    // reasons (there are race conditions with commits) but some applications are
1434                    // relying on this behavior so we continue to support it for older apps.
1435                    sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL);
1436                } else {
1437                    mConnection.deleteSurroundingText(1, 0);
1438                }
1439                if (ProductionFlag.IS_EXPERIMENTAL) {
1440                    ResearchLogger.latinIME_deleteSurroundingText(1);
1441                }
1442                if (mDeleteCount > DELETE_ACCELERATE_AT) {
1443                    mConnection.deleteSurroundingText(1, 0);
1444                    if (ProductionFlag.IS_EXPERIMENTAL) {
1445                        ResearchLogger.latinIME_deleteSurroundingText(1);
1446                    }
1447                }
1448            }
1449            if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
1450                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
1451            }
1452        }
1453    }
1454
1455    private boolean maybeStripSpace(final int code,
1456            final int spaceState, final boolean isFromSuggestionStrip) {
1457        if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1458            mConnection.removeTrailingSpace();
1459            return false;
1460        } else if ((SPACE_STATE_WEAK == spaceState
1461                || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
1462                && isFromSuggestionStrip) {
1463            if (mCurrentSettings.isWeakSpaceSwapper(code)) {
1464                return true;
1465            } else {
1466                if (mCurrentSettings.isWeakSpaceStripper(code)) {
1467                    mConnection.removeTrailingSpace();
1468                }
1469                return false;
1470            }
1471        } else {
1472            return false;
1473        }
1474    }
1475
1476    private void handleCharacter(final int primaryCode, final int x,
1477            final int y, final int spaceState) {
1478        boolean isComposingWord = mWordComposer.isComposingWord();
1479
1480        if (SPACE_STATE_PHANTOM == spaceState &&
1481                !mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode)) {
1482            if (isComposingWord) {
1483                // Sanity check
1484                throw new RuntimeException("Should not be composing here");
1485            }
1486            sendKeyCodePoint(Keyboard.CODE_SPACE);
1487        }
1488
1489        // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
1490        // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
1491        // thread here.
1492        if (!isComposingWord && (isAlphabet(primaryCode)
1493                || mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode))
1494                && mCurrentSettings.isSuggestionsRequested(mDisplayOrientation) &&
1495                !mConnection.isCursorTouchingWord(mCurrentSettings)) {
1496            // Reset entirely the composing state anyway, then start composing a new word unless
1497            // the character is a single quote. The idea here is, single quote is not a
1498            // separator and it should be treated as a normal character, except in the first
1499            // position where it should not start composing a word.
1500            isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode);
1501            // Here we don't need to reset the last composed word. It will be reset
1502            // when we commit this one, if we ever do; if on the other hand we backspace
1503            // it entirely and resume suggestions on the previous word, we'd like to still
1504            // have touch coordinates for it.
1505            resetComposingState(false /* alsoResetLastComposedWord */);
1506            clearSuggestions();
1507        }
1508        if (isComposingWord) {
1509            mWordComposer.add(
1510                    primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
1511            // If it's the first letter, make note of auto-caps state
1512            if (mWordComposer.size() == 1) {
1513                mWordComposer.setAutoCapitalized(
1514                        getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
1515            }
1516            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1517            mHandler.postUpdateSuggestions();
1518        } else {
1519            final boolean swapWeakSpace = maybeStripSpace(primaryCode,
1520                    spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
1521
1522            sendKeyCodePoint(primaryCode);
1523
1524            if (swapWeakSpace) {
1525                swapSwapperAndSpace();
1526                mSpaceState = SPACE_STATE_WEAK;
1527            }
1528            // Some characters are not word separators, yet they don't start a new
1529            // composing span. For these, we haven't changed the suggestion strip, and
1530            // if the "add to dictionary" hint is shown, we should do so now. Examples of
1531            // such characters include single quote, dollar, and others; the exact list is
1532            // the list of characters for which we enter handleCharacterWhileInBatchEdit
1533            // that don't match the test if ((isAlphabet...)) at the top of this method.
1534            if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) {
1535                mHandler.postUpdateBigramPredictions();
1536            }
1537        }
1538        Utils.Stats.onNonSeparator((char)primaryCode, x, y);
1539    }
1540
1541    // Returns true if we did an autocorrection, false otherwise.
1542    private boolean handleSeparator(final int primaryCode, final int x, final int y,
1543            final int spaceState) {
1544        // Should dismiss the "Touch again to save" message when handling separator
1545        if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
1546            mHandler.cancelUpdateBigramPredictions();
1547            mHandler.postUpdateSuggestions();
1548        }
1549
1550        boolean didAutoCorrect = false;
1551        // Handle separator
1552        if (mWordComposer.isComposingWord()) {
1553            // In certain languages where single quote is a separator, it's better
1554            // not to auto correct, but accept the typed word. For instance,
1555            // in Italian dov' should not be expanded to dove' because the elision
1556            // requires the last vowel to be removed.
1557            if (mCurrentSettings.mCorrectionEnabled && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
1558                commitCurrentAutoCorrection(primaryCode);
1559                didAutoCorrect = true;
1560            } else {
1561                commitTyped(primaryCode);
1562            }
1563        }
1564
1565        final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
1566                KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
1567
1568        if (SPACE_STATE_PHANTOM == spaceState &&
1569                mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
1570            sendKeyCodePoint(Keyboard.CODE_SPACE);
1571        }
1572        sendKeyCodePoint(primaryCode);
1573
1574        if (Keyboard.CODE_SPACE == primaryCode) {
1575            if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
1576                if (maybeDoubleSpace()) {
1577                    mSpaceState = SPACE_STATE_DOUBLE;
1578                } else if (!isShowingPunctuationList()) {
1579                    mSpaceState = SPACE_STATE_WEAK;
1580                }
1581            }
1582
1583            mHandler.startDoubleSpacesTimer();
1584            if (!mConnection.isCursorTouchingWord(mCurrentSettings)) {
1585                mHandler.cancelUpdateSuggestions();
1586                mHandler.postUpdateBigramPredictions();
1587            }
1588        } else {
1589            if (swapWeakSpace) {
1590                swapSwapperAndSpace();
1591                mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
1592            } else if (SPACE_STATE_PHANTOM == spaceState) {
1593                // If we are in phantom space state, and the user presses a separator, we want to
1594                // stay in phantom space state so that the next keypress has a chance to add the
1595                // space. For example, if I type "Good dat", pick "day" from the suggestion strip
1596                // then insert a comma and go on to typing the next word, I want the space to be
1597                // inserted automatically before the next word, the same way it is when I don't
1598                // input the comma.
1599                mSpaceState = SPACE_STATE_PHANTOM;
1600            }
1601
1602            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
1603            // already displayed or not, so it's okay.
1604            setPunctuationSuggestions();
1605        }
1606
1607        Utils.Stats.onSeparator((char)primaryCode, x, y);
1608
1609        return didAutoCorrect;
1610    }
1611
1612    private CharSequence getTextWithUnderline(final CharSequence text) {
1613        return mIsAutoCorrectionIndicatorOn
1614                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
1615                : text;
1616    }
1617
1618    private void handleClose() {
1619        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
1620        requestHideSelf(0);
1621        LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1622        if (inputView != null)
1623            inputView.closing();
1624    }
1625
1626    public boolean isShowingPunctuationList() {
1627        if (mSuggestionsView == null) return false;
1628        return mCurrentSettings.mSuggestPuncList == mSuggestionsView.getSuggestions();
1629    }
1630
1631    public boolean isSuggestionsStripVisible() {
1632        if (mSuggestionsView == null)
1633            return false;
1634        if (mSuggestionsView.isShowingAddToDictionaryHint())
1635            return true;
1636        if (!mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
1637            return false;
1638        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn())
1639            return true;
1640        return mCurrentSettings.isSuggestionsRequested(mDisplayOrientation);
1641    }
1642
1643    public void switchToKeyboardView() {
1644        if (DEBUG) {
1645            Log.d(TAG, "Switch to keyboard view.");
1646        }
1647        if (ProductionFlag.IS_EXPERIMENTAL) {
1648            ResearchLogger.latinIME_switchToKeyboardView();
1649        }
1650        View v = mKeyboardSwitcher.getKeyboardView();
1651        if (v != null) {
1652            // Confirms that the keyboard view doesn't have parent view.
1653            ViewParent p = v.getParent();
1654            if (p != null && p instanceof ViewGroup) {
1655                ((ViewGroup) p).removeView(v);
1656            }
1657            setInputView(v);
1658        }
1659        setSuggestionStripShown(isSuggestionsStripVisible());
1660        updateInputViewShown();
1661        mHandler.postUpdateSuggestions();
1662    }
1663
1664    public void clearSuggestions() {
1665        setSuggestions(SuggestedWords.EMPTY, false);
1666        setAutoCorrectionIndicator(false);
1667    }
1668
1669    private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) {
1670        if (mSuggestionsView != null) {
1671            mSuggestionsView.setSuggestions(words);
1672            mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
1673        }
1674    }
1675
1676    private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
1677        // Put a blue underline to a word in TextView which will be auto-corrected.
1678        if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
1679                && mWordComposer.isComposingWord()) {
1680            mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
1681            final CharSequence textWithUnderline =
1682                    getTextWithUnderline(mWordComposer.getTypedWord());
1683            mConnection.setComposingText(textWithUnderline, 1);
1684        }
1685    }
1686
1687    public void updateSuggestions() {
1688        mHandler.cancelUpdateSuggestions();
1689        mHandler.cancelUpdateBigramPredictions();
1690
1691        // Check if we have a suggestion engine attached.
1692        if ((mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation))) {
1693            if (mWordComposer.isComposingWord()) {
1694                Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
1695                mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
1696            }
1697            return;
1698        }
1699
1700        if (!mWordComposer.isComposingWord()) {
1701            // This is dead code: we can't come here with an empty word composer.
1702            setPunctuationSuggestions();
1703            return;
1704        }
1705
1706        // TODO: May need a better way of retrieving previous word
1707        final CharSequence prevWord = mConnection.getPreviousWord(mCurrentSettings.mWordSeparators);
1708        final CharSequence typedWord = mWordComposer.getTypedWord();
1709        // getSuggestedWords handles gracefully a null value of prevWord
1710        final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
1711                prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(),
1712                mCurrentSettings.mCorrectionEnabled, false);
1713
1714        // Basically, we update the suggestion strip only when suggestion count > 1.  However,
1715        // there is an exception: We update the suggestion strip whenever typed word's length
1716        // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
1717        // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
1718        // need to clear the previous state when the user starts typing a word (i.e. typed word's
1719        // length == 1).
1720        if (suggestedWords.size() > 1 || typedWord.length() == 1
1721                || !suggestedWords.mAllowsToBeAutoCorrected
1722                || mSuggestionsView.isShowingAddToDictionaryHint()) {
1723            showSuggestions(suggestedWords, typedWord);
1724        } else {
1725            SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
1726            if (previousSuggestions == mCurrentSettings.mSuggestPuncList) {
1727                previousSuggestions = SuggestedWords.EMPTY;
1728            }
1729            final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
1730                    SuggestedWords.getTypedWordAndPreviousSuggestions(
1731                            typedWord, previousSuggestions);
1732            final SuggestedWords obsoleteSuggestedWords =
1733                    new SuggestedWords(typedWordAndPreviousSuggestions,
1734                            false /* typedWordValid */,
1735                            false /* hasAutoCorrectionCandidate */,
1736                            false /* allowsToBeAutoCorrected */,
1737                            false /* isPunctuationSuggestions */,
1738                            true /* isObsoleteSuggestions */,
1739                            false /* isPrediction */);
1740            showSuggestions(obsoleteSuggestedWords, typedWord);
1741        }
1742    }
1743
1744    public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) {
1745        final CharSequence autoCorrection;
1746        if (suggestedWords.size() > 0) {
1747            if (suggestedWords.hasAutoCorrectionWord()) {
1748                autoCorrection = suggestedWords.getWord(1);
1749            } else {
1750                autoCorrection = typedWord;
1751            }
1752        } else {
1753            autoCorrection = null;
1754        }
1755        mWordComposer.setAutoCorrection(autoCorrection);
1756        final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
1757        setSuggestions(suggestedWords, isAutoCorrection);
1758        setAutoCorrectionIndicator(isAutoCorrection);
1759        setSuggestionStripShown(isSuggestionsStripVisible());
1760    }
1761
1762    private void commitCurrentAutoCorrection(final int separatorCodePoint) {
1763        // Complete any pending suggestions query first
1764        if (mHandler.hasPendingUpdateSuggestions()) {
1765            mHandler.cancelUpdateSuggestions();
1766            updateSuggestions();
1767        }
1768        final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
1769        if (autoCorrection != null) {
1770            final String typedWord = mWordComposer.getTypedWord();
1771            if (TextUtils.isEmpty(typedWord)) {
1772                throw new RuntimeException("We have an auto-correction but the typed word "
1773                        + "is empty? Impossible! I must commit suicide.");
1774            }
1775            Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
1776            if (ProductionFlag.IS_EXPERIMENTAL) {
1777                ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord,
1778                        autoCorrection.toString());
1779            }
1780            mExpectingUpdateSelection = true;
1781            commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
1782                    separatorCodePoint);
1783            if (!typedWord.equals(autoCorrection)) {
1784                // This will make the correction flash for a short while as a visual clue
1785                // to the user that auto-correction happened.
1786                mConnection.commitCorrection(
1787                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
1788                        typedWord, autoCorrection));
1789            }
1790        }
1791    }
1792
1793    @Override
1794    public void pickSuggestionManually(final int index, final CharSequence suggestion,
1795            final int x, final int y) {
1796        final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
1797        // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
1798        if (suggestion.length() == 1 && isShowingPunctuationList()) {
1799            // Word separators are suggested before the user inputs something.
1800            // So, LatinImeLogger logs "" as a user's input.
1801            LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
1802            // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
1803            if (ProductionFlag.IS_EXPERIMENTAL) {
1804                ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y);
1805            }
1806            final int primaryCode = suggestion.charAt(0);
1807            onCodeInput(primaryCode,
1808                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
1809                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
1810            return;
1811        }
1812
1813        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
1814            int firstChar = Character.codePointAt(suggestion, 0);
1815            if ((!mCurrentSettings.isWeakSpaceStripper(firstChar))
1816                    && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) {
1817                sendKeyCodePoint(Keyboard.CODE_SPACE);
1818            }
1819        }
1820
1821        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn()
1822                && mApplicationSpecifiedCompletions != null
1823                && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
1824            if (mSuggestionsView != null) {
1825                mSuggestionsView.clear();
1826            }
1827            mKeyboardSwitcher.updateShiftState();
1828            resetComposingState(true /* alsoResetLastComposedWord */);
1829            final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
1830            mConnection.beginBatchEdit(getCurrentInputConnection());
1831            mConnection.commitCompletion(completionInfo);
1832            mConnection.endBatchEdit();
1833            if (ProductionFlag.IS_EXPERIMENTAL) {
1834                ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
1835                        completionInfo.getText(), x, y);
1836            }
1837            return;
1838        }
1839
1840        // We need to log before we commit, because the word composer will store away the user
1841        // typed word.
1842        final String replacedWord = mWordComposer.getTypedWord().toString();
1843        LatinImeLogger.logOnManualSuggestion(replacedWord,
1844                suggestion.toString(), index, suggestedWords);
1845        if (ProductionFlag.IS_EXPERIMENTAL) {
1846            ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y);
1847        }
1848        mExpectingUpdateSelection = true;
1849        commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
1850                LastComposedWord.NOT_A_SEPARATOR);
1851        // Don't allow cancellation of manual pick
1852        mLastComposedWord.deactivate();
1853        mSpaceState = SPACE_STATE_PHANTOM;
1854        // TODO: is this necessary?
1855        mKeyboardSwitcher.updateShiftState();
1856
1857        // We should show the "Touch again to save" hint if the user pressed the first entry
1858        // AND either:
1859        // - There is no dictionary (we know that because we tried to load it => null != mSuggest
1860        //   AND mSuggest.hasMainDictionary() is false)
1861        // - There is a dictionary and the word is not in it
1862        // Please note that if mSuggest is null, it means that everything is off: suggestion
1863        // and correction, so we shouldn't try to show the hint
1864        final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
1865                // If there is no dictionary the hint should be shown.
1866                && (!mSuggest.hasMainDictionary()
1867                        // If "suggestion" is not in the dictionary, the hint should be shown.
1868                        || !AutoCorrection.isValidWord(
1869                                mSuggest.getUnigramDictionaries(), suggestion, true));
1870
1871        Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE,
1872                WordComposer.NOT_A_COORDINATE);
1873        if (!showingAddToDictionaryHint) {
1874            // If we're not showing the "Touch again to save", then show corrections again.
1875            // In case the cursor position doesn't change, make sure we show the suggestions again.
1876            updateBigramPredictions();
1877            // Updating the predictions right away may be slow and feel unresponsive on slower
1878            // terminals. On the other hand if we just postUpdateBigramPredictions() it will
1879            // take a noticeable delay to update them which may feel uneasy.
1880        } else {
1881            if (mIsUserDictionaryAvailable) {
1882                mSuggestionsView.showAddToDictionaryHint(
1883                        suggestion, mCurrentSettings.mHintToSaveText);
1884            } else {
1885                mHandler.postUpdateSuggestions();
1886            }
1887        }
1888    }
1889
1890    /**
1891     * Commits the chosen word to the text field and saves it for later retrieval.
1892     */
1893    private void commitChosenWord(final CharSequence chosenWord, final int commitType,
1894            final int separatorCode) {
1895        final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
1896        mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
1897                this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
1898        if (ProductionFlag.IS_EXPERIMENTAL) {
1899            ResearchLogger.latinIME_commitText(chosenWord);
1900        }
1901        // Add the word to the user history dictionary
1902        final CharSequence prevWord = addToUserHistoryDictionary(chosenWord);
1903        // TODO: figure out here if this is an auto-correct or if the best word is actually
1904        // what user typed. Note: currently this is done much later in
1905        // LastComposedWord#didCommitTypedWord by string equality of the remembered
1906        // strings.
1907        mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord.toString(),
1908                separatorCode, prevWord);
1909    }
1910
1911    public void updateBigramPredictions() {
1912        mHandler.cancelUpdateSuggestions();
1913        mHandler.cancelUpdateBigramPredictions();
1914
1915        if (mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
1916            if (mWordComposer.isComposingWord()) {
1917                Log.w(TAG, "Called updateBigramPredictions but suggestions were not requested!");
1918                mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
1919            }
1920            return;
1921        }
1922
1923        if (!mCurrentSettings.mBigramPredictionEnabled) {
1924            setPunctuationSuggestions();
1925            return;
1926        }
1927
1928        final SuggestedWords suggestedWords;
1929        if (mCurrentSettings.mCorrectionEnabled) {
1930            final CharSequence prevWord = mConnection.getThisWord(mCurrentSettings.mWordSeparators);
1931            if (!TextUtils.isEmpty(prevWord)) {
1932                suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
1933                        prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(),
1934                        mCurrentSettings.mCorrectionEnabled, true);
1935            } else {
1936                suggestedWords = null;
1937            }
1938        } else {
1939            suggestedWords = null;
1940        }
1941
1942        if (null != suggestedWords && suggestedWords.size() > 0) {
1943            // Explicitly supply an empty typed word (the no-second-arg version of
1944            // showSuggestions will retrieve the word near the cursor, we don't want that here)
1945            showSuggestions(suggestedWords, "");
1946        } else {
1947            clearSuggestions();
1948        }
1949    }
1950
1951    public void setPunctuationSuggestions() {
1952        if (mCurrentSettings.mBigramPredictionEnabled) {
1953            clearSuggestions();
1954        } else {
1955            setSuggestions(mCurrentSettings.mSuggestPuncList, false);
1956        }
1957        setAutoCorrectionIndicator(false);
1958        setSuggestionStripShown(isSuggestionsStripVisible());
1959    }
1960
1961    private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) {
1962        if (TextUtils.isEmpty(suggestion)) return null;
1963
1964        // If correction is not enabled, we don't add words to the user history dictionary.
1965        // That's to avoid unintended additions in some sensitive fields, or fields that
1966        // expect to receive non-words.
1967        if (!mCurrentSettings.mCorrectionEnabled) return null;
1968
1969        final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
1970        if (userHistoryDictionary != null) {
1971            final CharSequence prevWord
1972                    = mConnection.getPreviousWord(mCurrentSettings.mWordSeparators);
1973            final String secondWord;
1974            if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
1975                secondWord = suggestion.toString().toLowerCase(
1976                        mSubtypeSwitcher.getCurrentSubtypeLocale());
1977            } else {
1978                secondWord = suggestion.toString();
1979            }
1980            // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
1981            // We don't add words with 0-frequency (assuming they would be profanity etc.).
1982            final int maxFreq = AutoCorrection.getMaxFrequency(
1983                    mSuggest.getUnigramDictionaries(), suggestion);
1984            if (maxFreq == 0) return null;
1985            userHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
1986                    secondWord, maxFreq > 0);
1987            return prevWord;
1988        }
1989        return null;
1990    }
1991
1992    /**
1993     * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
1994     * word, else do nothing.
1995     */
1996    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
1997        final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mCurrentSettings);
1998        if (null != word) {
1999            restartSuggestionsOnWordBeforeCursor(word);
2000        }
2001    }
2002
2003    private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) {
2004        mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
2005        final int length = word.length();
2006        mConnection.deleteSurroundingText(length, 0);
2007        if (ProductionFlag.IS_EXPERIMENTAL) {
2008            ResearchLogger.latinIME_deleteSurroundingText(length);
2009        }
2010        mConnection.setComposingText(word, 1);
2011        mHandler.postUpdateSuggestions();
2012    }
2013
2014    private void revertCommit() {
2015        final CharSequence previousWord = mLastComposedWord.mPrevWord;
2016        final String originallyTypedWord = mLastComposedWord.mTypedWord;
2017        final CharSequence committedWord = mLastComposedWord.mCommittedWord;
2018        final int cancelLength = committedWord.length();
2019        final int separatorLength = LastComposedWord.getSeparatorLength(
2020                mLastComposedWord.mSeparatorCode);
2021        // TODO: should we check our saved separator against the actual contents of the text view?
2022        final int deleteLength = cancelLength + separatorLength;
2023        if (DEBUG) {
2024            if (mWordComposer.isComposingWord()) {
2025                throw new RuntimeException("revertCommit, but we are composing a word");
2026            }
2027            final String wordBeforeCursor =
2028                    mConnection.getTextBeforeCursor(deleteLength, 0)
2029                            .subSequence(0, cancelLength).toString();
2030            if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
2031                throw new RuntimeException("revertCommit check failed: we thought we were "
2032                        + "reverting \"" + committedWord
2033                        + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
2034            }
2035        }
2036        mConnection.deleteSurroundingText(deleteLength, 0);
2037        if (ProductionFlag.IS_EXPERIMENTAL) {
2038            ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
2039        }
2040        if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
2041            mUserHistoryDictionary.cancelAddingUserHistory(
2042                    previousWord.toString(), committedWord.toString());
2043        }
2044        if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) {
2045            // This is the case when we cancel a manual pick.
2046            // We should restart suggestion on the word right away.
2047            mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
2048            mConnection.setComposingText(originallyTypedWord, 1);
2049        } else {
2050            mConnection.commitText(originallyTypedWord, 1);
2051            // Re-insert the separator
2052            sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
2053            Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
2054                    WordComposer.NOT_A_COORDINATE);
2055            if (ProductionFlag.IS_EXPERIMENTAL) {
2056                ResearchLogger.latinIME_revertCommit(originallyTypedWord);
2057            }
2058            // Don't restart suggestion yet. We'll restart if the user deletes the
2059            // separator.
2060        }
2061        mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
2062        mHandler.cancelUpdateBigramPredictions();
2063        mHandler.postUpdateSuggestions();
2064    }
2065
2066    public boolean isWordSeparator(int code) {
2067        return mCurrentSettings.isWordSeparator(code);
2068    }
2069
2070    public boolean preferCapitalization() {
2071        return mWordComposer.isFirstCharCapitalized();
2072    }
2073
2074    // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
2075    // according to new language or mode.
2076    public void onRefreshKeyboard() {
2077        // When the device locale is changed in SetupWizard etc., this method may get called via
2078        // onConfigurationChanged before SoftInputWindow is shown.
2079        if (mKeyboardSwitcher.getKeyboardView() != null) {
2080            // Reload keyboard because the current language has been changed.
2081            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings);
2082        }
2083        initSuggest();
2084        loadSettings();
2085        // Since we just changed languages, we should re-evaluate suggestions with whatever word
2086        // we are currently composing. If we are not composing anything, we may want to display
2087        // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
2088        if (mConnection.isCursorTouchingWord(mCurrentSettings)) {
2089            mHandler.postUpdateSuggestions();
2090        } else {
2091            mHandler.postUpdateBigramPredictions();
2092        }
2093    }
2094
2095    // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to
2096    // {@link KeyboardSwitcher}.
2097    public void hapticAndAudioFeedback(final int primaryCode) {
2098        mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView());
2099    }
2100
2101    @Override
2102    public void onPressKey(int primaryCode) {
2103        mKeyboardSwitcher.onPressKey(primaryCode);
2104    }
2105
2106    @Override
2107    public void onReleaseKey(int primaryCode, boolean withSliding) {
2108        mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
2109
2110        // If accessibility is on, ensure the user receives keyboard state updates.
2111        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
2112            switch (primaryCode) {
2113            case Keyboard.CODE_SHIFT:
2114                AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
2115                break;
2116            case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
2117                AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
2118                break;
2119            }
2120        }
2121
2122        if (Keyboard.CODE_DELETE == primaryCode) {
2123            // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
2124            // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
2125            // pair-friendly way of deleting characters in InputConnection.
2126            final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0);
2127            if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
2128                mConnection.deleteSurroundingText(1, 0);
2129            }
2130        }
2131    }
2132
2133    // receive ringer mode change and network state change.
2134    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2135        @Override
2136        public void onReceive(Context context, Intent intent) {
2137            final String action = intent.getAction();
2138            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2139                mSubtypeSwitcher.onNetworkStateChanged(intent);
2140            } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2141                mFeedbackManager.onRingerModeChanged();
2142            }
2143        }
2144    };
2145
2146    private void launchSettings() {
2147        launchSettingsClass(SettingsActivity.class);
2148    }
2149
2150    public void launchDebugSettings() {
2151        launchSettingsClass(DebugSettingsActivity.class);
2152    }
2153
2154    private void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
2155        handleClose();
2156        Intent intent = new Intent();
2157        intent.setClass(LatinIME.this, settingsClass);
2158        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2159        startActivity(intent);
2160    }
2161
2162    private void showSubtypeSelectorAndSettings() {
2163        final CharSequence title = getString(R.string.english_ime_input_options);
2164        final CharSequence[] items = new CharSequence[] {
2165                // TODO: Should use new string "Select active input modes".
2166                getString(R.string.language_selection_title),
2167                getString(R.string.english_ime_settings),
2168        };
2169        final Context context = this;
2170        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2171            @Override
2172            public void onClick(DialogInterface di, int position) {
2173                di.dismiss();
2174                switch (position) {
2175                case 0:
2176                    Intent intent = CompatUtils.getInputLanguageSelectionIntent(
2177                            ImfUtils.getInputMethodIdOfThisIme(context),
2178                            Intent.FLAG_ACTIVITY_NEW_TASK
2179                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2180                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2181                    startActivity(intent);
2182                    break;
2183                case 1:
2184                    launchSettings();
2185                    break;
2186                }
2187            }
2188        };
2189        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2190                .setItems(items, listener)
2191                .setTitle(title);
2192        showOptionDialog(builder.create());
2193    }
2194
2195    /* package */ void showOptionDialog(AlertDialog dialog) {
2196        final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
2197        if (windowToken == null) return;
2198
2199        dialog.setCancelable(true);
2200        dialog.setCanceledOnTouchOutside(true);
2201
2202        final Window window = dialog.getWindow();
2203        final WindowManager.LayoutParams lp = window.getAttributes();
2204        lp.token = windowToken;
2205        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
2206        window.setAttributes(lp);
2207        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
2208
2209        mOptionsDialog = dialog;
2210        dialog.show();
2211    }
2212
2213    @Override
2214    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
2215        super.dump(fd, fout, args);
2216
2217        final Printer p = new PrintWriterPrinter(fout);
2218        p.println("LatinIME state :");
2219        final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
2220        final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
2221        p.println("  Keyboard mode = " + keyboardMode);
2222        p.println("  mIsSuggestionsSuggestionsRequested = "
2223                + mCurrentSettings.isSuggestionsRequested(mDisplayOrientation));
2224        p.println("  mCorrectionEnabled=" + mCurrentSettings.mCorrectionEnabled);
2225        p.println("  isComposingWord=" + mWordComposer.isComposingWord());
2226        p.println("  mSoundOn=" + mCurrentSettings.mSoundOn);
2227        p.println("  mVibrateOn=" + mCurrentSettings.mVibrateOn);
2228        p.println("  mKeyPreviewPopupOn=" + mCurrentSettings.mKeyPreviewPopupOn);
2229        p.println("  inputAttributes=" + mCurrentSettings.getInputAttributesDebugString());
2230    }
2231}
2232