/* * Copyright (c) 2013 Menny Even-Danan * * 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.anysoftkeyboard; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.net.Uri; import android.os.Handler; import android.os.IBinder; import android.os.SystemClock; import android.os.Vibrator; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.TypedValue; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewParent; import android.view.Window; import android.view.WindowManager; import android.view.animation.AnimationUtils; 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 android.widget.ImageView; import android.widget.SimpleAdapter; import android.widget.TextView; import android.widget.Toast; import com.anysoftkeyboard.LayoutSwitchAnimationListener.AnimationType; import com.anysoftkeyboard.api.KeyCodes; import com.anysoftkeyboard.devicespecific.Clipboard; import com.anysoftkeyboard.dictionaries.DictionaryAddOnAndBuilder; import com.anysoftkeyboard.dictionaries.EditableDictionary; import com.anysoftkeyboard.dictionaries.ExternalDictionaryFactory; import com.anysoftkeyboard.dictionaries.Suggest; import com.anysoftkeyboard.dictionaries.TextEntryState; import com.anysoftkeyboard.dictionaries.TextEntryState.State; import com.anysoftkeyboard.dictionaries.sqlite.AutoDictionary; import com.anysoftkeyboard.keyboards.AnyKeyboard; import com.anysoftkeyboard.keyboards.AnyKeyboard.AnyKey; import com.anysoftkeyboard.keyboards.AnyKeyboard.HardKeyboardTranslator; import com.anysoftkeyboard.keyboards.CondenseType; import com.anysoftkeyboard.keyboards.GenericKeyboard; import com.anysoftkeyboard.keyboards.Keyboard.Key; import com.anysoftkeyboard.keyboards.KeyboardAddOnAndBuilder; import com.anysoftkeyboard.keyboards.KeyboardSwitcher; import com.anysoftkeyboard.keyboards.KeyboardSwitcher.NextKeyboardType; import com.anysoftkeyboard.keyboards.physical.HardKeyboardActionImpl; import com.anysoftkeyboard.keyboards.physical.MyMetaKeyKeyListener; import com.anysoftkeyboard.keyboards.views.AnyKeyboardView; import com.anysoftkeyboard.keyboards.views.CandidateView; import com.anysoftkeyboard.keyboards.views.OnKeyboardActionListener; import com.anysoftkeyboard.quicktextkeys.QuickTextKey; import com.anysoftkeyboard.quicktextkeys.QuickTextKeyFactory; import com.anysoftkeyboard.receivers.PackagesChangedReceiver; import com.anysoftkeyboard.receivers.SoundPreferencesChangedReceiver; import com.anysoftkeyboard.receivers.SoundPreferencesChangedReceiver.SoundPreferencesChangedListener; import com.anysoftkeyboard.theme.KeyboardTheme; import com.anysoftkeyboard.theme.KeyboardThemeFactory; import com.anysoftkeyboard.ui.dev.DeveloperUtils; import com.anysoftkeyboard.ui.settings.MainSettingsActivity; import com.anysoftkeyboard.ui.tutorials.TipLayoutsSupport; import com.anysoftkeyboard.ui.tutorials.TutorialsProvider; import com.anysoftkeyboard.utils.IMEUtil.GCUtils; import com.anysoftkeyboard.utils.IMEUtil.GCUtils.MemRelatedOperation; import com.anysoftkeyboard.utils.Log; import com.anysoftkeyboard.utils.ModifierKeyState; import com.anysoftkeyboard.utils.Workarounds; import com.anysoftkeyboard.voice.VoiceInput; import com.menny.android.anysoftkeyboard.AnyApplication; import com.menny.android.anysoftkeyboard.BuildConfig; import com.menny.android.anysoftkeyboard.FeaturesSet; import com.menny.android.anysoftkeyboard.R; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; /** * Input method implementation for Qwerty'ish keyboard. */ public class AnySoftKeyboard extends InputMethodService implements OnKeyboardActionListener, OnSharedPreferenceChangeListener, AnyKeyboardContextProvider, SoundPreferencesChangedListener { private final static String TAG = "ASK"; private SharedPreferences mPrefs; private final com.anysoftkeyboard.Configuration mConfig; private final ModifierKeyState mShiftKeyState = new ModifierKeyState(); private final ModifierKeyState mControlKeyState = new ModifierKeyState(); private LayoutSwitchAnimationListener mSwitchAnimator; private boolean mDistinctMultiTouch = true; private AnyKeyboardView mInputView; private View mCandidatesParent; private CandidateView mCandidateView; // private View mRestartSuggestionsView; private static final long MINIMUM_REFRESH_TIME_FOR_DICTIONARIES = 30 * 1000; private long mLastDictionaryRefresh = -1; private int mMinimumWordCorrectionLength = 2; private Suggest mSuggest; private CompletionInfo[] mCompletions; private AlertDialog mOptionsDialog; private AlertDialog mQuickTextKeyDialog; KeyboardSwitcher mKeyboardSwitcher; private final HardKeyboardActionImpl mHardKeyboardAction = new HardKeyboardActionImpl(); private long mMetaState; private HashSet<Character> mSentenceSeparators = new HashSet<Character>(); // private BTreeDictionary mContactsDictionary; private EditableDictionary mUserDictionary; private AutoDictionary mAutoDictionary; private WordComposer mWord = new WordComposer(); private int mOrientation = Configuration.ORIENTATION_PORTRAIT; private int mCommittedLength; /* * Do we do prediction now */ private boolean mPredicting; /* * is prediction needed for the current input connection */ private boolean mPredictionOn; /* * is out-side completions needed */ private boolean mCompletionOn; private boolean mAutoSpace; private boolean mAutoCorrectOn = false; private boolean mAllowSuggestionsRestart = true; private boolean mCurrentlyAllowSuggestionRestart = true; private boolean mJustAutoAddedWord = false; /* * This will help us detect multi-tap on the SHIFT key for caps-lock */ private long mShiftStartTime = 0; private boolean mCapsLock; private static final String SMILEY_PLUGIN_ID = "0077b34d-770f-4083-83e4-081957e06c27"; private boolean mSmileyOnShortPress; private String mOverrideQuickTextText = null; private boolean mAutoCap; private boolean mQuickFixes; /* * Configuration flag. Should we support dictionary suggestions */ private boolean mShowSuggestions = false; private boolean mAutoComplete; // private int mCorrectionMode; private String mKeyboardChangeNotificationType; private static final String KEYBOARD_NOTIFICATION_ALWAYS = "1"; private static final String KEYBOARD_NOTIFICATION_ON_PHYSICAL = "2"; private static final String KEYBOARD_NOTIFICATION_NEVER = "3"; /* * This will help us find out if UNDO_COMMIT is still possible to be done */ private int mUndoCommitCursorPosition = -2; private AudioManager mAudioManager; private boolean mSilentMode; private boolean mSoundOn; // between 0..100. This is the custom volume private int mSoundVolume; private Vibrator mVibrator; private int mVibrationDuration; private CondenseType mKeyboardInCondensedMode = CondenseType.None; private final Handler mHandler = new KeyboardUIStateHanlder(this); private boolean mJustAddedAutoSpace; private CharSequence mJustAddOnText = null; private boolean mLastCharacterWasShifted = false; protected IBinder mImeToken = null; private InputMethodManager mInputMethodManager; private final boolean mConnectbotTabHack = true; private VoiceInput mVoiceRecognitionTrigger; public AnySoftKeyboard() { mConfig = AnyApplication.getConfig(); } @Override public AbstractInputMethodImpl onCreateInputMethodInterface() { return new InputMethodImpl() { @Override public void attachToken(IBinder token) { super.attachToken(token); mImeToken = token; } }; } @Override public void onCreate() { super.onCreate(); mPrefs = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); if (DeveloperUtils.hasTracingRequested(getApplicationContext())) { try { DeveloperUtils.startTracing(); Toast.makeText(getApplicationContext(), R.string.debug_tracing_starting, Toast.LENGTH_SHORT).show(); } catch (Exception e) { //see issue https://github.com/AnySoftKeyboard/AnySoftKeyboard/issues/105 //I might get a "Permission denied" error. e.printStackTrace(); Toast.makeText(getApplicationContext(), R.string.debug_tracing_starting_failed, Toast.LENGTH_LONG).show(); } } Log.i(TAG, "****** AnySoftKeyboard v%s (%d) service started.", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE); if (!BuildConfig.DEBUG && BuildConfig.VERSION_NAME.endsWith("-SNAPSHOT")) throw new RuntimeException("You can not run a 'RELEASE' build with a SNAPSHOT postfix!"); // I'm handling animations. No need for any nifty ROMs assistance. // I can't use this function with my own animations, since the // WindowManager can // only use system resources. /* * Not right now... performance of my animations is lousy.. * getWindow().getWindow().setWindowAnimations(0); */ mInputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); updateRingerMode(); // register to receive ringer mode changes for silent mode registerReceiver(mSoundPreferencesChangedReceiver, mSoundPreferencesChangedReceiver.createFilterToRegisterOn()); // register to receive packages changes registerReceiver(mPackagesChangedReceiver, mPackagesChangedReceiver.createFilterToRegisterOn()); mVibrator = ((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)); loadSettings(); mConfig.addChangedListener(this); mKeyboardSwitcher = new KeyboardSwitcher(this); mOrientation = getResources().getConfiguration().orientation; mSentenceSeparators = getCurrentKeyboard().getSentenceSeparators(); if (mSuggest == null) { initSuggest(/* getResources().getConfiguration().locale.toString() */); } if (mKeyboardChangeNotificationType .equals(KEYBOARD_NOTIFICATION_ALWAYS)) { notifyKeyboardChangeIfNeeded(); } mVoiceRecognitionTrigger = AnyApplication.getFrankenRobot().embody( new VoiceInput.VoiceInputDiagram(this)); mSwitchAnimator = new LayoutSwitchAnimationListener(this); } private void initSuggest() { mSuggest = new Suggest(this); mSuggest.setCorrectionMode(mQuickFixes, mShowSuggestions); mSuggest.setMinimumWordLengthForCorrection(mMinimumWordCorrectionLength); setDictionariesForCurrentKeyboard(); } @Override public void onDestroy() { Log.i(TAG, "AnySoftKeyboard has been destroyed! Cleaning resources.."); mSwitchAnimator.onDestory(); mConfig.removeChangedListener(this); unregisterReceiver(mSoundPreferencesChangedReceiver); unregisterReceiver(mPackagesChangedReceiver); mInputMethodManager.hideStatusIcon(mImeToken); if (mInputView != null) mInputView.onViewNotRequired(); mInputView = null; mKeyboardSwitcher.setInputView(null); mSuggest.setAutoDictionary(null); mSuggest.setContactsDictionary(getApplicationContext(), false); mSuggest.setMainDictionary(getApplicationContext(), null); mSuggest.setUserDictionary(null); if (DeveloperUtils.hasTracingStarted()) { DeveloperUtils.stopTracing(); Toast.makeText( getApplicationContext(), getString(R.string.debug_tracing_finished, DeveloperUtils.getTraceFile()), Toast.LENGTH_SHORT) .show(); } super.onDestroy(); } @Override public void onFinishInputView(boolean finishingInput) { super.onFinishInputView(finishingInput); if (!mKeyboardChangeNotificationType .equals(KEYBOARD_NOTIFICATION_ALWAYS)) { mInputMethodManager.hideStatusIcon(mImeToken); } // Remove pending messages related to update suggestions abortCorrection(true, false); } AnyKeyboardView getInputView() { return mInputView; } @Override public View onCreateInputView() { if (mInputView != null) mInputView.onViewNotRequired(); mInputView = null; GCUtils.getInstance().peformOperationWithMemRetry(TAG, new MemRelatedOperation() { public void operation() { mInputView = (AnyKeyboardView) getLayoutInflater() .inflate(R.layout.main_keyboard_layout, null); } }, true); // reseting token users mOptionsDialog = null; mQuickTextKeyDialog = null; mKeyboardSwitcher.setInputView(mInputView); mInputView.setOnKeyboardActionListener(this); mDistinctMultiTouch = mInputView.hasDistinctMultitouch(); return mInputView; } @Override public void setInputView(View view) { super.setInputView(view); ViewParent parent = view.getParent(); if (parent instanceof View) { // this is required for animations, so the background will be // consist. ((View) parent).setBackgroundResource(R.drawable.ask_wallpaper); } else { Log.w(TAG, "*** It seams that the InputView parent is not a View!! This is very strange."); } } @Override public View onCreateCandidatesView() { mKeyboardSwitcher.makeKeyboards(false); final ViewGroup candidateViewContainer = (ViewGroup) getLayoutInflater() .inflate(R.layout.candidates, null); mCandidatesParent = null; mCandidateView = (CandidateView) candidateViewContainer .findViewById(R.id.candidates); mCandidateView.setService(this); setCandidatesViewShown(false); final KeyboardTheme theme = KeyboardThemeFactory .getCurrentKeyboardTheme(getApplicationContext()); final TypedArray a = theme.getPackageContext().obtainStyledAttributes( null, R.styleable.AnyKeyboardViewTheme, 0, theme.getThemeResId()); int closeTextColor = getResources().getColor(R.color.candidate_other); float fontSizePixel = getResources().getDimensionPixelSize( R.dimen.candidate_font_height); try { closeTextColor = a.getColor( R.styleable.AnyKeyboardViewTheme_suggestionOthersTextColor, closeTextColor); fontSizePixel = a.getDimension( R.styleable.AnyKeyboardViewTheme_suggestionTextSize, fontSizePixel); } catch (Exception e) { e.printStackTrace(); } a.recycle(); mCandidateCloseText = (TextView) candidateViewContainer .findViewById(R.id.close_suggestions_strip_text); View closeIcon = candidateViewContainer .findViewById(R.id.close_suggestions_strip_icon); if (mCandidateCloseText != null && closeIcon != null) {// why? In API3 // it is not // supported closeIcon.setOnClickListener(new OnClickListener() { // two seconds is enough. private final static long DOUBLE_TAP_TIMEOUT = 2 * 1000; public void onClick(View v) { mHandler.removeMessages(KeyboardUIStateHanlder.MSG_REMOVE_CLOSE_SUGGESTIONS_HINT); mCandidateCloseText.setVisibility(View.VISIBLE); mCandidateCloseText.startAnimation(AnimationUtils.loadAnimation(getApplicationContext(), R.anim.close_candidates_hint_in)); mHandler.sendMessageDelayed(mHandler.obtainMessage(KeyboardUIStateHanlder.MSG_REMOVE_CLOSE_SUGGESTIONS_HINT), DOUBLE_TAP_TIMEOUT - 50); } }); mCandidateCloseText.setTextColor(closeTextColor); mCandidateCloseText.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePixel); mCandidateCloseText.setOnClickListener(new OnClickListener() { public void onClick(View v) { mHandler.removeMessages(KeyboardUIStateHanlder.MSG_REMOVE_CLOSE_SUGGESTIONS_HINT); mCandidateCloseText.setVisibility(View.GONE); abortCorrection(true, true); } }); } final TextView tipsNotification = (TextView) candidateViewContainer .findViewById(R.id.tips_notification_on_candidates); if (tipsNotification != null) {// why? in API 3 it is not supported if (mConfig.getShowTipsNotification() && TutorialsProvider.shouldShowTips(getApplicationContext())) { final String TIPS_NOTIFICATION_KEY = "TIPS_NOTIFICATION_KEY"; TipLayoutsSupport.addTipToCandidate(getApplicationContext(), tipsNotification, TIPS_NOTIFICATION_KEY, new OnClickListener() { @Override public void onClick(View v) { TutorialsProvider.showTips(getApplicationContext()); } }); } } /* * At some point I wanted the user to click a View to restart the * suggestions. I don't any more. mRestartSuggestionsView = * candidateViewContainer.findViewById(R.id.restart_suggestions); if * (mRestartSuggestionsView != null) { * mRestartSuggestionsView.setOnClickListener(new OnClickListener() { * public void onClick(View v) { v.setVisibility(View.GONE); * InputConnection ic = getCurrentInputConnection(); * performRestartWordSuggestion(ic, getCursorPosition(ic)); } }); } */ return candidateViewContainer; } @Override public void onStartInput(EditorInfo attribute, boolean restarting) { Log.d(TAG, "onStartInput(EditorInfo:" + attribute.imeOptions + "," + attribute.inputType + ", restarting:" + restarting + ")"); super.onStartInput(attribute, restarting); abortCorrection(true, false); if (!restarting) { TextEntryState.newSession(this); // Clear shift states. mMetaState = 0; mCurrentlyAllowSuggestionRestart = mAllowSuggestionsRestart; } else { // something very fishy happening here... // this is the only way I can get around it. // it seems that when a onStartInput is called with restarting == // true // suggestions restart fails :( // see Browser when editing multiline textbox mCurrentlyAllowSuggestionRestart = false; } } @Override public void onStartInputView(final EditorInfo attribute, final boolean restarting) { Log.d(TAG, "onStartInputView(EditorInfo:" + attribute.imeOptions + "," + attribute.inputType + ", restarting:" + restarting + ")"); super.onStartInputView(attribute, restarting); if (mVoiceRecognitionTrigger != null) { mVoiceRecognitionTrigger.onStartInputView(); } if (mInputView == null) { return; } mInputView.setKeyboardActionType(attribute.imeOptions); mKeyboardSwitcher.makeKeyboards(false); mPredictionOn = false; mCompletionOn = false; mCompletions = null; mCapsLock = false; switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) { case EditorInfo.TYPE_CLASS_DATETIME: Log.d(TAG, "Setting MODE_DATETIME as keyboard due to a TYPE_CLASS_DATETIME input."); mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_DATETIME, attribute, restarting); break; case EditorInfo.TYPE_CLASS_NUMBER: Log.d(TAG, "Setting MODE_NUMBERS as keyboard due to a TYPE_CLASS_NUMBER input."); mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_NUMBERS, attribute, restarting); break; case EditorInfo.TYPE_CLASS_PHONE: Log.d(TAG, "Setting MODE_PHONE as keyboard due to a TYPE_CLASS_PHONE input."); mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_PHONE, attribute, restarting); break; case EditorInfo.TYPE_CLASS_TEXT: Log.d(TAG, "A TYPE_CLASS_TEXT input."); final int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION; switch (variation) { case EditorInfo.TYPE_TEXT_VARIATION_PASSWORD: case EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: case 0xe0:// API 11 EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD: Log.d(TAG, "A password TYPE_CLASS_TEXT input with no prediction"); mPredictionOn = false; break; default: mPredictionOn = true; } if (mConfig.getInsertSpaceAfterCandidatePick()) { switch (variation) { case EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: case EditorInfo.TYPE_TEXT_VARIATION_URI: case 0xd0:// API 11 // EditorInfo.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: mAutoSpace = false; break; default: mAutoSpace = true; } } else { // some users don't want auto-space mAutoSpace = false; } switch (variation) { case EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: case 0xd0:// API 11 // EditorInfo.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: Log.d(TAG, "Setting MODE_EMAIL as keyboard due to a TYPE_TEXT_VARIATION_EMAIL_ADDRESS input."); mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_EMAIL, attribute, restarting); mPredictionOn = false; break; case EditorInfo.TYPE_TEXT_VARIATION_URI: Log.d(TAG, "Setting MODE_URL as keyboard due to a TYPE_TEXT_VARIATION_URI input."); mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_URL, attribute, restarting); mPredictionOn = false; break; case EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE: Log.d(TAG, "Setting MODE_IM as keyboard due to a TYPE_TEXT_VARIATION_SHORT_MESSAGE input."); mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_IM, attribute, restarting); break; default: Log.d(TAG, "Setting MODE_TEXT as keyboard due to a default input."); mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, attribute, restarting); } final int textFlag = attribute.inputType & EditorInfo.TYPE_MASK_FLAGS; switch (textFlag) { case 0x00080000:// FROM API // 5:EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS: case EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE: Log.d(TAG, "Input requested NO_SUGGESTIONS, or it is AUTO_COMPLETE by itself."); mPredictionOn = false; break; default: // we'll keep the previous mPredictionOn value } break; default: Log.d(TAG, "Setting MODE_TEXT as keyboard due to a default input."); // No class. Probably a console window, or no GUI input // connection mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, attribute, restarting); mPredictionOn = false; mAutoSpace = true; } mPredicting = false; // mDeleteCount = 0; mJustAddedAutoSpace = false; setCandidatesViewShown(false); // loadSettings(); updateShiftKeyState(attribute); if (mSuggest != null) { mSuggest.setCorrectionMode(mQuickFixes, mShowSuggestions); } mPredictionOn = mPredictionOn && (mShowSuggestions/* || mQuickFixes */); if (mCandidateView != null) mCandidateView.setSuggestions(null, false, false, false); if (mPredictionOn) { if ((SystemClock.elapsedRealtime() - mLastDictionaryRefresh) > MINIMUM_REFRESH_TIME_FOR_DICTIONARIES) setDictionariesForCurrentKeyboard(); } else { // this will release memory setDictionariesForCurrentKeyboard(); } } @Override public void hideWindow() { if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } if (mQuickTextKeyDialog != null && mQuickTextKeyDialog.isShowing()) { mQuickTextKeyDialog.dismiss(); mQuickTextKeyDialog = null; } super.hideWindow(); TextEntryState.endSession(); } @Override public void onFinishInput() { Log.d(TAG, "onFinishInput()"); super.onFinishInput(); if (mInputView != null) { mInputView.closing(); } if (!mKeyboardChangeNotificationType .equals(KEYBOARD_NOTIFICATION_ALWAYS)) { mInputMethodManager.hideStatusIcon(mImeToken); } } /* * this function is called EVERYTIME them selection is changed. This also * includes the underlined suggestions. */ @Override public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); Log.d(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd + ", nss=" + newSelStart + ", nse=" + newSelEnd + ", cs=" + candidatesStart + ", ce=" + candidatesEnd); mWord.setGlobalCursorPosition(newSelEnd); if (!isPredictionOn()/* || mInputView == null || !mInputView.isShown() */) return;// not relevant if no prediction is needed. final InputConnection ic = getCurrentInputConnection(); if (ic == null) return;// well, I can't do anything without this connection Log.d(TAG, "onUpdateSelection: ok, let's see what can be done"); if (newSelStart != newSelEnd) { // text selection. can't predict in this mode Log.d(TAG, "onUpdateSelection: text selection."); abortCorrection(true, false); } else { // we have the following options (we are in an input which requires // predicting (mPredictionOn == true): // 1) predicting and moved inside the word // 2) predicting and moved outside the word // 2.1) to a new word // 2.2) to no word land // 3) not predicting // 3.1) to a new word // 3.2) to no word land // so, 1 and 2 requires that predicting is currently done, and the // cursor moved if (mPredicting) { if (newSelStart >= candidatesStart && newSelStart <= candidatesEnd) { // 1) predicting and moved inside the word - just update the // cursor position and shift state // inside the currently selected word int cursorPosition = newSelEnd - candidatesStart; if (mWord.setCursorPostion(cursorPosition)) { Log.d(TAG, "onUpdateSelection: cursor moving inside the predicting word"); } } else { Log.d(TAG, "onUpdateSelection: cursor moving outside the currently predicting word"); abortCorrection(true, false); // ask user whether to restart postRestartWordSuggestion(); // there has been a cursor movement. Maybe a shift state is // required too? postUpdateShiftKeyState(); } } else { Log.d(TAG, "onUpdateSelection: not predicting at this moment, maybe the cursor is now at a new word?"); if (TextEntryState.getState() == State.ACCEPTED_DEFAULT) { if (mUndoCommitCursorPosition == oldSelStart && mUndoCommitCursorPosition != newSelStart) { Log.d(TAG, "onUpdateSelection: I am in ACCEPTED_DEFAULT state, but the user moved the cursor, so it is not possible to undo_commit now."); abortCorrection(true, false); } else if (mUndoCommitCursorPosition == -2) { Log.d(TAG, "onUpdateSelection: I am in ACCEPTED_DEFAULT state, time to store the position - I can only undo-commit from here."); mUndoCommitCursorPosition = newSelStart; } } postRestartWordSuggestion(); // there has been a cursor movement. Maybe a shift state is // required too? postUpdateShiftKeyState(); } } } private void postRestartWordSuggestion() { mHandler.removeMessages(KeyboardUIStateHanlder.MSG_RESTART_NEW_WORD_SUGGESTIONS); /* * if (mRestartSuggestionsView != null) * mRestartSuggestionsView.setVisibility(View.GONE); */ mHandler.sendMessageDelayed( mHandler.obtainMessage(KeyboardUIStateHanlder.MSG_RESTART_NEW_WORD_SUGGESTIONS), 500); } private static int getCursorPosition(InputConnection connection) { if (connection == null) return 0; ExtractedText extracted = connection.getExtractedText( new ExtractedTextRequest(), 0); if (extracted == null) return 0; return extracted.startOffset + extracted.selectionStart; } private boolean canRestartWordSuggestion(final InputConnection ic) { if (mPredicting || !isPredictionOn() || !mAllowSuggestionsRestart || !mCurrentlyAllowSuggestionRestart || mInputView == null || !mInputView.isShown()) { // why? // mPredicting - if I'm predicting a word, I can not restart it.. // right? I'm inside that word! // isPredictionOn() - this is obvious. // mAllowSuggestionsRestart - config settings // mCurrentlyAllowSuggestionRestart - workaround for // onInputStart(restarting == true) // mInputView == null - obvious, no? Log.d(TAG, "performRestartWordSuggestion: no need to restart: mPredicting=%s, isPredictionOn=%s, mAllowSuggestionsRestart=%s, mCurrentlyAllowSuggestionRestart=%s" , mPredicting, isPredictionOn(), mAllowSuggestionsRestart, mCurrentlyAllowSuggestionRestart); return false; } else if (!isCursorTouchingWord()) { Log.d(TAG, "User moved cursor to no-man land. Bye bye."); return false; } return true; } public void performRestartWordSuggestion(final InputConnection ic) { // I assume ASK DOES NOT predict at this moment! // 2) predicting and moved outside the word - abort predicting, update // shift state // 2.1) to a new word - restart predicting on the new word // 2.2) to no word land - nothing else // this means that the new cursor position is outside the candidates // underline // this can be either because the cursor is really outside the // previously underlined (suggested) // or nothing was suggested. // in this case, we would like to reset the prediction and restart // if the user clicked inside a different word // restart required? if (canRestartWordSuggestion(ic)) {// 2.1 ic.beginBatchEdit();// don't want any events till I finish handling // this touch Log.d(TAG, "User moved cursor to a word. Should I restart predition?"); abortCorrection(true, false); // locating the word CharSequence toLeft = ""; CharSequence toRight = ""; while (true) { Log.d(TAG, "Checking left offset " + toLeft.length() + ". Currently have '" + toLeft + "'"); CharSequence newToLeft = ic.getTextBeforeCursor( toLeft.length() + 1, 0); if (TextUtils.isEmpty(newToLeft) || isWordSeparator(newToLeft.charAt(0)) || newToLeft.length() == toLeft.length()) { break; } toLeft = newToLeft; } while (true) { Log.d(TAG, "Checking right offset " + toRight.length() + ". Currently have '" + toRight + "'"); CharSequence newToRight = ic.getTextAfterCursor( toRight.length() + 1, 0); if (TextUtils.isEmpty(newToRight) || isWordSeparator(newToRight.charAt(newToRight .length() - 1)) || newToRight.length() == toRight.length()) { break; } toRight = newToRight; } CharSequence word = toLeft.toString() + toRight.toString(); Log.d(TAG, "Starting new prediction on word '" + word + "'."); mPredicting = word.length() > 0; mUndoCommitCursorPosition = -2;// so it will be marked the next time mWord.reset(); final int[] alekNearByKeys = new int[1]; for (int index = 0; index < word.length(); index++) { final char c = word.charAt(index); if (index == 0) mWord.setFirstCharCapitalized(Character.isUpperCase(c)); alekNearByKeys[0] = c; mWord.add(c, alekNearByKeys); TextEntryState.typedCharacter((char) c, false); } ic.deleteSurroundingText(toLeft.length(), toRight.length()); ic.setComposingText(word, 1); // repositioning the cursor if (toRight.length() > 0) { final int cursorPosition = getCursorPosition(ic) - toRight.length(); Log.d(TAG, "Repositioning the cursor inside the word to position " + cursorPosition); ic.setSelection(cursorPosition, cursorPosition); } mWord.setCursorPostion(toLeft.length()); ic.endBatchEdit(); postUpdateSuggestions(); } else { Log.d(TAG, "performRestartWordSuggestion canRestartWordSuggestion == false"); } } private void onPhysicalKeyboardKeyPressed() { if (mConfig.hideSoftKeyboardWhenPhysicalKeyPressed()) hideWindow(); // For all other keys, if we want to do transformations on // text being entered with a hard keyboard, we need to process // it and do the appropriate action. // using physical keyboard is more annoying with candidate view in // the way // so we disable it. // to clear the underline. abortCorrection(true, false); } @Override public void onDisplayCompletions(CompletionInfo[] completions) { if (FeaturesSet.DEBUG_LOG) { Log.d(TAG, "Received completions:"); for (int i = 0; i < (completions != null ? completions.length : 0); i++) { Log.d(TAG, " #" + i + ": " + completions[i]); } } // completions should be shown if dictionary requires, or if we are in // full-screen and have outside completions if (mCompletionOn || (isFullscreenMode() && (completions != null))) { Log.v(TAG, "Received completions: completion should be shown: " + mCompletionOn + " fullscreen:" + isFullscreenMode()); mCompletions = completions; // we do completions :) mCompletionOn = true; if (completions == null) { Log.v(TAG, "Received completions: completion is NULL. Clearing suggestions."); mCandidateView.setSuggestions(null, false, false, false); return; } List<CharSequence> stringList = new ArrayList<CharSequence>(); for (int i = 0; i < (completions != null ? completions.length : 0); i++) { CompletionInfo ci = completions[i]; if (ci != null) stringList.add(ci.getText()); } Log.v(TAG, "Received completions: setting to suggestions view " + stringList.size() + " completions."); // CharSequence typedWord = mWord.getTypedWord(); setSuggestions(stringList, true, true, true); mWord.setPreferredWord(null); // I mean, if I'm here, it must be shown... setCandidatesViewShown(true); } else { Log.v(TAG, "Received completions: completions should not be shown."); } } @Override public void setCandidatesViewShown(boolean shown) { // we show predication only in on-screen keyboard // (onEvaluateInputViewShown) // or if the physical keyboard supports candidates // (mPredictionLandscape) final boolean shouldShow = shouldCandidatesStripBeShown() && shown; final boolean currentlyShown = mCandidatesParent != null && mCandidatesParent.getVisibility() == View.VISIBLE; super.setCandidatesViewShown(shouldShow); if (shouldShow != currentlyShown) { // I believe (can't confirm it) that candidates animation is kinda // rare, // and it is better to load it on demand, then to keep it in memory // always.. if (shouldShow) { mCandidatesParent.setAnimation(AnimationUtils.loadAnimation( getApplicationContext(), R.anim.candidates_bottom_to_up_enter)); } else { mCandidatesParent.setAnimation(AnimationUtils.loadAnimation( getApplicationContext(), R.anim.candidates_up_to_bottom_exit)); } } } @Override public void setCandidatesView(View view) { super.setCandidatesView(view); mCandidatesParent = view.getParent() instanceof View ? (View) view .getParent() : null; } private void clearSuggestions() { setSuggestions(null, false, false, false); } private void setSuggestions(List<CharSequence> suggestions, boolean completions, boolean typedWordValid, boolean haveMinimalSuggestion) { if (mCandidateView != null) { mCandidateView.setSuggestions(suggestions, completions, typedWordValid, haveMinimalSuggestion); } } @Override public void onComputeInsets(InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); if (!isFullscreenMode()) { outInsets.contentTopInsets = outInsets.visibleTopInsets; } } @Override public boolean onEvaluateFullscreenMode() { switch (mOrientation) { case Configuration.ORIENTATION_LANDSCAPE: return mConfig.getUseFullScreenInputInLandscape(); default: return mConfig.getUseFullScreenInputInPortrait(); } } @Override public boolean onKeyDown(final int keyCode, KeyEvent event) { final boolean shouldTranslateSpecialKeys = isInputViewShown(); Log.d(TAG, "isInputViewShown=" + shouldTranslateSpecialKeys); if (event.isPrintingKey()) onPhysicalKeyboardKeyPressed(); mHardKeyboardAction.initializeAction(event, mMetaState); InputConnection ic = getCurrentInputConnection(); Log.d(TAG, "Event: Key:" + event.getKeyCode() + " Shift:" + ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) + " ALT:" + ((event.getMetaState() & KeyEvent.META_ALT_ON) != 0) + " Repeats:" + event.getRepeatCount()); switch (keyCode) { /**** * SPEACIAL translated HW keys If you add new keys here, do not forget * to add to the */ case KeyEvent.KEYCODE_CAMERA: if (shouldTranslateSpecialKeys && mConfig.useCameraKeyForBackspaceBackword()) { handleBackword(getCurrentInputConnection()); return true; } // DO NOT DELAY CAMERA KEY with unneeded checks in default mark return super.onKeyDown(keyCode, event); case KeyEvent.KEYCODE_FOCUS: if (shouldTranslateSpecialKeys && mConfig.useCameraKeyForBackspaceBackword()) { handleDeleteLastCharacter(false); return true; } // DO NOT DELAY FOCUS KEY with unneeded checks in default mark return super.onKeyDown(keyCode, event); case KeyEvent.KEYCODE_VOLUME_UP: if (shouldTranslateSpecialKeys && mConfig.useVolumeKeyForLeftRight()) { sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_LEFT); return true; } // DO NOT DELAY VOLUME UP KEY with unneeded checks in default // mark return super.onKeyDown(keyCode, event); case KeyEvent.KEYCODE_VOLUME_DOWN: if (shouldTranslateSpecialKeys && mConfig.useVolumeKeyForLeftRight()) { sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_RIGHT); return true; } // DO NOT DELAY VOLUME DOWN KEY with unneeded checks in default // mark return super.onKeyDown(keyCode, event); /**** * END of SPEACIAL translated HW keys code section */ case KeyEvent.KEYCODE_BACK: if (event.getRepeatCount() == 0 && mInputView != null) { if (mInputView.handleBack()) { // consuming the meta keys if (ic != null) { ic.clearMetaKeyStates(Integer.MAX_VALUE);// translated, // so we // also take // care of // the // metakeys. } mMetaState = 0; return true; } /* * else if (mTutorial != null) { mTutorial.close(); mTutorial = * null; } */ } break; case 0x000000cc:// API 14: KeyEvent.KEYCODE_LANGUAGE_SWITCH switchToNextPhysicalKeyboard(ic); return true; case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_RIGHT: if (event.isAltPressed() && Workarounds.isAltSpaceLangSwitchNotPossible()) { switchToNextPhysicalKeyboard(ic); return true; } // NOTE: letting it fallthru to the other meta-keys case KeyEvent.KEYCODE_ALT_LEFT: case KeyEvent.KEYCODE_ALT_RIGHT: case KeyEvent.KEYCODE_SYM: Log.d(TAG + "-meta-key", getMetaKeysStates("onKeyDown before handle")); mMetaState = MyMetaKeyKeyListener.handleKeyDown(mMetaState, keyCode, event); Log.d(TAG + "-meta-key", getMetaKeysStates("onKeyDown after handle")); break; case KeyEvent.KEYCODE_SPACE: if ((event.isAltPressed() && !Workarounds .isAltSpaceLangSwitchNotPossible()) || event.isShiftPressed()) { switchToNextPhysicalKeyboard(ic); return true; } // NOTE: // letting it fall through to the "default" default: // Fix issue 185, check if we should process key repeat if (!mConfig.getUseRepeatingKeys() && event.getRepeatCount() > 0) return true; if (mKeyboardSwitcher.isCurrentKeyboardPhysical()) { // sometimes, the physical keyboard will delete input, and // then // add some. // we'll try to make it nice if (ic != null) ic.beginBatchEdit(); try { // issue 393, backword on the hw keyboard! if (mConfig.useBackword() && keyCode == KeyEvent.KEYCODE_DEL && event.isShiftPressed()) { handleBackword(ic); return true; } else/* if (event.isPrintingKey()) */ { // http://article.gmane.org/gmane.comp.handhelds.openmoko.android-freerunner/629 AnyKeyboard current = mKeyboardSwitcher .getCurrentKeyboard(); HardKeyboardTranslator keyTranslator = (HardKeyboardTranslator) current; if (BuildConfig.DEBUG) { final String keyboardName = current .getKeyboardName(); Log.d(TAG, "Asking '" + keyboardName + "' to translate key: " + keyCode); Log.v(TAG, "Hard Keyboard Action before translation: Shift: " + mHardKeyboardAction .isShiftActive() + ", Alt: " + mHardKeyboardAction.isAltActive() + ", Key code: " + mHardKeyboardAction.getKeyCode() + ", changed: " + mHardKeyboardAction .getKeyCodeWasChanged()); } keyTranslator.translatePhysicalCharacter( mHardKeyboardAction, this); Log.v(TAG, "Hard Keyboard Action after translation: Key code: " + mHardKeyboardAction.getKeyCode() + ", changed: " + mHardKeyboardAction .getKeyCodeWasChanged()); if (mHardKeyboardAction.getKeyCodeWasChanged()) { final int translatedChar = mHardKeyboardAction .getKeyCode(); // typing my own. onKey(translatedChar, null, -1, new int[]{translatedChar}, true/* * simualting * fromUI */); // my handling // we are at a regular key press, so we'll // update // our meta-state member mMetaState = MyMetaKeyKeyListener .adjustMetaAfterKeypress(mMetaState); Log.d(TAG + "-meta-key", getMetaKeysStates("onKeyDown after adjust - translated")); return true; } } } finally { if (ic != null) ic.endBatchEdit(); } } if (event.isPrintingKey()) { // we are at a regular key press, so we'll update our // meta-state // member mMetaState = MyMetaKeyKeyListener .adjustMetaAfterKeypress(mMetaState); Log.d(TAG + "-meta-key", getMetaKeysStates("onKeyDown after adjust")); } } return super.onKeyDown(keyCode, event); } private void switchToNextPhysicalKeyboard(InputConnection ic) { // consuming the meta keys if (ic != null) { ic.clearMetaKeyStates(Integer.MAX_VALUE);// translated, so // we also take // care of the // metakeys. } mMetaState = 0; // only physical keyboard nextKeyboard(getCurrentInputEditorInfo(), NextKeyboardType.AlphabetSupportsPhysical); } private void notifyKeyboardChangeIfNeeded() { // Log.d("anySoftKeyboard","notifyKeyboardChangeIfNeeded"); // Thread.dumpStack(); if (mKeyboardSwitcher == null)// happens on first onCreate. return; if ((mKeyboardSwitcher.isAlphabetMode()) && !mKeyboardChangeNotificationType .equals(KEYBOARD_NOTIFICATION_NEVER)) { mInputMethodManager.showStatusIcon(mImeToken, getCurrentKeyboard() .getKeyboardContext().getPackageName(), getCurrentKeyboard().getKeyboardIconResId()); } } public AnyKeyboard getCurrentKeyboard() { return mKeyboardSwitcher.getCurrentKeyboard(); } public KeyboardSwitcher getKeyboardSwitcher() { return mKeyboardSwitcher; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { // Issue 248 case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_UP: if (isInputViewShown() == false) { return super.onKeyUp(keyCode, event); } if (mConfig.useVolumeKeyForLeftRight()) { // no need of vol up/down sound updateShiftKeyState(getCurrentInputEditorInfo()); return true; } case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: 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); updateShiftKeyState(getCurrentInputEditorInfo()); return true; } break; case KeyEvent.KEYCODE_ALT_LEFT: case KeyEvent.KEYCODE_ALT_RIGHT: case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_RIGHT: case KeyEvent.KEYCODE_SYM: mMetaState = MyMetaKeyKeyListener.handleKeyUp(mMetaState, keyCode, event); Log.d("AnySoftKeyboard-meta-key", getMetaKeysStates("onKeyUp")); setInputConnectionMetaStateAsCurrentMetaKeyKeyListenerState(); break; } boolean r = super.onKeyUp(keyCode, event); updateShiftKeyState(getCurrentInputEditorInfo()); return r; } private String getMetaKeysStates(String place) { final int shiftState = MyMetaKeyKeyListener.getMetaState(mMetaState, MyMetaKeyKeyListener.META_SHIFT_ON); final int altState = MyMetaKeyKeyListener.getMetaState(mMetaState, MyMetaKeyKeyListener.META_ALT_ON); final int symState = MyMetaKeyKeyListener.getMetaState(mMetaState, MyMetaKeyKeyListener.META_SYM_ON); return "Meta keys state at " + place + "- SHIFT:" + shiftState + ", ALT:" + altState + " SYM:" + symState + " bits:" + MyMetaKeyKeyListener.getMetaState(mMetaState) + " state:" + mMetaState; } private void setInputConnectionMetaStateAsCurrentMetaKeyKeyListenerState() { InputConnection ic = getCurrentInputConnection(); if (ic != null) { int clearStatesFlags = 0; if (MyMetaKeyKeyListener.getMetaState(mMetaState, MyMetaKeyKeyListener.META_ALT_ON) == 0) clearStatesFlags += KeyEvent.META_ALT_ON; if (MyMetaKeyKeyListener.getMetaState(mMetaState, MyMetaKeyKeyListener.META_SHIFT_ON) == 0) clearStatesFlags += KeyEvent.META_SHIFT_ON; if (MyMetaKeyKeyListener.getMetaState(mMetaState, MyMetaKeyKeyListener.META_SYM_ON) == 0) clearStatesFlags += KeyEvent.META_SYM_ON; Log.d("AnySoftKeyboard-meta-key", getMetaKeysStates("setInputConnectionMetaStateAsCurrentMetaKeyKeyListenerState with flags: " + clearStatesFlags)); ic.clearMetaKeyStates(clearStatesFlags); } } private boolean addToDictionaries(WordComposer suggestion, AutoDictionary.AdditionType type) { boolean added = checkAddToDictionary(suggestion, type); if (added) { Log.i(TAG, "Word '" + suggestion + "' was added to the auto-dictionary."); } return added; } /** * Adds to the UserBigramDictionary and/or AutoDictionary * */ private boolean checkAddToDictionary(WordComposer suggestion, AutoDictionary.AdditionType type/* * , boolean addToBigramDictionary */) { if (suggestion == null || suggestion.length() < 1) return false; // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be // adding words in situations where the user or application really // didn't // want corrections enabled or learned. if (!mQuickFixes && !mShowSuggestions) return false; if (suggestion != null && mAutoDictionary != null) { String suggestionToCheck = suggestion.getTypedWord().toString(); if (/* * !addToBigramDictionary && * mAutoDictionary.isValidWord(suggestionToCheck)//this check is * for promoting from Auto to User || */(!mSuggest.isValidWord(suggestionToCheck))) { final boolean added = mAutoDictionary.addWord(suggestion, type, this); if (added && mCandidateView != null) { mCandidateView.notifyAboutWordAdded(suggestion.getTypedWord()); } return added; } /* * if (mUserBigramDictionary != null) { CharSequence prevWord = * EditingUtil.getPreviousWord(getCurrentInputConnection(), * mSentenceSeparators); if (!TextUtils.isEmpty(prevWord)) { * mUserBigramDictionary.addBigrams(prevWord.toString(), * suggestion.toString()); } } */ } return false; } private void commitTyped(InputConnection inputConnection) { if (mPredicting) { mPredicting = false; if (mWord.length() > 0) { if (inputConnection != null) { inputConnection.commitText( mWord.getTypedWord(), 1); } mCommittedLength = mWord.length();// mComposing.length(); TextEntryState .acceptedTyped(mWord.getTypedWord()); addToDictionaries(mWord, AutoDictionary.AdditionType.Typed); } if (mHandler.hasMessages(KeyboardUIStateHanlder.MSG_UPDATE_SUGGESTIONS)) { postUpdateSuggestions(-1); } } } private void postUpdateShiftKeyState() { mHandler.removeMessages(KeyboardUIStateHanlder.MSG_UPDATE_SHIFT_STATE); mHandler.sendMessageDelayed( mHandler.obtainMessage(KeyboardUIStateHanlder.MSG_UPDATE_SHIFT_STATE), 150); } public void updateShiftKeyState(EditorInfo attr) { mHandler.removeMessages(KeyboardUIStateHanlder.MSG_UPDATE_SHIFT_STATE); InputConnection ic = getCurrentInputConnection(); if (ic != null && attr != null && mKeyboardSwitcher.isAlphabetMode() && (mInputView != null)) { final boolean inputSaysCaps = getCursorCapsMode(ic, attr) != 0; if (inputSaysCaps) mShiftStartTime = SystemClock.elapsedRealtime(); mInputView.setShifted(mShiftKeyState.isMomentary() || mCapsLock || inputSaysCaps); } } 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; if (!mConfig.shouldswapPunctuationAndSpace()) return; CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); if (BuildConfig.DEBUG) { String seps = ""; for (Character c : mSentenceSeparators) seps += c; Log.d(TAG, "swapPunctuationAndSpace: lastTwo: '" + lastTwo + "', mSentenceSeparators " + mSentenceSeparators.size() + " '" + seps + "'"); } if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == KeyCodes.SPACE && mSentenceSeparators.contains(lastTwo.charAt(1))) { ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(lastTwo.charAt(1) + " ", 1); ic.endBatchEdit(); updateShiftKeyState(getCurrentInputEditorInfo()); mJustAddedAutoSpace = true; Log.d(TAG, "swapPunctuationAndSpace: YES"); } } private void reswapPeriodAndSpace() { final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; CharSequence lastThree = ic.getTextBeforeCursor(3, 0); if (lastThree != null && lastThree.length() == 3 && lastThree.charAt(0) == '.' && lastThree.charAt(1) == KeyCodes.SPACE && lastThree.charAt(2) == '.') { ic.beginBatchEdit(); ic.deleteSurroundingText(3, 0); ic.commitText(".. ", 1); ic.endBatchEdit(); updateShiftKeyState(getCurrentInputEditorInfo()); } } private void doubleSpace() { // if (!mAutoPunctuate) return; if (!mConfig.isDoubleSpaceChangesToPeriod()) 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) == KeyCodes.SPACE && lastThree.charAt(2) == KeyCodes.SPACE) { ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(". ", 1); ic.endBatchEdit(); updateShiftKeyState(getCurrentInputEditorInfo()); mJustAddedAutoSpace = true; } } 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) == KeyCodes.SPACE) { ic.deleteSurroundingText(1, 0); } } public boolean addWordToDictionary(String word) { if (mUserDictionary != null) { boolean added = mUserDictionary.addWord(word, 128); if (added && mCandidateView != null) mCandidateView.notifyAboutWordAdded(word); return added; } else { return false; } } public void removeFromUserDictionary(String word) { if (mUserDictionary != null) { mUserDictionary.deleteWord(word); abortCorrection(true, false); if (mCandidateView != null) mCandidateView.notifyAboutRemovedWord(word); } } /** * Helper to determine if a given character code is alphabetic. */ private boolean isAlphabet(int code) { // inner letters have more options: ' in English. " in Hebrew, and more. if (mPredicting) return getCurrentKeyboard().isInnerWordLetter((char) code); else return getCurrentKeyboard().isStartOfWordLetter((char) code); } public void onMultiTapStarted() { final InputConnection ic = getCurrentInputConnection(); if (ic != null) ic.beginBatchEdit(); handleDeleteLastCharacter(true); if (mInputView != null) mInputView.setShifted(mLastCharacterWasShifted); } public void onMultiTapEndeded() { final InputConnection ic = getCurrentInputConnection(); if (ic != null) ic.endBatchEdit(); } public void onKey(int primaryCode, Key key, int multiTapIndex, int[] nearByKeyCodes, boolean fromUI) { Log.d(TAG, "onKey " + primaryCode); // Thread.dumpStack(); final InputConnection ic = getCurrentInputConnection(); switch (primaryCode) { case KeyCodes.DELETE_WORD: if (ic == null)// if we don't want to do anything, lets check // null first. break; handleBackword(ic); break; case KeyCodes.DELETE: if (ic == null)// if we don't want to do anything, lets check // null first. break; // we do backword if the shift is pressed while pressing // backspace (like in a PC) // but this is true ONLY if the device has multitouch, or the // user specifically asked for it if (mInputView != null && mInputView.isShifted() && !mInputView.getKeyboard().isShiftLocked() && ((mDistinctMultiTouch && mShiftKeyState.isMomentary()) || mConfig .useBackword())) { handleBackword(ic); } else { handleDeleteLastCharacter(false); } break; case KeyCodes.CLEAR_INPUT: if (ic != null) { ic.beginBatchEdit(); commitTyped(ic); ic.deleteSurroundingText(Integer.MAX_VALUE, Integer.MAX_VALUE); ic.endBatchEdit(); } break; case KeyCodes.SHIFT: if ((!mDistinctMultiTouch) || !fromUI) handleShift(false); break; case KeyCodes.CTRL: if ((!mDistinctMultiTouch) || !fromUI) handleControl(false); break; case KeyCodes.ARROW_LEFT: sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_LEFT); break; case KeyCodes.ARROW_RIGHT: sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_RIGHT); break; case KeyCodes.ARROW_UP: sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_UP); break; case KeyCodes.ARROW_DOWN: sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_DOWN); break; case KeyCodes.MOVE_HOME: if (Workarounds.getApiLevel() >= 11) { sendDownUpKeyEvents(0x0000007a/* * API 11: * KeyEvent.KEYCODE_MOVE_HOME */); } else { if (ic != null) { CharSequence textBefore = ic.getTextBeforeCursor(1024, 0); if (!TextUtils.isEmpty(textBefore)) { int newPosition = textBefore.length() - 1; while (newPosition > 0) { char chatAt = textBefore.charAt(newPosition - 1); if (chatAt == '\n' || chatAt == '\r') { break; } newPosition--; } if (newPosition < 0) newPosition = 0; ic.setSelection(newPosition, newPosition); } } } break; case KeyCodes.MOVE_END: if (Workarounds.getApiLevel() >= 11) { //API 11: KeyEvent.KEYCODE_MOVE_END sendDownUpKeyEvents(0x0000007b); } else { if (ic != null) { CharSequence textAfter = ic.getTextAfterCursor(1024, 0); if (!TextUtils.isEmpty(textAfter)) { int newPosition = 1; while (newPosition < textAfter.length()) { char chatAt = textAfter.charAt(newPosition); if (chatAt == '\n' || chatAt == '\r') { break; } newPosition++; } if (newPosition > textAfter.length()) newPosition = textAfter.length(); try { CharSequence textBefore = ic.getTextBeforeCursor(Integer.MAX_VALUE, 0); if (!TextUtils.isEmpty(textBefore)) { newPosition = newPosition + textBefore.length(); } ic.setSelection(newPosition, newPosition); } catch (Throwable e/*I'm using Integer.MAX_VALUE, it's scary.*/) { Log.w(TAG, "Failed to getTextBeforeCursor.", e); } } } } break; case KeyCodes.VOICE_INPUT: if (mVoiceRecognitionTrigger != null) mVoiceRecognitionTrigger .startVoiceRecognition(getCurrentKeyboard() .getDefaultDictionaryLocale()); break; case KeyCodes.CANCEL: if (mOptionsDialog == null || !mOptionsDialog.isShowing()) { handleClose(); } break; case KeyCodes.SETTINGS: showOptionsMenu(); break; case KeyCodes.SPLIT_LAYOUT: case KeyCodes.MERGE_LAYOUT: case KeyCodes.COMPACT_LAYOUT_TO_RIGHT: case KeyCodes.COMPACT_LAYOUT_TO_LEFT: if (getCurrentKeyboard() != null && mInputView != null) { mKeyboardInCondensedMode = CondenseType.fromKeyCode(primaryCode); AnyKeyboard currentKeyboard = getCurrentKeyboard(); setKeyboardStuffBeforeSetToView(currentKeyboard); mInputView.setKeyboard(currentKeyboard); } break; case KeyCodes.DOMAIN: onText(mConfig.getDomainText()); break; case KeyCodes.QUICK_TEXT: QuickTextKey quickTextKey = QuickTextKeyFactory .getCurrentQuickTextKey(this); if (mSmileyOnShortPress) { if (TextUtils.isEmpty(mOverrideQuickTextText)) onText(quickTextKey.getKeyOutputText()); else onText(mOverrideQuickTextText); } else { if (quickTextKey.isPopupKeyboardUsed()) { showQuickTextKeyPopupKeyboard(quickTextKey); } else { showQuickTextKeyPopupList(quickTextKey); } } break; case KeyCodes.QUICK_TEXT_POPUP: quickTextKey = QuickTextKeyFactory.getCurrentQuickTextKey(this); if (quickTextKey.getId().equals(SMILEY_PLUGIN_ID) && !mSmileyOnShortPress) { if (TextUtils.isEmpty(mOverrideQuickTextText)) onText(quickTextKey.getKeyOutputText()); else onText(mOverrideQuickTextText); } else { if (quickTextKey.isPopupKeyboardUsed()) { showQuickTextKeyPopupKeyboard(quickTextKey); } else { showQuickTextKeyPopupList(quickTextKey); } } break; case KeyCodes.MODE_SYMOBLS: nextKeyboard(getCurrentInputEditorInfo(), NextKeyboardType.Symbols); break; case KeyCodes.MODE_ALPHABET: if (mKeyboardSwitcher.shouldPopupForLanguageSwitch()) { showLanguageSelectionDialog(); } else nextKeyboard(getCurrentInputEditorInfo(), NextKeyboardType.Alphabet); break; case KeyCodes.UTILITY_KEYBOARD: mInputView.openUtilityKeyboard(); break; case KeyCodes.MODE_ALPHABET_POPUP: showLanguageSelectionDialog(); break; case KeyCodes.ALT: nextAlterKeyboard(getCurrentInputEditorInfo()); break; case KeyCodes.KEYBOARD_CYCLE: nextKeyboard(getCurrentInputEditorInfo(), NextKeyboardType.Any); break; case KeyCodes.KEYBOARD_REVERSE_CYCLE: nextKeyboard(getCurrentInputEditorInfo(), NextKeyboardType.PreviousAny); break; case KeyCodes.KEYBOARD_CYCLE_INSIDE_MODE: nextKeyboard(getCurrentInputEditorInfo(), NextKeyboardType.AnyInsideMode); break; case KeyCodes.KEYBOARD_MODE_CHANGE: nextKeyboard(getCurrentInputEditorInfo(), NextKeyboardType.OtherMode); break; case KeyCodes.CLIPBOARD: Clipboard cp = AnyApplication.getFrankenRobot().embody( new Clipboard.ClipboardDiagram(getApplicationContext())); CharSequence clipboardText = cp.getText(); if (!TextUtils.isEmpty(clipboardText)) { onText(clipboardText); } break; case KeyCodes.TAB: sendTab(); break; case KeyCodes.ESCAPE: sendEscape(); break; default: // Issue 146: Right to left langs require reversed parenthesis if (mKeyboardSwitcher.isRightToLeftMode()) { if (primaryCode == (int) ')') primaryCode = (int) '('; else if (primaryCode == (int) '(') primaryCode = (int) ')'; } if (isWordSeparator(primaryCode)) { handleSeparator(primaryCode); } else { if (mInputView != null && mInputView.isControl() && primaryCode >= 32 && primaryCode < 127) { // http://en.wikipedia.org/wiki/Control_character#How_control_characters_map_to_keyboards int controlCode = primaryCode & 31; Log.d(TAG, "CONTROL state: Char was " + primaryCode + " and now it is " + controlCode); if (controlCode == 9) { sendTab(); } else { ic.commitText(Character.toString((char) controlCode), 1); } } else { handleCharacter(primaryCode, key, multiTapIndex, nearByKeyCodes); } // reseting the mSpaceSent, which is set to true upon // selecting // candidate mJustAddedAutoSpace = false; } // Cancel the just reverted state // mJustRevertedSeparator = null; if (mKeyboardSwitcher.isKeyCodeRequireSwitchingToAlphabet(primaryCode)) { mKeyboardSwitcher.nextKeyboard(getCurrentInputEditorInfo(), NextKeyboardType.Alphabet); } break; } } private boolean isConnectbot() { EditorInfo ei = getCurrentInputEditorInfo(); String pkg = ei.packageName; return ((pkg.equalsIgnoreCase("org.connectbot") || pkg.equalsIgnoreCase("org.woltage.irssiconnectbot") || pkg .equalsIgnoreCase("com.pslib.connectbot")) && ei.inputType == 0); // FIXME } private void sendTab() { InputConnection ic = getCurrentInputConnection(); if (ic == null) return; boolean tabHack = isConnectbot() && mConnectbotTabHack; // FIXME: tab and ^I don't work in connectbot, hackish workaround if (tabHack) { ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER)); ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER)); ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_I)); ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_I)); } else { ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB)); ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_TAB)); } } private void sendEscape() { InputConnection ic = getCurrentInputConnection(); if (ic == null) return; if (isConnectbot()) { sendKeyChar((char) 27); } else { ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, 111 /* KEYCODE_ESCAPE */)); ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, 111 /* KEYCODE_ESCAPE */)); } } public void setKeyboardStuffBeforeSetToView(AnyKeyboard currentKeyboard) { currentKeyboard.setCondensedKeys(mKeyboardInCondensedMode); } private void showLanguageSelectionDialog() { KeyboardAddOnAndBuilder[] builders = mKeyboardSwitcher .getEnabledKeyboardsBuilders(); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); builder.setIcon(R.drawable.ic_launcher); builder.setTitle(getResources().getString( R.string.select_keyboard_popup_title)); builder.setNegativeButton(android.R.string.cancel, null); ArrayList<CharSequence> keyboardsIds = new ArrayList<CharSequence>(); ArrayList<CharSequence> keyboards = new ArrayList<CharSequence>(); // going over all enabled keyboards for (KeyboardAddOnAndBuilder keyboardBuilder : builders) { keyboardsIds.add(keyboardBuilder.getId()); String name = keyboardBuilder.getName(); keyboards.add(name); } final CharSequence[] ids = new CharSequence[keyboardsIds.size()]; final CharSequence[] items = new CharSequence[keyboards.size()]; keyboardsIds.toArray(ids); keyboards.toArray(items); builder.setItems(items, new DialogInterface.OnClickListener() { public void onClick(DialogInterface di, int position) { di.dismiss(); if ((position < 0) || (position >= items.length)) { Log.d(TAG, "Keyboard selection popup canceled"); } else { CharSequence id = ids[position]; Log.d(TAG, "User selected " + items[position] + " with id " + id); EditorInfo currentEditorInfo = getCurrentInputEditorInfo(); AnyKeyboard currentKeyboard = mKeyboardSwitcher .nextAlphabetKeyboard(currentEditorInfo, id.toString()); setKeyboardFinalStuff(currentEditorInfo, NextKeyboardType.Alphabet, currentKeyboard); } } }); 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(); } public void onText(CharSequence text) { Log.d(TAG, "onText: '" + text + "'"); InputConnection ic = getCurrentInputConnection(); if (ic == null) return; ic.beginBatchEdit(); if (mPredicting) { commitTyped(ic); } abortCorrection(true, false); ic.commitText(text, 1); ic.endBatchEdit(); updateShiftKeyState(getCurrentInputEditorInfo()); // mJustRevertedSeparator = null; mJustAddedAutoSpace = false; mJustAddOnText = text; } private boolean performOnTextDeletion(InputConnection ic) { if (mJustAddOnText != null && ic != null) { final CharSequence onTextText = mJustAddOnText; mJustAddOnText = null; //just now, the user had cause onText to add text to input. //but after that, immediately pressed delete. So I'm guessing deleting the entire text is needed final int onTextLength = onTextText.length(); Log.d(TAG, "Deleting the entire 'onText' input "+onTextText); CharSequence cs = ic.getTextBeforeCursor(onTextLength, 0); if (onTextText.equals(cs)) { ic.deleteSurroundingText(onTextLength, 0); postUpdateShiftKeyState(); return true; } } return false; } private static boolean isBackwordStopChar(int c) { return !Character.isLetter(c);// c == 32 || // PUNCTUATION_CHARACTERS.contains(c); } private void handleBackword(InputConnection ic) { if (ic == null) { return; } if (performOnTextDeletion(ic)) return; if (mPredicting) { mWord.reset(); mPredicting = false; ic.setComposingText("", 1); postUpdateSuggestions(); postUpdateShiftKeyState(); return; } // I will not delete more than 128 characters. Just a safe-guard. // this will also allow me do just one call to getTextBeforeCursor! // Which is alway good. This is a part of issue 951. CharSequence cs = ic.getTextBeforeCursor(128, 0); if (TextUtils.isEmpty(cs)) { return;// nothing to delete } // TWO OPTIONS // 1) Either we do like Linux and Windows (and probably ALL desktop // OSes): // Delete all the characters till a complete word was deleted: /* * What to do: We delete until we find a separator (the function * isBackwordStopChar). Note that we MUST delete a delete a whole word! * So if the backword starts at separators, we'll delete those, and then * the word before: "test this, ," -> "test " */ // Pro: same as desktop // Con: when auto-caps is on (the default), this will delete the // previous word, which can be annoying.. // E.g., Writing a sentence, then a period, then ASK will auto-caps, // then when the user press backspace (for some reason), // the entire previous word deletes. // 2) Or we delete all the characters till we encounter a separator, but // delete at least one character. /* * What to do: We delete until we find a separator (the function * isBackwordStopChar). Note that we MUST delete a delete at least one * character "test this, " -> "test this," -> "test this" -> "test " */ // Pro: Supports auto-caps, and mostly similar to desktop OSes // Con: Not all desktop use-cases are here. // For now, I go with option 2, but I'm open for discussion. // 2b) "test this, " -> "test this" final int inputLength = cs.length(); int idx = inputLength - 1;// it's OK since we checked whether cs is // empty after retrieving it. while (idx > 0 && !isBackwordStopChar((int) cs.charAt(idx))) { idx--; } /* * while (true) { cs = ic.getTextBeforeCursor(idx, 0); //issue 951 if * (TextUtils.isEmpty(cs)) {//it seems that it is possible that * getTextBeforeCursor will return NULL return;//nothing to * delete//issue 951 } csl = cs.length(); if (csl < idx) { // read text * is smaller than requested. We are at start break; } ++idx; int cc = * cs.charAt(0); boolean isBackwordStopChar = isBackwordStopChar(cc); if * (stopCharAtTheEnd) { if (!isBackwordStopChar){ --csl; break; } * continue; } if (isBackwordStopChar) { --csl; break; } } */ // we want to delete at least one character // ic.deleteSurroundingText(csl == 0 ? 1 : csl, 0); ic.deleteSurroundingText(inputLength - idx, 0);// it is always > 0 ! postUpdateShiftKeyState(); } private void handleDeleteLastCharacter(boolean forMultitap) { InputConnection ic = getCurrentInputConnection(); if (!forMultitap && performOnTextDeletion(ic)) return; boolean deleteChar = false; if (mPredicting) { final boolean wordManipulation = mWord.length() > 0 && mWord.cursorPosition() > 0;// mComposing.length(); if (wordManipulation) { mWord.deleteLast(); final int cursorPosition; if (mWord.cursorPosition() != mWord.length()) cursorPosition = getCursorPosition(ic); else cursorPosition = -1; if (cursorPosition >= 0) ic.beginBatchEdit(); ic.setComposingText(mWord.getTypedWord(), 1); if (mWord.length() == 0) { mPredicting = false; } else if (cursorPosition >= 0) { ic.setSelection(cursorPosition - 1, cursorPosition - 1); } if (cursorPosition >= 0) ic.endBatchEdit(); postUpdateSuggestions(); } else { ic.deleteSurroundingText(1, 0); } } else { deleteChar = true; } TextEntryState.backspace(); if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) { revertLastWord(deleteChar); return; } else if (deleteChar) { if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { // Go back to the suggestion mode if the user canceled the // "Touch again to save". // NOTE: In gerenal, we don't revert the word when backspacing // from a manual suggestion pick. We deliberately chose a // different behavior only in the case of picking the first // suggestion (typed word). It's intentional to have made this // inconsistent with backspacing after selecting other // suggestions. revertLastWord(deleteChar); } else { if (!forMultitap) { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); } else { // this code tries to delete the text in a different way, // because of multi-tap stuff // using "deleteSurroundingText" will actually get the input // updated faster! // but will not handle "delete all selected text" feature, // hence the "if (!forMultitap)" above final CharSequence beforeText = ic == null? null : ic.getTextBeforeCursor(1, 0); final int textLengthBeforeDelete = (TextUtils.isEmpty(beforeText)) ? 0 : beforeText.length(); if (textLengthBeforeDelete > 0) ic.deleteSurroundingText(1, 0); else sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); } } } // mJustRevertedSeparator = null; // handleShiftStateAfterBackspace(); } /* * private void handleShiftStateAfterBackspace() { * switch(mLastCharacterShiftState) { //this code will help use in the case * that //a double/triple tap occur while first one was shifted case * LAST_CHAR_SHIFT_STATE_SHIFTED: if (mInputView != null) * mInputView.setShifted(true); mLastCharacterShiftState = * LAST_CHAR_SHIFT_STATE_DEFAULT; break; // case * LAST_CHAR_SHIFT_STATE_UNSHIFTED: // if (mInputView != null) // * mInputView.setShifted(false); // mLastCharacterShiftState = * LAST_CHAR_SHIFT_STATE_DEFAULT; // break; default: * updateShiftKeyState(getCurrentInputEditorInfo()); break; } } */ private void handleControl(boolean reset) { if (mInputView == null) return; if (reset) { mInputView.setControl(false); } else { mInputView.setControl(!mInputView.isControl()); } } private void handleShift(boolean reset) { // user is above anything automatic. mHandler.removeMessages(KeyboardUIStateHanlder.MSG_UPDATE_SHIFT_STATE); if (mInputView != null && mKeyboardSwitcher.isAlphabetMode()) { // shift pressed and this is an alphabet keyboard // we want to do: // 1)if keyboard is unshifted -> shift view and keyboard // 2)if keyboard is shifted -> capslock keyboard // 3)if keyboard is capslocked -> unshift view and keyboard // final AnyKeyboard currentKeyboard = // mKeyboardSwitcher.getCurrentKeyboard(); final boolean caps; if (reset) { mInputView.setShifted(false); caps = false; } else { if (!mInputView.isShifted()) { mShiftStartTime = SystemClock.elapsedRealtime(); mInputView.setShifted(true); caps = false; } else { if (mInputView.isShiftLocked()) { mInputView.setShifted(false); caps = false; } else { // if this is a quick tap, then move to caps locks, else // back to unshifted. if ((SystemClock.elapsedRealtime() - mShiftStartTime) < mConfig .getMultiTapTimeout()) { Log.d(TAG, "handleShift: current keyboard is shifted, within multi-tap period."); mInputView.setShifted(true); caps = true; } else { Log.d(TAG, "handleShift: current keyboard is shifted, not within multi-tap period."); mInputView.setShifted(false); caps = false; } } } } mCapsLock = caps; mInputView.setShiftLocked(mCapsLock); } } private void abortCorrection(boolean force, boolean forever) { if (force || TextEntryState.isCorrecting()) { Log.d(TAG, "abortCorrection will actually abort correct"); mHandler.removeMessages(KeyboardUIStateHanlder.MSG_UPDATE_SUGGESTIONS); mHandler.removeMessages(KeyboardUIStateHanlder.MSG_RESTART_NEW_WORD_SUGGESTIONS); final InputConnection ic = getCurrentInputConnection(); if (ic != null) ic.finishComposingText(); clearSuggestions(); TextEntryState.reset(); mUndoCommitCursorPosition = -2; mWord.reset(); mPredicting = false; mJustAddedAutoSpace = false; if (forever) { Log.d(TAG, "abortCorrection will abort correct forever"); mPredictionOn = false; setCandidatesViewShown(false); if (mSuggest != null) { mSuggest.setCorrectionMode(false, false); } } } } private void handleCharacter(final int primaryCode, Key key, int multiTapIndex, int[] nearByKeyCodes) { Log.d(TAG, "handleCharacter: " + primaryCode + ", isPredictionOn:" + isPredictionOn() + ", mPredicting:" + mPredicting); if (!mPredicting && isPredictionOn() && isAlphabet(primaryCode) && !isCursorTouchingWord()) { mPredicting = true; mUndoCommitCursorPosition = -2;// so it will be marked the next time mWord.reset(); } mLastCharacterWasShifted = (mInputView != null) && mInputView.isShifted(); // if (mLastSelectionStart == mLastSelectionEnd && // TextEntryState.isCorrecting()) { // abortCorrection(false); // } final int primaryCodeForShow; if (mInputView != null) { if (mInputView.isShifted()) { if (key != null && key instanceof AnyKey) { AnyKey anyKey = (AnyKey) key; int[] shiftCodes = anyKey.shiftedCodes; primaryCodeForShow = shiftCodes != null && shiftCodes.length > multiTapIndex ? shiftCodes[multiTapIndex] : Character.toUpperCase(primaryCode); } else { primaryCodeForShow = Character.toUpperCase(primaryCode); } } else { primaryCodeForShow = primaryCode; } } else { primaryCodeForShow = primaryCode; } if (mPredicting) { if ((mInputView != null) && mInputView.isShifted() && mWord.cursorPosition() == 0) { mWord.setFirstCharCapitalized(true); } final InputConnection ic = getCurrentInputConnection(); if (mWord.add(primaryCodeForShow, nearByKeyCodes)) { Toast note = Toast .makeText( getApplicationContext(), "Check the logcat for a note from AnySoftKeyboard developers!", Toast.LENGTH_LONG); note.show(); Log.i(TAG, "*******************" + "\nNICE!!! You found the our easter egg! http://www.dailymotion.com/video/x3zg90_gnarls-barkley-crazy-2006-mtv-star_music\n" + "\nAnySoftKeyboard R&D team would like to thank you for using our keyboard application." + "\nWe hope you enjoying it, we enjoyed making it." + "\nWhile developing this application, we heard Gnarls Barkley's Crazy quite a lot, and would like to share it with you." + "\n" + "\nThanks." + "\nMenny Even Danan, Hezi Cohen, Hugo Lopes, Henrik Andersson, Sami Salonen, and Lado Kumsiashvili." + "\n*******************"); Intent easterEgg = new Intent( Intent.ACTION_VIEW, Uri.parse("http://www.dailymotion.com/video/x3zg90_gnarls-barkley-crazy-2006-mtv-star_music")); easterEgg.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(easterEgg); } if (ic != null) { final int cursorPosition; if (mWord.cursorPosition() != mWord.length()) { Log.d(TAG, "Cursor is not at the end of the word. I'll need to reposition"); cursorPosition = getCursorPosition(ic); } else { cursorPosition = -1; } if (cursorPosition >= 0) ic.beginBatchEdit(); ic.setComposingText(mWord.getTypedWord(), 1); if (cursorPosition >= 0) { ic.setSelection(cursorPosition + 1, cursorPosition + 1); ic.endBatchEdit(); } } // this should be done ONLY if the key is a letter, and not a inner // character (like '). if (Character.isLetter((char) primaryCodeForShow)) { postUpdateSuggestions(); } else { // just replace the typed word in the candidates view if (mCandidateView != null) mCandidateView.replaceTypedWord(mWord.getTypedWord()); } } else { sendKeyChar((char) primaryCodeForShow); } // updateShiftKeyState(getCurrentInputEditorInfo()); // measureCps(); TextEntryState.typedCharacter((char) primaryCodeForShow, false); } private void handleSeparator(int primaryCode) { Log.d(TAG, "handleSeparator: " + primaryCode); // Should dismiss the "Touch again to save" message when handling // separator if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { postUpdateSuggestions(); } boolean pickedDefault = false; // Handle separator InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); } // this is a special case, when the user presses a separator WHILE // inside the predicted word. // in this case, I will want to just dump the separator. final boolean separatorInsideWord = (mWord.cursorPosition() < mWord.length()); if (mPredicting && !separatorInsideWord) { // 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. //Also, ACTION does not invoke default picking. See https://github.com/AnySoftKeyboard/AnySoftKeyboard/issues/198 if (mAutoCorrectOn && primaryCode != '\'' && primaryCode != KeyCodes.ENTER /* * && (mJustRevertedSeparator == null || * mJustRevertedSeparator.length() == 0 || * mJustRevertedSeparator.charAt(0) != primaryCode) */) { pickedDefault = pickDefaultSuggestion(); // Picked the suggestion by the space key. We consider this // as "added an auto space". if (primaryCode == KeyCodes.SPACE) { mJustAddedAutoSpace = true; } } else { commitTyped(ic); abortCorrection(true, false); } } else if (separatorInsideWord) { // when puting a separator in the middile of a word, there is no // need to do correction, or keep knowledge abortCorrection(true, false); } if (mJustAddedAutoSpace && primaryCode == KeyCodes.ENTER) { removeTrailingSpace(); mJustAddedAutoSpace = false; } sendKeyChar((char) primaryCode); // Handle the case of ". ." -> " .." with auto-space if necessary // before changing the TextEntryState. if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED && primaryCode == '.') { reswapPeriodAndSpace(); } TextEntryState.typedCharacter((char) primaryCode, true); if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED && primaryCode != KeyCodes.ENTER) { swapPunctuationAndSpace(); } else if (/* isPredictionOn() && */primaryCode == ' ') { doubleSpace(); } if (pickedDefault && mWord.getPreferredWord() != null) { TextEntryState.acceptedDefault(mWord.getTypedWord(), mWord.getPreferredWord()); } updateShiftKeyState(getCurrentInputEditorInfo()); if (ic != null) { ic.endBatchEdit(); } } private void handleClose() { boolean closeSelf = true; if (mInputView != null) closeSelf = mInputView.closing(); if (closeSelf) { commitTyped(getCurrentInputConnection()); requestHideSelf(0); abortCorrection(true, true); TextEntryState.endSession(); } } // private void checkToggleCapsLock() { // if (mKeyboardSwitcher.getCurrentKeyboard().isShifted()) { // toggleCapsLock(); // } // } private void postUpdateSuggestions() { postUpdateSuggestions(100); } // private void postRestartWordSuggestion(int cursorPosition) // { // mHandler.removeMessages(MSG_RESTART_NEW_WORD_SUGGESTIONS); // Message msg = mHandler.obtainMessage(MSG_RESTART_NEW_WORD_SUGGESTIONS); // msg.arg1 = cursorPosition; // mHandler.sendMessageDelayed(msg, 600); // } /** * posts an update suggestions request to the messages queue. Removes any previous request. * @param delay negative value will cause the call to be done now, in this thread. */ private void postUpdateSuggestions(long delay) { mHandler.removeMessages(KeyboardUIStateHanlder.MSG_UPDATE_SUGGESTIONS); if (delay > 0) mHandler.sendMessageDelayed(mHandler.obtainMessage(KeyboardUIStateHanlder.MSG_UPDATE_SUGGESTIONS), delay); else if (delay == 0) mHandler.sendMessage(mHandler.obtainMessage(KeyboardUIStateHanlder.MSG_UPDATE_SUGGESTIONS)); else performUpdateSuggestions(); } private boolean isPredictionOn() { boolean predictionOn = mPredictionOn; // if (!onEvaluateInputViewShown()) predictionOn &= // mPredictionLandscape; return predictionOn; } private boolean shouldCandidatesStripBeShown() { return mShowSuggestions && onEvaluateInputViewShown(); } /*package*/ void performUpdateSuggestions() { Log.d(TAG, "performUpdateSuggestions: has mSuggest:" + (mSuggest != null) + ", isPredictionOn:" + isPredictionOn() + ", mPredicting:" + mPredicting + ", mQuickFixes:" + mQuickFixes + " mShowSuggestions:" + mShowSuggestions); // Check if we have a suggestion engine attached. if (mSuggest == null) { return; } // final boolean showSuggestions = (mCandidateView != null && // mPredicting // && isPredictionOn() && shouldCandidatesStripBeShown()); if (mCandidateCloseText != null)// in API3 this variable is null mCandidateCloseText.setVisibility(View.GONE); if (!mPredicting) { if (mCandidateView != null) mCandidateView.setSuggestions(null, false, false, false); return; } List<CharSequence> stringList = mSuggest.getSuggestions(/* mInputView, */mWord, false); 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 (mShowSuggestions || mQuickFixes) { correctionAvailable |= typedWordValid; } // Don't auto-correct words with multiple capital letter correctionAvailable &= !mWord.isMostlyCaps(); correctionAvailable &= !TextEntryState.isCorrecting(); mCandidateView.setSuggestions(stringList, false, typedWordValid, correctionAvailable); if (stringList.size() > 0) { if (correctionAvailable && !typedWordValid && stringList.size() > 1) { mWord.setPreferredWord(stringList.get(1)); } else { mWord.setPreferredWord(typedWord); } } else { mWord.setPreferredWord(null); } setCandidatesViewShown(shouldCandidatesStripBeShown() || mCompletionOn); } private boolean pickDefaultSuggestion() { // Complete any pending candidate query first if (mHandler.hasMessages(KeyboardUIStateHanlder.MSG_UPDATE_SUGGESTIONS)) { postUpdateSuggestions(-1); } final CharSequence bestWord = mWord.getPreferredWord(); Log.d(TAG, "pickDefaultSuggestion: bestWord:" + bestWord); if (!TextUtils.isEmpty(bestWord)) { final CharSequence typedWord = mWord.getTypedWord(); TextEntryState.acceptedDefault(typedWord, bestWord); // mJustAccepted = true; final boolean fixed = !typedWord.equals(pickSuggestion(bestWord, !bestWord.equals(typedWord))); if (!fixed) {//if the word typed was auto-replaced, we should not learn it. // Add the word to the auto dictionary if it's not a known word addToDictionaries(mWord, AutoDictionary.AdditionType.Typed); } return true; } return false; } public void pickSuggestionManually(int index, CharSequence suggestion) { Log.d(TAG, "pickSuggestionManually: index " + index + " suggestion " + suggestion); final boolean correcting = TextEntryState.isCorrecting(); final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); } try { 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()); return; } pickSuggestion(suggestion, correcting); TextEntryState.acceptedSuggestion(mWord.getTypedWord(), suggestion); // Follow it with a space if (mAutoSpace && !correcting) { sendSpace(); mJustAddedAutoSpace = true; } // Add the word to the auto dictionary if it's not a known word mJustAutoAddedWord = false; if (index == 0) { mJustAutoAddedWord = addToDictionaries(mWord, AutoDictionary.AdditionType.Picked); } final boolean showingAddToDictionaryHint = !mJustAutoAddedWord && index == 0 && (mQuickFixes || mShowSuggestions) && !mSuggest.isValidWord(suggestion)// this is for the case // that the word was // auto-added upon // picking && !mSuggest.isValidWord(suggestion.toString() .toLowerCase()); if (!mJustAutoAddedWord) { /* * if (!correcting) { // Fool the state watcher so that a * subsequent backspace will // not do a revert, unless // we * just did a correction, in which case we need to stay in // * TextEntryState.State.PICKED_SUGGESTION state. * TextEntryState.typedCharacter((char) KeyCodes.SPACE, true); * setNextSuggestions(); } else if (!showingAddToDictionaryHint) * { // If we're not showing the "Touch again to save", then * show // corrections again. // In case the cursor position * doesn't change, make sure we show // the suggestions again. * clearSuggestions(); // postUpdateOldSuggestions(); } */ if (showingAddToDictionaryHint && mCandidateView != null) { mCandidateView.showAddToDictionaryHint(suggestion); } } } finally { if (ic != null) { ic.endBatchEdit(); } } } /** * Commits the chosen word to the text field and saves it for later * retrieval. * * @param suggestion the suggestion picked by the user to be committed to the text * field * @param correcting whether this is due to a correction of an existing word. */ private CharSequence pickSuggestion(CharSequence suggestion, boolean correcting) { if (mCapsLock) { suggestion = suggestion.toString().toUpperCase(); } else if (preferCapitalization() || (mKeyboardSwitcher.isAlphabetMode() && (mInputView != null) && mInputView .isShifted())) { suggestion = Character.toUpperCase(suggestion.charAt(0)) + suggestion.subSequence(1, suggestion.length()).toString(); } mWord.setPreferredWord(suggestion); InputConnection ic = getCurrentInputConnection(); if (ic != null) { if (correcting) { AnyApplication.getDeviceSpecific() .commitCorrectionToInputConnection(ic, mWord); // and drawing popout text mInputView.popTextOutOfKey(mWord.getPreferredWord()); } else { ic.commitText(suggestion, 1); } } mPredicting = false; mCommittedLength = suggestion.length(); if (mCandidateView != null) { mCandidateView.setSuggestions(null, false, false, false); } // If we just corrected a word, then don't show punctuations if (!correcting) { setNextSuggestions(); } updateShiftKeyState(getCurrentInputEditorInfo()); return suggestion; } private boolean isCursorTouchingWord() { InputConnection ic = getCurrentInputConnection(); if (ic == null) return false; CharSequence toLeft = ic.getTextBeforeCursor(1, 0); // It is not exactly clear to me why, but sometimes, although I request // 1 character, I get // the entire text. This causes me to incorrectly detect restart // suggestions... if (!TextUtils.isEmpty(toLeft) && toLeft.length() == 1 && !isWordSeparator(toLeft.charAt(0))) { return true; } CharSequence toRight = ic.getTextAfterCursor(1, 0); if (!TextUtils.isEmpty(toRight) && toRight.length() == 1 && !isWordSeparator(toRight.charAt(0))) { return true; } return false; } public void revertLastWord(boolean deleteChar) { Log.d(TAG, "revertLastWord deleteChar:" + deleteChar + ", mWord.size:" + mWord.length() + " mPredicting:" + mPredicting + " mCommittedLength" + mCommittedLength); final int length = mWord.length();// mComposing.length(); if (!mPredicting && length > 0) { final CharSequence typedWord = mWord.getTypedWord(); final InputConnection ic = getCurrentInputConnection(); mPredicting = true; mUndoCommitCursorPosition = -2; 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(typedWord/* mComposing */, 1); TextEntryState.backspace(); ic.endBatchEdit(); postUpdateSuggestions(-1); if (mJustAutoAddedWord && mUserDictionary != null) { // we'll also need to REMOVE the word from the user dictionary // now... // Since the user revert the commited word, and ASK auto-added // that word, this word will need to be removed. Log.i(TAG, "Since the word '" + typedWord + "' was auto-added to the user-dictionary, it will not be deleted."); removeFromUserDictionary(typedWord.toString()); } } else { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); // mJustRevertedSeparator = null; } } // private void setOldSuggestions() { // //mShowingVoiceSuggestions = false; // if (mCandidateView != null && // mCandidateView.isShowingAddToDictionaryHint()) { // return; // } // InputConnection ic = getCurrentInputConnection(); // if (ic == null) return; // if (!mPredicting) { // // Extract the selected or touching text // EditingUtil.SelectedWord touching = // EditingUtil.getWordAtCursorOrSelection(ic, // mLastSelectionStart, mLastSelectionEnd, mWordSeparators); // // if (touching != null && touching.word.length() > 1) { // ic.beginBatchEdit(); // // if (!applyVoiceAlternatives(touching) && // !applyTypedAlternatives(touching)) { // abortCorrection(true); // } else { // TextEntryState.selectedForCorrection(); // EditingUtil.underlineWord(ic, touching); // } // // ic.endBatchEdit(); // } else { // abortCorrection(true); // setNextSuggestions(); // Show the punctuation suggestions list // } // } else { // abortCorrection(true); // } // } private static final List<CharSequence> msEmptyNextSuggestions = new ArrayList<CharSequence>( 0); private void setNextSuggestions() { setSuggestions( /* mSuggest.getInitialSuggestions() */msEmptyNextSuggestions, false, false, false); } public boolean isWordSeparator(int code) { return (!isAlphabet(code)); } private void sendSpace() { sendKeyChar((char) KeyCodes.SPACE); updateShiftKeyState(getCurrentInputEditorInfo()); } public boolean preferCapitalization() { return mWord.isFirstCharCapitalized(); } private void nextAlterKeyboard(EditorInfo currentEditorInfo) { Log.d(TAG, "nextAlterKeyboard: currentEditorInfo.inputType=" + currentEditorInfo.inputType); // AnyKeyboard currentKeyboard = mKeyboardSwitcher.getCurrentKeyboard(); if (getCurrentKeyboard() == null) { Log.d(TAG, "nextKeyboard: Looking for next keyboard. No current keyboard."); } else { Log.d(TAG, "nextKeyboard: Looking for next keyboard. Current keyboard is:" + getCurrentKeyboard().getKeyboardName()); } mKeyboardSwitcher.nextAlterKeyboard(currentEditorInfo); Log.i(TAG, "nextAlterKeyboard: Setting next keyboard to: " + getCurrentKeyboard().getKeyboardName()); } private void nextKeyboard(EditorInfo currentEditorInfo, KeyboardSwitcher.NextKeyboardType type) { Log.d(TAG, "nextKeyboard: currentEditorInfo.inputType=" + currentEditorInfo.inputType + " type:" + type); // in numeric keyboards, the LANG key will go back to the original // alphabet keyboard- // so no need to look for the next keyboard, 'mLastSelectedKeyboard' // holds the last // keyboard used. AnyKeyboard keyboard = mKeyboardSwitcher.nextKeyboard( currentEditorInfo, type); if (!(keyboard instanceof GenericKeyboard)) { mSentenceSeparators = keyboard.getSentenceSeparators(); } setKeyboardFinalStuff(currentEditorInfo, type, keyboard); } private void setKeyboardFinalStuff(EditorInfo currentEditorInfo, KeyboardSwitcher.NextKeyboardType type, AnyKeyboard currentKeyboard) { updateShiftKeyState(currentEditorInfo); mCapsLock = currentKeyboard.isShiftLocked(); mShiftKeyState.reset(); mControlKeyState.reset(); // changing dictionary setDictionariesForCurrentKeyboard(); // Notifying if needed if ((mKeyboardChangeNotificationType .equals(KEYBOARD_NOTIFICATION_ALWAYS)) || (mKeyboardChangeNotificationType .equals(KEYBOARD_NOTIFICATION_ON_PHYSICAL) && (type == NextKeyboardType.AlphabetSupportsPhysical))) { notifyKeyboardChangeIfNeeded(); } postUpdateSuggestions(); } public void onSwipeRight(boolean onSpaceBar, boolean twoFingersGesture) { final int keyCode = mConfig.getGestureSwipeRightKeyCode(onSpaceBar, twoFingersGesture); Log.d(TAG, "onSwipeRight " + ((onSpaceBar) ? " + space" : "") + ((twoFingersGesture) ? " + two-fingers" : "") + " => code " + keyCode); if (keyCode != 0) mSwitchAnimator .doSwitchAnimation(AnimationType.SwipeRight, keyCode); } public void onSwipeLeft(boolean onSpaceBar, boolean twoFingersGesture) { final int keyCode = mConfig.getGestureSwipeLeftKeyCode(onSpaceBar, twoFingersGesture); Log.d(TAG, "onSwipeLeft " + ((onSpaceBar) ? " + space" : "") + ((twoFingersGesture) ? " + two-fingers" : "") + " => code " + keyCode); if (keyCode != 0) mSwitchAnimator.doSwitchAnimation(AnimationType.SwipeLeft, keyCode); } public void onSwipeDown(boolean onSpaceBar) { final int keyCode = mConfig.getGestureSwipeDownKeyCode(); Log.d(TAG, "onSwipeDown " + ((onSpaceBar) ? " + space" : "") + " => code " + keyCode); if (keyCode != 0) onKey(keyCode, null, -1, new int[]{keyCode}, false); } public void onSwipeUp(boolean onSpaceBar) { final int keyCode = mConfig.getGestureSwipeUpKeyCode(onSpaceBar); Log.d(TAG, "onSwipeUp " + ((onSpaceBar) ? " + space" : "") + " => code " + keyCode); if (keyCode != 0) { onKey(keyCode, null, -1, new int[]{keyCode}, false); } } public void onPinch() { final int keyCode = mConfig.getGesturePinchKeyCode(); Log.d(TAG, "onPinch => code " + keyCode); if (keyCode != 0) onKey(keyCode, null, -1, new int[]{keyCode}, false); } public void onSeparate() { final int keyCode = mConfig.getGestureSeparateKeyCode(); Log.d(TAG, "onSeparate => code " + keyCode); if (keyCode != 0) onKey(keyCode, null, -1, new int[]{keyCode}, false); } private void sendKeyDown(InputConnection ic, int key) { if (ic != null) ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, key)); } private void sendKeyUp(InputConnection ic, int key) { if (ic != null) ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, key)); } public void onPress(int primaryCode) { InputConnection ic = getCurrentInputConnection(); Log.d(TAG, "onPress:" + primaryCode); if (mVibrationDuration > 0 && primaryCode != 0) { mVibrator.vibrate(mVibrationDuration); } if (mDistinctMultiTouch && primaryCode == KeyCodes.SHIFT) { mShiftKeyState.onPress(); handleShift(false); } else { mShiftKeyState.onOtherKeyPressed(); } if (mDistinctMultiTouch && primaryCode == KeyCodes.CTRL) { mControlKeyState.onPress(); handleControl(false); sendKeyDown(ic, 113); // KeyEvent.KEYCODE_CTRL_LEFT (API 11 and up) } else { mControlKeyState.onOtherKeyPressed(); } if (mSoundOn && (!mSilentMode) && primaryCode != 0) { final int keyFX; switch (primaryCode) { case 13: case KeyCodes.ENTER: keyFX = AudioManager.FX_KEYPRESS_RETURN; break; case KeyCodes.DELETE: keyFX = AudioManager.FX_KEYPRESS_DELETE; break; case KeyCodes.SPACE: keyFX = AudioManager.FX_KEYPRESS_SPACEBAR; break; default: keyFX = AudioManager.FX_KEY_CLICK; } final float fxVolume; // creating scoop to make sure volume and maxVolume // are not used { final int volume; final int maxVolume; if (mSoundVolume > 0) { volume = mSoundVolume; maxVolume = 100; // pre-eclair // volume is between 0..8 (float) // eclair // volume is between 0..1 (float) if (Workarounds.getApiLevel() >= 5) { fxVolume = ((float) volume) / ((float) maxVolume); } else { fxVolume = 8 * ((float) volume) / ((float) maxVolume); } } else { fxVolume = -1.0f; } } Log.d(TAG, "Sound on key-pressed. Sound ID:" + keyFX + " with volume " + fxVolume); mAudioManager.playSoundEffect(keyFX, fxVolume); } } public void onRelease(int primaryCode) { InputConnection ic = getCurrentInputConnection(); Log.d(TAG, "onRelease:" + primaryCode); if (mDistinctMultiTouch && primaryCode == KeyCodes.SHIFT) { if (mShiftKeyState.isMomentary()) handleShift(true); mShiftKeyState.onRelease(); } if (mDistinctMultiTouch && primaryCode == KeyCodes.CTRL) { if (mControlKeyState.isMomentary()) handleControl(true); sendKeyUp(ic, 113); // KeyEvent.KEYCODE_CTRL_LEFT mControlKeyState.onRelease(); } // the user lifted the finger, let's handle the shift if (primaryCode != KeyCodes.SHIFT) updateShiftKeyState(getCurrentInputEditorInfo()); // and set the control state. Checking if the inputview is null // this is weird, I agree, how can onRelease be called, if the inputview // is null // well, there are some cases where the onRelease is called with a // delayed message, and in this case the view may already be disposed! // Issue #94. if (primaryCode != KeyCodes.CTRL && mInputView != null) mInputView.setControl(mControlKeyState.isMomentary()); } // receive ringer mode changes to detect silent mode private final SoundPreferencesChangedReceiver mSoundPreferencesChangedReceiver = new SoundPreferencesChangedReceiver( this); private final PackagesChangedReceiver mPackagesChangedReceiver = new PackagesChangedReceiver( this); // update flags for silent mode public void updateRingerMode() { mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); } private void loadSettings() { // Get the settings preferences SharedPreferences sp = PreferenceManager .getDefaultSharedPreferences(this); mVibrationDuration = Integer .parseInt(sp .getString( getString(R.string.settings_key_vibrate_on_key_press_duration), getString(R.string.settings_default_vibrate_on_key_press_duration))); mSoundOn = sp.getBoolean(getString(R.string.settings_key_sound_on), getResources().getBoolean(R.bool.settings_default_sound_on)); if (mSoundOn) { Log.i(TAG, "Loading sounds effects from AUDIO_SERVICE due to configuration change."); mAudioManager.loadSoundEffects(); } // checking the volume boolean customVolume = sp.getBoolean("use_custom_sound_volume", false); int newVolume; if (customVolume) { newVolume = sp.getInt("custom_sound_volume", 0) + 1; Log.i(TAG, "Custom volume checked: " + newVolume + " out of 100"); } else { Log.i(TAG, "Custom volume un-checked."); newVolume = -1; } mSoundVolume = newVolume; // in order to support the old type of configuration mKeyboardChangeNotificationType = sp .getString( getString(R.string.settings_key_physical_keyboard_change_notification_type), getString(R.string.settings_default_physical_keyboard_change_notification_type)); // now clearing the notification, and it will be re-shown if needed mInputMethodManager.hideStatusIcon(mImeToken); // mNotificationManager.cancel(KEYBOARD_NOTIFICATION_ID); // should it be always on? if (mKeyboardChangeNotificationType .equals(KEYBOARD_NOTIFICATION_ALWAYS)) notifyKeyboardChangeIfNeeded(); mAutoCap = sp.getBoolean("auto_caps", true); mShowSuggestions = sp.getBoolean("candidates_on", true); setDictionariesForCurrentKeyboard(); mAutoComplete = sp.getBoolean("auto_complete", false) && mShowSuggestions; mQuickFixes = sp.getBoolean("quick_fix", true); mAllowSuggestionsRestart = sp.getBoolean( getString(R.string.settings_key_allow_suggestions_restart), getResources().getBoolean( R.bool.settings_default_allow_suggestions_restart)); mAutoCorrectOn = /* mSuggest != null && *//* * Suggestion always exists, * maybe not at the moment, but * shortly */ (mAutoComplete/* || mQuickFixes */); // mCorrectionMode = mAutoComplete ? 2 // : (/*mShowSuggestions*/ mQuickFixes ? 1 : 0); mSmileyOnShortPress = sp .getBoolean( getString(R.string.settings_key_emoticon_long_press_opens_popup), getResources() .getBoolean( R.bool.settings_default_emoticon_long_press_opens_popup)); // mSmileyPopupType = // sp.getString(getString(R.string.settings_key_smiley_popup_type), // getString(R.string.settings_default_smiley_popup_type)); mOverrideQuickTextText = sp.getString( getString(R.string.settings_key_emoticon_default_text), null); mMinimumWordCorrectionLength = sp .getInt(getString(R.string.settings_key_min_length_for_word_correction__), 2); if (mSuggest != null) mSuggest.setMinimumWordLengthForCorrection(mMinimumWordCorrectionLength); setInitialCondensedState(getResources().getConfiguration()); } private void setDictionariesForCurrentKeyboard() { if (mSuggest != null) { if (!mPredictionOn) { Log.d(TAG, "No suggestion is required. I'll try to release memory from the dictionary."); // DictionaryFactory.getInstance().releaseAllDictionaries(); mSuggest.setMainDictionary(getApplicationContext(), null); mSuggest.setUserDictionary(null); mSuggest.setAutoDictionary(null); mLastDictionaryRefresh = -1; } else { mLastDictionaryRefresh = SystemClock.elapsedRealtime(); // It null at the creation of the application. if ((mKeyboardSwitcher != null) && mKeyboardSwitcher.isAlphabetMode()) { AnyKeyboard currentKeyobard = mKeyboardSwitcher .getCurrentKeyboard(); // if there is a mapping in the settings, we'll use that, // else we'll // return the default String mappingSettingsKey = getDictionaryOverrideKey(currentKeyobard); String defaultDictionary = currentKeyobard .getDefaultDictionaryLocale(); String dictionaryValue = mPrefs.getString( mappingSettingsKey, null); DictionaryAddOnAndBuilder dictionaryBuilder = null; if (dictionaryValue == null) { dictionaryBuilder = ExternalDictionaryFactory .getDictionaryBuilderByLocale(currentKeyobard .getDefaultDictionaryLocale(), getApplicationContext()); } else { Log.d(TAG, "Default dictionary '" + (defaultDictionary == null ? "None" : defaultDictionary) + "' for keyboard '" + currentKeyobard .getKeyboardPrefId() + "' has been overriden to '" + dictionaryValue + "'"); dictionaryBuilder = ExternalDictionaryFactory .getDictionaryBuilderById(dictionaryValue, getApplicationContext()); } mSuggest.setMainDictionary(getApplicationContext(), dictionaryBuilder); String localeForSupportingDictionaries = dictionaryBuilder != null ? dictionaryBuilder .getLanguage() : defaultDictionary; mUserDictionary = mSuggest.getDictionaryFactory() .createUserDictionary(getApplicationContext(), localeForSupportingDictionaries); mSuggest.setUserDictionary(mUserDictionary); mAutoDictionary = mSuggest.getDictionaryFactory().createAutoDictionary(getApplicationContext(),localeForSupportingDictionaries); mSuggest.setAutoDictionary(mAutoDictionary); mSuggest.setContactsDictionary(getApplicationContext(), mConfig.useContactsDictionary()); } } } } private String getDictionaryOverrideKey(AnyKeyboard currentKeyboard) { String mappingSettingsKey = currentKeyboard.getKeyboardPrefId() + "_override_dictionary"; return mappingSettingsKey; } private void launchSettings() { handleClose(); Intent intent = new Intent(); intent.setClass(AnySoftKeyboard.this, MainSettingsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } private void launchDictionaryOverriding() { final String dictionaryOverridingKey = getDictionaryOverrideKey(getCurrentKeyboard()); final String dictionaryOverrideValue = mPrefs.getString( dictionaryOverridingKey, null); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); builder.setIcon(R.drawable.ic_launcher); builder.setTitle(getResources().getString( R.string.override_dictionary_title, getCurrentKeyboard().getKeyboardName())); builder.setNegativeButton(android.R.string.cancel, null); ArrayList<CharSequence> dictionaryIds = new ArrayList<CharSequence>(); ArrayList<CharSequence> dictionaries = new ArrayList<CharSequence>(); // null dictionary is handled as the default for the keyboard dictionaryIds.add(null); final String SELECTED = "\u2714 "; final String NOT_SELECTED = "- "; if (dictionaryOverrideValue == null) dictionaries.add(SELECTED + getString(R.string.override_dictionary_default)); else dictionaries.add(NOT_SELECTED + getString(R.string.override_dictionary_default)); // going over all installed dictionaries for (DictionaryAddOnAndBuilder dictionaryBuilder : ExternalDictionaryFactory .getAllAvailableExternalDictionaries(getApplicationContext())) { dictionaryIds.add(dictionaryBuilder.getId()); String description; if (dictionaryOverrideValue != null && dictionaryBuilder.getId() .equals(dictionaryOverrideValue)) description = SELECTED; else description = NOT_SELECTED; description += dictionaryBuilder.getName(); if (!TextUtils.isEmpty(dictionaryBuilder.getDescription())) { description += " (" + dictionaryBuilder.getDescription() + ")"; } dictionaries.add(description); } final CharSequence[] ids = new CharSequence[dictionaryIds.size()]; final CharSequence[] items = new CharSequence[dictionaries.size()]; dictionaries.toArray(items); dictionaryIds.toArray(ids); builder.setItems(items, new DialogInterface.OnClickListener() { public void onClick(DialogInterface di, int position) { di.dismiss(); Editor editor = mPrefs.edit(); switch (position) { case 0: Log.d(TAG, "Dictionary overriden disabled. User selected default."); editor.remove(dictionaryOverridingKey); showToastMessage(R.string.override_disabled, true); break; default: if ((position < 0) || (position >= items.length)) { Log.d(TAG, "Dictionary override dialog canceled."); } else { CharSequence id = ids[position]; String selectedDictionaryId = (id == null) ? null : id .toString(); String selectedLanguageString = items[position] .toString(); Log.d(TAG, "Dictionary override. User selected " + selectedLanguageString + " which corresponds to id " + ((selectedDictionaryId == null) ? "(null)" : selectedDictionaryId)); editor.putString(dictionaryOverridingKey, selectedDictionaryId); showToastMessage( getString(R.string.override_enabled, selectedLanguageString), true); } break; } editor.commit(); setDictionariesForCurrentKeyboard(); } }); 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 showOptionsMenu() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); builder.setIcon(R.drawable.ic_launcher); builder.setNegativeButton(android.R.string.cancel, null); CharSequence itemSettings = getString(R.string.ime_settings); CharSequence itemOverrideDictionary = getString(R.string.override_dictionary); CharSequence itemInputMethod = getString(R.string.change_ime); builder.setItems(new CharSequence[]{itemSettings, itemOverrideDictionary, itemInputMethod}, new DialogInterface.OnClickListener() { public void onClick(DialogInterface di, int position) { di.dismiss(); switch (position) { case 0: launchSettings(); break; case 1: launchDictionaryOverriding(); break; case 2: ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)) .showInputMethodPicker(); break; } } }); builder.setTitle(getResources().getString(R.string.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(); } @Override public void onConfigurationChanged(Configuration newConfig) { // If orientation changed while predicting, commit the change if (newConfig.orientation != mOrientation) { setInitialCondensedState(newConfig); commitTyped(getCurrentInputConnection()); mOrientation = newConfig.orientation; mKeyboardSwitcher.makeKeyboards(true); // new WxH. need new object. mSentenceSeparators = getCurrentKeyboard().getSentenceSeparators(); if (mKeyboardChangeNotificationType .equals(KEYBOARD_NOTIFICATION_ALWAYS))// should // it // be // always // on? notifyKeyboardChangeIfNeeded(); } super.onConfigurationChanged(newConfig); } private void setInitialCondensedState(Configuration newConfig) { final String defaultCondensed = mConfig.getInitialKeyboardCondenseState(); mKeyboardInCondensedMode = CondenseType.None; if (defaultCondensed.equals("split_always")) { mKeyboardInCondensedMode = CondenseType.Split; } else if (defaultCondensed.equals("split_in_landscape")) { if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) mKeyboardInCondensedMode = CondenseType.Split; else mKeyboardInCondensedMode = CondenseType.None; } else if (defaultCondensed.equals("compact_right_always")) { mKeyboardInCondensedMode = CondenseType.CompactToRight; } else if (defaultCondensed.equals("compact_left_always")) { mKeyboardInCondensedMode = CondenseType.CompactToLeft; } Log.d(TAG, "setInitialCondensedState: defaultCondensed is " + defaultCondensed + " and mKeyboardInCondensedMode is " + mKeyboardInCondensedMode); } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Log.d(TAG, "onSharedPreferenceChanged - key:" + key); AnyApplication.requestBackupToCloud(); boolean isKeyboardKey = key .startsWith(KeyboardAddOnAndBuilder.KEYBOARD_PREF_PREFIX); boolean isDictionaryKey = key.startsWith("dictionary_"); boolean isQuickTextKey = key .equals(getString(R.string.settings_key_active_quick_text_key)); if (isKeyboardKey || isDictionaryKey || isQuickTextKey) { mKeyboardSwitcher.makeKeyboards(true); } loadSettings(); if (isDictionaryKey || key.equals(getString(R.string.settings_key_use_contacts_dictionary)) || key.equals(getString(R.string.settings_key_auto_dictionary_threshold))) { setDictionariesForCurrentKeyboard(); } else if ( // key.equals(getString(R.string.settings_key_top_keyboard_row_id)) || key.equals(getString(R.string.settings_key_ext_kbd_bottom_row_key)) || key.equals(getString(R.string.settings_key_ext_kbd_top_row_key)) || key.equals(getString(R.string.settings_key_ext_kbd_ext_ketboard_key)) || key.equals(getString(R.string.settings_key_ext_kbd_hidden_bottom_row_key)) || key.equals(getString(R.string.settings_key_keyboard_theme_key)) || key.equals("zoom_factor_keys_in_portrait") || key.equals("zoom_factor_keys_in_landscape") || key.equals(getString(R.string.settings_key_smiley_icon_on_smileys_key)) || key.equals(getString(R.string.settings_key_long_press_timeout)) || key.equals(getString(R.string.settings_key_multitap_timeout)) || key.equals(getString(R.string.settings_key_default_split_state))) { // in some cases we do want to force keyboards recreations resetKeyboardView(key .equals(getString(R.string.settings_key_keyboard_theme_key))); } } /* * public void appendCharactersToInput(CharSequence textToCommit) { if * (DEBUG) Log.d(TAG, "appendCharactersToInput: '"+ textToCommit+"'"); * for(int index=0; index<textToCommit.length(); index++) { final char c = * textToCommit.charAt(index); mWord.add(c, new int[]{c}); } * //mComposing.append(textToCommit); if (mPredictionOn) * getCurrentInputConnection().setComposingText(mWord.getTypedWord(), * textToCommit.length()); else commitTyped(getCurrentInputConnection()); * updateShiftKeyState(getCurrentInputEditorInfo()); } */ public void deleteLastCharactersFromInput(int countToDelete) { if (countToDelete == 0) return; final int currentLength = mWord.length(); boolean shouldDeleteUsingCompletion; if (currentLength > 0) { shouldDeleteUsingCompletion = true; if (currentLength > countToDelete) { // mComposing.delete(currentLength - countToDelete, // currentLength); int deletesLeft = countToDelete; while (deletesLeft > 0) { mWord.deleteLast(); deletesLeft--; } } else { // mComposing.setLength(0); mWord.reset(); } } else { shouldDeleteUsingCompletion = false; } InputConnection ic = getCurrentInputConnection(); if (ic != null) { if (mPredictionOn && shouldDeleteUsingCompletion) { ic.setComposingText(mWord.getTypedWord()/* mComposing */, 1); // updateCandidates(); } else { ic.deleteSurroundingText(countToDelete, 0); } } postUpdateShiftKeyState(); } public void showToastMessage(int resId, boolean forShortTime) { CharSequence text = getResources().getText(resId); showToastMessage(text, forShortTime); } private void showToastMessage(CharSequence text, boolean forShortTime) { int duration = forShortTime ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; Log.v(TAG, "showToastMessage: '" + text + "'. For: " + duration); Toast.makeText(this.getApplication(), text, duration).show(); } @Override public void onLowMemory() { Log.w(TAG, "The OS has reported that it is low on memory!. I'll try to clear some cache."); mKeyboardSwitcher.onLowMemory(); // DictionaryFactory.getInstance().onLowMemory(mSuggest.getMainDictionary()); super.onLowMemory(); } /*package*/ TextView mCandidateCloseText; private void showQuickTextKeyPopupKeyboard(QuickTextKey quickTextKey) { if (mInputView != null) { /*if (quickTextKey.getPackageContext() == getApplicationContext()) { mInputView.simulateLongPress(KeyCodes.QUICK_TEXT); } else {*/ mInputView.showQuickTextPopupKeyboard(quickTextKey); /*}*/ } } private void showQuickTextKeyPopupList(final QuickTextKey key) { if (mQuickTextKeyDialog == null) { String[] names = key.getPopupListNames(); final String[] texts = key.getPopupListValues(); int[] icons = key.getPopupListIconResIds(); final int N = names.length; List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>(); for (int i = 0; i < N; i++) { HashMap<String, Object> entry = new HashMap<String, Object>(); entry.put("name", names[i]); entry.put("text", texts[i]); if (icons != null) entry.put("icons", icons[i]); entries.add(entry); } int layout; String[] from; int[] to; if (icons == null) { layout = R.layout.quick_text_key_menu_item_without_icon; from = new String[]{"name", "text"}; to = new int[]{R.id.quick_text_name, R.id.quick_text_output}; } else { layout = R.layout.quick_text_key_menu_item_with_icon; from = new String[]{"name", "text", "icons"}; to = new int[]{R.id.quick_text_name, R.id.quick_text_output, R.id.quick_text_icon}; } final SimpleAdapter a = new SimpleAdapter(this, entries, layout, from, to); SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() { public boolean setViewValue(View view, Object data, String textRepresentation) { if (view instanceof ImageView) { Drawable img = key.getPackageContext().getResources() .getDrawable((Integer) data); ((ImageView) view).setImageDrawable(img); return true; } return false; } }; a.setViewBinder(viewBinder); AlertDialog.Builder b = new AlertDialog.Builder(this); b.setTitle(getString(R.string.menu_insert_smiley)); b.setCancelable(true); b.setAdapter(a, new DialogInterface.OnClickListener() { @SuppressWarnings("unchecked") // I know, I know, it is not safe to cast, but I created the // list, and willing to pay the price. public final void onClick(DialogInterface dialog, int which) { HashMap<String, Object> item = (HashMap<String, Object>) a .getItem(which); onText((String) item.get("text")); dialog.dismiss(); } }); mQuickTextKeyDialog = b.create(); Window window = mQuickTextKeyDialog.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); } mQuickTextKeyDialog.show(); } public boolean promoteToUserDictionary(String word, int frequency) { if (mUserDictionary.isValidWord(word)) return false; else return mUserDictionary.addWord(word, frequency); } public WordComposer getCurrentWord() { return mWord; } /** * Override this to control when the soft input area should be shown to the * user. The default implementation only shows the input view when there is * no hard keyboard or the keyboard is hidden. If you change what this * returns, you will need to call {@link #updateInputViewShown()} yourself * whenever the returned value may have changed to have it re-evalauted and * applied. This needs to be re-coded for Issue 620 */ @Override public boolean onEvaluateInputViewShown() { Configuration config = getResources().getConfiguration(); return config.keyboard == Configuration.KEYBOARD_NOKEYS || config.hardKeyboardHidden == Configuration.KEYBOARDHIDDEN_YES; } public void onCancel() { // don't know what to do here. } public void resetKeyboardView(boolean recreateView) { handleClose(); if (mKeyboardSwitcher != null) mKeyboardSwitcher.makeKeyboards(true); if (recreateView) { // also recreate keyboard view setInputView(onCreateInputView()); setCandidatesView(onCreateCandidatesView()); setCandidatesViewShown(false); } } }