/* * Copyright (C) 2008-2009 Google Inc. * * 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 com.android.inputmethod.voice.EditingUtil; import com.android.inputmethod.voice.FieldContext; import com.android.inputmethod.voice.SettingsUtil; import com.android.inputmethod.voice.VoiceInput; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.inputmethodservice.Keyboard; import android.inputmethodservice.KeyboardView; import android.media.AudioManager; import android.os.Debug; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; import android.speech.RecognitionManager; import android.text.AutoText; import android.text.ClipboardManager; import android.text.TextUtils; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; /** * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService implements KeyboardView.OnKeyboardActionListener, VoiceInput.UiListener, SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = "LatinIME"; static final boolean DEBUG = false; static final boolean TRACE = false; static final boolean VOICE_INSTALLED = true; static final boolean ENABLE_VOICE_BUTTON = true; private static final String PREF_VIBRATE_ON = "vibrate_on"; private static final String PREF_SOUND_ON = "sound_on"; private static final String PREF_AUTO_CAP = "auto_cap"; private static final String PREF_QUICK_FIXES = "quick_fixes"; private static final String PREF_SHOW_SUGGESTIONS = "show_suggestions"; private static final String PREF_AUTO_COMPLETE = "auto_complete"; private static final String PREF_VOICE_MODE = "voice_mode"; // Whether or not the user has used voice input before (and thus, whether to show the // first-run warning dialog or not). private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input"; // Whether or not the user has used voice input from an unsupported locale UI before. // For example, the user has a Chinese UI but activates voice input. private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE = "has_used_voice_input_unsupported_locale"; // A list of locales which are supported by default for voice input, unless we get a // different list from Gservices. public static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES = "en " + "en_US " + "en_GB " + "en_AU " + "en_CA " + "en_IE " + "en_IN " + "en_NZ " + "en_SG " + "en_ZA "; // The private IME option used to indicate that no microphone should be shown for a // given text field. For instance this is specified by the search dialog when the // dialog is already showing a voice search button. private static final String IME_OPTION_NO_MICROPHONE = "nm"; public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; public static final String PREF_INPUT_LANGUAGE = "input_language"; private static final int MSG_UPDATE_SUGGESTIONS = 0; private static final int MSG_START_TUTORIAL = 1; private static final int MSG_UPDATE_SHIFT_STATE = 2; private static final int MSG_VOICE_RESULTS = 3; private static final int MSG_START_LISTENING_AFTER_SWIPE = 4; // If we detect a swipe gesture within N ms of typing, then swipe is // ignored, since it may in fact be two key presses in quick succession. private static final long MIN_MILLIS_AFTER_TYPING_BEFORE_SWIPE = 1000; // If we detect a swipe gesture, and the user types N ms later, cancel the // swipe since it was probably a false trigger. private static final long MIN_MILLIS_AFTER_SWIPE_TO_WAIT_FOR_TYPING = 500; // 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; // Weight added to a user picking a new word from the suggestion strip static final int FREQUENCY_FOR_PICKED = 3; // Weight added to a user typing a new word that doesn't get corrected (or is reverted) static final int FREQUENCY_FOR_TYPED = 1; // A word that is frequently typed and get's promoted to the user dictionary, uses this // frequency. static final int FREQUENCY_FOR_AUTO_ADD = 250; static final int KEYCODE_ENTER = '\n'; static final int KEYCODE_SPACE = ' '; // Contextual menu positions private static final int POS_SETTINGS = 0; private static final int POS_METHOD = 1; private LatinKeyboardView mInputView; private CandidateViewContainer mCandidateViewContainer; private CandidateView mCandidateView; private Suggest mSuggest; private CompletionInfo[] mCompletions; private AlertDialog mOptionsDialog; private AlertDialog mVoiceWarningDialog; KeyboardSwitcher mKeyboardSwitcher; private UserDictionary mUserDictionary; private ContactsDictionary mContactsDictionary; private ExpandableDictionary mAutoDictionary; private Hints mHints; Resources mResources; private String mLocale; private LanguageSwitcher mLanguageSwitcher; private StringBuilder mComposing = new StringBuilder(); private WordComposer mWord = new WordComposer(); private int mCommittedLength; private boolean mPredicting; private boolean mRecognizing; private boolean mAfterVoiceInput; private boolean mImmediatelyAfterVoiceInput; private boolean mShowingVoiceSuggestions; private boolean mImmediatelyAfterVoiceSuggestions; private boolean mVoiceInputHighlighted; private boolean mEnableVoiceButton; private CharSequence mBestWord; private boolean mPredictionOn; private boolean mCompletionOn; private boolean mHasDictionary; private boolean mAutoSpace; private boolean mJustAddedAutoSpace; private boolean mAutoCorrectEnabled; private boolean mAutoCorrectOn; private boolean mCapsLock; private boolean mPasswordText; private boolean mEmailText; private boolean mVibrateOn; private boolean mSoundOn; private boolean mAutoCap; private boolean mQuickFixes; private boolean mHasUsedVoiceInput; private boolean mHasUsedVoiceInputUnsupportedLocale; private boolean mLocaleSupportedForVoiceInput; private boolean mShowSuggestions; private boolean mSuggestionShouldReplaceCurrentWord; private boolean mIsShowingHint; private int mCorrectionMode; private boolean mEnableVoice = true; private boolean mVoiceOnPrimary; private int mOrientation; private List mSuggestPuncList; // Indicates whether the suggestion strip is to be on in landscape private boolean mJustAccepted; private CharSequence mJustRevertedSeparator; private int mDeleteCount; private long mLastKeyTime; private Tutorial mTutorial; private AudioManager mAudioManager; // Align sound effect volume on music volume private final float FX_VOLUME = -1.0f; private boolean mSilentMode; private String mWordSeparators; private String mSentenceSeparators; private VoiceInput mVoiceInput; private VoiceResults mVoiceResults = new VoiceResults(); private long mSwipeTriggerTimeMillis; // For each word, a list of potential replacements, usually from voice. private Map> mWordToSuggestions = new HashMap>(); private class VoiceResults { List candidates; Map> alternatives; } private boolean mRefreshKeyboardRequired; Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_SUGGESTIONS: updateSuggestions(); break; case MSG_START_TUTORIAL: if (mTutorial == null) { if (mInputView.isShown()) { mTutorial = new Tutorial(LatinIME.this, mInputView); mTutorial.start(); } else { // Try again soon if the view is not yet showing sendMessageDelayed(obtainMessage(MSG_START_TUTORIAL), 100); } } break; case MSG_UPDATE_SHIFT_STATE: updateShiftKeyState(getCurrentInputEditorInfo()); break; case MSG_VOICE_RESULTS: handleVoiceResults(); break; case MSG_START_LISTENING_AFTER_SWIPE: if (mLastKeyTime < mSwipeTriggerTimeMillis) { startListening(true); } } } }; @Override public void onCreate() { super.onCreate(); //setStatusIcon(R.drawable.ime_qwerty); mResources = getResources(); final Configuration conf = mResources.getConfiguration(); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); mLanguageSwitcher = new LanguageSwitcher(this); mLanguageSwitcher.loadLocales(prefs); mKeyboardSwitcher = new KeyboardSwitcher(this, this); mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); boolean enableMultipleLanguages = mLanguageSwitcher.getLocaleCount() > 0; String inputLanguage = mLanguageSwitcher.getInputLanguage(); if (inputLanguage == null) { inputLanguage = conf.locale.toString(); } initSuggest(inputLanguage); mOrientation = conf.orientation; initSuggestPuncList(); // register to receive ringer mode changes for silent mode IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); registerReceiver(mReceiver, filter); if (VOICE_INSTALLED) { mVoiceInput = new VoiceInput(this, this); mHints = new Hints(this, new Hints.Display() { public void showHint(int viewResource) { LayoutInflater inflater = (LayoutInflater) getSystemService( Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(viewResource, null); setCandidatesView(view); setCandidatesViewShown(true); mIsShowingHint = true; } }); } prefs.registerOnSharedPreferenceChangeListener(this); } private void initSuggest(String locale) { mLocale = locale; Resources orig = getResources(); Configuration conf = orig.getConfiguration(); Locale saveLocale = conf.locale; boolean different = !conf.locale.getCountry().equalsIgnoreCase(locale.substring(0, 2)); conf.locale = new Locale(locale); orig.updateConfiguration(conf, orig.getDisplayMetrics()); if (mSuggest != null) { mSuggest.close(); } mSuggest = new Suggest(this, R.raw.main); mSuggest.setAutoTextEnabled(!different); if (mUserDictionary != null) mUserDictionary.close(); mUserDictionary = new UserDictionary(this); if (mContactsDictionary == null) { mContactsDictionary = new ContactsDictionary(this); } // TODO: Save and restore the dictionary for the current input language. if (mAutoDictionary == null) { mAutoDictionary = new AutoDictionary(this); } mSuggest.setUserDictionary(mUserDictionary); mSuggest.setContactsDictionary(mContactsDictionary); mSuggest.setAutoDictionary(mAutoDictionary); updateCorrectionMode(); mWordSeparators = mResources.getString(R.string.word_separators); mSentenceSeparators = mResources.getString(R.string.sentence_separators); conf.locale = saveLocale; orig.updateConfiguration(conf, orig.getDisplayMetrics()); } @Override public void onDestroy() { mUserDictionary.close(); mContactsDictionary.close(); unregisterReceiver(mReceiver); if (VOICE_INSTALLED) { mVoiceInput.destroy(); } super.onDestroy(); } @Override public void onConfigurationChanged(Configuration conf) { if (!TextUtils.equals(conf.locale.toString(), mLocale)) { initSuggest(conf.locale.toString()); } // If orientation changed while predicting, commit the change if (conf.orientation != mOrientation) { InputConnection ic = getCurrentInputConnection(); commitTyped(ic); if (ic != null) ic.finishComposingText(); // For voice input mOrientation = conf.orientation; } reloadKeyboards(); super.onConfigurationChanged(conf); } @Override public View onCreateInputView() { mInputView = (LatinKeyboardView) getLayoutInflater().inflate( R.layout.input, null); mKeyboardSwitcher.setInputView(mInputView); mKeyboardSwitcher.makeKeyboards(true); mInputView.setOnKeyboardActionListener(this); mKeyboardSwitcher.setKeyboardMode( KeyboardSwitcher.MODE_TEXT, 0, shouldShowVoiceButton(makeFieldContext(), getCurrentInputEditorInfo())); return mInputView; } @Override public void onInitializeInterface() { // Create a new view associated with voice input if the old // view is stuck in another layout (e.g. if switching from // portrait to landscape while speaking) // NOTE: This must be done here because for some reason // onCreateInputView isn't called after an orientation change while // speech rec is in progress. if (mVoiceInput != null && mVoiceInput.getView().getParent() != null) { mVoiceInput.newView(); } super.onInitializeInterface(); } @Override public View onCreateCandidatesView() { mKeyboardSwitcher.makeKeyboards(true); mCandidateViewContainer = (CandidateViewContainer) getLayoutInflater().inflate( R.layout.candidates, null); mCandidateViewContainer.initViews(); mCandidateView = (CandidateView) mCandidateViewContainer.findViewById(R.id.candidates); mCandidateView.setService(this); setCandidatesViewShown(true); return mCandidateViewContainer; } @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { // In landscape mode, this method gets called without the input view being created. if (mInputView == null) { return; } if (mRefreshKeyboardRequired) { mRefreshKeyboardRequired = false; toggleLanguage(true, true); } mKeyboardSwitcher.makeKeyboards(false); TextEntryState.newSession(this); // Most such things we decide below in the switch statement, but we need to know // now whether this is a password text field, because we need to know now (before // the switch statement) whether we want to enable the voice button. mPasswordText = false; int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION; if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) { mPasswordText = true; } mEnableVoiceButton = shouldShowVoiceButton(makeFieldContext(), attribute); final boolean enableVoiceButton = mEnableVoiceButton && mEnableVoice; mAfterVoiceInput = false; mImmediatelyAfterVoiceInput = false; mShowingVoiceSuggestions = false; mImmediatelyAfterVoiceSuggestions = false; mVoiceInputHighlighted = false; boolean disableAutoCorrect = false; mWordToSuggestions.clear(); mInputTypeNoAutoCorrect = false; mPredictionOn = false; mCompletionOn = false; mCompletions = null; mCapsLock = false; mEmailText = false; switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) { case EditorInfo.TYPE_CLASS_NUMBER: case EditorInfo.TYPE_CLASS_DATETIME: mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_SYMBOLS, attribute.imeOptions, enableVoiceButton); break; case EditorInfo.TYPE_CLASS_PHONE: mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_PHONE, attribute.imeOptions, enableVoiceButton); break; case EditorInfo.TYPE_CLASS_TEXT: mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, attribute.imeOptions, enableVoiceButton); //startPrediction(); mPredictionOn = true; // Make sure that passwords are not displayed in candidate view if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ) { mPredictionOn = false; } if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { mEmailText = true; } if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || variation == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME) { mAutoSpace = false; } else { mAutoSpace = true; } if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { mPredictionOn = false; mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_EMAIL, attribute.imeOptions, enableVoiceButton); } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) { mPredictionOn = false; mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_URL, attribute.imeOptions, enableVoiceButton); } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_IM, attribute.imeOptions, enableVoiceButton); } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) { mPredictionOn = false; } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_WEB, attribute.imeOptions, enableVoiceButton); // If it's a browser edit field and auto correct is not ON explicitly, then // disable auto correction, but keep suggestions on. if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) { mInputTypeNoAutoCorrect = true; } } // If NO_SUGGESTIONS is set, don't do prediction. if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) { mPredictionOn = false; mInputTypeNoAutoCorrect = true; } // If it's not multiline and the autoCorrect flag is not set, then don't correct if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 && (attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) { mInputTypeNoAutoCorrect = true; } if ((attribute.inputType&EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { mPredictionOn = false; mCompletionOn = true && isFullscreenMode(); } updateShiftKeyState(attribute); break; default: mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, attribute.imeOptions, enableVoiceButton); updateShiftKeyState(attribute); } mInputView.closing(); mComposing.setLength(0); mPredicting = false; mDeleteCount = 0; mJustAddedAutoSpace = false; loadSettings(); setCandidatesViewShown(false); setSuggestions(null, false, false, false); // Override auto correct if (disableAutoCorrect) { mAutoCorrectOn = false; if (mCorrectionMode == Suggest.CORRECTION_FULL) { mCorrectionMode = Suggest.CORRECTION_BASIC; } } // If the dictionary is not big enough, don't auto correct mHasDictionary = mSuggest.hasMainDictionary(); updateCorrectionMode(); mInputView.setProximityCorrectionEnabled(true); mPredictionOn = mPredictionOn && mCorrectionMode > 0; checkTutorial(attribute.privateImeOptions); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } @Override public void onFinishInput() { super.onFinishInput(); if (VOICE_INSTALLED && mAfterVoiceInput) { mVoiceInput.logInputEnded(); } if (VOICE_INSTALLED) { mVoiceInput.flushLogs(); } if (mInputView != null) { mInputView.closing(); } if (VOICE_INSTALLED && mRecognizing) { mVoiceInput.cancel(); } } @Override public void onUpdateExtractedText(int token, ExtractedText text) { super.onUpdateExtractedText(token, text); InputConnection ic = getCurrentInputConnection(); if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) { mVoiceInput.logTextModified(); if (mHints.showPunctuationHintIfNecessary(ic)) { mVoiceInput.logPunctuationHintDisplayed(); } } mImmediatelyAfterVoiceInput = false; } @Override public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); if (DEBUG) { Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd + ", nss=" + newSelStart + ", nse=" + newSelEnd + ", cs=" + candidatesStart + ", ce=" + candidatesEnd); } mSuggestionShouldReplaceCurrentWord = false; // If the current selection in the text view changes, we should // clear whatever candidate text we have. if ((((mComposing.length() > 0 && mPredicting) || mVoiceInputHighlighted) && (newSelStart != candidatesEnd || newSelEnd != candidatesEnd))) { mComposing.setLength(0); mPredicting = false; updateSuggestions(); TextEntryState.reset(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.finishComposingText(); } mVoiceInputHighlighted = false; } else if (!mPredicting && !mJustAccepted) { switch (TextEntryState.getState()) { case TextEntryState.STATE_ACCEPTED_DEFAULT: TextEntryState.reset(); // fall through case TextEntryState.STATE_SPACE_AFTER_PICKED: mJustAddedAutoSpace = false; // The user moved the cursor. break; } } mJustAccepted = false; postUpdateShiftKeyState(); if (VOICE_INSTALLED) { if (mShowingVoiceSuggestions) { if (mImmediatelyAfterVoiceSuggestions) { mImmediatelyAfterVoiceSuggestions = false; } else { updateSuggestions(); mShowingVoiceSuggestions = false; } } if (VoiceInput.ENABLE_WORD_CORRECTIONS) { // If we have alternatives for the current word, then show them. String word = EditingUtil.getWordAtCursor( getCurrentInputConnection(), getWordSeparators()); if (word != null && mWordToSuggestions.containsKey(word.trim())) { mSuggestionShouldReplaceCurrentWord = true; final List suggestions = mWordToSuggestions.get(word.trim()); setSuggestions(suggestions, false, true, true); setCandidatesViewShown(true); } } } } @Override public void hideWindow() { if (mAfterVoiceInput) mVoiceInput.logInputEnded(); if (TRACE) Debug.stopMethodTracing(); if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) { mVoiceInput.logKeyboardWarningDialogDismissed(); mVoiceWarningDialog.dismiss(); mVoiceWarningDialog = null; } if (mTutorial != null) { mTutorial.close(); mTutorial = null; } if (VOICE_INSTALLED & mRecognizing) { mVoiceInput.cancel(); } super.hideWindow(); TextEntryState.endSession(); } @Override public void onDisplayCompletions(CompletionInfo[] completions) { if (false) { Log.i("foo", "Received completions:"); for (int i=0; i<(completions != null ? completions.length : 0); i++) { Log.i("foo", " #" + i + ": " + completions[i]); } } if (mCompletionOn) { mCompletions = completions; if (completions == null) { setSuggestions(null, false, false, false); return; } List stringList = new ArrayList(); for (int i=0; i<(completions != null ? completions.length : 0); i++) { CompletionInfo ci = completions[i]; if (ci != null) stringList.add(ci.getText()); } //CharSequence typedWord = mWord.getTypedWord(); setSuggestions(stringList, true, true, true); mBestWord = null; setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); } } @Override public void setCandidatesViewShown(boolean shown) { // TODO: Remove this if we support candidates with hard keyboard if (onEvaluateInputViewShown()) { super.setCandidatesViewShown(shown); } } @Override public void onComputeInsets(InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); if (!isFullscreenMode()) { outInsets.contentTopInsets = outInsets.visibleTopInsets; } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: if (event.getRepeatCount() == 0 && mInputView != null) { if (mInputView.handleBack()) { return true; } else if (mTutorial != null) { mTutorial.close(); mTutorial = null; } } break; case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: // If tutorial is visible, don't allow dpad to work if (mTutorial != null) { return true; } break; } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: // If tutorial is visible, don't allow dpad to work if (mTutorial != null) { return true; } // Enable shift key and DPAD to do selections if (mInputView != null && mInputView.isShown() && mInputView.isShifted()) { event = new KeyEvent(event.getDownTime(), event.getEventTime(), event.getAction(), event.getKeyCode(), event.getRepeatCount(), event.getDeviceId(), event.getScanCode(), KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); InputConnection ic = getCurrentInputConnection(); if (ic != null) ic.sendKeyEvent(event); return true; } break; } return super.onKeyUp(keyCode, event); } private void revertVoiceInput() { InputConnection ic = getCurrentInputConnection(); if (ic != null) ic.commitText("", 1); updateSuggestions(); mVoiceInputHighlighted = false; } private void commitVoiceInput() { InputConnection ic = getCurrentInputConnection(); if (ic != null) ic.finishComposingText(); updateSuggestions(); mVoiceInputHighlighted = false; } private void reloadKeyboards() { if (mKeyboardSwitcher == null) { mKeyboardSwitcher = new KeyboardSwitcher(this, this); } mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); if (mInputView != null) { mKeyboardSwitcher.setVoiceMode(mEnableVoice, mVoiceOnPrimary); } mKeyboardSwitcher.makeKeyboards(true); } private void commitTyped(InputConnection inputConnection) { if (mPredicting) { mPredicting = false; if (mComposing.length() > 0) { if (inputConnection != null) { inputConnection.commitText(mComposing, 1); } mCommittedLength = mComposing.length(); TextEntryState.acceptedTyped(mComposing); mAutoDictionary.addWord(mComposing.toString(), FREQUENCY_FOR_TYPED); } updateSuggestions(); } } private void postUpdateShiftKeyState() { mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SHIFT_STATE), 300); } public void updateShiftKeyState(EditorInfo attr) { InputConnection ic = getCurrentInputConnection(); if (attr != null && mInputView != null && mKeyboardSwitcher.isAlphabetMode() && ic != null) { mInputView.setShifted(mCapsLock || getCursorCapsMode(ic, attr) != 0); } } private int getCursorCapsMode(InputConnection ic, EditorInfo attr) { int caps = 0; EditorInfo ei = getCurrentInputEditorInfo(); if (mAutoCap && ei != null && ei.inputType != EditorInfo.TYPE_NULL) { caps = ic.getCursorCapsMode(attr.inputType); } return caps; } private void swapPunctuationAndSpace() { final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == KEYCODE_SPACE && isSentenceSeparator(lastTwo.charAt(1))) { ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(lastTwo.charAt(1) + " ", 1); ic.endBatchEdit(); updateShiftKeyState(getCurrentInputEditorInfo()); mJustAddedAutoSpace = true; } } private void doubleSpace() { //if (!mAutoPunctuate) return; if (mCorrectionMode == Suggest.CORRECTION_NONE) return; final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; CharSequence lastThree = ic.getTextBeforeCursor(3, 0); if (lastThree != null && lastThree.length() == 3 && Character.isLetterOrDigit(lastThree.charAt(0)) && lastThree.charAt(1) == KEYCODE_SPACE && lastThree.charAt(2) == KEYCODE_SPACE) { ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(". ", 1); ic.endBatchEdit(); updateShiftKeyState(getCurrentInputEditorInfo()); mJustAddedAutoSpace = true; } } private void maybeRemovePreviousPeriod(CharSequence text) { final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; // When the text's first character is '.', remove the previous period // if there is one. CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == '.' && text.charAt(0) == '.') { ic.deleteSurroundingText(1, 0); } } private void removeTrailingSpace() { final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == KEYCODE_SPACE) { ic.deleteSurroundingText(1, 0); } } public boolean addWordToDictionary(String word) { mUserDictionary.addWord(word, 128); return true; } private boolean isAlphabet(int code) { if (Character.isLetter(code)) { return true; } else { return false; } } // Implementation of KeyboardViewListener public void onKey(int primaryCode, int[] keyCodes) { long when = SystemClock.uptimeMillis(); if (primaryCode != Keyboard.KEYCODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { mDeleteCount = 0; } mLastKeyTime = when; switch (primaryCode) { case Keyboard.KEYCODE_DELETE: handleBackspace(); mDeleteCount++; break; case Keyboard.KEYCODE_SHIFT: handleShift(); break; case Keyboard.KEYCODE_CANCEL: if (mOptionsDialog == null || !mOptionsDialog.isShowing()) { handleClose(); } break; case LatinKeyboardView.KEYCODE_OPTIONS: showOptionsMenu(); break; case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE: toggleLanguage(false, true); break; case LatinKeyboardView.KEYCODE_PREV_LANGUAGE: toggleLanguage(false, false); break; case LatinKeyboardView.KEYCODE_SHIFT_LONGPRESS: if (mCapsLock) { handleShift(); } else { toggleCapsLock(); } break; case Keyboard.KEYCODE_MODE_CHANGE: changeKeyboardMode(); break; case LatinKeyboardView.KEYCODE_VOICE: if (VOICE_INSTALLED) { startListening(false /* was a button press, was not a swipe */); } break; case 9 /*Tab*/: sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB); break; default: if (primaryCode != KEYCODE_ENTER) { mJustAddedAutoSpace = false; } if (isWordSeparator(primaryCode)) { handleSeparator(primaryCode); } else { handleCharacter(primaryCode, keyCodes); } // Cancel the just reverted state mJustRevertedSeparator = null; } if (mKeyboardSwitcher.onKey(primaryCode)) { changeKeyboardMode(); } } public void onText(CharSequence text) { InputConnection ic = getCurrentInputConnection(); if (ic == null) return; ic.beginBatchEdit(); if (mPredicting) { commitTyped(ic); } maybeRemovePreviousPeriod(text); ic.commitText(text, 1); ic.endBatchEdit(); updateShiftKeyState(getCurrentInputEditorInfo()); mJustRevertedSeparator = null; mJustAddedAutoSpace = false; } private void handleBackspace() { if (VOICE_INSTALLED && mVoiceInputHighlighted) { revertVoiceInput(); return; } boolean deleteChar = false; InputConnection ic = getCurrentInputConnection(); if (ic == null) return; if (mPredicting) { final int length = mComposing.length(); if (length > 0) { mComposing.delete(length - 1, length); mWord.deleteLast(); ic.setComposingText(mComposing, 1); if (mComposing.length() == 0) { mPredicting = false; } postUpdateSuggestions(); } else { ic.deleteSurroundingText(1, 0); } } else { deleteChar = true; } postUpdateShiftKeyState(); TextEntryState.backspace(); if (TextEntryState.getState() == TextEntryState.STATE_UNDO_COMMIT) { revertLastWord(deleteChar); return; } else if (deleteChar) { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); if (mDeleteCount > DELETE_ACCELERATE_AT) { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); } } mJustRevertedSeparator = null; } private void handleShift() { mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); if (mKeyboardSwitcher.isAlphabetMode()) { // Alphabet keyboard checkToggleCapsLock(); mInputView.setShifted(mCapsLock || !mInputView.isShifted()); } else { mKeyboardSwitcher.toggleShift(); } } private void handleCharacter(int primaryCode, int[] keyCodes) { if (VOICE_INSTALLED && mVoiceInputHighlighted) { commitVoiceInput(); } if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) { if (!mPredicting) { mPredicting = true; mComposing.setLength(0); mWord.reset(); } } if (mInputView.isShifted()) { // TODO: This doesn't work with ß, need to fix it in the next release. if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT || keyCodes[0] > Character.MAX_CODE_POINT) { return; } primaryCode = new String(keyCodes, 0, 1).toUpperCase().charAt(0); } if (mPredicting) { if (mInputView.isShifted() && mComposing.length() == 0) { mWord.setCapitalized(true); } mComposing.append((char) primaryCode); mWord.add(primaryCode, keyCodes); InputConnection ic = getCurrentInputConnection(); if (ic != null) { // If it's the first letter, make note of auto-caps state if (mWord.size() == 1) { mWord.setAutoCapitalized( getCursorCapsMode(ic, getCurrentInputEditorInfo()) != 0); } ic.setComposingText(mComposing, 1); } postUpdateSuggestions(); } else { sendKeyChar((char)primaryCode); } updateShiftKeyState(getCurrentInputEditorInfo()); measureCps(); TextEntryState.typedCharacter((char) primaryCode, isWordSeparator(primaryCode)); } private void handleSeparator(int primaryCode) { if (VOICE_INSTALLED && mVoiceInputHighlighted) { commitVoiceInput(); } boolean pickedDefault = false; // Handle separator InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); } if (mPredicting) { // In certain languages where single quote is a separator, it's better // not to auto correct, but accept the typed word. For instance, // in Italian dov' should not be expanded to dove' because the elision // requires the last vowel to be removed. if (mAutoCorrectOn && primaryCode != '\'' && (mJustRevertedSeparator == null || mJustRevertedSeparator.length() == 0 || mJustRevertedSeparator.charAt(0) != primaryCode)) { pickDefaultSuggestion(); pickedDefault = true; // Picked the suggestion by the space key. We consider this // as "added an auto space". if (primaryCode == KEYCODE_SPACE) { mJustAddedAutoSpace = true; } } else { commitTyped(ic); } } if (mJustAddedAutoSpace && primaryCode == KEYCODE_ENTER) { removeTrailingSpace(); mJustAddedAutoSpace = false; } sendKeyChar((char)primaryCode); TextEntryState.typedCharacter((char) primaryCode, true); if (TextEntryState.getState() == TextEntryState.STATE_PUNCTUATION_AFTER_ACCEPTED && primaryCode != KEYCODE_ENTER) { swapPunctuationAndSpace(); } else if (isPredictionOn() && primaryCode == KEYCODE_SPACE) { //else if (TextEntryState.STATE_SPACE_AFTER_ACCEPTED) { doubleSpace(); } if (pickedDefault && mBestWord != null) { TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); } updateShiftKeyState(getCurrentInputEditorInfo()); if (ic != null) { ic.endBatchEdit(); } } private void handleClose() { commitTyped(getCurrentInputConnection()); if (VOICE_INSTALLED & mRecognizing) { mVoiceInput.cancel(); } requestHideSelf(0); mInputView.closing(); TextEntryState.endSession(); } private void checkToggleCapsLock() { if (mInputView.getKeyboard().isShifted()) { toggleCapsLock(); } } private void toggleCapsLock() { mCapsLock = !mCapsLock; if (mKeyboardSwitcher.isAlphabetMode()) { ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); } } private void postUpdateSuggestions() { mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100); } private boolean isPredictionOn() { boolean predictionOn = mPredictionOn; //if (isFullscreenMode()) predictionOn &= mPredictionLandscape; return predictionOn; } private boolean isCandidateStripVisible() { return isPredictionOn() && mShowSuggestions; } public void onCancelVoice() { if (mRecognizing) { switchToKeyboardView(); } } private void switchToKeyboardView() { mHandler.post(new Runnable() { public void run() { mRecognizing = false; if (mInputView != null) { setInputView(mInputView); } updateInputViewShown(); }}); } private void switchToRecognitionStatusView() { mHandler.post(new Runnable() { public void run() { mRecognizing = true; setInputView(mVoiceInput.getView()); updateInputViewShown(); }}); } private void startListening(boolean swipe) { if (!mHasUsedVoiceInput || (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale)) { // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel. showVoiceWarningDialog(swipe); } else { reallyStartListening(swipe); } } private void reallyStartListening(boolean swipe) { if (!mHasUsedVoiceInput) { // The user has started a voice input, so remember that in the // future (so we don't show the warning dialog after the first run). SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true); editor.commit(); mHasUsedVoiceInput = true; } if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) { // The user has started a voice input from an unsupported locale, so remember that // in the future (so we don't show the warning dialog the next time they do this). SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true); editor.commit(); mHasUsedVoiceInputUnsupportedLocale = true; } // Clear N-best suggestions setSuggestions(null, false, false, true); FieldContext context = new FieldContext( getCurrentInputConnection(), getCurrentInputEditorInfo(), mLanguageSwitcher.getInputLanguage(), mLanguageSwitcher.getEnabledLanguages()); mVoiceInput.startListening(context, swipe); switchToRecognitionStatusView(); } private void showVoiceWarningDialog(final boolean swipe) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); builder.setIcon(R.drawable.ic_mic_dialog); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { mVoiceInput.logKeyboardWarningDialogOk(); reallyStartListening(swipe); } }); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { mVoiceInput.logKeyboardWarningDialogCancel(); } }); if (mLocaleSupportedForVoiceInput) { String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" + getString(R.string.voice_warning_how_to_turn_off); builder.setMessage(message); } else { String message = getString(R.string.voice_warning_locale_not_supported) + "\n\n" + getString(R.string.voice_warning_may_not_understand) + "\n\n" + getString(R.string.voice_warning_how_to_turn_off); builder.setMessage(message); } builder.setTitle(R.string.voice_warning_title); mVoiceWarningDialog = builder.create(); Window window = mVoiceWarningDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); lp.token = mInputView.getWindowToken(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); mVoiceInput.logKeyboardWarningDialogShown(); mVoiceWarningDialog.show(); } public void onVoiceResults(List candidates, Map> alternatives) { if (!mRecognizing) { return; } mVoiceResults.candidates = candidates; mVoiceResults.alternatives = alternatives; mHandler.sendMessage(mHandler.obtainMessage(MSG_VOICE_RESULTS)); } private void handleVoiceResults() { mAfterVoiceInput = true; mImmediatelyAfterVoiceInput = true; InputConnection ic = getCurrentInputConnection(); if (!isFullscreenMode()) { // Start listening for updates to the text from typing, etc. if (ic != null) { ExtractedTextRequest req = new ExtractedTextRequest(); ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR); } } vibrate(); switchToKeyboardView(); final List nBest = new ArrayList(); boolean capitalizeFirstWord = preferCapitalization() || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted()); for (String c : mVoiceResults.candidates) { if (capitalizeFirstWord) { c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length()); } nBest.add(c); } if (nBest.size() == 0) { return; } String bestResult = nBest.get(0).toString(); mVoiceInput.logVoiceInputDelivered(); mHints.registerVoiceResult(bestResult); if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text commitTyped(ic); EditingUtil.appendText(ic, bestResult); if (ic != null) ic.endBatchEdit(); // Show N-Best alternates, if there is more than one choice. if (nBest.size() > 1) { mImmediatelyAfterVoiceSuggestions = true; mShowingVoiceSuggestions = true; setSuggestions(nBest.subList(1, nBest.size()), false, true, true); setCandidatesViewShown(true); } mVoiceInputHighlighted = true; mWordToSuggestions.putAll(mVoiceResults.alternatives); } private void setSuggestions( List suggestions, boolean completions, boolean typedWordValid, boolean haveMinimalSuggestion) { if (mIsShowingHint) { setCandidatesView(mCandidateViewContainer); mIsShowingHint = false; } if (mCandidateView != null) { mCandidateView.setSuggestions( suggestions, completions, typedWordValid, haveMinimalSuggestion); } } private void updateSuggestions() { mSuggestionShouldReplaceCurrentWord = false; ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null); // Check if we have a suggestion engine attached. if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) { return; } if (!mPredicting) { setNextSuggestions(); return; } List stringList = mSuggest.getSuggestions(mInputView, mWord, false); int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies(); ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(nextLettersFrequencies); boolean correctionAvailable = mSuggest.hasMinimalCorrection(); //|| mCorrectionMode == mSuggest.CORRECTION_FULL; CharSequence typedWord = mWord.getTypedWord(); // If we're in basic correct boolean typedWordValid = mSuggest.isValidWord(typedWord) || (preferCapitalization() && mSuggest.isValidWord(typedWord.toString().toLowerCase())); if (mCorrectionMode == Suggest.CORRECTION_FULL) { correctionAvailable |= typedWordValid; } // Don't auto-correct words with multiple capital letter correctionAvailable &= !mWord.isMostlyCaps(); setSuggestions(stringList, false, typedWordValid, correctionAvailable); if (stringList.size() > 0) { if (correctionAvailable && !typedWordValid && stringList.size() > 1) { mBestWord = stringList.get(1); } else { mBestWord = typedWord; } } else { mBestWord = null; } setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); } private void pickDefaultSuggestion() { // Complete any pending candidate query first if (mHandler.hasMessages(MSG_UPDATE_SUGGESTIONS)) { mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); updateSuggestions(); } if (mBestWord != null) { TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); mJustAccepted = true; pickSuggestion(mBestWord); } } public void pickSuggestionManually(int index, CharSequence suggestion) { if (mAfterVoiceInput && mShowingVoiceSuggestions) mVoiceInput.logNBestChoose(index); InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); } if (mCompletionOn && mCompletions != null && index >= 0 && index < mCompletions.length) { CompletionInfo ci = mCompletions[index]; if (ic != null) { ic.commitCompletion(ci); } mCommittedLength = suggestion.length(); if (mCandidateView != null) { mCandidateView.clear(); } updateShiftKeyState(getCurrentInputEditorInfo()); if (ic != null) { ic.endBatchEdit(); } return; } // If this is a punctuation, apply it through the normal key press if (suggestion.length() == 1 && isWordSeparator(suggestion.charAt(0))) { onKey(suggestion.charAt(0), null); if (ic != null) { ic.endBatchEdit(); } return; } mJustAccepted = true; pickSuggestion(suggestion); TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); // Follow it with a space if (mAutoSpace) { sendSpace(); mJustAddedAutoSpace = true; } // Fool the state watcher so that a subsequent backspace will not do a revert TextEntryState.typedCharacter((char) KEYCODE_SPACE, true); if (index == 0 && !mSuggest.isValidWord(suggestion)) { mCandidateView.showAddToDictionaryHint(suggestion); } if (ic != null) { ic.endBatchEdit(); } } private void pickSuggestion(CharSequence suggestion) { if (mCapsLock) { suggestion = suggestion.toString().toUpperCase(); } else if (preferCapitalization() || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted())) { suggestion = suggestion.toString().toUpperCase().charAt(0) + suggestion.subSequence(1, suggestion.length()).toString(); } InputConnection ic = getCurrentInputConnection(); if (ic != null) { if (mSuggestionShouldReplaceCurrentWord) { EditingUtil.deleteWordAtCursor(ic, getWordSeparators()); } if (!VoiceInput.DELETE_SYMBOL.equals(suggestion)) { ic.commitText(suggestion, 1); } } // Add the word to the auto dictionary if it's not a known word if (mAutoDictionary.isValidWord(suggestion) || !mSuggest.isValidWord(suggestion)) { mAutoDictionary.addWord(suggestion.toString(), FREQUENCY_FOR_PICKED); } mPredicting = false; mCommittedLength = suggestion.length(); setNextSuggestions(); updateShiftKeyState(getCurrentInputEditorInfo()); } private void setNextSuggestions() { setSuggestions(mSuggestPuncList, false, false, false); } private boolean isCursorTouchingWord() { InputConnection ic = getCurrentInputConnection(); if (ic == null) return false; CharSequence toLeft = ic.getTextBeforeCursor(1, 0); CharSequence toRight = ic.getTextAfterCursor(1, 0); if (!TextUtils.isEmpty(toLeft) && !isWordSeparator(toLeft.charAt(0))) { return true; } if (!TextUtils.isEmpty(toRight) && !isWordSeparator(toRight.charAt(0))) { return true; } return false; } public void revertLastWord(boolean deleteChar) { final int length = mComposing.length(); if (!mPredicting && length > 0) { final InputConnection ic = getCurrentInputConnection(); mPredicting = true; ic.beginBatchEdit(); mJustRevertedSeparator = ic.getTextBeforeCursor(1, 0); if (deleteChar) ic.deleteSurroundingText(1, 0); int toDelete = mCommittedLength; CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); if (toTheLeft != null && toTheLeft.length() > 0 && isWordSeparator(toTheLeft.charAt(0))) { toDelete--; } ic.deleteSurroundingText(toDelete, 0); ic.setComposingText(mComposing, 1); TextEntryState.backspace(); ic.endBatchEdit(); postUpdateSuggestions(); } else { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); mJustRevertedSeparator = null; } } protected String getWordSeparators() { return mWordSeparators; } public boolean isWordSeparator(int code) { String separators = getWordSeparators(); return separators.contains(String.valueOf((char)code)); } public boolean isSentenceSeparator(int code) { return mSentenceSeparators.contains(String.valueOf((char)code)); } private void sendSpace() { sendKeyChar((char)KEYCODE_SPACE); updateShiftKeyState(getCurrentInputEditorInfo()); //onKey(KEY_SPACE[0], KEY_SPACE); } public boolean preferCapitalization() { return mWord.isCapitalized(); } public void swipeRight() { if (userHasNotTypedRecently() && VOICE_INSTALLED && mEnableVoice && fieldCanDoVoice(makeFieldContext())) { startListening(true /* was a swipe */); } if (LatinKeyboardView.DEBUG_AUTO_PLAY) { ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)); CharSequence text = cm.getText(); if (!TextUtils.isEmpty(text)) { mInputView.startPlaying(text.toString()); } } } private void toggleLanguage(boolean reset, boolean next) { if (reset) { mLanguageSwitcher.reset(); } else { if (next) { mLanguageSwitcher.next(); } else { mLanguageSwitcher.prev(); } } int currentKeyboardMode = mKeyboardSwitcher.getKeyboardMode(); reloadKeyboards(); mKeyboardSwitcher.makeKeyboards(true); mKeyboardSwitcher.setKeyboardMode(currentKeyboardMode, 0, mEnableVoiceButton && mEnableVoice); initSuggest(mLanguageSwitcher.getInputLanguage()); mLanguageSwitcher.persist(); updateShiftKeyState(getCurrentInputEditorInfo()); } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (PREF_SELECTED_LANGUAGES.equals(key)) { mLanguageSwitcher.loadLocales(sharedPreferences); mRefreshKeyboardRequired = true; } } public void swipeLeft() { } public void swipeDown() { handleClose(); } public void swipeUp() { //launchSettings(); } public void onPress(int primaryCode) { vibrate(); playKeyClick(primaryCode); } public void onRelease(int primaryCode) { // Reset any drag flags in the keyboard ((LatinKeyboard) mInputView.getKeyboard()).keyReleased(); //vibrate(); } private FieldContext makeFieldContext() { return new FieldContext( getCurrentInputConnection(), getCurrentInputEditorInfo(), mLanguageSwitcher.getInputLanguage(), mLanguageSwitcher.getEnabledLanguages()); } private boolean fieldCanDoVoice(FieldContext fieldContext) { return !mPasswordText && mVoiceInput != null && !mVoiceInput.isBlacklistedField(fieldContext); } private boolean fieldIsRecommendedForVoice(FieldContext fieldContext) { // TODO: Move this logic into the VoiceInput method. return !mPasswordText && !mEmailText && mVoiceInput.isRecommendedField(fieldContext); } private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) { return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext) && !(attribute != null && attribute.privateImeOptions != null && attribute.privateImeOptions.equals(IME_OPTION_NO_MICROPHONE)) && RecognitionManager.isRecognitionAvailable(this); } // receive ringer mode changes to detect silent mode private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { updateRingerMode(); } }; // update flags for silent mode private void updateRingerMode() { if (mAudioManager == null) { mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); } if (mAudioManager != null) { mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); } } private boolean userHasNotTypedRecently() { return (SystemClock.uptimeMillis() - mLastKeyTime) > MIN_MILLIS_AFTER_TYPING_BEFORE_SWIPE; } /* * Only trigger a swipe action if the user hasn't typed X millis before * now, and if they don't type Y millis after the swipe is detected. This * delays the onset of the swipe action by Y millis. */ private void conservativelyTriggerSwipeAction(final Runnable action) { if (userHasNotTypedRecently()) { mSwipeTriggerTimeMillis = System.currentTimeMillis(); mHandler.sendMessageDelayed( mHandler.obtainMessage(MSG_START_LISTENING_AFTER_SWIPE), MIN_MILLIS_AFTER_SWIPE_TO_WAIT_FOR_TYPING); } } private void playKeyClick(int primaryCode) { // if mAudioManager is null, we don't have the ringer state yet // mAudioManager will be set by updateRingerMode if (mAudioManager == null) { if (mInputView != null) { updateRingerMode(); } } if (mSoundOn && !mSilentMode) { // FIXME: Volume and enable should come from UI settings // FIXME: These should be triggered after auto-repeat logic int sound = AudioManager.FX_KEYPRESS_STANDARD; switch (primaryCode) { case Keyboard.KEYCODE_DELETE: sound = AudioManager.FX_KEYPRESS_DELETE; break; case KEYCODE_ENTER: sound = AudioManager.FX_KEYPRESS_RETURN; break; case KEYCODE_SPACE: sound = AudioManager.FX_KEYPRESS_SPACEBAR; break; } mAudioManager.playSoundEffect(sound, FX_VOLUME); } } private void vibrate() { if (!mVibrateOn) { return; } if (mInputView != null) { mInputView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } private void checkTutorial(String privateImeOptions) { if (privateImeOptions == null) return; if (privateImeOptions.equals("com.android.setupwizard:ShowTutorial")) { if (mTutorial == null) startTutorial(); } else if (privateImeOptions.equals("com.android.setupwizard:HideTutorial")) { if (mTutorial != null) { if (mTutorial.close()) { mTutorial = null; } } } } private void startTutorial() { mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TUTORIAL), 500); } void tutorialDone() { mTutorial = null; } void promoteToUserDictionary(String word, int frequency) { if (mUserDictionary.isValidWord(word)) return; mUserDictionary.addWord(word, frequency); } private void updateCorrectionMode() { mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false; mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes) && !mInputTypeNoAutoCorrect && mHasDictionary; mCorrectionMode = mAutoCorrectOn ? Suggest.CORRECTION_FULL : (mQuickFixes ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); if (mSuggest != null) { mSuggest.setCorrectionMode(mCorrectionMode); } } protected void launchSettings() { launchSettings(LatinIMESettings.class); } protected void launchSettings(Class settingsClass) { handleClose(); Intent intent = new Intent(); intent.setClass(LatinIME.this, settingsClass); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } private void loadSettings() { // Get the settings preferences SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, false); mSoundOn = sp.getBoolean(PREF_SOUND_ON, false); mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true); mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false); mHasUsedVoiceInputUnsupportedLocale = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false); // Get the current list of supported locales and check the current locale against that // list. We cache this value so as not to check it every time the user starts a voice // input. Because this method is called by onStartInputView, this should mean that as // long as the locale doesn't change while the user is keeping the IME open, the // value should never be stale. String supportedLocalesString = SettingsUtil.getSettingsString( getContentResolver(), SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); ArrayList voiceInputSupportedLocales = newArrayList(supportedLocalesString.split("\\s+")); mLocaleSupportedForVoiceInput = voiceInputSupportedLocales.contains(mLocale); // If there is no auto text data, then quickfix is forced to "on", so that the other options // will continue to work if (AutoText.getSize(mInputView) < 1) mQuickFixes = true; mShowSuggestions = sp.getBoolean(PREF_SHOW_SUGGESTIONS, true) & mQuickFixes; if (VOICE_INSTALLED) { final String voiceMode = sp.getString(PREF_VOICE_MODE, ""); boolean enableVoice = !voiceMode.equals(getString(R.string.voice_mode_off)); boolean voiceOnPrimary = voiceMode.equals(getString(R.string.voice_mode_main)); if (mKeyboardSwitcher != null && (enableVoice != mEnableVoice || voiceOnPrimary != mVoiceOnPrimary)) { mKeyboardSwitcher.setVoiceMode(enableVoice, voiceOnPrimary); } mEnableVoice = enableVoice; mVoiceOnPrimary = voiceOnPrimary; } mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE, mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions; updateCorrectionMode(); mLanguageSwitcher.loadLocales(sp); } private void initSuggestPuncList() { mSuggestPuncList = new ArrayList(); String suggestPuncs = mResources.getString(R.string.suggested_punctuations); if (suggestPuncs != null) { for (int i = 0; i < suggestPuncs.length(); i++) { mSuggestPuncList.add(suggestPuncs.subSequence(i, i + 1)); } } } private void showOptionsMenu() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); builder.setIcon(R.drawable.ic_dialog_keyboard); builder.setNegativeButton(android.R.string.cancel, null); CharSequence itemSettings = getString(R.string.english_ime_settings); CharSequence itemInputMethod = getString(R.string.inputMethod); builder.setItems(new CharSequence[] { itemSettings, itemInputMethod}, new DialogInterface.OnClickListener() { public void onClick(DialogInterface di, int position) { di.dismiss(); switch (position) { case POS_SETTINGS: launchSettings(); break; case POS_METHOD: ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)) .showInputMethodPicker(); break; } } }); builder.setTitle(mResources.getString(R.string.english_ime_name)); mOptionsDialog = builder.create(); Window window = mOptionsDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); lp.token = mInputView.getWindowToken(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); mOptionsDialog.show(); } private void changeKeyboardMode() { mKeyboardSwitcher.toggleSymbols(); if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) { ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); } updateShiftKeyState(getCurrentInputEditorInfo()); } public static ArrayList newArrayList(E... elements) { int capacity = (elements.length * 110) / 100 + 5; ArrayList list = new ArrayList(capacity); Collections.addAll(list, elements); return list; } @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { super.dump(fd, fout, args); final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode()); p.println(" mCapsLock=" + mCapsLock); p.println(" mComposing=" + mComposing.toString()); p.println(" mPredictionOn=" + mPredictionOn); p.println(" mCorrectionMode=" + mCorrectionMode); p.println(" mPredicting=" + mPredicting); p.println(" mAutoCorrectOn=" + mAutoCorrectOn); p.println(" mAutoSpace=" + mAutoSpace); p.println(" mCompletionOn=" + mCompletionOn); p.println(" TextEntryState.state=" + TextEntryState.getState()); p.println(" mSoundOn=" + mSoundOn); p.println(" mVibrateOn=" + mVibrateOn); } // Characters per second measurement private static final boolean PERF_DEBUG = false; private long mLastCpsTime; private static final int CPS_BUFFER_SIZE = 16; private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE]; private int mCpsIndex; private boolean mInputTypeNoAutoCorrect; private void measureCps() { if (!LatinIME.PERF_DEBUG) return; long now = System.currentTimeMillis(); if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial mCpsIntervals[mCpsIndex] = now - mLastCpsTime; mLastCpsTime = now; mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE; long total = 0; for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i]; System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total)); } class AutoDictionary extends ExpandableDictionary { // If the user touches a typed word 2 times or more, it will become valid. private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED; // If the user touches a typed word 5 times or more, it will be added to the user dict. private static final int PROMOTION_THRESHOLD = 4 * FREQUENCY_FOR_PICKED; public AutoDictionary(Context context) { super(context); } @Override public boolean isValidWord(CharSequence word) { final int frequency = getWordFrequency(word); return frequency >= VALIDITY_THRESHOLD; } @Override public void addWord(String word, int addFrequency) { final int length = word.length(); // Don't add very short or very long words. if (length < 2 || length > getMaxWordLength()) return; if (mWord.isAutoCapitalized()) { // Remove caps before adding word = Character.toLowerCase(word.charAt(0)) + word.substring(1); } int freq = getWordFrequency(word); freq = freq < 0 ? addFrequency : freq + addFrequency; super.addWord(word, freq); if (freq >= PROMOTION_THRESHOLD) { LatinIME.this.promoteToUserDictionary(word, FREQUENCY_FOR_AUTO_ADD); } } } }