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