/* * 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 static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII; import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; import android.app.Activity; 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.pm.ApplicationInfo; 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.Build.VERSION_CODES; import android.os.Debug; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.InputType; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.SuggestionSpan; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.event.EventInterpreter; import com.android.inputmethod.keyboard.KeyDetector; 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.MainKeyboardView; import com.android.inputmethod.latin.RichInputConnection.Range; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.Utils.Stats; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.suggestions.SuggestionStripView; import com.android.inputmethod.research.ResearchLogger; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Locale; import java.util.TreeSet; /** * Input method implementation for Qwerty'ish keyboard. */ public final class LatinIME extends InputMethodService implements KeyboardActionListener, SuggestionStripView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener, Suggest.SuggestInitializationListener { private static final String TAG = LatinIME.class.getSimpleName(); private static final boolean TRACE = false; private static boolean DEBUG; 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"; 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 final Settings mSettings; private View mExtractArea; private View mKeyPreviewBackingView; private View mSuggestionsContainer; private SuggestionStripView mSuggestionStripView; // Never null private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; @UsedForTesting Suggest mSuggest; private CompletionInfo[] mApplicationSpecifiedCompletions; private ApplicationInfo mTargetApplicationInfo; private RichInputMethodManager mRichImm; @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeSwitcher mSubtypeSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); // At start, create a default event interpreter that does nothing by passing it no decoder spec. // The event interpreter should never be null. private EventInterpreter mEventInterpreter = new EventInterpreter(this); private boolean mIsMainDictionaryAvailable; private UserBinaryDictionary mUserDictionary; private UserHistoryDictionary mUserHistoryDictionary; private boolean mIsUserDictionaryAvailable; private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; private PositionalInfoForUserDictPendingAddition mPositionalInfoForUserDictPendingAddition = null; private final WordComposer mWordComposer = new WordComposer(); private final RichInputConnection mConnection = new RichInputConnection(this); private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); // 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 TreeSet mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); // Member variables for remembering the current device orientation. private int mDisplayOrientation; // Object for reacting to adding/removing a dictionary pack. // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack // Service yet. private BroadcastReceiver mDictionaryPackInstallReceiver = ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS ? null : new DictionaryPackInstallBroadcastReceiver(this); // Keeps track of most recently inserted text (multi-character key) for reverting private String mEnteredText; private boolean mIsAutoCorrectionIndicatorOn; private AlertDialog mOptionsDialog; private final boolean mIsHardwareAcceleratedDrawingEnabled; public final UIHandler mHandler = new UIHandler(this); public static final class UIHandler extends StaticInnerHandlerWrapper { private static final int MSG_UPDATE_SHIFT_STATE = 0; private static final int MSG_PENDING_IMS_CALLBACK = 1; private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; private static final int MSG_RESUME_SUGGESTIONS = 4; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; private int mDelayUpdateSuggestions; private int mDelayUpdateShiftState; private long mDoubleSpacePeriodTimeout; private long mDoubleSpacePeriodTimerStart; public UIHandler(final LatinIME outerInstance) { super(outerInstance); } public void onCreate() { final Resources res = getOuterInstance().getResources(); mDelayUpdateSuggestions = res.getInteger(R.integer.config_delay_update_suggestions); mDelayUpdateShiftState = res.getInteger(R.integer.config_delay_update_shift_state); mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout); } @Override public void handleMessage(final Message msg) { final LatinIME latinIme = getOuterInstance(); final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; switch (msg.what) { case MSG_UPDATE_SUGGESTION_STRIP: latinIme.updateSuggestionStrip(); break; case MSG_UPDATE_SHIFT_STATE: switcher.updateShiftState(); break; case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj, msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); break; case MSG_RESUME_SUGGESTIONS: latinIme.restartSuggestionsOnWordTouchedByCursor(); break; } } public void postUpdateSuggestionStrip() { sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions); } public void postResumeSuggestions() { sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); } public void cancelUpdateSuggestionStrip() { removeMessages(MSG_UPDATE_SUGGESTION_STRIP); } public boolean hasPendingUpdateSuggestions() { return hasMessages(MSG_UPDATE_SUGGESTION_STRIP); } 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 showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, final boolean dismissGestureFloatingPreviewText) { removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); final int arg1 = dismissGestureFloatingPreviewText ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : 0; obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords) .sendToTarget(); } public void startDoubleSpacePeriodTimer() { mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis(); } public void cancelDoubleSpacePeriodTimer() { mDoubleSpacePeriodTimerStart = 0; } public boolean isAcceptingDoubleSpacePeriod() { return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart < mDoubleSpacePeriodTimeout; } // 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(final LatinIME latinIme, final EditorInfo editorInfo, boolean restarting) { if (mHasPendingFinishInputView) latinIme.onFinishInputViewInternal(mHasPendingFinishInput); if (mHasPendingFinishInput) latinIme.onFinishInputInternal(); if (mHasPendingStartInput) latinIme.onStartInputInternal(editorInfo, restarting); resetPendingImsCallback(); } public void onStartInput(final EditorInfo editorInfo, final 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(final EditorInfo editorInfo, final 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(final 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(); } } } static final class SubtypeState { private InputMethodSubtype mLastActiveSubtype; private boolean mCurrentSubtypeUsed; public void currentSubtypeUsed() { mCurrentSubtypeUsed = true; } public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) { final InputMethodSubtype currentSubtype = richImm.getInputMethodManager() .getCurrentInputMethodSubtype(); final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; final boolean currentSubtypeUsed = mCurrentSubtypeUsed; if (currentSubtypeUsed) { mLastActiveSubtype = currentSubtype; mCurrentSubtypeUsed = false; } if (currentSubtypeUsed && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype) && !currentSubtype.equals(lastActiveSubtype)) { richImm.setInputMethodAndSubtype(token, lastActiveSubtype); return; } richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */); } } public LatinIME() { super(); mSettings = Settings.getInstance(); mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); mIsHardwareAcceleratedDrawingEnabled = InputMethodServiceCompatUtils.enableHardwareAcceleration(this); Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled); } @Override public void onCreate() { Settings.init(this); LatinImeLogger.init(this); RichInputMethodManager.init(this); mRichImm = RichInputMethodManager.getInstance(); SubtypeSwitcher.init(this); KeyboardSwitcher.init(this); AudioAndHapticFeedbackManager.init(this); AccessibilityUtils.init(this); super.onCreate(); mHandler.onCreate(); DEBUG = LatinImeLogger.sDBG; // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}. loadSettings(); initSuggest(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest); } mDisplayOrientation = getResources().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); // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack // Service yet. if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 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(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); } } // Has to be package-visible for unit tests @UsedForTesting void loadSettings() { final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); final InputAttributes inputAttributes = new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode()); mSettings.loadSettings(locale, inputAttributes); // May need to reset the contacts dictionary depending on the user settings. resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); } // Note that this method is called from a non-UI thread. @Override public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) { mIsMainDictionaryAvailable = isMainDictionaryAvailable; final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable); } } private void initSuggest() { final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); final String localeStr = subtypeLocale.toString(); final ContactsBinaryDictionary oldContactsDictionary; if (mSuggest != null) { oldContactsDictionary = mSuggest.getContactsDictionary(); mSuggest.close(); } else { oldContactsDictionary = null; } mSuggest = new Suggest(this /* Context */, subtypeLocale, this /* SuggestInitializationListener */); if (mSettings.getCurrent().mCorrectionEnabled) { mSuggest.setAutoCorrectionThreshold(mSettings.getCurrent().mAutoCorrectionThreshold); } mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.getInstance().initSuggest(mSuggest); } mUserDictionary = new UserBinaryDictionary(this, localeStr); mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); mSuggest.setUserDictionary(mUserDictionary); resetContactsDictionary(oldContactsDictionary); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, prefs); mSuggest.setUserHistoryDictionary(mUserHistoryDictionary); } /** * Resets the contacts dictionary in mSuggest according to the user settings. * * This method takes an optional contacts dictionary to use when the locale hasn't changed * since the contacts dictionary can be opened or closed as necessary depending on the settings. * * @param oldContactsDictionary an optional dictionary to use, or null */ private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) { final boolean shouldSetDictionary = (null != mSuggest && mSettings.getCurrent().mUseContactsDict); final ContactsBinaryDictionary 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 { final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); if (null != oldContactsDictionary) { if (!oldContactsDictionary.mLocale.equals(locale)) { // If the locale has changed then recreate the contacts dictionary. This // allows locale dependent rules for handling bigram name predictions. oldContactsDictionary.close(); dictionaryToUse = new ContactsBinaryDictionary(this, locale); } else { // 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 ContactsBinaryDictionary(this, locale); } } if (null != mSuggest) { mSuggest.setContactsDictionary(dictionaryToUse); } } /* package private */ void resetSuggestMainDict() { final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */); mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); } @Override public void onDestroy() { if (mSuggest != null) { mSuggest.close(); mSuggest = null; } mSettings.onDestroy(); unregisterReceiver(mReceiver); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.getInstance().onDestroy(); } // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack // Service yet. if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { unregisterReceiver(mDictionaryPackInstallReceiver); } LatinImeLogger.commit(); LatinImeLogger.onDestroy(); super.onDestroy(); } @Override public void onConfigurationChanged(final Configuration conf) { // If orientation changed while predicting, commit the change if (mDisplayOrientation != conf.orientation) { mDisplayOrientation = conf.orientation; mHandler.startOrientationChanging(); mConnection.beginBatchEdit(); commitTyped(LastComposedWord.NOT_A_SEPARATOR); mConnection.finishComposingText(); mConnection.endBatchEdit(); if (isShowingOptionDialog()) { mOptionsDialog.dismiss(); } } super.onConfigurationChanged(conf); } @Override public View onCreateInputView() { return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); } @Override public void setInputView(final 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); mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); if (mSuggestionStripView != null) mSuggestionStripView.setListener(this, view); if (LatinImeLogger.sVISUALDEBUG) { mKeyPreviewBackingView.setBackgroundColor(0x10FF0000); } } @Override public void setCandidatesView(final View view) { // To ensure that CandidatesView will never be set. return; } @Override public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { mHandler.onStartInput(editorInfo, restarting); } @Override public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { mHandler.onStartInputView(editorInfo, restarting); } @Override public void onFinishInputView(final boolean finishingInput) { mHandler.onFinishInputView(finishingInput); } @Override public void onFinishInput() { mHandler.onFinishInput(); } @Override public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() // is not guaranteed. It may even be called at the same time on a different thread. mSubtypeSwitcher.onSubtypeChanged(subtype); loadKeyboard(); } private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInput(editorInfo, restarting); } @SuppressWarnings("deprecation") private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInputView(editorInfo, restarting); final KeyboardSwitcher switcher = mKeyboardSwitcher; final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView(); final SettingsValues currentSettings = mSettings.getCurrent(); 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)); Log.d(TAG, "All caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) + ", sentence caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) + ", word caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs); } if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) { Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions); Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead"); } if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) { Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions); Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead"); } mTargetApplicationInfo = TargetApplicationGetter.getCachedApplicationInfo(editorInfo.packageName); if (null == mTargetApplicationInfo) { new TargetApplicationGetter(this /* context */, this /* listener */) .execute(editorInfo.packageName); } LatinImeLogger.onStartInputView(editorInfo); // In landscape mode, this method gets called without the input view being created. if (mainKeyboardView == null) { return; } // Forward this event to the accessibility utilities, if enabled. final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); if (accessUtils.isTouchExplorationEnabled()) { accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting); } final boolean inputTypeChanged = !currentSettings.isSameInputType(editorInfo); final boolean isDifferentTextField = !restarting || inputTypeChanged; if (isDifferentTextField) { mSubtypeSwitcher.updateParametersOnStartInputView(); } // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? updateFullscreenMode(); mApplicationSpecifiedCompletions = null; // The app calling setText() has the effect of clearing the composing // span, so we should reset our state unconditionally, even if restarting is true. mEnteredText = null; resetComposingState(true /* alsoResetLastComposedWord */); mDeleteCount = 0; mSpaceState = SPACE_STATE_NONE; mRecapitalizeStatus.deactivate(); mCurrentlyPressedHardwareKeys.clear(); // Note: the following does a round-trip IPC on the main thread: be careful final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); if (null != mSuggest && null != currentLocale && !currentLocale.equals(mSuggest.mLocale)) { initSuggest(); } if (mSuggestionStripView != null) { // This will set the punctuation suggestions if next word suggestion is off; // otherwise it will clear the suggestion strip. setPunctuationSuggestions(); } mSuggestedWords = SuggestedWords.EMPTY; mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart); if (isDifferentTextField) { mainKeyboardView.closing(); loadSettings(); if (mSuggest != null && currentSettings.mCorrectionEnabled) { mSuggest.setAutoCorrectionThreshold(currentSettings.mAutoCorrectionThreshold); } switcher.loadKeyboard(editorInfo, currentSettings); } else if (restarting) { // TODO: Come up with a more comprehensive way to reset the keyboard layout when // a keyboard layout set doesn't get reloaded in this method. switcher.resetKeyboardStateToAlphabet(); // In apps like Talk, we come here when the text is sent and the field gets emptied and // we need to re-evaluate the shift state, but not the whole layout which would be // disruptive. // Space state must be updated before calling updateShiftState switcher.updateShiftState(); } setSuggestionStripShownInternal( isSuggestionsStripVisible(), /* needsInputViewShown */ false); mLastSelectionStart = editorInfo.initialSelStart; mLastSelectionEnd = editorInfo.initialSelEnd; mHandler.cancelUpdateSuggestionStrip(); mHandler.cancelDoubleSpacePeriodTimer(); mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable); mainKeyboardView.setKeyPreviewPopupEnabled(currentSettings.mKeyPreviewPopupOn, currentSettings.mKeyPreviewPopupDismissDelay); mainKeyboardView.setSlidingKeyInputPreviewEnabled( currentSettings.mSlidingKeyInputPreviewEnabled); mainKeyboardView.setGestureHandlingEnabledByUser( currentSettings.mGestureInputEnabled); mainKeyboardView.setGesturePreviewMode(currentSettings.mGesturePreviewTrailEnabled, currentSettings.mGestureFloatingPreviewTextEnabled); // If we have a user dictionary addition in progress, we should check now if we should // replace the previously committed string with the word that has actually been added // to the user dictionary. if (null != mPositionalInfoForUserDictPendingAddition && mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( mConnection, editorInfo, mLastSelectionEnd, currentLocale)) { mPositionalInfoForUserDictPendingAddition = null; } // If tryReplaceWithActualWord returns false, we don't know what word was // added to the user dictionary yet, so we keep the data and defer processing. The word will // be replaced when the user dictionary reports back with the actual word, which ends // up calling #onWordAddedToUserDictionary() in this class. if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } // Callback for the TargetApplicationGetter @Override public void onTargetApplicationKnown(final ApplicationInfo info) { mTargetApplicationInfo = info; } @Override public void onWindowHidden() { super.onWindowHidden(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } } private void onFinishInputInternal() { super.onFinishInput(); LatinImeLogger.commit(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } } private void onFinishInputViewInternal(final boolean finishingInput) { super.onFinishInputView(finishingInput); mKeyboardSwitcher.onFinishInputView(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.cancelAllMessages(); } // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestionStrip(); resetComposingState(true /* alsoResetLastComposedWord */); // Notify ResearchLogger if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart, mLastSelectionEnd, getCurrentInputConnection()); } } @Override public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, final int newSelStart, final int newSelEnd, final int composingSpanStart, final 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); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { final boolean expectingUpdateSelectionFromLogger = ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection(); ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, composingSpanEnd, mExpectingUpdateSelection, expectingUpdateSelectionFromLogger, mConnection); if (expectingUpdateSelectionFromLogger) { // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work return; } } // 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 && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) { // 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 updateShiftState. // 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) { // If we are composing a word and moving the cursor, we would want to set a // suggestion span for recorrection to work correctly. Unfortunately, that // would involve the keyboard committing some new text, which would move the // cursor back to where it was. Latin IME could then fix the position of the cursor // again, but the asynchronous nature of the calls results in this wreaking havoc // with selection on double tap and the like. // Another option would be to send suggestions each time we set the composing // text, but that is probably too expensive to do, so we decided to leave things // as is. resetEntireInputState(newSelStart); } // We moved the cursor. If we are touching a word, we need to resume suggestion, // unless suggestions are off. if (isSuggestionsStripVisible()) { mHandler.postResumeSuggestions(); } // Reset the last recapitalization. mRecapitalizeStatus.deactivate(); mKeyboardSwitcher.updateShiftState(); } mExpectingUpdateSelection = false; // Make a note of the cursor position mLastSelectionStart = newSelStart; mLastSelectionEnd = newSelEnd; mSubtypeState.currentSubtypeUsed(); } /** * 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 (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) 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(final int dx, final int dy) { if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; super.onExtractedCursorMovement(dx, dy); } @Override public void hideWindow() { LatinImeLogger.commit(); mKeyboardSwitcher.onHideWindow(); if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { AccessibleKeyboardViewProxy.getInstance().onHideWindow(); } if (TRACE) Debug.stopMethodTracing(); if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } super.hideWindow(); } @Override public void onDisplayCompletions(final 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 (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return; if (applicationSpecifiedCompletions == null) { clearSuggestionStrip(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onDisplayCompletions(null); } return; } mApplicationSpecifiedCompletions = CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions); final ArrayList applicationSuggestedWords = SuggestedWords.getFromApplicationSpecifiedCompletions( applicationSpecifiedCompletions); final SuggestedWords suggestedWords = new SuggestedWords( applicationSuggestedWords, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, false /* isPrediction */); // When in fullscreen mode, show completions generated by the application final boolean isAutoCorrection = false; setSuggestedWords(suggestedWords, isAutoCorrection); setAutoCorrectionIndicator(isAutoCorrection); setSuggestionStripShown(true); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); } } private void setSuggestionStripShownInternal(final boolean shown, final boolean needsInputViewShown) { // TODO: Modify this if we support suggestions with hard keyboard if (onEvaluateInputViewShown() && mSuggestionsContainer != null) { final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); final boolean inputViewShown = (mainKeyboardView != null) ? mainKeyboardView.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(final boolean shown) { setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); } private int getAdjustedBackingViewHeight() { final int currentHeight = mKeyPreviewBackingView.getHeight(); if (currentHeight > 0) { return currentHeight; } final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView == null) { return 0; } final int keyboardHeight = mainKeyboardView.getHeight(); final int suggestionsHeight = mSuggestionsContainer.getHeight(); final int displayHeight = getResources().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 = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); mKeyPreviewBackingView.setLayoutParams(params); return params.height; } @Override public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView == null || mSuggestionsContainer == null) { return; } final int adjustedBackingHeight = getAdjustedBackingViewHeight(); final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE); final int backingHeight = backingGone ? 0 : adjustedBackingHeight; // 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 suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0 : mSuggestionsContainer.getHeight(); final int extraHeight = extractHeight + backingHeight + suggestionsHeight; int visibleTopY = extraHeight; // Need to set touchable region only if input view is being shown if (mainKeyboardView.isShown()) { if (mSuggestionsContainer.getVisibility() == View.VISIBLE) { visibleTopY -= suggestionsHeight; } final int touchY = mainKeyboardView.isShowingMoreKeysPanel() ? 0 : visibleTopY; final int touchWidth = mainKeyboardView.getWidth(); final int touchHeight = mainKeyboardView.getHeight() + extraHeight // Extend touchable region below the keyboard. + EXTENDED_TOUCHABLE_REGION_HEIGHT; outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight); } outInsets.contentTopInsets = visibleTopY; outInsets.visibleTopInsets = visibleTopY; } @Override public boolean onEvaluateFullscreenMode() { // Reread resource value here, because this method is called by framework anytime as needed. final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources()); if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) { // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI // without NO_FULLSCREEN doesn't work as expected. Because of this we need this // hack for now. Let's get rid of this once the framework gets fixed. final EditorInfo ei = getCurrentInputEditorInfo(); return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0)); } else { return false; } } @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); } // 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 about it. private void resetEntireInputState(final int newCursorPosition) { resetComposingState(true /* alsoResetLastComposedWord */); if (mSettings.getCurrent().mBigramPredictionEnabled) { clearSuggestionStrip(); } else { setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false); } mConnection.resetCachesUponCursorMove(newCursorPosition); } private void resetComposingState(final boolean alsoResetLastComposedWord) { mWordComposer.reset(); if (alsoResetLastComposedWord) mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; } private void commitTyped(final String separatorString) { if (!mWordComposer.isComposingWord()) return; final String typedWord = mWordComposer.getTypedWord(); if (typedWord.length() > 0) { if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); } commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString); } } // Called from the KeyboardSwitcher which needs to know auto caps state to display // the right layout. public int getCurrentAutoCapsState() { if (!mSettings.getCurrent().mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; final EditorInfo ei = getCurrentInputEditorInfo(); if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; final int inputType = ei.inputType; // Warning: this depends on mSpaceState, which may not be the most current value. If // mSpaceState gets updated later, whoever called this may need to be told about it. return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(), SPACE_STATE_PHANTOM == mSpaceState); } public int getCurrentRecapitalizeState() { if (!mRecapitalizeStatus.isActive() || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { // Not recapitalizing at the moment return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; } return mRecapitalizeStatus.getCurrentMode(); } // Factor in auto-caps and manual caps and compute the current caps mode. private int getActualCapsMode() { final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode(); if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode; final int auto = getCurrentAutoCapsState(); if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; } if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED; return WordComposer.CAPS_MODE_OFF; } private void swapSwapperAndSpace() { final CharSequence lastTwo = mConnection.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) == Constants.CODE_SPACE) { mConnection.deleteSurroundingText(2, 0); final String text = lastTwo.charAt(1) + " "; mConnection.commitText(text, 1); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text); } mKeyboardSwitcher.updateShiftState(); } } private boolean maybeDoubleSpacePeriod() { if (!mSettings.getCurrent().mCorrectionEnabled) return false; if (!mSettings.getCurrent().mUseDoubleSpacePeriod) return false; if (!mHandler.isAcceptingDoubleSpacePeriod()) return false; final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0); if (lastThree != null && lastThree.length() == 3 && canBeFollowedByDoubleSpacePeriod(lastThree.charAt(0)) && lastThree.charAt(1) == Constants.CODE_SPACE && lastThree.charAt(2) == Constants.CODE_SPACE) { mHandler.cancelDoubleSpacePeriodTimer(); mConnection.deleteSurroundingText(2, 0); final String textToInsert = ". "; mConnection.commitText(textToInsert, 1); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert, false /* isBatchMode */); } mKeyboardSwitcher.updateShiftState(); return true; } return false; } private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { // TODO: Check again whether there really ain't a better way to check this. // TODO: This should probably be language-dependant... return Character.isLetterOrDigit(codePoint) || codePoint == Constants.CODE_SINGLE_QUOTE || codePoint == Constants.CODE_DOUBLE_QUOTE || codePoint == Constants.CODE_CLOSING_PARENTHESIS || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET; } // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is // pressed. @Override public void addWordToUserDictionary(final String word) { if (TextUtils.isEmpty(word)) { // Probably never supposed to happen, but just in case. mPositionalInfoForUserDictPendingAddition = null; return; } final String wordToEdit; if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) { wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); } else { wordToEdit = word; } mUserDictionary.addWordToUserDictionary(wordToEdit); } public void onWordAddedToUserDictionary(final String newSpelling) { // If word was added but not by us, bail out if (null == mPositionalInfoForUserDictPendingAddition) return; if (mWordComposer.isComposingWord()) { // We are late... give up and return mPositionalInfoForUserDictPendingAddition = null; return; } mPositionalInfoForUserDictPendingAddition.setActualWordBeingAdded(newSpelling); if (mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( mConnection, getCurrentInputEditorInfo(), mLastSelectionEnd, mSubtypeSwitcher.getCurrentSubtypeLocale())) { mPositionalInfoForUserDictPendingAddition = null; } } private static boolean isAlphabet(final int code) { return Character.isLetter(code); } private void onSettingsKeyPressed() { if (isShowingOptionDialog()) return; showSubtypeSelectorAndSettings(); } // Virtual codes representing custom requests. These are used in onCustomRequest() below. public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1; @Override public boolean onCustomRequest(final int requestCode) { if (isShowingOptionDialog()) return false; switch (requestCode) { case CODE_SHOW_INPUT_METHOD_PICKER: if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { mRichImm.getInputMethodManager().showInputMethodPicker(); return true; } return false; } return false; } private boolean isShowingOptionDialog() { return mOptionsDialog != null && mOptionsDialog.isShowing(); } private void performEditorAction(final int actionId) { mConnection.performEditorAction(actionId); } // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. private void handleLanguageSwitchKey() { final IBinder token = getWindow().getWindow().getAttributes().token; if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) { mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); return; } mSubtypeState.switchSubtype(token, mRichImm); } private void sendDownUpKeyEventForBackwardCompatibility(final int code) { final long eventTime = SystemClock.uptimeMillis(); mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); } private void sendKeyCodePoint(final int code) { if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_sendKeyCodePoint(code); } // TODO: Remove this special handling of digit letters. // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. if (code >= '0' && code <= '9') { sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0); return; } if (Constants.CODE_ENTER == code && mTargetApplicationInfo != null && mTargetApplicationInfo.targetSdkVersion < VERSION_CODES.JELLY_BEAN) { // Backward compatibility mode. Before Jelly bean, the keyboard would simulate // a hardware keyboard event on pressing enter or delete. This is bad for many // reasons (there are race conditions with commits) but some applications are // relying on this behavior so we continue to support it for older apps. sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER); } else { final String text = new String(new int[] { code }, 0, 1); mConnection.commitText(text, text.length()); } } // Implementation of {@link KeyboardActionListener}. @Override public void onCodeInput(final int primaryCode, final int x, final int y) { if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); } final long when = SystemClock.uptimeMillis(); if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { mDeleteCount = 0; } mLastKeyTime = when; mConnection.beginBatchEdit(); 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 period timer, mLastKeyTime, and the space state. if (primaryCode != Constants.CODE_SPACE) { mHandler.cancelDoubleSpacePeriodTimer(); } boolean didAutoCorrect = false; switch (primaryCode) { case Constants.CODE_DELETE: mSpaceState = SPACE_STATE_NONE; handleBackspace(spaceState); mDeleteCount++; mExpectingUpdateSelection = true; LatinImeLogger.logOnDelete(x, y); break; case Constants.CODE_SHIFT: // Note: calling back to the keyboard on Shift key is handled in onPressKey() // and onReleaseKey(). final Keyboard currentKeyboard = switcher.getKeyboard(); if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for // alphabetic shift and shift while in symbol layout. handleRecapitalize(); } break; case Constants.CODE_SWITCH_ALPHA_SYMBOL: // Note: calling back to the keyboard on symbol key is handled in onPressKey() // and onReleaseKey(). break; case Constants.CODE_SETTINGS: onSettingsKeyPressed(); break; case Constants.CODE_SHORTCUT: mSubtypeSwitcher.switchToShortcutIME(this); break; case Constants.CODE_ACTION_NEXT: performEditorAction(EditorInfo.IME_ACTION_NEXT); break; case Constants.CODE_ACTION_PREVIOUS: performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); break; case Constants.CODE_LANGUAGE_SWITCH: handleLanguageSwitchKey(); break; case Constants.CODE_RESEARCH: if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.getInstance().onResearchKeySelected(this); } break; case Constants.CODE_ENTER: final EditorInfo editorInfo = getCurrentInputEditorInfo(); final int imeOptionsActionId = InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { // Either we have an actionLabel and we should performEditorAction with actionId // regardless of its value. performEditorAction(editorInfo.actionId); } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { // We didn't have an actionLabel, but we had another action to execute. // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it // means there should be an action and the app didn't bother to set a specific // code for it - presumably it only handles one. It does not have to be treated // in any specific way: anything that is not IME_ACTION_NONE should be sent to // performEditorAction. performEditorAction(imeOptionsActionId); } else { // No action label, and the action from imeOptions is NONE: this is a regular // enter key that should input a carriage return. didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); } break; case Constants.CODE_SHIFT_ENTER: didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); break; default: didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState); break; } switcher.onCodeInput(primaryCode); // Reset after any single keystroke, except shift and symbol-shift if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) mLastComposedWord.deactivate(); if (Constants.CODE_DELETE != primaryCode) { mEnteredText = null; } mConnection.endBatchEdit(); } private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y, final int spaceState) { mSpaceState = SPACE_STATE_NONE; final boolean didAutoCorrect; if (mSettings.getCurrent().isWordSeparator(primaryCode)) { didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); } else { didAutoCorrect = false; if (SPACE_STATE_PHANTOM == spaceState) { if (mSettings.isInternal()) { if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { Stats.onAutoCorrection( "", mWordComposer.getTypedWord(), " ", mWordComposer); } } if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can insert the character at the current cursor position. resetEntireInputState(mLastSelectionStart); } else { commitTyped(LastComposedWord.NOT_A_SEPARATOR); } } final int keyX, keyY; final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { keyX = x; keyY = y; } else { keyX = Constants.NOT_A_COORDINATE; keyY = Constants.NOT_A_COORDINATE; } handleCharacter(primaryCode, keyX, keyY, spaceState); } mExpectingUpdateSelection = true; return didAutoCorrect; } // Called from PointerTracker through the KeyboardActionListener interface @Override public void onTextInput(final String rawText) { mConnection.beginBatchEdit(); if (mWordComposer.isComposingWord()) { commitCurrentAutoCorrection(rawText); } else { resetComposingState(true /* alsoResetLastComposedWord */); } mHandler.postUpdateSuggestionStrip(); final String text = specificTldProcessingOnTextInput(rawText); if (SPACE_STATE_PHANTOM == mSpaceState) { promotePhantomSpace(); } mConnection.commitText(text, 1); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */); } mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState mSpaceState = SPACE_STATE_NONE; mKeyboardSwitcher.updateShiftState(); mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT); mEnteredText = text; } @Override public void onStartBatchInput() { BatchInputUpdater.getInstance().onStartBatchInput(this); mHandler.cancelUpdateSuggestionStrip(); mConnection.beginBatchEdit(); if (mWordComposer.isComposingWord()) { if (mSettings.isInternal()) { if (mWordComposer.isBatchMode()) { Stats.onAutoCorrection("", mWordComposer.getTypedWord(), " ", mWordComposer); } } final int wordComposerSize = mWordComposer.size(); // Since isComposingWord() is true, the size is at least 1. final int lastChar = mWordComposer.getCodeBeforeCursor(); if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can insert the batch input at the current cursor position. resetEntireInputState(mLastSelectionStart); } else if (wordComposerSize <= 1) { // We auto-correct the previous (typed, not gestured) string iff it's one character // long. The reason for this is, even in the middle of gesture typing, you'll still // tap one-letter words and you want them auto-corrected (typically, "i" in English // should become "I"). However for any longer word, we assume that the reason for // tapping probably is that the word you intend to type is not in the dictionary, // so we do not attempt to correct, on the assumption that if that was a dictionary // word, the user would probably have gestured instead. commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); } else { commitTyped(LastComposedWord.NOT_A_SEPARATOR); } mExpectingUpdateSelection = true; } final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); if (Character.isLetterOrDigit(codePointBeforeCursor) || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) { mSpaceState = SPACE_STATE_PHANTOM; } mConnection.endBatchEdit(); mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); } private static final class BatchInputUpdater implements Handler.Callback { private final Handler mHandler; private LatinIME mLatinIme; private final Object mLock = new Object(); private boolean mInBatchInput; // synchronized using {@link #mLock}. private BatchInputUpdater() { final HandlerThread handlerThread = new HandlerThread( BatchInputUpdater.class.getSimpleName()); handlerThread.start(); mHandler = new Handler(handlerThread.getLooper(), this); } // Initialization-on-demand holder private static final class OnDemandInitializationHolder { public static final BatchInputUpdater sInstance = new BatchInputUpdater(); } public static BatchInputUpdater getInstance() { return OnDemandInitializationHolder.sInstance; } private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; @Override public boolean handleMessage(final Message msg) { switch (msg.what) { case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: updateBatchInput((InputPointers)msg.obj); break; } return true; } // Run in the UI thread. public void onStartBatchInput(final LatinIME latinIme) { synchronized (mLock) { mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); mLatinIme = latinIme; mInBatchInput = true; } } // Run in the Handler thread. private void updateBatchInput(final InputPointers batchPointers) { synchronized (mLock) { if (!mInBatchInput) { // Batch input has ended or canceled while the message was being delivered. return; } final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( suggestedWords, false /* dismissGestureFloatingPreviewText */); } } // Run in the UI thread. public void onUpdateBatchInput(final InputPointers batchPointers) { if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) { return; } mHandler.obtainMessage( MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers) .sendToTarget(); } public void onCancelBatchInput() { synchronized (mLock) { mInBatchInput = false; mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); } } // Run in the UI thread. public SuggestedWords onEndBatchInput(final InputPointers batchPointers) { synchronized (mLock) { mInBatchInput = false; final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( suggestedWords, true /* dismissGestureFloatingPreviewText */); return suggestedWords; } } // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to // be synchronized. private SuggestedWords getSuggestedWordsGestureLocked(final InputPointers batchPointers) { mLatinIme.mWordComposer.setBatchInputPointers(batchPointers); final SuggestedWords suggestedWords = mLatinIme.getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_GESTURE); final int suggestionCount = suggestedWords.size(); if (suggestionCount <= 1) { final String mostProbableSuggestion = (suggestionCount == 0) ? null : suggestedWords.getWord(0); return mLatinIme.getOlderSuggestions(mostProbableSuggestion); } return suggestedWords; } } private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, final boolean dismissGestureFloatingPreviewText) { showSuggestionStrip(suggestedWords, null); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); mainKeyboardView.showGestureFloatingPreviewText(suggestedWords); if (dismissGestureFloatingPreviewText) { mainKeyboardView.dismissGestureFloatingPreviewText(); } } @Override public void onUpdateBatchInput(final InputPointers batchPointers) { BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers); } @Override public void onEndBatchInput(final InputPointers batchPointers) { final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput( batchPointers); final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); if (TextUtils.isEmpty(batchInputText)) { return; } mWordComposer.setBatchInputWord(batchInputText); mConnection.beginBatchEdit(); if (SPACE_STATE_PHANTOM == mSpaceState) { promotePhantomSpace(); } mConnection.setComposingText(batchInputText, 1); mExpectingUpdateSelection = true; mConnection.endBatchEdit(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); } // Space state must be updated before calling updateShiftState mSpaceState = SPACE_STATE_PHANTOM; mKeyboardSwitcher.updateShiftState(); } private String specificTldProcessingOnTextInput(final String text) { if (text.length() <= 1 || text.charAt(0) != Constants.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; // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Constants.CODE_PERIOD) { return text.substring(1); } else { return text; } } // Called from PointerTracker through the KeyboardActionListener interface @Override public void onCancelInput() { // User released a finger outside any key mKeyboardSwitcher.onCancelInput(); } @Override public void onCancelBatchInput() { BatchInputUpdater.getInstance().onCancelBatchInput(); } private void handleBackspace(final int spaceState) { // In many cases, we may have to put the keyboard in auto-shift state again. However // we want to wait a few milliseconds before doing it to avoid the keyboard flashing // during key repeat. mHandler.postUpdateShiftState(); if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can remove the character at the current cursor position. resetEntireInputState(mLastSelectionStart); // When we exit this if-clause, mWordComposer.isComposingWord() will return false. } if (mWordComposer.isComposingWord()) { final int length = mWordComposer.size(); if (length > 0) { if (mWordComposer.isBatchMode()) { if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { final String word = mWordComposer.getTypedWord(); ResearchLogger.latinIME_handleBackspace_batch(word, 1); ResearchLogger.getInstance().uncommitCurrentLogUnit( word, false /* dumpCurrentLogUnit */); } final String rejectedSuggestion = mWordComposer.getTypedWord(); mWordComposer.reset(); mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); } else { mWordComposer.deleteLast(); } mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); mHandler.postUpdateSuggestionStrip(); } else { mConnection.deleteSurroundingText(1, 0); } } else { if (mLastComposedWord.canRevertCommit()) { if (mSettings.isInternal()) { Stats.onAutoCorrectionCancellation(); } revertCommit(); return; } if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(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. final int length = mEnteredText.length(); mConnection.deleteSurroundingText(length, 0); mEnteredText = null; // 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 (SPACE_STATE_DOUBLE == spaceState) { mHandler.cancelDoubleSpacePeriodTimer(); if (mConnection.revertDoubleSpacePeriod()) { // 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 (mConnection.revertSwapPunctuation()) { // 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 numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to // happen, and if it's wrong, the next call to onUpdateSelection will correct it, // but we want to set it right away to avoid it being used with the wrong values // later (typically, in a subsequent press on backspace). mLastSelectionEnd = mLastSelectionStart; mConnection.deleteSurroundingText(numCharsDeleted, 0); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_handleBackspace(numCharsDeleted); } } 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"); } if (mTargetApplicationInfo != null && mTargetApplicationInfo.targetSdkVersion < VERSION_CODES.JELLY_BEAN) { // Backward compatibility mode. Before Jelly bean, the keyboard would simulate // a hardware keyboard event on pressing enter or delete. This is bad for many // reasons (there are race conditions with commits) but some applications are // relying on this behavior so we continue to support it for older apps. sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL); } else { mConnection.deleteSurroundingText(1, 0); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_handleBackspace(1); } if (mDeleteCount > DELETE_ACCELERATE_AT) { mConnection.deleteSurroundingText(1, 0); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_handleBackspace(1); } } } if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) { restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(); } } } /* * Strip a trailing space if necessary and returns whether it's a swap weak space situation. */ private boolean maybeStripSpace(final int code, final int spaceState, final boolean isFromSuggestionStrip) { if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { mConnection.removeTrailingSpace(); return false; } if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState) && isFromSuggestionStrip) { if (mSettings.getCurrent().isUsuallyPrecededBySpace(code)) return false; if (mSettings.getCurrent().isUsuallyFollowedBySpace(code)) return true; mConnection.removeTrailingSpace(); } return false; } private void handleCharacter(final int primaryCode, final int x, final int y, final int spaceState) { boolean isComposingWord = mWordComposer.isComposingWord(); // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. // See onStartBatchInput() to see how to do it. if (SPACE_STATE_PHANTOM == spaceState && !mSettings.getCurrent().isWordConnector(primaryCode)) { if (isComposingWord) { // Sanity check throw new RuntimeException("Should not be composing here"); } promotePhantomSpace(); } if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can insert the character at the current cursor position. resetEntireInputState(mLastSelectionStart); isComposingWord = false; } // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI // thread here. if (!isComposingWord && (isAlphabet(primaryCode) || mSettings.getCurrent().isWordConnector(primaryCode)) && mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation) && !mConnection.isCursorTouchingWord(mSettings.getCurrent())) { // 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 = (Constants.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 */); } if (isComposingWord) { final int keyX, keyY; if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) { final KeyDetector keyDetector = mKeyboardSwitcher.getMainKeyboardView().getKeyDetector(); keyX = keyDetector.getTouchX(x); keyY = keyDetector.getTouchY(y); } else { keyX = x; keyY = y; } mWordComposer.add(primaryCode, keyX, keyY); // If it's the first letter, make note of auto-caps state if (mWordComposer.size() == 1) { mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); } mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } else { final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x); sendKeyCodePoint(primaryCode); if (swapWeakSpace) { swapSwapperAndSpace(); mSpaceState = SPACE_STATE_WEAK; } // In case the "add to dictionary" hint was still displayed. if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint(); } mHandler.postUpdateSuggestionStrip(); if (mSettings.isInternal()) { Utils.Stats.onNonSeparator((char)primaryCode, x, y); } } private void handleRecapitalize() { if (mLastSelectionStart == mLastSelectionEnd) return; // No selection // If we have a recapitalize in progress, use it; otherwise, create a new one. if (!mRecapitalizeStatus.isActive() || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { final CharSequence selectedText = mConnection.getSelectedText(0 /* flags, 0 for no styles */); if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd, selectedText.toString(), mSettings.getCurrentLocale(), mSettings.getWordSeparators()); // We trim leading and trailing whitespace. mRecapitalizeStatus.trim(); // Trimming the object may have changed the length of the string, and we need to // reposition the selection handles accordingly. As this result in an IPC call, // only do it if it's actually necessary, in other words if the recapitalize status // is not set at the same place as before. if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); } } mRecapitalizeStatus.rotate(); final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); mConnection.deleteSurroundingText(numCharsDeleted, 0); mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); // Match the keyboard to the new state. mKeyboardSwitcher.updateShiftState(); } // Returns true if we did an autocorrection, false otherwise. private boolean handleSeparator(final int primaryCode, final int x, final int y, final int spaceState) { if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); } boolean didAutoCorrect = false; if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can insert the separator at the current cursor position. resetEntireInputState(mLastSelectionStart); } if (mWordComposer.isComposingWord()) { if (mSettings.getCurrent().mCorrectionEnabled) { // TODO: maybe cache Strings in an sparse array or something commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1)); didAutoCorrect = true; } else { commitTyped(new String(new int[]{primaryCode}, 0, 1)); } } final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x); if (SPACE_STATE_PHANTOM == spaceState && mSettings.getCurrent().isUsuallyPrecededBySpace(primaryCode)) { promotePhantomSpace(); } sendKeyCodePoint(primaryCode); if (Constants.CODE_SPACE == primaryCode) { if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) { if (maybeDoubleSpacePeriod()) { mSpaceState = SPACE_STATE_DOUBLE; } else if (!isShowingPunctuationList()) { mSpaceState = SPACE_STATE_WEAK; } } mHandler.startDoubleSpacePeriodTimer(); mHandler.postUpdateSuggestionStrip(); } else { if (swapWeakSpace) { swapSwapperAndSpace(); mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; } else if (SPACE_STATE_PHANTOM == spaceState && mSettings.getCurrent().isUsuallyFollowedBySpace(primaryCode)) { // 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. // The case is a little different if the separator is a space stripper. Such a // separator does not normally need a space on the right (that's the difference // between swappers and strippers), so we should not stay in phantom space state if // the separator is a stripper. Hence the additional test above. 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(); } if (mSettings.isInternal()) { Utils.Stats.onSeparator((char)primaryCode, x, y); } mKeyboardSwitcher.updateShiftState(); return didAutoCorrect; } private CharSequence getTextWithUnderline(final String text) { return mIsAutoCorrectionIndicatorOn ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) : text; } private void handleClose() { // TODO: Verify that words are logged properly when IME is closed. commitTyped(LastComposedWord.NOT_A_SEPARATOR); requestHideSelf(0); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } } // TODO: make this private // Outside LatinIME, only used by the test suite. @UsedForTesting boolean isShowingPunctuationList() { if (mSuggestedWords == null) return false; return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords; } private boolean isSuggestionsStripVisible() { if (mSuggestionStripView == null) return false; if (mSuggestionStripView.isShowingAddToDictionaryHint()) return true; if (null == mSettings.getCurrent()) return false; if (!mSettings.getCurrent().isSuggestionStripVisibleInOrientation(mDisplayOrientation)) return false; if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return true; return mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation); } private void clearSuggestionStrip() { setSuggestedWords(SuggestedWords.EMPTY, false); setAutoCorrectionIndicator(false); } private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { mSuggestedWords = words; if (mSuggestionStripView != null) { mSuggestionStripView.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. if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator && mWordComposer.isComposingWord()) { mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; final CharSequence textWithUnderline = getTextWithUnderline(mWordComposer.getTypedWord()); // TODO: when called from an updateSuggestionStrip() call that results from a posted // message, this is called outside any batch edit. Potentially, this may result in some // janky flickering of the screen, although the display speed makes it unlikely in // the practice. mConnection.setComposingText(textWithUnderline, 1); } } private void updateSuggestionStrip() { mHandler.cancelUpdateSuggestionStrip(); // Check if we have a suggestion engine attached. if (mSuggest == null || !mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) { if (mWordComposer.isComposingWord()) { Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " + "requested!"); } return; } if (!mWordComposer.isComposingWord() && !mSettings.getCurrent().mBigramPredictionEnabled) { setPunctuationSuggestions(); return; } final SuggestedWords suggestedWords = getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_TYPING); final String typedWord = mWordComposer.getTypedWord(); showSuggestionStrip(suggestedWords, typedWord); } private SuggestedWords getSuggestedWords(final int sessionId) { final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); if (keyboard == null || mSuggest == null) { return SuggestedWords.EMPTY; } // Get the word on which we should search the bigrams. If we are composing a word, it's // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we // should just skip whitespace if any, so 1. // TODO: this is slow (2-way IPC) - we should probably cache this instead. final String prevWord = mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators, mWordComposer.isComposingWord() ? 2 : 1); return mSuggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(), mSettings.getBlockPotentiallyOffensive(), mSettings.getCurrent().mCorrectionEnabled, sessionId); } private SuggestedWords getSuggestedWordsOrOlderSuggestions(final int sessionId) { return maybeRetrieveOlderSuggestions(mWordComposer.getTypedWord(), getSuggestedWords(sessionId)); } private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord, final SuggestedWords suggestedWords) { // TODO: consolidate this into getSuggestedWords // We update the suggestion strip only when we have some suggestions to show, i.e. when // the suggestion count is > 1; else, we leave the old suggestions, with the typed word // replaced with the new one. However, when the word is a dictionary word, or when the // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to // revert to suggestions - although it is unclear how we can come here if it's displayed. if (suggestedWords.size() > 1 || typedWord.length() <= 1 || suggestedWords.mTypedWordValid || null == mSuggestionStripView || mSuggestionStripView.isShowingAddToDictionaryHint()) { return suggestedWords; } else { return getOlderSuggestions(typedWord); } } private SuggestedWords getOlderSuggestions(final String typedWord) { SuggestedWords previousSuggestedWords = mSuggestedWords; if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) { previousSuggestedWords = SuggestedWords.EMPTY; } if (typedWord == null) { return previousSuggestedWords; } final ArrayList typedWordAndPreviousSuggestions = SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, previousSuggestedWords); return new SuggestedWords(typedWordAndPreviousSuggestions, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, false /* isPunctuationSuggestions */, true /* isObsoleteSuggestions */, false /* isPrediction */); } private void showSuggestionStrip(final SuggestedWords suggestedWords, final String typedWord) { if (suggestedWords.isEmpty()) { clearSuggestionStrip(); return; } final String autoCorrection; if (suggestedWords.mWillAutoCorrect) { autoCorrection = suggestedWords.getWord(1); } else { autoCorrection = typedWord; } mWordComposer.setAutoCorrection(autoCorrection); final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); setSuggestedWords(suggestedWords, isAutoCorrection); setAutoCorrectionIndicator(isAutoCorrection); setSuggestionStripShown(isSuggestionsStripVisible()); } private void commitCurrentAutoCorrection(final String separatorString) { // Complete any pending suggestions query first if (mHandler.hasPendingUpdateSuggestions()) { updateSuggestionStrip(); } final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); final String typedWord = mWordComposer.getTypedWord(); final String autoCorrection = (typedAutoCorrection != null) ? typedAutoCorrection : typedWord; if (autoCorrection != null) { if (TextUtils.isEmpty(typedWord)) { throw new RuntimeException("We have an auto-correction but the typed word " + "is empty? Impossible! I must commit suicide."); } if (mSettings.isInternal()) { Stats.onAutoCorrection(typedWord, autoCorrection, separatorString, mWordComposer); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { final SuggestedWords suggestedWords = mSuggestedWords; ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, separatorString, mWordComposer.isBatchMode(), suggestedWords); } mExpectingUpdateSelection = true; commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separatorString); if (!typedWord.equals(autoCorrection)) { // This will make the correction flash for a short while as a visual clue // to the user that auto-correction happened. It has no other effect; in particular // note that this won't affect the text inside the text field AT ALL: it only makes // the segment of text starting at the supplied index and running for the length // of the auto-correction flash. At this moment, the "typedWord" argument is // ignored by TextView. mConnection.commitCorrection( new CorrectionInfo(mLastSelectionEnd - typedWord.length(), typedWord, autoCorrection)); } } } // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} // interface @Override public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { final SuggestedWords suggestedWords = mSuggestedWords; final String suggestion = suggestionInfo.mWord; // 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, index, suggestedWords); // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. final int primaryCode = suggestion.charAt(0); onCodeInput(primaryCode, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, false /* isBatchMode */, suggestedWords.mIsPrediction); } return; } mConnection.beginBatchEdit(); if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0 // In the batch input mode, a manually picked suggested word should just replace // the current batch input text and there is no need for a phantom space. && !mWordComposer.isBatchMode()) { final int firstChar = Character.codePointAt(suggestion, 0); if (!mSettings.getCurrent().isWordSeparator(firstChar) || mSettings.getCurrent().isUsuallyPrecededBySpace(firstChar)) { promotePhantomSpace(); } } if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn() && mApplicationSpecifiedCompletions != null && index >= 0 && index < mApplicationSpecifiedCompletions.length) { mSuggestedWords = SuggestedWords.EMPTY; if (mSuggestionStripView != null) { mSuggestionStripView.clear(); } mKeyboardSwitcher.updateShiftState(); resetComposingState(true /* alsoResetLastComposedWord */); final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; mConnection.commitCompletion(completionInfo); mConnection.endBatchEdit(); return; } // We need to log before we commit, because the word composer will store away the user // typed word. final String replacedWord = mWordComposer.getTypedWord(); LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); mExpectingUpdateSelection = true; commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, mWordComposer.isBatchMode()); } mConnection.endBatchEdit(); // Don't allow cancellation of manual pick mLastComposedWord.deactivate(); // Space state must be updated before calling updateShiftState mSpaceState = SPACE_STATE_PHANTOM; mKeyboardSwitcher.updateShiftState(); // We should show the "Touch again to save" hint if the user pressed the first entry // AND it's in none of our current dictionaries (main, user or otherwise). // 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 final boolean showingAddToDictionaryHint = SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind && mSuggest != null // If the suggestion is not in the dictionary, the hint should be shown. && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true); if (mSettings.isInternal()) { Stats.onSeparator((char)Constants.CODE_SPACE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); } if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) { mSuggestionStripView.showAddToDictionaryHint( suggestion, mSettings.getCurrent().mHintToSaveText); } else { // If we're not showing the "Touch again to save", then update the suggestion strip. mHandler.postUpdateSuggestionStrip(); } } /** * Commits the chosen word to the text field and saves it for later retrieval. */ private void commitChosenWord(final String chosenWord, final int commitType, final String separatorString) { final SuggestedWords suggestedWords = mSuggestedWords; mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); // Add the word to the user history dictionary final String prevWord = addToUserHistoryDictionary(chosenWord); // 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, chosenWord, separatorString, prevWord); } private void setPunctuationSuggestions() { if (mSettings.getCurrent().mBigramPredictionEnabled) { clearSuggestionStrip(); } else { setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false); } setAutoCorrectionIndicator(false); setSuggestionStripShown(isSuggestionsStripVisible()); } private String addToUserHistoryDictionary(final String suggestion) { if (TextUtils.isEmpty(suggestion)) return null; if (mSuggest == null) return null; // If correction is not enabled, we don't add words to the user history dictionary. // That's to avoid unintended additions in some sensitive fields, or fields that // expect to receive non-words. if (!mSettings.getCurrent().mCorrectionEnabled) return null; final Suggest suggest = mSuggest; final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary; if (suggest == null || userHistoryDictionary == null) { // Avoid concurrent issue return null; } final String prevWord = mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators, 2); final String secondWord; if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); } else { secondWord = suggestion; } // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". // We don't add words with 0-frequency (assuming they would be profanity etc.). final int maxFreq = AutoCorrection.getMaxFrequency( suggest.getUnigramDictionaries(), suggestion); if (maxFreq == 0) return null; userHistoryDictionary.addToUserHistory(prevWord, secondWord, maxFreq > 0); return prevWord; } /** * Check if the cursor is touching a word. If so, restart suggestions on this word, else * do nothing. */ private void restartSuggestionsOnWordTouchedByCursor() { // If the cursor is not touching a word, or if there is a selection, return right away. if (mLastSelectionStart != mLastSelectionEnd) return; if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return; final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(), 0 /* additionalPrecedingWordsCount */); if (null == range) return; // Happens if we don't have an input connection at all final ArrayList suggestions = CollectionUtils.newArrayList(); final String typedWord = range.mWord.toString(); if (range.mWord instanceof SpannableString) { final SpannableString spannableString = (SpannableString)range.mWord; int i = 0; for (Object object : spannableString.getSpans(0, spannableString.length(), SuggestionSpan.class)) { SuggestionSpan span = (SuggestionSpan)object; for (String s : span.getSuggestions()) { ++i; if (!TextUtils.equals(s, typedWord)) { suggestions.add(new SuggestedWordInfo(s, SuggestionStripView.MAX_SUGGESTIONS - i, SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED)); } } } } mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard()); mWordComposer.setCursorPositionWithinWord(range.mCharsBefore); mConnection.setComposingRegion(mLastSelectionStart - range.mCharsBefore, mLastSelectionEnd + range.mCharsAfter); final SuggestedWords suggestedWords; if (suggestions.isEmpty()) { // We come here if there weren't any suggestion spans on this word. We will try to // compute suggestions for it instead. final SuggestedWords suggestedWordsIncludingTypedWord = getSuggestedWords(Suggest.SESSION_TYPING); if (suggestedWordsIncludingTypedWord.size() > 1) { // We were able to compute new suggestions for this word. // Remove the typed word, since we don't want to display it in this case. // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false. suggestedWords = suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord(); } else { // No saved suggestions, and we were unable to compute any good one either. // Rather than displaying an empty suggestion strip, we'll display the original // word alone in the middle. // Since there is only one word, willAutoCorrect is false. suggestedWords = suggestedWordsIncludingTypedWord; } } else { // We found suggestion spans in the word. We'll create the SuggestedWords out of // them, and make willAutoCorrect false. suggestedWords = new SuggestedWords(suggestions, true /* typedWordValid */, false /* willAutoCorrect */, false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, false /* isPrediction */); } // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. // We never want to auto-correct on a resumed suggestion. Please refer to the three // places above where suggestedWords is affected. showSuggestionStrip(suggestedWords, typedWord); } /** * 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 CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent()); if (null != word) { final String wordString = word.toString(); restartSuggestionsOnWordBeforeCursor(wordString); // TODO: Handle the case where the user manually moves the cursor and then backs up over // a separator. In that case, the current log unit should not be uncommitted. if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString, true /* dumpCurrentLogUnit */); } } } private void restartSuggestionsOnWordBeforeCursor(final String word) { mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); final int length = word.length(); mConnection.deleteSurroundingText(length, 0); mConnection.setComposingText(word, 1); mHandler.postUpdateSuggestionStrip(); } private void revertCommit() { final String previousWord = mLastComposedWord.mPrevWord; final String originallyTypedWord = mLastComposedWord.mTypedWord; final String committedWord = mLastComposedWord.mCommittedWord; final int cancelLength = committedWord.length(); final int separatorLength = LastComposedWord.getSeparatorLength( mLastComposedWord.mSeparatorString); // TODO: should we check our saved separator against the actual contents of the text view? final int deleteLength = cancelLength + separatorLength; if (DEBUG) { if (mWordComposer.isComposingWord()) { throw new RuntimeException("revertCommit, but we are composing a word"); } final CharSequence wordBeforeCursor = mConnection.getTextBeforeCursor(deleteLength, 0) .subSequence(0, cancelLength); if (!TextUtils.equals(committedWord, wordBeforeCursor)) { throw new RuntimeException("revertCommit check failed: we thought we were " + "reverting \"" + committedWord + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); } } mConnection.deleteSurroundingText(deleteLength, 0); if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord); } mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1); if (mSettings.isInternal()) { Stats.onSeparator(mLastComposedWord.mSeparatorString, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord, mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString); ResearchLogger.getInstance().uncommitCurrentLogUnit(committedWord, true /* dumpCurrentLogUnit */); } // Don't restart suggestion yet. We'll restart if the user deletes the // separator. mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; // We have a separator between the word and the cursor: we should show predictions. mHandler.postUpdateSuggestionStrip(); } // This essentially inserts a space, and that's it. public void promotePhantomSpace() { if (mSettings.getCurrent().shouldInsertSpacesAutomatically() && !mConnection.textBeforeCursorLooksLikeURL()) { sendKeyCodePoint(Constants.CODE_SPACE); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_promotePhantomSpace(); } } } // Used by the RingCharBuffer public boolean isWordSeparator(final int code) { return mSettings.getCurrent().isWordSeparator(code); } // TODO: Make this private // Outside LatinIME, only used by the {@link InputTestsBase} test suite. @UsedForTesting void loadKeyboard() { // TODO: Why are we calling {@link #loadSettings()} and {@link #initSuggest()} in a // different order than in {@link #onStartInputView}? initSuggest(); loadSettings(); if (mKeyboardSwitcher.getMainKeyboardView() != null) { // Reload keyboard because the current language has been changed. mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); } // 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 the updateSuggestionStrip anyway). mHandler.postUpdateSuggestionStrip(); } // Callback called by PointerTracker through the KeyboardActionListener. This is called when a // key is depressed; release matching call is onReleaseKey below. @Override public void onPressKey(final int primaryCode) { mKeyboardSwitcher.onPressKey(primaryCode); } // Callback by PointerTracker through the KeyboardActionListener. This is called when a key // is released; press matching call is onPressKey above. @Override public void onReleaseKey(final int primaryCode, final boolean withSliding) { mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); // If accessibility is on, ensure the user receives keyboard state updates. if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { switch (primaryCode) { case Constants.CODE_SHIFT: AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); break; case Constants.CODE_SWITCH_ALPHA_SYMBOL: AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); break; } } if (Constants.CODE_DELETE == primaryCode) { // This is a stopgap solution to avoid leaving a high surrogate alone in a text view. // In the future, we need to deprecate deteleSurroundingText() and have a surrogate // pair-friendly way of deleting characters in InputConnection. // TODO: use getCodePointBeforeCursor instead to improve performance final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0); if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) { mConnection.deleteSurroundingText(1, 0); } } } // Hooks for hardware keyboard @Override public boolean onKeyDown(final int keyCode, final KeyEvent event) { if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event); // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if // it doesn't know what to do with it and leave it to the application. For example, // hardware key events for adjusting the screen's brightness are passed as is. if (mEventInterpreter.onHardwareKeyEvent(event)) { final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); mCurrentlyPressedHardwareKeys.add(keyIdentifier); return true; } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(final int keyCode, final KeyEvent event) { final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { return true; } return super.onKeyUp(keyCode, event); } // onKeyDown and onKeyUp are the main events we are interested in. There are two more events // related to handling of hardware key events that we may want to implement in the future: // boolean onKeyLongPress(final int keyCode, final KeyEvent event); // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event); // receive ringer mode change and network state change. private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final 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)) { mKeyboardSwitcher.onRingerModeChanged(); } } }; private void launchSettings() { handleClose(); launchSubActivity(SettingsActivity.class); } public void launchKeyboardedDialogActivity(final Class activityClass) { // Put the text in the attached EditText into a safe, saved state before switching to a // new activity that will also use the soft keyboard. commitTyped(LastComposedWord.NOT_A_SEPARATOR); launchSubActivity(activityClass); } private void launchSubActivity(final Class activityClass) { Intent intent = new Intent(); intent.setClass(LatinIME.this, activityClass); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); 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(Utils.getAcitivityTitleResId(this, SettingsActivity.class)), }; final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface di, int position) { di.dismiss(); switch (position) { case 0: final Intent intent = IntentUtils.getInputLanguageSelectionIntent( mRichImm.getInputMethodIdOfThisIme(), 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); showOptionDialog(builder.create()); } public void showOptionDialog(final AlertDialog dialog) { final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); if (windowToken == null) { return; } dialog.setCancelable(true); dialog.setCanceledOnTouchOutside(true); final Window window = dialog.getWindow(); final WindowManager.LayoutParams lp = window.getAttributes(); lp.token = windowToken; lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); mOptionsDialog = dialog; dialog.show(); } // TODO: can this be removed somehow without breaking the tests? @UsedForTesting /* package for test */ String getFirstSuggestedWord() { return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null; } public void debugDumpStateAndCrashWithException(final String context) { final StringBuilder s = new StringBuilder(); s.append("Target application : ").append(mTargetApplicationInfo.name) .append("\nPackage : ").append(mTargetApplicationInfo.packageName) .append("\nTarget app sdk version : ") .append(mTargetApplicationInfo.targetSdkVersion) .append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes) .append("\nContext : ").append(context); throw new RuntimeException(s.toString()); } @Override protected void dump(final FileDescriptor fd, final PrintWriter fout, final 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); final SettingsValues settingsValues = mSettings.getCurrent(); p.println(" mIsSuggestionsSuggestionsRequested = " + settingsValues.isSuggestionsRequested(mDisplayOrientation)); p.println(" mCorrectionEnabled=" + settingsValues.mCorrectionEnabled); p.println(" isComposingWord=" + mWordComposer.isComposingWord()); p.println(" mSoundOn=" + settingsValues.mSoundOn); p.println(" mVibrateOn=" + settingsValues.mVibrateOn); p.println(" mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn); p.println(" inputAttributes=" + settingsValues.mInputAttributes); } }