/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.android.inputmethod.latin; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.net.ConnectivityManager; import android.os.Debug; import android.os.IBinder; import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; import android.text.InputType; import android.text.TextUtils; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.InputConnection; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.compat.CompatUtils; import com.android.inputmethod.compat.EditorInfoCompatUtils; import com.android.inputmethod.compat.InputConnectionCompatUtils; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; import com.android.inputmethod.compat.InputMethodServiceCompatWrapper; import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper; import com.android.inputmethod.compat.InputTypeCompatUtils; import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.deprecated.LanguageSwitcherProxy; import com.android.inputmethod.deprecated.VoiceProxy; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.KeyboardView; import com.android.inputmethod.keyboard.LatinKeyboardView; import com.android.inputmethod.latin.suggestions.SuggestionsView; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Locale; /** * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodServiceCompatWrapper implements KeyboardActionListener, SuggestionsView.Listener { private static final String TAG = LatinIME.class.getSimpleName(); private static final boolean TRACE = false; private static boolean DEBUG; /** * The private IME option used to indicate that no microphone should be * shown for a given text field. For instance, this is specified by the * search dialog when the dialog is already showing a voice search button. * * @deprecated Use {@link LatinIME#IME_OPTION_NO_MICROPHONE} with package name prefixed. */ @SuppressWarnings("dep-ann") public static final String IME_OPTION_NO_MICROPHONE_COMPAT = "nm"; /** * The private IME option used to indicate that no microphone should be * shown for a given text field. For instance, this is specified by the * search dialog when the dialog is already showing a voice search button. */ public static final String IME_OPTION_NO_MICROPHONE = "noMicrophoneKey"; /** * The private IME option used to indicate that no settings key should be * shown for a given text field. */ public static final String IME_OPTION_NO_SETTINGS_KEY = "noSettingsKey"; /** * The private IME option used to indicate that the given text field needs * ASCII code points input. * * @deprecated Use {@link EditorInfo#IME_FLAG_FORCE_ASCII}. */ @SuppressWarnings("dep-ann") public static final String IME_OPTION_FORCE_ASCII = "forceAscii"; /** * The subtype extra value used to indicate that the subtype keyboard layout is capable for * typing ASCII characters. */ public static final String SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE = "AsciiCapable"; /** * The subtype extra value used to indicate that the subtype keyboard layout supports touch * position correction. */ public static final String SUBTYPE_EXTRA_VALUE_SUPPORT_TOUCH_POSITION_CORRECTION = "SupportTouchPositionCorrection"; /** * The subtype extra value used to indicate that the subtype keyboard layout should be loaded * from the specified locale. */ public static final String SUBTYPE_EXTRA_VALUE_KEYBOARD_LOCALE = "KeyboardLocale"; private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; // How many continuous deletes at which to start deleting at a higher speed. private static final int DELETE_ACCELERATE_AT = 20; // Key events coming any faster than this are long-presses. private static final int QUICK_PRESS = 200; private static final int PENDING_IMS_CALLBACK_DURATION = 800; /** * The name of the scheme used by the Package Manager to warn of a new package installation, * replacement or removal. */ private static final String SCHEME_PACKAGE = "package"; // TODO: migrate this to SettingsValues private int mSuggestionVisibility; private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE = R.string.prefs_suggestion_visibility_show_value; private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE = R.string.prefs_suggestion_visibility_show_only_portrait_value; private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE = R.string.prefs_suggestion_visibility_hide_value; private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] { SUGGESTION_VISIBILILTY_SHOW_VALUE, SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE, SUGGESTION_VISIBILILTY_HIDE_VALUE }; private static final int SPACE_STATE_NONE = 0; // Double space: the state where the user pressed space twice quickly, which LatinIME // resolved as period-space. Undoing this converts the period to a space. private static final int SPACE_STATE_DOUBLE = 1; // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip // have just been swapped. Undoing this swaps them back; the space is still considered weak. private static final int SPACE_STATE_SWAP_PUNCTUATION = 2; // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak // spaces happen when the user presses space, accepting the current suggestion (whether // it's an auto-correction or not). private static final int SPACE_STATE_WEAK = 3; // Phantom space: a not-yet-inserted space that should get inserted on the next input, // character provided it's not a separator. If it's a separator, the phantom space is dropped. // Phantom spaces happen when a user chooses a word from the suggestion strip. private static final int SPACE_STATE_PHANTOM = 4; // Current space state of the input method. This can be any of the above constants. private int mSpaceState; private SettingsValues mSettingsValues; private InputAttributes mInputAttributes; private View mExtractArea; private View mKeyPreviewBackingView; private View mSuggestionsContainer; private SuggestionsView mSuggestionsView; /* package for tests */ Suggest mSuggest; private CompletionInfo[] mApplicationSpecifiedCompletions; private InputMethodManagerCompatWrapper mImm; private Resources mResources; private SharedPreferences mPrefs; private final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeSwitcher mSubtypeSwitcher; private VoiceProxy mVoiceProxy; private boolean mShouldSwitchToLastSubtype = true; private UserDictionary mUserDictionary; private UserBigramDictionary mUserBigramDictionary; private UserUnigramDictionary mUserUnigramDictionary; private boolean mIsUserDictionaryAvailable; private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; private WordComposer mWordComposer = new WordComposer(); private int mCorrectionMode; // Keep track of the last selection range to decide if we need to show word alternatives private static final int NOT_A_CURSOR_POSITION = -1; private int mLastSelectionStart = NOT_A_CURSOR_POSITION; private int mLastSelectionEnd = NOT_A_CURSOR_POSITION; // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't // "expect" it, it means the user actually moved the cursor. private boolean mExpectingUpdateSelection; private int mDeleteCount; private long mLastKeyTime; private AudioAndHapticFeedbackManager mFeedbackManager; // Member variables for remembering the current device orientation. private int mDisplayOrientation; // Object for reacting to adding/removing a dictionary pack. private BroadcastReceiver mDictionaryPackInstallReceiver = new DictionaryPackInstallBroadcastReceiver(this); // Keeps track of most recently inserted text (multi-character key) for reverting private CharSequence mEnteredText; private boolean mIsAutoCorrectionIndicatorOn; public final UIHandler mHandler = new UIHandler(this); public static class UIHandler extends StaticInnerHandlerWrapper { private static final int MSG_UPDATE_SHIFT_STATE = 1; private static final int MSG_VOICE_RESULTS = 2; private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 3; private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 4; private static final int MSG_SPACE_TYPED = 5; private static final int MSG_SET_BIGRAM_PREDICTIONS = 6; private static final int MSG_PENDING_IMS_CALLBACK = 7; private static final int MSG_UPDATE_SUGGESTIONS = 8; private int mDelayBeforeFadeoutLanguageOnSpacebar; private int mDelayUpdateSuggestions; private int mDelayUpdateShiftState; private int mDurationOfFadeoutLanguageOnSpacebar; private float mFinalFadeoutFactorOfLanguageOnSpacebar; private long mDoubleSpacesTurnIntoPeriodTimeout; public UIHandler(LatinIME outerInstance) { super(outerInstance); } public void onCreate() { final Resources res = getOuterInstance().getResources(); mDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger( R.integer.config_delay_before_fadeout_language_on_spacebar); mDelayUpdateSuggestions = res.getInteger(R.integer.config_delay_update_suggestions); mDelayUpdateShiftState = res.getInteger(R.integer.config_delay_update_shift_state); mDurationOfFadeoutLanguageOnSpacebar = res.getInteger( R.integer.config_duration_of_fadeout_language_on_spacebar); mFinalFadeoutFactorOfLanguageOnSpacebar = res.getInteger( R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f; mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger( R.integer.config_double_spaces_turn_into_period_timeout); } @Override public void handleMessage(Message msg) { final LatinIME latinIme = getOuterInstance(); final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; final LatinKeyboardView inputView = switcher.getKeyboardView(); switch (msg.what) { case MSG_UPDATE_SUGGESTIONS: latinIme.updateSuggestions(); break; case MSG_UPDATE_SHIFT_STATE: switcher.updateShiftState(); break; case MSG_SET_BIGRAM_PREDICTIONS: latinIme.updateBigramPredictions(); break; case MSG_VOICE_RESULTS: final Keyboard keyboard = switcher.getKeyboard(); latinIme.mVoiceProxy.handleVoiceResults(latinIme.preferCapitalization() || (keyboard != null && keyboard.isShiftedOrShiftLocked())); break; case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR: setSpacebarTextFadeFactor(inputView, (1.0f + mFinalFadeoutFactorOfLanguageOnSpacebar) / 2, (Keyboard)msg.obj); sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj), mDurationOfFadeoutLanguageOnSpacebar); break; case MSG_DISMISS_LANGUAGE_ON_SPACEBAR: setSpacebarTextFadeFactor(inputView, mFinalFadeoutFactorOfLanguageOnSpacebar, (Keyboard)msg.obj); break; } } public void postUpdateSuggestions() { removeMessages(MSG_UPDATE_SUGGESTIONS); sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions); } public void cancelUpdateSuggestions() { removeMessages(MSG_UPDATE_SUGGESTIONS); } public boolean hasPendingUpdateSuggestions() { return hasMessages(MSG_UPDATE_SUGGESTIONS); } public void postUpdateShiftState() { removeMessages(MSG_UPDATE_SHIFT_STATE); sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState); } public void cancelUpdateShiftState() { removeMessages(MSG_UPDATE_SHIFT_STATE); } public void postUpdateBigramPredictions() { removeMessages(MSG_SET_BIGRAM_PREDICTIONS); sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions); } public void cancelUpdateBigramPredictions() { removeMessages(MSG_SET_BIGRAM_PREDICTIONS); } public void updateVoiceResults() { sendMessage(obtainMessage(MSG_VOICE_RESULTS)); } private static void setSpacebarTextFadeFactor(LatinKeyboardView inputView, float fadeFactor, Keyboard oldKeyboard) { if (inputView == null) return; final Keyboard keyboard = inputView.getKeyboard(); if (keyboard == oldKeyboard) { inputView.updateSpacebar(fadeFactor, SubtypeSwitcher.getInstance().needsToDisplayLanguage( keyboard.mId.mLocale)); } } public void startDisplayLanguageOnSpacebar(boolean localeChanged) { final LatinIME latinIme = getOuterInstance(); removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR); removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR); final LatinKeyboardView inputView = latinIme.mKeyboardSwitcher.getKeyboardView(); if (inputView != null) { final Keyboard keyboard = latinIme.mKeyboardSwitcher.getKeyboard(); // The language is always displayed when the delay is negative. final boolean needsToDisplayLanguage = localeChanged || mDelayBeforeFadeoutLanguageOnSpacebar < 0; // The language is never displayed when the delay is zero. if (mDelayBeforeFadeoutLanguageOnSpacebar != 0) { setSpacebarTextFadeFactor(inputView, needsToDisplayLanguage ? 1.0f : mFinalFadeoutFactorOfLanguageOnSpacebar, keyboard); } // The fadeout animation will start when the delay is positive. if (localeChanged && mDelayBeforeFadeoutLanguageOnSpacebar > 0) { sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard), mDelayBeforeFadeoutLanguageOnSpacebar); } } } public void startDoubleSpacesTimer() { removeMessages(MSG_SPACE_TYPED); sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), mDoubleSpacesTurnIntoPeriodTimeout); } public void cancelDoubleSpacesTimer() { removeMessages(MSG_SPACE_TYPED); } public boolean isAcceptingDoubleSpaces() { return hasMessages(MSG_SPACE_TYPED); } // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccessiveImsCallback; private boolean mHasPendingStartInput; private boolean mHasPendingFinishInputView; private boolean mHasPendingFinishInput; private EditorInfo mAppliedEditorInfo; public void startOrientationChanging() { removeMessages(MSG_PENDING_IMS_CALLBACK); resetPendingImsCallback(); mIsOrientationChanging = true; final LatinIME latinIme = getOuterInstance(); if (latinIme.isInputViewShown()) { latinIme.mKeyboardSwitcher.saveKeyboardState(); } } private void resetPendingImsCallback() { mHasPendingFinishInputView = false; mHasPendingFinishInput = false; mHasPendingStartInput = false; } private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo, boolean restarting) { if (mHasPendingFinishInputView) latinIme.onFinishInputViewInternal(mHasPendingFinishInput); if (mHasPendingFinishInput) latinIme.onFinishInputInternal(); if (mHasPendingStartInput) latinIme.onStartInputInternal(editorInfo, restarting); resetPendingImsCallback(); } public void onStartInput(EditorInfo editorInfo, boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the second onStartInput after orientation changed. mHasPendingStartInput = true; } else { if (mIsOrientationChanging && restarting) { // This is the first onStartInput after orientation changed. mIsOrientationChanging = false; mPendingSuccessiveImsCallback = true; } final LatinIME latinIme = getOuterInstance(); executePendingImsCallback(latinIme, editorInfo, restarting); latinIme.onStartInputInternal(editorInfo, restarting); } } public void onStartInputView(EditorInfo editorInfo, boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK) && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { // Typically this is the second onStartInputView after orientation changed. resetPendingImsCallback(); } else { if (mPendingSuccessiveImsCallback) { // This is the first onStartInputView after orientation changed. mPendingSuccessiveImsCallback = false; resetPendingImsCallback(); sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), PENDING_IMS_CALLBACK_DURATION); } final LatinIME latinIme = getOuterInstance(); executePendingImsCallback(latinIme, editorInfo, restarting); latinIme.onStartInputViewInternal(editorInfo, restarting); mAppliedEditorInfo = editorInfo; } } public void onFinishInputView(boolean finishingInput) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the first onFinishInputView after orientation changed. mHasPendingFinishInputView = true; } else { final LatinIME latinIme = getOuterInstance(); latinIme.onFinishInputViewInternal(finishingInput); mAppliedEditorInfo = null; } } public void onFinishInput() { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the first onFinishInput after orientation changed. mHasPendingFinishInput = true; } else { final LatinIME latinIme = getOuterInstance(); executePendingImsCallback(latinIme, null, false); latinIme.onFinishInputInternal(); } } } public LatinIME() { super(); mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); } @Override public void onCreate() { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs = prefs; LatinImeLogger.init(this, prefs); LanguageSwitcherProxy.init(this, prefs); InputMethodManagerCompatWrapper.init(this); SubtypeSwitcher.init(this); KeyboardSwitcher.init(this, prefs); AccessibilityUtils.init(this); super.onCreate(); mImm = InputMethodManagerCompatWrapper.getInstance(); mHandler.onCreate(); DEBUG = LatinImeLogger.sDBG; final Resources res = getResources(); mResources = res; loadSettings(); // TODO: remove the following when it's not needed by updateCorrectionMode() any more mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */); Utils.GCUtils.getInstance().reset(); boolean tryGC = true; for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { try { initSuggest(); tryGC = false; } catch (OutOfMemoryError e) { tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e); } } mDisplayOrientation = res.getConfiguration().orientation; // Register to receive ringer mode change and network state change. // Also receive installation and removal of a dictionary pack. final IntentFilter filter = new IntentFilter(); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); registerReceiver(mReceiver, filter); mVoiceProxy = VoiceProxy.init(this, prefs, mHandler); final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); packageFilter.addDataScheme(SCHEME_PACKAGE); registerReceiver(mDictionaryPackInstallReceiver, packageFilter); final IntentFilter newDictFilter = new IntentFilter(); newDictFilter.addAction( DictionaryPackInstallBroadcastReceiver.NEW_DICTIONARY_INTENT_ACTION); registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); } // Has to be package-visible for unit tests /* package */ void loadSettings() { if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mSettingsValues = new SettingsValues(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr()); mFeedbackManager = new AudioAndHapticFeedbackManager(this, mSettingsValues); resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); } private void initSuggest() { final String localeStr = mSubtypeSwitcher.getInputLocaleStr(); final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr); final Resources res = mResources; final Locale savedLocale = LocaleUtils.setSystemLocale(res, keyboardLocale); final ContactsDictionary oldContactsDictionary; if (mSuggest != null) { oldContactsDictionary = mSuggest.getContactsDictionary(); mSuggest.close(); } else { oldContactsDictionary = null; } int mainDicResId = DictionaryFactory.getMainDictionaryResourceId(res); mSuggest = new Suggest(this, mainDicResId, keyboardLocale); if (mSettingsValues.mAutoCorrectEnabled) { mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold); } mUserDictionary = new UserDictionary(this, localeStr); mSuggest.setUserDictionary(mUserDictionary); mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); resetContactsDictionary(oldContactsDictionary); mUserUnigramDictionary = new UserUnigramDictionary(this, this, localeStr, Suggest.DIC_USER_UNIGRAM); mSuggest.setUserUnigramDictionary(mUserUnigramDictionary); mUserBigramDictionary = new UserBigramDictionary(this, this, localeStr, Suggest.DIC_USER_BIGRAM); mSuggest.setUserBigramDictionary(mUserBigramDictionary); updateCorrectionMode(); LocaleUtils.setSystemLocale(res, savedLocale); } /** * Resets the contacts dictionary in mSuggest according to the user settings. * * This method takes an optional contacts dictionary to use. Since the contacts dictionary * does not depend on the locale, it can be reused across different instances of Suggest. * The dictionary will also be opened or closed as necessary depending on the settings. * * @param oldContactsDictionary an optional dictionary to use, or null */ private void resetContactsDictionary(final ContactsDictionary oldContactsDictionary) { final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict); final ContactsDictionary dictionaryToUse; if (!shouldSetDictionary) { // Make sure the dictionary is closed. If it is already closed, this is a no-op, // so it's safe to call it anyways. if (null != oldContactsDictionary) oldContactsDictionary.close(); dictionaryToUse = null; } else if (null != oldContactsDictionary) { // Make sure the old contacts dictionary is opened. If it is already open, this is a // no-op, so it's safe to call it anyways. oldContactsDictionary.reopen(this); dictionaryToUse = oldContactsDictionary; } else { dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS); } if (null != mSuggest) { mSuggest.setContactsDictionary(dictionaryToUse); } } /* package private */ void resetSuggestMainDict() { final String localeStr = mSubtypeSwitcher.getInputLocaleStr(); final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr); int mainDicResId = DictionaryFactory.getMainDictionaryResourceId(mResources); mSuggest.resetMainDict(this, mainDicResId, keyboardLocale); } @Override public void onDestroy() { if (mSuggest != null) { mSuggest.close(); mSuggest = null; } unregisterReceiver(mReceiver); unregisterReceiver(mDictionaryPackInstallReceiver); mVoiceProxy.destroy(); LatinImeLogger.commit(); LatinImeLogger.onDestroy(); super.onDestroy(); } @Override public void onConfigurationChanged(Configuration conf) { mSubtypeSwitcher.onConfigurationChanged(conf); // If orientation changed while predicting, commit the change if (mDisplayOrientation != conf.orientation) { mDisplayOrientation = conf.orientation; mHandler.startOrientationChanging(); final InputConnection ic = getCurrentInputConnection(); commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR); if (ic != null) ic.finishComposingText(); // For voice input if (isShowingOptionDialog()) mOptionsDialog.dismiss(); } mVoiceProxy.startChangingConfiguration(); super.onConfigurationChanged(conf); mVoiceProxy.onConfigurationChanged(conf); mVoiceProxy.finishChangingConfiguration(); // This will work only when the subtype is not supported. LanguageSwitcherProxy.onConfigurationChanged(conf); } @Override public View onCreateInputView() { return mKeyboardSwitcher.onCreateInputView(); } @Override public void setInputView(View view) { super.setInputView(view); mExtractArea = getWindow().getWindow().getDecorView() .findViewById(android.R.id.extractArea); mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); mSuggestionsContainer = view.findViewById(R.id.suggestions_container); mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view); if (mSuggestionsView != null) mSuggestionsView.setListener(this, view); if (LatinImeLogger.sVISUALDEBUG) { mKeyPreviewBackingView.setBackgroundColor(0x10FF0000); } } @Override public void setCandidatesView(View view) { // To ensure that CandidatesView will never be set. return; } @Override public void onStartInput(EditorInfo editorInfo, boolean restarting) { mHandler.onStartInput(editorInfo, restarting); } @Override public void onStartInputView(EditorInfo editorInfo, boolean restarting) { mHandler.onStartInputView(editorInfo, restarting); } @Override public void onFinishInputView(boolean finishingInput) { mHandler.onFinishInputView(finishingInput); } @Override public void onFinishInput() { mHandler.onFinishInput(); } private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) { super.onStartInput(editorInfo, restarting); } private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) { super.onStartInputView(editorInfo, restarting); final KeyboardSwitcher switcher = mKeyboardSwitcher; LatinKeyboardView inputView = switcher.getKeyboardView(); if (editorInfo == null) { Log.e(TAG, "Null EditorInfo in onStartInputView()"); if (LatinImeLogger.sDBG) { throw new NullPointerException("Null EditorInfo in onStartInputView()"); } return; } if (DEBUG) { Log.d(TAG, "onStartInputView: editorInfo:" + String.format("inputType=0x%08x imeOptions=0x%08x", editorInfo.inputType, editorInfo.imeOptions)); } if (StringUtils.inPrivateImeOptions(null, IME_OPTION_NO_MICROPHONE_COMPAT, editorInfo)) { Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions); Log.w(TAG, "Use " + getPackageName() + "." + IME_OPTION_NO_MICROPHONE + " instead"); } if (StringUtils.inPrivateImeOptions(getPackageName(), IME_OPTION_FORCE_ASCII, editorInfo)) { Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions); Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead"); } LatinImeLogger.onStartInputView(editorInfo); // In landscape mode, this method gets called without the input view being created. if (inputView == null) { return; } // Forward this event to the accessibility utilities, if enabled. final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); if (accessUtils.isTouchExplorationEnabled()) { accessUtils.onStartInputViewInternal(editorInfo, restarting); } mSubtypeSwitcher.updateParametersOnStartInputView(); // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to // know now whether this is a password text field, because we need to know now whether we // want to enable the voice button. final int inputType = editorInfo.inputType; mVoiceProxy.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(inputType) || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)); // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? updateFullscreenMode(); mLastSelectionStart = editorInfo.initialSelStart; mLastSelectionEnd = editorInfo.initialSelEnd; mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode()); mApplicationSpecifiedCompletions = null; inputView.closing(); mEnteredText = null; resetComposingState(true /* alsoResetLastComposedWord */); mDeleteCount = 0; mSpaceState = SPACE_STATE_NONE; loadSettings(); updateCorrectionMode(); updateSuggestionVisibility(mResources); if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) { mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold); } mVoiceProxy.loadSettings(editorInfo, mPrefs); // This will work only when the subtype is not supported. LanguageSwitcherProxy.loadSettings(); if (mSubtypeSwitcher.isKeyboardMode()) { switcher.loadKeyboard(editorInfo, mSettingsValues); } if (mSuggestionsView != null) mSuggestionsView.clear(); setSuggestionStripShownInternal( isSuggestionsStripVisible(), /* needsInputViewShown */ false); // Delay updating suggestions because keyboard input view may not be shown at this point. mHandler.postUpdateSuggestions(); mHandler.cancelDoubleSpacesTimer(); inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn, mSettingsValues.mKeyPreviewPopupDismissDelay); inputView.setProximityCorrectionEnabled(true); mVoiceProxy.onStartInputView(inputView.getWindowToken()); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } @Override public void onWindowHidden() { super.onWindowHidden(); KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) inputView.closing(); } private void onFinishInputInternal() { super.onFinishInput(); LatinImeLogger.commit(); mVoiceProxy.flushVoiceInputLogs(); KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) inputView.closing(); if (mUserUnigramDictionary != null) mUserUnigramDictionary.flushPendingWrites(); if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); } private void onFinishInputViewInternal(boolean finishingInput) { super.onFinishInputView(finishingInput); mKeyboardSwitcher.onFinishInputView(); KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) inputView.cancelAllMessages(); // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestions(); } @Override public void onUpdateExtractedText(int token, ExtractedText text) { super.onUpdateExtractedText(token, text); mVoiceProxy.showPunctuationHintIfNecessary(); } @Override public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int composingSpanStart, int composingSpanEnd) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, composingSpanEnd); if (DEBUG) { Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd + ", lss=" + mLastSelectionStart + ", lse=" + mLastSelectionEnd + ", nss=" + newSelStart + ", nse=" + newSelEnd + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd); } mVoiceProxy.setCursorAndSelection(newSelEnd, newSelStart); // TODO: refactor the following code to be less contrived. // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means // that the cursor is not at the end of the composing span, or there is a selection. // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place // as last time we were called (if there is a selection, it means the start hasn't // changed, so it's the end that did). final boolean selectionChanged = (newSelStart != composingSpanEnd || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart; // if composingSpanStart and composingSpanEnd are -1, it means there is no composing // span in the view - we can use that to narrow down whether the cursor was moved // by us or not. If we are composing a word but there is no composing span, then // we know for sure the cursor moved while we were composing and we should reset // the state. final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; if (!mExpectingUpdateSelection) { // TAKE CARE: there is a race condition when we enter this test even when the user // did not explicitly move the cursor. This happens when typing fast, where two keys // turn this flag on in succession and both onUpdateSelection() calls arrive after // the second one - the first call successfully avoids this test, but the second one // enters. For the moment we rely on noComposingSpan to further reduce the impact. // TODO: the following is probably better done in resetEntireInputState(). // it should only happen when the cursor moved, and the very purpose of the // test below is to narrow down whether this happened or not. Likewise with // the call to postUpdateShiftState. // We set this to NONE because after a cursor move, we don't want the space // state-related special processing to kick in. mSpaceState = SPACE_STATE_NONE; if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) { resetEntireInputState(); } mHandler.postUpdateShiftState(); } mExpectingUpdateSelection = false; // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not // here. It would probably be too expensive to call directly here but we may want to post a // message to delay it. The point would be to unify behavior between backspace to the // end of a word and manually put the pointer at the end of the word. // Make a note of the cursor position mLastSelectionStart = newSelStart; mLastSelectionEnd = newSelEnd; } /** * This is called when the user has clicked on the extracted text view, * when running in fullscreen mode. The default implementation hides * the suggestions view when this happens, but only if the extracted text * editor has a vertical scroll bar because its text doesn't fit. * Here we override the behavior due to the possibility that a re-correction could * cause the suggestions strip to disappear and re-appear. */ @Override public void onExtractedTextClicked() { if (isSuggestionsRequested()) return; super.onExtractedTextClicked(); } /** * This is called when the user has performed a cursor movement in the * extracted text view, when it is running in fullscreen mode. The default * implementation hides the suggestions view when a vertical movement * happens, but only if the extracted text editor has a vertical scroll bar * because its text doesn't fit. * Here we override the behavior due to the possibility that a re-correction could * cause the suggestions strip to disappear and re-appear. */ @Override public void onExtractedCursorMovement(int dx, int dy) { if (isSuggestionsRequested()) return; super.onExtractedCursorMovement(dx, dy); } @Override public void hideWindow() { LatinImeLogger.commit(); mKeyboardSwitcher.onHideWindow(); if (TRACE) Debug.stopMethodTracing(); if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } mVoiceProxy.hideVoiceWindow(); super.hideWindow(); } @Override public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) { if (DEBUG) { Log.i(TAG, "Received completions:"); if (applicationSpecifiedCompletions != null) { for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); } } } if (mInputAttributes.mApplicationSpecifiedCompletionOn) { mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; if (applicationSpecifiedCompletions == null) { clearSuggestions(); return; } SuggestedWords.Builder builder = new SuggestedWords.Builder() .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions) .setTypedWordValid(false) .setHasMinimalSuggestion(false); // When in fullscreen mode, show completions generated by the application final SuggestedWords words = builder.build(); final boolean isAutoCorrection = false; setSuggestions(words, isAutoCorrection); setAutoCorrectionIndicator(isAutoCorrection); // TODO: is this the right thing to do? What should we auto-correct to in // this case? This says to keep whatever the user typed. mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); setSuggestionStripShown(true); } } private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) { // TODO: Modify this if we support suggestions with hard keyboard if (onEvaluateInputViewShown() && mSuggestionsContainer != null) { final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false; final boolean shouldShowSuggestions = shown && (needsInputViewShown ? inputViewShown : true); if (isFullscreenMode()) { mSuggestionsContainer.setVisibility( shouldShowSuggestions ? View.VISIBLE : View.GONE); } else { mSuggestionsContainer.setVisibility( shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); } } } private void setSuggestionStripShown(boolean shown) { setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); } private void adjustInputViewHeight() { if (mKeyPreviewBackingView.getHeight() > 0) { return; } final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); if (keyboardView == null) return; final int keyboardHeight = keyboardView.getHeight(); final int suggestionsHeight = mSuggestionsContainer.getHeight(); final int displayHeight = mResources.getDisplayMetrics().heightPixels; final Rect rect = new Rect(); mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect); final int notificationBarHeight = rect.top; final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight - keyboardHeight; final LayoutParams params = mKeyPreviewBackingView.getLayoutParams(); params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight); mKeyPreviewBackingView.setLayoutParams(params); } @Override public void onComputeInsets(InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView == null || mSuggestionsContainer == null) return; adjustInputViewHeight(); // In fullscreen mode, the height of the extract area managed by InputMethodService should // be considered. // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; final int backingHeight = (mKeyPreviewBackingView.getVisibility() == View.GONE) ? 0 : mKeyPreviewBackingView.getHeight(); final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0 : mSuggestionsContainer.getHeight(); final int extraHeight = extractHeight + backingHeight + suggestionsHeight; int touchY = extraHeight; // Need to set touchable region only if input view is being shown final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); if (keyboardView != null && keyboardView.isShown()) { if (mSuggestionsContainer.getVisibility() == View.VISIBLE) { touchY -= suggestionsHeight; } final int touchWidth = inputView.getWidth(); final int touchHeight = inputView.getHeight() + extraHeight // Extend touchable region below the keyboard. + EXTENDED_TOUCHABLE_REGION_HEIGHT; if (DEBUG) { Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth + " height=" + touchHeight); } setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight); } outInsets.contentTopInsets = touchY; outInsets.visibleTopInsets = touchY; } @Override public boolean onEvaluateFullscreenMode() { // Reread resource value here, because this method is called by framework anytime as needed. final boolean isFullscreenModeAllowed = mSettingsValues.isFullscreenModeAllowed(getResources()); return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed; } @Override public void updateFullscreenMode() { super.updateFullscreenMode(); if (mKeyPreviewBackingView == null) return; // In fullscreen mode, no need to have extra space to show the key preview. // If not, we should have extra space above the keyboard to show the key preview. mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: if (event.getRepeatCount() == 0) { if (mSuggestionsView != null && mSuggestionsView.handleBack()) { return true; } final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); if (keyboardView != null && keyboardView.handleBack()) { return true; } } break; } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); // Enable shift key and DPAD to do selections if ((keyboardView != null && keyboardView.isShown()) && (keyboard != null && keyboard.isShiftedOrShiftLocked())) { KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(), event.getAction(), event.getKeyCode(), event.getRepeatCount(), event.getDeviceId(), event.getScanCode(), KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); final InputConnection ic = getCurrentInputConnection(); if (ic != null) ic.sendKeyEvent(newEvent); return true; } break; } return super.onKeyUp(keyCode, event); } // This will reset the whole input state to the starting state. It will clear // the composing word, reset the last composed word, tell the inputconnection // and the composingStateManager about it. private void resetEntireInputState() { resetComposingState(true /* alsoResetLastComposedWord */); updateSuggestions(); final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.finishComposingText(); } mVoiceProxy.setVoiceInputHighlighted(false); } private void resetComposingState(final boolean alsoResetLastComposedWord) { mWordComposer.reset(); if (alsoResetLastComposedWord) mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; } public void commitTyped(final InputConnection ic, final int separatorCode) { if (!mWordComposer.isComposingWord()) return; final CharSequence typedWord = mWordComposer.getTypedWord(); if (typedWord.length() > 0) { mLastComposedWord = mWordComposer.commitWord( LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(), separatorCode); if (ic != null) { ic.commitText(typedWord, 1); } addToUserUnigramAndBigramDictionaries(typedWord, UserUnigramDictionary.FREQUENCY_FOR_TYPED); } updateSuggestions(); } public boolean getCurrentAutoCapsState() { final InputConnection ic = getCurrentInputConnection(); EditorInfo ei = getCurrentInputEditorInfo(); if (mSettingsValues.mAutoCap && ic != null && ei != null && ei.inputType != InputType.TYPE_NULL) { return ic.getCursorCapsMode(ei.inputType) != 0; } return false; } // "ic" may be null private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) { if (null == ic) return; CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) { ic.deleteSurroundingText(2, 0); ic.commitText(lastTwo.charAt(1) + " ", 1); mKeyboardSwitcher.updateShiftState(); } } private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) { if (mCorrectionMode == Suggest.CORRECTION_NONE) return false; if (ic == null) return false; final CharSequence lastThree = ic.getTextBeforeCursor(3, 0); if (lastThree != null && lastThree.length() == 3 && StringUtils.canBeFollowedByPeriod(lastThree.charAt(0)) && lastThree.charAt(1) == Keyboard.CODE_SPACE && lastThree.charAt(2) == Keyboard.CODE_SPACE && mHandler.isAcceptingDoubleSpaces()) { mHandler.cancelDoubleSpacesTimer(); ic.deleteSurroundingText(2, 0); ic.commitText(". ", 1); mKeyboardSwitcher.updateShiftState(); return true; } return false; } // "ic" may be null private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) { if (ic == null) return; final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { ic.deleteSurroundingText(1, 0); } } @Override public boolean addWordToDictionary(String word) { mUserDictionary.addWord(word, 128); // Suggestion strip should be updated after the operation of adding word to the // user dictionary mHandler.postUpdateSuggestions(); return true; } private static boolean isAlphabet(int code) { return Character.isLetter(code); } private void onSettingsKeyPressed() { if (isShowingOptionDialog()) return; if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) { showSubtypeSelectorAndSettings(); } else if (SubtypeUtils.hasMultipleEnabledIMEsOrSubtypes( false /* exclude aux subtypes */)) { showOptionsMenu(); } else { launchSettings(); } } // Virtual codes representing custom requests. These are used in onCustomRequest() below. public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1; public static final int CODE_HAPTIC_AND_AUDIO_FEEDBACK = 2; @Override public boolean onCustomRequest(int requestCode) { if (isShowingOptionDialog()) return false; switch (requestCode) { case CODE_SHOW_INPUT_METHOD_PICKER: if (SubtypeUtils.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { mImm.showInputMethodPicker(); return true; } return false; case CODE_HAPTIC_AND_AUDIO_FEEDBACK: hapticAndAudioFeedback(Keyboard.CODE_UNSPECIFIED); return true; } return false; } private boolean isShowingOptionDialog() { return mOptionsDialog != null && mOptionsDialog.isShowing(); } private static int getActionId(Keyboard keyboard) { return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE; } private void performeEditorAction(int actionId) { final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.performEditorAction(actionId); } } private void handleLanguageSwitchKey() { final boolean includesOtherImes = !mSettingsValues.mIncludesOtherImesInLanguageSwitchList; final IBinder token = getWindow().getWindow().getAttributes().token; if (mShouldSwitchToLastSubtype) { final InputMethodSubtypeCompatWrapper lastSubtype = mImm.getLastInputMethodSubtype(); final boolean lastSubtypeBelongsToThisIme = SubtypeUtils.checkIfSubtypeBelongsToThisIme( this, lastSubtype); if ((includesOtherImes || lastSubtypeBelongsToThisIme) && mImm.switchToLastInputMethod(token)) { mShouldSwitchToLastSubtype = false; } else { mImm.switchToNextInputMethod(token, !includesOtherImes); mShouldSwitchToLastSubtype = true; } } else { mImm.switchToNextInputMethod(token, !includesOtherImes); } } private void sendKeyCodePoint(int code) { // TODO: Remove this special handling of digit letters. // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. if (code >= '0' && code <= '9') { super.sendKeyChar((char)code); return; } final InputConnection ic = getCurrentInputConnection(); if (ic != null) { final String text = new String(new int[] { code }, 0, 1); ic.commitText(text, text.length()); } } // Implementation of {@link KeyboardActionListener}. @Override public void onCodeInput(int primaryCode, int x, int y) { final long when = SystemClock.uptimeMillis(); if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { mDeleteCount = 0; } mLastKeyTime = when; final KeyboardSwitcher switcher = mKeyboardSwitcher; // The space state depends only on the last character pressed and its own previous // state. Here, we revert the space state to neutral if the key is actually modifying // the input contents (any non-shift key), which is what we should do for // all inputs that do not result in a special state. Each character handling is then // free to override the state as they see fit. final int spaceState = mSpaceState; if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false; // TODO: Consolidate the double space timer, mLastKeyTime, and the space state. if (primaryCode != Keyboard.CODE_SPACE) { mHandler.cancelDoubleSpacesTimer(); } boolean didAutoCorrect = false; switch (primaryCode) { case Keyboard.CODE_DELETE: mSpaceState = SPACE_STATE_NONE; handleBackspace(spaceState); mDeleteCount++; mExpectingUpdateSelection = true; mShouldSwitchToLastSubtype = true; LatinImeLogger.logOnDelete(); break; case Keyboard.CODE_SHIFT: case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: // Shift and symbol key is handled in onPressKey() and onReleaseKey(). break; case Keyboard.CODE_SETTINGS: onSettingsKeyPressed(); break; case Keyboard.CODE_SHORTCUT: mSubtypeSwitcher.switchToShortcutIME(); break; case Keyboard.CODE_ACTION_ENTER: performeEditorAction(getActionId(switcher.getKeyboard())); break; case Keyboard.CODE_ACTION_NEXT: performeEditorAction(EditorInfo.IME_ACTION_NEXT); break; case Keyboard.CODE_ACTION_PREVIOUS: EditorInfoCompatUtils.performEditorActionPrevious(getCurrentInputConnection()); break; case Keyboard.CODE_LANGUAGE_SWITCH: handleLanguageSwitchKey(); break; default: mSpaceState = SPACE_STATE_NONE; if (mSettingsValues.isWordSeparator(primaryCode)) { didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); } else { handleCharacter(primaryCode, x, y, spaceState); } mExpectingUpdateSelection = true; mShouldSwitchToLastSubtype = true; break; } switcher.onCodeInput(primaryCode); // Reset after any single keystroke if (!didAutoCorrect) mLastComposedWord.deactivate(); mEnteredText = null; } @Override public void onTextInput(CharSequence text) { mVoiceProxy.commitVoiceInput(); final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; ic.beginBatchEdit(); commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR); text = specificTldProcessingOnTextInput(ic, text); if (SPACE_STATE_PHANTOM == mSpaceState) { sendKeyCodePoint(Keyboard.CODE_SPACE); } ic.commitText(text, 1); ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT); mSpaceState = SPACE_STATE_NONE; mEnteredText = text; resetComposingState(true /* alsoResetLastComposedWord */); } // ic may not be null private CharSequence specificTldProcessingOnTextInput(final InputConnection ic, final CharSequence text) { if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD || !Character.isLetter(text.charAt(1))) { // Not a tld: do nothing. return text; } // We have a TLD (or something that looks like this): make sure we don't add // a space even if currently in phantom mode. mSpaceState = SPACE_STATE_NONE; final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Keyboard.CODE_PERIOD) { return text.subSequence(1, text.length()); } else { return text; } } @Override public void onCancelInput() { // User released a finger outside any key mKeyboardSwitcher.onCancelInput(); } private void handleBackspace(final int spaceState) { if (mVoiceProxy.logAndRevertVoiceInput()) return; final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; ic.beginBatchEdit(); handleBackspaceWhileInBatchEdit(spaceState, ic); ic.endBatchEdit(); } // "ic" may not be null. private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) { mVoiceProxy.handleBackspace(); // In many cases, we may have to put the keyboard in auto-shift state again. mHandler.postUpdateShiftState(); if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { // Cancel multi-character input: remove the text we just entered. // This is triggered on backspace after a key that inputs multiple characters, // like the smiley key or the .com key. ic.deleteSurroundingText(mEnteredText.length(), 0); // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. // In addition we know that spaceState is false, and that we should not be // reverting any autocorrect at this point. So we can safely return. return; } if (mWordComposer.isComposingWord()) { final int length = mWordComposer.size(); if (length > 0) { mWordComposer.deleteLast(); ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); // If we have deleted the last remaining character of a word, then we are not // isComposingWord() any more. if (!mWordComposer.isComposingWord()) { // Not composing word any more, so we can show bigrams. mHandler.postUpdateBigramPredictions(); } else { // Still composing a word, so we still have letters to deduce a suggestion from. mHandler.postUpdateSuggestions(); } } else { ic.deleteSurroundingText(1, 0); } } else { if (mLastComposedWord.canRevertCommit()) { Utils.Stats.onAutoCorrectionCancellation(); revertCommit(ic); return; } if (SPACE_STATE_DOUBLE == spaceState) { if (revertDoubleSpaceWhileInBatchEdit(ic)) { // No need to reset mSpaceState, it has already be done (that's why we // receive it as a parameter) return; } } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { if (revertSwapPunctuation(ic)) { // Likewise return; } } // No cancelling of commit/double space/swap: we have a regular backspace. // We should backspace one char and restart suggestion if at the end of a word. if (mLastSelectionStart != mLastSelectionEnd) { // If there is a selection, remove it. final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart; ic.setSelection(mLastSelectionEnd, mLastSelectionEnd); ic.deleteSurroundingText(lengthToDelete, 0); } else { // There is no selection, just delete one character. if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { // This should never happen. Log.e(TAG, "Backspace when we don't know the selection position"); } ic.deleteSurroundingText(1, 0); if (mDeleteCount > DELETE_ACCELERATE_AT) { ic.deleteSurroundingText(1, 0); } } if (isSuggestionsRequested()) { restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic); } } } // ic may be null private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code, final int spaceState, final boolean isFromSuggestionStrip) { if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { removeTrailingSpaceWhileInBatchEdit(ic); return false; } else if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState) && isFromSuggestionStrip) { if (mSettingsValues.isWeakSpaceSwapper(code)) { return true; } else { if (mSettingsValues.isWeakSpaceStripper(code)) { removeTrailingSpaceWhileInBatchEdit(ic); } return false; } } else { return false; } } private void handleCharacter(final int primaryCode, final int x, final int y, final int spaceState) { mVoiceProxy.handleCharacter(); final InputConnection ic = getCurrentInputConnection(); if (null != ic) ic.beginBatchEdit(); // TODO: if ic is null, does it make any sense to call this? handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic); if (null != ic) ic.endBatchEdit(); } // "ic" may be null without this crashing, but the behavior will be really strange private void handleCharacterWhileInBatchEdit(final int primaryCode, final int x, final int y, final int spaceState, final InputConnection ic) { boolean isComposingWord = mWordComposer.isComposingWord(); if (SPACE_STATE_PHANTOM == spaceState && !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) { if (isComposingWord) { // Sanity check throw new RuntimeException("Should not be composing here"); } sendKeyCodePoint(Keyboard.CODE_SPACE); } if ((isAlphabet(primaryCode) || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) && isSuggestionsRequested() && !isCursorTouchingWord()) { if (!isComposingWord) { // Reset entirely the composing state anyway, then start composing a new word unless // the character is a single quote. The idea here is, single quote is not a // separator and it should be treated as a normal character, except in the first // position where it should not start composing a word. isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode); // Here we don't need to reset the last composed word. It will be reset // when we commit this one, if we ever do; if on the other hand we backspace // it entirely and resume suggestions on the previous word, we'd like to still // have touch coordinates for it. resetComposingState(false /* alsoResetLastComposedWord */); clearSuggestions(); } } if (isComposingWord) { mWordComposer.add( primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector()); if (ic != null) { // If it's the first letter, make note of auto-caps state if (mWordComposer.size() == 1) { mWordComposer.setAutoCapitalized(getCurrentAutoCapsState()); } ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } mHandler.postUpdateSuggestions(); } else { final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x); sendKeyCodePoint(primaryCode); if (swapWeakSpace) { swapSwapperAndSpaceWhileInBatchEdit(ic); mSpaceState = SPACE_STATE_WEAK; } // Some characters are not word separators, yet they don't start a new // composing span. For these, we haven't changed the suggestion strip, and // if the "add to dictionary" hint is shown, we should do so now. Examples of // such characters include single quote, dollar, and others; the exact list is // the list of characters for which we enter handleCharacterWhileInBatchEdit // that don't match the test if ((isAlphabet...)) at the top of this method. if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) { mHandler.postUpdateBigramPredictions(); } } Utils.Stats.onNonSeparator((char)primaryCode, x, y); } // Returns true if we did an autocorrection, false otherwise. private boolean handleSeparator(final int primaryCode, final int x, final int y, final int spaceState) { mVoiceProxy.handleSeparator(); // Should dismiss the "Touch again to save" message when handling separator if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) { mHandler.cancelUpdateBigramPredictions(); mHandler.postUpdateSuggestions(); } boolean didAutoCorrect = false; // Handle separator final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); } if (mWordComposer.isComposingWord()) { // In certain languages where single quote is a separator, it's better // not to auto correct, but accept the typed word. For instance, // in Italian dov' should not be expanded to dove' because the elision // requires the last vowel to be removed. final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) { commitCurrentAutoCorrection(primaryCode, ic); didAutoCorrect = true; } else { commitTyped(ic, primaryCode); } } final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x); if (SPACE_STATE_PHANTOM == spaceState && mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) { sendKeyCodePoint(Keyboard.CODE_SPACE); } sendKeyCodePoint(primaryCode); if (Keyboard.CODE_SPACE == primaryCode) { if (isSuggestionsRequested()) { if (maybeDoubleSpaceWhileInBatchEdit(ic)) { mSpaceState = SPACE_STATE_DOUBLE; } else if (!isShowingPunctuationList()) { mSpaceState = SPACE_STATE_WEAK; } } mHandler.startDoubleSpacesTimer(); if (!isCursorTouchingWord()) { mHandler.cancelUpdateSuggestions(); mHandler.postUpdateBigramPredictions(); } } else { if (swapWeakSpace) { swapSwapperAndSpaceWhileInBatchEdit(ic); mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; } else if (SPACE_STATE_PHANTOM == spaceState) { // If we are in phantom space state, and the user presses a separator, we want to // stay in phantom space state so that the next keypress has a chance to add the // space. For example, if I type "Good dat", pick "day" from the suggestion strip // then insert a comma and go on to typing the next word, I want the space to be // inserted automatically before the next word, the same way it is when I don't // input the comma. mSpaceState = SPACE_STATE_PHANTOM; } // Set punctuation right away. onUpdateSelection will fire but tests whether it is // already displayed or not, so it's okay. setPunctuationSuggestions(); } Utils.Stats.onSeparator((char)primaryCode, x, y); if (ic != null) { ic.endBatchEdit(); } return didAutoCorrect; } private CharSequence getTextWithUnderline(final CharSequence text) { return mIsAutoCorrectionIndicatorOn ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) : text; } private void handleClose() { commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR); mVoiceProxy.handleClose(); requestHideSelf(0); LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) inputView.closing(); } public boolean isSuggestionsRequested() { return mInputAttributes.mIsSettingsSuggestionStripOn && (mCorrectionMode > 0 || isShowingSuggestionsStrip()); } public boolean isShowingPunctuationList() { if (mSuggestionsView == null) return false; return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions(); } public boolean isShowingSuggestionsStrip() { return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE) || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT); } public boolean isSuggestionsStripVisible() { if (mSuggestionsView == null) return false; if (mSuggestionsView.isShowingAddToDictionaryHint()) return true; if (!isShowingSuggestionsStrip()) return false; if (mInputAttributes.mApplicationSpecifiedCompletionOn) return true; return isSuggestionsRequested(); } public void switchToKeyboardView() { if (DEBUG) { Log.d(TAG, "Switch to keyboard view."); } View v = mKeyboardSwitcher.getKeyboardView(); if (v != null) { // Confirms that the keyboard view doesn't have parent view. ViewParent p = v.getParent(); if (p != null && p instanceof ViewGroup) { ((ViewGroup) p).removeView(v); } setInputView(v); } setSuggestionStripShown(isSuggestionsStripVisible()); updateInputViewShown(); mHandler.postUpdateSuggestions(); } public void clearSuggestions() { setSuggestions(SuggestedWords.EMPTY, false); setAutoCorrectionIndicator(false); } public void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) { if (mSuggestionsView != null) { mSuggestionsView.setSuggestions(words); mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); } } private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { // Put a blue underline to a word in TextView which will be auto-corrected. final InputConnection ic = getCurrentInputConnection(); if (ic != null) { if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator) { if (mWordComposer.isComposingWord()) { mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; final CharSequence textWithUnderline = getTextWithUnderline(mWordComposer.getTypedWord()); ic.setComposingText(textWithUnderline, 1); } } } } public void updateSuggestions() { // Check if we have a suggestion engine attached. if ((mSuggest == null || !isSuggestionsRequested()) && !mVoiceProxy.isVoiceInputHighlighted()) { if (mWordComposer.isComposingWord()) { Log.w(TAG, "Called updateSuggestions but suggestions were not requested!"); mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); } return; } mHandler.cancelUpdateSuggestions(); mHandler.cancelUpdateBigramPredictions(); if (!mWordComposer.isComposingWord()) { setPunctuationSuggestions(); return; } // TODO: May need a better way of retrieving previous word final InputConnection ic = getCurrentInputConnection(); final CharSequence prevWord; if (null == ic) { prevWord = null; } else { prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators); } final CharSequence typedWord = mWordComposer.getTypedWord(); final int quotesCount = mWordComposer.trailingSingleQuotesCount(); // getSuggestedWordBuilder handles gracefully a null value of prevWord final SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(mWordComposer, prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode); boolean autoCorrectionAvailable = mSuggest.hasAutoCorrection(); // Here, we want to promote a whitelisted word if exists. // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid" // but still autocorrected from - in the case the whitelist only capitalizes the word. // The whitelist should be case-insensitive, so it's not possible to be consistent with // a boolean flag. Right now this is handled with a slight hack in // WhitelistDictionary#shouldForciblyAutoCorrectFrom. final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected( mSuggest.getUnigramDictionaries(), // If the typed string ends with a single quote, for dictionary lookup purposes // we behave as if the single quote was not here. Here, we are looking up the // typed string in the dictionary (to avoid autocorrecting from an existing // word, so for consistency this lookup should be made WITHOUT the trailing // single quote. quotesCount > 0 ? typedWord.subSequence(0, typedWord.length() - quotesCount) : typedWord, preferCapitalization()); if (mCorrectionMode == Suggest.CORRECTION_FULL || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { autoCorrectionAvailable |= (!allowsToBeAutoCorrected); } // Don't auto-correct words with multiple capital letter autoCorrectionAvailable &= !mWordComposer.isMostlyCaps(); // Basically, we update the suggestion strip only when suggestion count > 1. However, // there is an exception: We update the suggestion strip whenever typed word's length // is 1 or typed word is found in dictionary, regardless of suggestion count. Actually, // in most cases, suggestion count is 1 when typed word's length is 1, but we do always // need to clear the previous state when the user starts typing a word (i.e. typed word's // length == 1). if (typedWord != null) { if (builder.size() > 1 || typedWord.length() == 1 || (!allowsToBeAutoCorrected) || mSuggestionsView.isShowingAddToDictionaryHint()) { builder.setTypedWordValid(!allowsToBeAutoCorrected).setHasMinimalSuggestion( autoCorrectionAvailable); } else { SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions(); if (previousSuggestions == mSettingsValues.mSuggestPuncList) { if (builder.size() == 0) { return; } previousSuggestions = SuggestedWords.EMPTY; } builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions); } } if (Suggest.shouldBlockAutoCorrectionBySafetyNet(builder, mSuggest)) { builder.setShouldBlockAutoCorrectionBySafetyNet(); } showSuggestions(builder.build(), typedWord); } public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) { final CharSequence autoCorrection; if (suggestedWords.size() > 0) { if (!suggestedWords.mShouldBlockAutoCorrectionBySafetyNet && suggestedWords.hasAutoCorrectionWord()) { autoCorrection = suggestedWords.getWord(1); } else { autoCorrection = typedWord; } } else { autoCorrection = null; } mWordComposer.setAutoCorrection(autoCorrection); final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); setSuggestions(suggestedWords, isAutoCorrection); setAutoCorrectionIndicator(isAutoCorrection); setSuggestionStripShown(isSuggestionsStripVisible()); } private void commitCurrentAutoCorrection(final int separatorCodePoint, final InputConnection ic) { // Complete any pending suggestions query first if (mHandler.hasPendingUpdateSuggestions()) { mHandler.cancelUpdateSuggestions(); updateSuggestions(); } final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull(); if (autoCorrection != null) { final String typedWord = mWordComposer.getTypedWord(); if (TextUtils.isEmpty(typedWord)) { throw new RuntimeException("We have an auto-correction but the typed word " + "is empty? Impossible! I must commit suicide."); } Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint); mExpectingUpdateSelection = true; commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separatorCodePoint); // Add the word to the user unigram dictionary if it's not a known word addToUserUnigramAndBigramDictionaries(autoCorrection, UserUnigramDictionary.FREQUENCY_FOR_TYPED); if (!typedWord.equals(autoCorrection) && null != ic) { // This will make the correction flash for a short while as a visual clue // to the user that auto-correction happened. InputConnectionCompatUtils.commitCorrection(ic, mLastSelectionEnd - typedWord.length(), typedWord, autoCorrection); } } } @Override public void pickSuggestionManually(final int index, final CharSequence suggestion) { final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions(); mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion, mSettingsValues.mWordSeparators); if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) { int firstChar = Character.codePointAt(suggestion, 0); if ((!mSettingsValues.isWeakSpaceStripper(firstChar)) && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) { sendKeyCodePoint(Keyboard.CODE_SPACE); } } if (mInputAttributes.mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null && index >= 0 && index < mApplicationSpecifiedCompletions.length) { if (mSuggestionsView != null) { mSuggestionsView.clear(); } mKeyboardSwitcher.updateShiftState(); resetComposingState(true /* alsoResetLastComposedWord */); final InputConnection ic = getCurrentInputConnection(); if (ic != null) { final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; ic.commitCompletion(completionInfo); } return; } // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput if (suggestion.length() == 1 && isShowingPunctuationList()) { // Word separators are suggested before the user inputs something. // So, LatinImeLogger logs "" as a user's input. LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords); // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. final int primaryCode = suggestion.charAt(0); onCodeInput(primaryCode, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE); return; } // We need to log before we commit, because the word composer will store away the user // typed word. LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(), suggestion.toString(), index, suggestedWords); mExpectingUpdateSelection = true; commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); // Add the word to the auto dictionary if it's not a known word if (index == 0) { addToUserUnigramAndBigramDictionaries(suggestion, UserUnigramDictionary.FREQUENCY_FOR_PICKED); } else { addToOnlyBigramDictionary(suggestion, 1); } mSpaceState = SPACE_STATE_PHANTOM; // TODO: is this necessary? mKeyboardSwitcher.updateShiftState(); // We should show the "Touch again to save" hint if the user pressed the first entry // AND either: // - There is no dictionary (we know that because we tried to load it => null != mSuggest // AND mSuggest.hasMainDictionary() is false) // - There is a dictionary and the word is not in it // Please note that if mSuggest is null, it means that everything is off: suggestion // and correction, so we shouldn't try to show the hint // We used to look at mCorrectionMode here, but showing the hint should have nothing // to do with the autocorrection setting. final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null // If there is no dictionary the hint should be shown. && (!mSuggest.hasMainDictionary() // If "suggestion" is not in the dictionary, the hint should be shown. || !AutoCorrection.isValidWord( mSuggest.getUnigramDictionaries(), suggestion, true)); Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); if (!showingAddToDictionaryHint) { // If we're not showing the "Touch again to save", then show corrections again. // In case the cursor position doesn't change, make sure we show the suggestions again. updateBigramPredictions(); // Updating the predictions right away may be slow and feel unresponsive on slower // terminals. On the other hand if we just postUpdateBigramPredictions() it will // take a noticeable delay to update them which may feel uneasy. } else { if (mIsUserDictionaryAvailable) { mSuggestionsView.showAddToDictionaryHint( suggestion, mSettingsValues.mHintToSaveText); } else { mHandler.postUpdateSuggestions(); } } } /** * Commits the chosen word to the text field and saves it for later retrieval. */ private void commitChosenWord(final CharSequence bestWord, final int commitType, final int separatorCode) { final InputConnection ic = getCurrentInputConnection(); if (ic != null) { mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators); if (mSettingsValues.mEnableSuggestionSpanInsertion) { final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions(); ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( this, bestWord, suggestedWords), 1); } else { ic.commitText(bestWord, 1); } } // TODO: figure out here if this is an auto-correct or if the best word is actually // what user typed. Note: currently this is done much later in // LastComposedWord#didCommitTypedWord by string equality of the remembered // strings. mLastComposedWord = mWordComposer.commitWord(commitType, bestWord.toString(), separatorCode); } private static final WordComposer sEmptyWordComposer = new WordComposer(); public void updateBigramPredictions() { if (mSuggest == null || !isSuggestionsRequested()) return; if (!mSettingsValues.mBigramPredictionEnabled) { setPunctuationSuggestions(); return; } final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(), mSettingsValues.mWordSeparators); SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(sEmptyWordComposer, prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode); if (builder.size() > 0) { // Explicitly supply an empty typed word (the no-second-arg version of // showSuggestions will retrieve the word near the cursor, we don't want that here) showSuggestions(builder.build(), ""); } else { if (!isShowingPunctuationList()) setPunctuationSuggestions(); } } public void setPunctuationSuggestions() { setSuggestions(mSettingsValues.mSuggestPuncList, false); setAutoCorrectionIndicator(false); setSuggestionStripShown(isSuggestionsStripVisible()); } private void addToUserUnigramAndBigramDictionaries(CharSequence suggestion, int frequencyDelta) { checkAddToDictionary(suggestion, frequencyDelta, false); } private void addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta) { checkAddToDictionary(suggestion, frequencyDelta, true); } /** * Adds to the UserBigramDictionary and/or UserUnigramDictionary * @param selectedANotTypedWord true if it should be added to bigram dictionary if possible */ private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta, boolean selectedANotTypedWord) { if (suggestion == null || suggestion.length() < 1) return; // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be // adding words in situations where the user or application really didn't // want corrections enabled or learned. if (!(mCorrectionMode == Suggest.CORRECTION_FULL || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { return; } if (null != mSuggest && null != mUserUnigramDictionary) { final boolean selectedATypedWordAndItsInUserUnigramDic = !selectedANotTypedWord && mUserUnigramDictionary.isValidWord(suggestion); final boolean isValidWord = AutoCorrection.isValidWord( mSuggest.getUnigramDictionaries(), suggestion, true); final boolean needsToAddToUserUnigramDictionary = selectedATypedWordAndItsInUserUnigramDic || !isValidWord; if (needsToAddToUserUnigramDictionary) { mUserUnigramDictionary.addWord(suggestion.toString(), frequencyDelta); } } if (mUserBigramDictionary != null) { // We don't want to register as bigrams words separated by a separator. // For example "I will, and you too" : we don't want the pair ("will" "and") to be // a bigram. final InputConnection ic = getCurrentInputConnection(); if (null != ic) { final CharSequence prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators); if (!TextUtils.isEmpty(prevWord)) { mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString()); } } } } public boolean isCursorTouchingWord() { final InputConnection ic = getCurrentInputConnection(); if (ic == null) return false; CharSequence before = ic.getTextBeforeCursor(1, 0); CharSequence after = ic.getTextAfterCursor(1, 0); if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0))) { return true; } if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0))) { return true; } return false; } // "ic" must not be null private static boolean sameAsTextBeforeCursor(final InputConnection ic, final CharSequence text) { final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0); return TextUtils.equals(text, beforeText); } // "ic" must not be null /** * Check if the cursor is actually at the end of a word. If so, restart suggestions on this * word, else do nothing. */ private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord( final InputConnection ic) { // Bail out if the cursor is not at the end of a word (cursor must be preceded by // non-whitespace, non-separator, non-start-of-text) // Example ("|" is the cursor here) : "|a" " |a" " | " all get rejected here. final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0); if (TextUtils.isEmpty(textBeforeCursor) || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return; // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, // separator or end of line/text) // Example: "test|" "te|st" get rejected here final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0); if (!TextUtils.isEmpty(textAfterCursor) && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return; // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) // Example: " -|" gets rejected here but "e-|" and "e|" are okay CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators); // We don't suggest on leading single quotes, so we have to remove them from the word if // it starts with single quotes. while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) { word = word.subSequence(1, word.length()); } if (TextUtils.isEmpty(word)) return; final char firstChar = word.charAt(0); // we just tested that word is not empty if (word.length() == 1 && !Character.isLetter(firstChar)) return; // We only suggest on words that start with a letter or a symbol that is excluded from // word separators (see #handleCharacterWhileInBatchEdit). if (!(isAlphabet(firstChar) || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) { return; } // Okay, we are at the end of a word. Restart suggestions. restartSuggestionsOnWordBeforeCursor(ic, word); } // "ic" must not be null private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic, final CharSequence word) { mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); ic.deleteSurroundingText(word.length(), 0); ic.setComposingText(word, 1); mHandler.postUpdateSuggestions(); } // "ic" must not be null private void revertCommit(final InputConnection ic) { final String originallyTypedWord = mLastComposedWord.mTypedWord; final CharSequence committedWord = mLastComposedWord.mCommittedWord; final int cancelLength = committedWord.length(); final int separatorLength = LastComposedWord.getSeparatorLength( mLastComposedWord.mSeparatorCode); // TODO: should we check our saved separator against the actual contents of the text view? if (DEBUG) { if (mWordComposer.isComposingWord()) { throw new RuntimeException("revertCommit, but we are composing a word"); } final String wordBeforeCursor = ic.getTextBeforeCursor(cancelLength + separatorLength, 0) .subSequence(0, cancelLength).toString(); if (!TextUtils.equals(committedWord, wordBeforeCursor)) { throw new RuntimeException("revertCommit check failed: we thought we were " + "reverting \"" + committedWord + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); } } ic.deleteSurroundingText(cancelLength + separatorLength, 0); if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) { // This is the case when we cancel a manual pick. // We should restart suggestion on the word right away. mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord); ic.setComposingText(originallyTypedWord, 1); } else { ic.commitText(originallyTypedWord, 1); // Re-insert the separator sendKeyCodePoint(mLastComposedWord.mSeparatorCode); Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); // Don't restart suggestion yet. We'll restart if the user deletes the // separator. } mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; mHandler.cancelUpdateBigramPredictions(); mHandler.postUpdateSuggestions(); } // "ic" must not be null private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) { mHandler.cancelDoubleSpacesTimer(); // Here we test whether we indeed have a period and a space before us. This should not // be needed, but it's there just in case something went wrong. final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); if (!". ".equals(textBeforeCursor)) { // Theoretically we should not be coming here if there isn't ". " before the // cursor, but the application may be changing the text while we are typing, so // anything goes. We should not crash. Log.d(TAG, "Tried to revert double-space combo but we didn't find " + "\". \" just before the cursor."); return false; } ic.deleteSurroundingText(2, 0); ic.commitText(" ", 1); return true; } private static boolean revertSwapPunctuation(final InputConnection ic) { // Here we test whether we indeed have a space and something else before us. This should not // be needed, but it's there just in case something went wrong. final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to // enter surrogate pairs this code will have been removed. if (TextUtils.isEmpty(textBeforeCursor) || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) { // We may only come here if the application is changing the text while we are typing. // This is quite a broken case, but not logically impossible, so we shouldn't crash, // but some debugging log may be in order. Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " + "find a space just before the cursor."); return false; } ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1); ic.endBatchEdit(); return true; } public boolean isWordSeparator(int code) { return mSettingsValues.isWordSeparator(code); } public boolean preferCapitalization() { return mWordComposer.isFirstCharCapitalized(); } // Notify that language or mode have been changed and toggleLanguage will update KeyboardID // according to new language or mode. public void onRefreshKeyboard() { if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) { // Before Honeycomb, Voice IME is in LatinIME and it changes the current input view, // so that we need to re-create the keyboard input view here. setInputView(mKeyboardSwitcher.onCreateInputView()); } // When the device locale is changed in SetupWizard etc., this method may get called via // onConfigurationChanged before SoftInputWindow is shown. if (mKeyboardSwitcher.getKeyboardView() != null) { // Reload keyboard because the current language has been changed. mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues); } initSuggest(); loadSettings(); // Since we just changed languages, we should re-evaluate suggestions with whatever word // we are currently composing. If we are not composing anything, we may want to display // predictions or punctuation signs (which is done by updateBigramPredictions anyway). if (isCursorTouchingWord()) { mHandler.postUpdateSuggestions(); } else { mHandler.postUpdateBigramPredictions(); } } public void hapticAndAudioFeedback(final int primaryCode) { mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView()); } @Override public void onPressKey(int primaryCode) { mKeyboardSwitcher.onPressKey(primaryCode); } @Override public void onReleaseKey(int primaryCode, boolean withSliding) { mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); // If accessibility is on, ensure the user receives keyboard state updates. if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { switch (primaryCode) { case Keyboard.CODE_SHIFT: AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); break; case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); break; } } } // receive ringer mode change and network state change. private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { mSubtypeSwitcher.onNetworkStateChanged(intent); } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { mFeedbackManager.onRingerModeChanged(); } } }; // TODO: remove this method when VoiceProxy has been removed public void vibrate() { mFeedbackManager.vibrate(mKeyboardSwitcher.getKeyboardView()); } public boolean isAutoCapitalized() { return mWordComposer.isAutoCapitalized(); } private void updateCorrectionMode() { // TODO: cleanup messy flags final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE; mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect) ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; } private void updateSuggestionVisibility(final Resources res) { final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting; for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) { if (suggestionVisiblityStr.equals(res.getString(visibility))) { mSuggestionVisibility = visibility; break; } } } protected void launchSettings() { launchSettingsClass(Settings.class); } public void launchDebugSettings() { launchSettingsClass(DebugSettings.class); } protected void launchSettingsClass(Class settingsClass) { handleClose(); Intent intent = new Intent(); intent.setClass(LatinIME.this, settingsClass); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } private void showSubtypeSelectorAndSettings() { final CharSequence title = getString(R.string.english_ime_input_options); final CharSequence[] items = new CharSequence[] { // TODO: Should use new string "Select active input modes". getString(R.string.language_selection_title), getString(R.string.english_ime_settings), }; final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface di, int position) { di.dismiss(); switch (position) { case 0: Intent intent = CompatUtils.getInputLanguageSelectionIntent( SubtypeUtils.getInputMethodId(getPackageName()), Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; case 1: launchSettings(); break; } } }; final AlertDialog.Builder builder = new AlertDialog.Builder(this) .setItems(items, listener) .setTitle(title); showOptionDialogInternal(builder.create()); } private void showOptionsMenu() { final CharSequence title = getString(R.string.english_ime_input_options); final CharSequence[] items = new CharSequence[] { getString(R.string.selectInputMethod), getString(R.string.english_ime_settings), }; final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface di, int position) { di.dismiss(); switch (position) { case 0: mImm.showInputMethodPicker(); break; case 1: launchSettings(); break; } } }; final AlertDialog.Builder builder = new AlertDialog.Builder(this) .setItems(items, listener) .setTitle(title); showOptionDialogInternal(builder.create()); } @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { super.dump(fd, fout, args); final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; p.println(" Keyboard mode = " + keyboardMode); p.println(" mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn); p.println(" mCorrectionMode=" + mCorrectionMode); p.println(" isComposingWord=" + mWordComposer.isComposingWord()); p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled); p.println(" mSoundOn=" + mSettingsValues.mSoundOn); p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn); p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn); p.println(" mInputAttributes=" + mInputAttributes.toString()); } }