LatinIME.java revision 04f815eca4088ece879b1d9fb482bb7a342bfe4f
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import com.android.inputmethod.keyboard.Keyboard; 20import com.android.inputmethod.keyboard.KeyboardActionListener; 21import com.android.inputmethod.keyboard.KeyboardId; 22import com.android.inputmethod.keyboard.KeyboardSwitcher; 23import com.android.inputmethod.keyboard.KeyboardView; 24import com.android.inputmethod.keyboard.LatinKeyboard; 25import com.android.inputmethod.keyboard.LatinKeyboardView; 26import com.android.inputmethod.latin.Utils.RingCharBuffer; 27import com.android.inputmethod.voice.VoiceIMEConnector; 28 29import android.app.AlertDialog; 30import android.content.BroadcastReceiver; 31import android.content.Context; 32import android.content.DialogInterface; 33import android.content.Intent; 34import android.content.IntentFilter; 35import android.content.SharedPreferences; 36import android.content.res.Configuration; 37import android.content.res.Resources; 38import android.inputmethodservice.InputMethodService; 39import android.media.AudioManager; 40import android.os.Debug; 41import android.os.Handler; 42import android.os.Message; 43import android.os.SystemClock; 44import android.os.Vibrator; 45import android.preference.PreferenceActivity; 46import android.preference.PreferenceManager; 47import android.text.InputType; 48import android.text.TextUtils; 49import android.util.DisplayMetrics; 50import android.util.Log; 51import android.util.PrintWriterPrinter; 52import android.util.Printer; 53import android.view.Gravity; 54import android.view.HapticFeedbackConstants; 55import android.view.KeyEvent; 56import android.view.LayoutInflater; 57import android.view.View; 58import android.view.ViewGroup; 59import android.view.ViewParent; 60import android.view.Window; 61import android.view.WindowManager; 62import android.view.inputmethod.CompletionInfo; 63import android.view.inputmethod.CorrectionInfo; 64import android.view.inputmethod.EditorInfo; 65import android.view.inputmethod.ExtractedText; 66import android.view.inputmethod.ExtractedTextRequest; 67import android.view.inputmethod.InputConnection; 68import android.view.inputmethod.InputMethodManager; 69import android.view.inputmethod.InputMethodSubtype; 70import android.widget.HorizontalScrollView; 71import android.widget.LinearLayout; 72 73import java.io.FileDescriptor; 74import java.io.PrintWriter; 75import java.util.ArrayList; 76import java.util.Arrays; 77import java.util.Locale; 78 79/** 80 * Input method implementation for Qwerty'ish keyboard. 81 */ 82public class LatinIME extends InputMethodService implements KeyboardActionListener, 83 SharedPreferences.OnSharedPreferenceChangeListener { 84 private static final String TAG = "LatinIME"; 85 private static final boolean PERF_DEBUG = false; 86 private static final boolean DEBUG = false; 87 private static final boolean TRACE = false; 88 89 private static final int DELAY_UPDATE_SUGGESTIONS = 180; 90 private static final int DELAY_UPDATE_OLD_SUGGESTIONS = 300; 91 private static final int DELAY_UPDATE_SHIFT_STATE = 300; 92 93 // How many continuous deletes at which to start deleting at a higher speed. 94 private static final int DELETE_ACCELERATE_AT = 20; 95 // Key events coming any faster than this are long-presses. 96 private static final int QUICK_PRESS = 200; 97 98 // Contextual menu positions 99 private static final int POS_METHOD = 0; 100 private static final int POS_SETTINGS = 1; 101 102 private int mSuggestionVisibility; 103 private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE 104 = R.string.prefs_suggestion_visibility_show_value; 105 private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE 106 = R.string.prefs_suggestion_visibility_show_only_portrait_value; 107 private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE 108 = R.string.prefs_suggestion_visibility_hide_value; 109 110 private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] { 111 SUGGESTION_VISIBILILTY_SHOW_VALUE, 112 SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE, 113 SUGGESTION_VISIBILILTY_HIDE_VALUE 114 }; 115 116 private View mCandidateViewContainer; 117 private CandidateView mCandidateView; 118 private Suggest mSuggest; 119 private CompletionInfo[] mApplicationSpecifiedCompletions; 120 121 private AlertDialog mOptionsDialog; 122 123 private InputMethodManager mImm; 124 private KeyboardSwitcher mKeyboardSwitcher; 125 private SubtypeSwitcher mSubtypeSwitcher; 126 private VoiceIMEConnector mVoiceConnector; 127 128 private UserDictionary mUserDictionary; 129 private UserBigramDictionary mUserBigramDictionary; 130 private ContactsDictionary mContactsDictionary; 131 private AutoDictionary mAutoDictionary; 132 133 private Resources mResources; 134 private SharedPreferences mPrefs; 135 136 private final StringBuilder mComposing = new StringBuilder(); 137 private WordComposer mWord = new WordComposer(); 138 private CharSequence mBestWord; 139 private boolean mHasValidSuggestions; 140 private boolean mIsSettingsSuggestionStripOn; 141 private boolean mApplicationSpecifiedCompletionOn; 142 private boolean mHasDictionary; 143 private boolean mAutoSpace; 144 private boolean mJustAddedAutoSpace; 145 private boolean mAutoCorrectEnabled; 146 private boolean mReCorrectionEnabled; 147 private boolean mBigramSuggestionEnabled; 148 private boolean mAutoCorrectOn; 149 private boolean mVibrateOn; 150 private boolean mSoundOn; 151 private boolean mPopupOn; 152 private boolean mAutoCap; 153 private boolean mQuickFixes; 154 private boolean mConfigSwipeDownDismissKeyboardEnabled; 155 private int mConfigDelayBeforeFadeoutLanguageOnSpacebar; 156 private int mConfigDurationOfFadeoutLanguageOnSpacebar; 157 private float mConfigFinalFadeoutFactorOfLanugageOnSpacebar; 158 159 private int mCorrectionMode; 160 private int mCommittedLength; 161 private int mOrientation; 162 // Keep track of the last selection range to decide if we need to show word alternatives 163 private int mLastSelectionStart; 164 private int mLastSelectionEnd; 165 private SuggestedWords mSuggestPuncList; 166 167 // Input type is such that we should not auto-correct 168 private boolean mInputTypeNoAutoCorrect; 169 170 // Indicates whether the suggestion strip is to be on in landscape 171 private boolean mJustAccepted; 172 private boolean mJustReverted; 173 private int mDeleteCount; 174 private long mLastKeyTime; 175 176 private AudioManager mAudioManager; 177 // Align sound effect volume on music volume 178 private static final float FX_VOLUME = -1.0f; 179 private boolean mSilentMode; 180 181 /* package */ String mWordSeparators; 182 private String mSentenceSeparators; 183 private String mSuggestPuncs; 184 // TODO: Move this flag to VoiceIMEConnector 185 private boolean mConfigurationChanging; 186 187 // Keeps track of most recently inserted text (multi-character key) for reverting 188 private CharSequence mEnteredText; 189 private boolean mRefreshKeyboardRequired; 190 191 private final ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>(); 192 193 public abstract static class WordAlternatives { 194 protected CharSequence mChosenWord; 195 196 public WordAlternatives() { 197 // Nothing 198 } 199 200 public WordAlternatives(CharSequence chosenWord) { 201 mChosenWord = chosenWord; 202 } 203 204 @Override 205 public int hashCode() { 206 return mChosenWord.hashCode(); 207 } 208 209 public abstract CharSequence getOriginalWord(); 210 211 public CharSequence getChosenWord() { 212 return mChosenWord; 213 } 214 215 public abstract SuggestedWords.Builder getAlternatives(); 216 } 217 218 public class TypedWordAlternatives extends WordAlternatives { 219 private WordComposer word; 220 221 public TypedWordAlternatives() { 222 // Nothing 223 } 224 225 public TypedWordAlternatives(CharSequence chosenWord, WordComposer wordComposer) { 226 super(chosenWord); 227 word = wordComposer; 228 } 229 230 @Override 231 public CharSequence getOriginalWord() { 232 return word.getTypedWord(); 233 } 234 235 @Override 236 public SuggestedWords.Builder getAlternatives() { 237 return getTypedSuggestions(word); 238 } 239 } 240 241 public final UIHandler mHandler = new UIHandler(); 242 243 public class UIHandler extends Handler { 244 private static final int MSG_UPDATE_SUGGESTIONS = 0; 245 private static final int MSG_UPDATE_OLD_SUGGESTIONS = 1; 246 private static final int MSG_UPDATE_SHIFT_STATE = 2; 247 private static final int MSG_VOICE_RESULTS = 3; 248 private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 4; 249 private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 5; 250 251 @Override 252 public void handleMessage(Message msg) { 253 final KeyboardSwitcher switcher = mKeyboardSwitcher; 254 final LatinKeyboardView inputView = switcher.getInputView(); 255 switch (msg.what) { 256 case MSG_UPDATE_SUGGESTIONS: 257 updateSuggestions(); 258 break; 259 case MSG_UPDATE_OLD_SUGGESTIONS: 260 setOldSuggestions(); 261 break; 262 case MSG_UPDATE_SHIFT_STATE: 263 switcher.updateShiftState(); 264 break; 265 case MSG_VOICE_RESULTS: 266 mVoiceConnector.handleVoiceResults(preferCapitalization() 267 || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked())); 268 break; 269 case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR: 270 if (inputView != null) 271 inputView.setSpacebarTextFadeFactor( 272 (1.0f + mConfigFinalFadeoutFactorOfLanugageOnSpacebar) / 2, 273 (LatinKeyboard)msg.obj); 274 sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj), 275 mConfigDurationOfFadeoutLanguageOnSpacebar); 276 break; 277 case MSG_DISMISS_LANGUAGE_ON_SPACEBAR: 278 if (inputView != null) 279 inputView.setSpacebarTextFadeFactor( 280 mConfigFinalFadeoutFactorOfLanugageOnSpacebar, (LatinKeyboard)msg.obj); 281 break; 282 } 283 } 284 285 public void postUpdateSuggestions() { 286 removeMessages(MSG_UPDATE_SUGGESTIONS); 287 sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), DELAY_UPDATE_SUGGESTIONS); 288 } 289 290 public void cancelUpdateSuggestions() { 291 removeMessages(MSG_UPDATE_SUGGESTIONS); 292 } 293 294 public boolean hasPendingUpdateSuggestions() { 295 return hasMessages(MSG_UPDATE_SUGGESTIONS); 296 } 297 298 public void postUpdateOldSuggestions() { 299 removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); 300 sendMessageDelayed(obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), 301 DELAY_UPDATE_OLD_SUGGESTIONS); 302 } 303 304 public void cancelUpdateOldSuggestions() { 305 removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); 306 } 307 308 public void postUpdateShiftKeyState() { 309 removeMessages(MSG_UPDATE_SHIFT_STATE); 310 sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), DELAY_UPDATE_SHIFT_STATE); 311 } 312 313 public void cancelUpdateShiftState() { 314 removeMessages(MSG_UPDATE_SHIFT_STATE); 315 } 316 317 public void updateVoiceResults() { 318 sendMessage(obtainMessage(MSG_VOICE_RESULTS)); 319 } 320 321 public void startDisplayLanguageOnSpacebar() { 322 removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR); 323 removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR); 324 final LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); 325 if (inputView != null) { 326 final LatinKeyboard keyboard = inputView.getLatinKeyboard(); 327 // The language is never displayed when the delay is zero. 328 if (mConfigDelayBeforeFadeoutLanguageOnSpacebar != 0) 329 inputView.setSpacebarTextFadeFactor(1.0f, keyboard); 330 // The language is always displayed when the delay is negative. 331 if (mConfigDelayBeforeFadeoutLanguageOnSpacebar > 0) { 332 sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard), 333 mConfigDelayBeforeFadeoutLanguageOnSpacebar); 334 } 335 } 336 } 337 } 338 339 @Override 340 public void onCreate() { 341 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 342 mPrefs = prefs; 343 LatinImeLogger.init(this, prefs); 344 SubtypeSwitcher.init(this, prefs); 345 KeyboardSwitcher.init(this, prefs); 346 347 super.onCreate(); 348 349 mImm = ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)); 350 mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 351 mKeyboardSwitcher = KeyboardSwitcher.getInstance(); 352 353 final Resources res = getResources(); 354 mResources = res; 355 mReCorrectionEnabled = prefs.getBoolean(Settings.PREF_RECORRECTION_ENABLED, 356 res.getBoolean(R.bool.default_recorrection_enabled)); 357 mConfigSwipeDownDismissKeyboardEnabled = res.getBoolean( 358 R.bool.config_swipe_down_dismiss_keyboard_enabled); 359 mConfigDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger( 360 R.integer.config_delay_before_fadeout_language_on_spacebar); 361 mConfigDurationOfFadeoutLanguageOnSpacebar = res.getInteger( 362 R.integer.config_duration_of_fadeout_language_on_spacebar); 363 mConfigFinalFadeoutFactorOfLanugageOnSpacebar = res.getInteger( 364 R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f; 365 366 Utils.GCUtils.getInstance().reset(); 367 boolean tryGC = true; 368 for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { 369 try { 370 initSuggest(); 371 tryGC = false; 372 } catch (OutOfMemoryError e) { 373 tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e); 374 } 375 } 376 377 mOrientation = res.getConfiguration().orientation; 378 initSuggestPuncList(); 379 380 // register to receive ringer mode changes for silent mode 381 IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); 382 registerReceiver(mReceiver, filter); 383 mVoiceConnector = VoiceIMEConnector.init(this, prefs, mHandler); 384 prefs.registerOnSharedPreferenceChangeListener(this); 385 } 386 387 /** 388 * Returns a main dictionary resource id 389 * @return main dictionary resource id 390 */ 391 public static int getMainDictionaryResourceId(Resources res) { 392 final String MAIN_DIC_NAME = "main"; 393 String packageName = LatinIME.class.getPackage().getName(); 394 return res.getIdentifier(MAIN_DIC_NAME, "raw", packageName); 395 } 396 397 private void initSuggest() { 398 updateAutoTextEnabled(); 399 String locale = mSubtypeSwitcher.getInputLocaleStr(); 400 401 Locale savedLocale = mSubtypeSwitcher.changeSystemLocale(new Locale(locale)); 402 if (mSuggest != null) { 403 mSuggest.close(); 404 } 405 final SharedPreferences prefs = mPrefs; 406 mQuickFixes = prefs.getBoolean(Settings.PREF_QUICK_FIXES, true); 407 408 final Resources res = mResources; 409 int mainDicResId = getMainDictionaryResourceId(res); 410 mSuggest = new Suggest(this, mainDicResId); 411 loadAndSetAutoCorrectionThreshold(prefs); 412 413 mUserDictionary = new UserDictionary(this, locale); 414 mSuggest.setUserDictionary(mUserDictionary); 415 416 mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS); 417 mSuggest.setContactsDictionary(mContactsDictionary); 418 419 mAutoDictionary = new AutoDictionary(this, this, locale, Suggest.DIC_AUTO); 420 mSuggest.setAutoDictionary(mAutoDictionary); 421 422 mUserBigramDictionary = new UserBigramDictionary(this, this, locale, Suggest.DIC_USER); 423 mSuggest.setUserBigramDictionary(mUserBigramDictionary); 424 425 updateCorrectionMode(); 426 mWordSeparators = res.getString(R.string.word_separators); 427 mSentenceSeparators = res.getString(R.string.sentence_separators); 428 429 mSubtypeSwitcher.changeSystemLocale(savedLocale); 430 } 431 432 @Override 433 public void onDestroy() { 434 if (mSuggest != null) { 435 mSuggest.close(); 436 mSuggest = null; 437 } 438 unregisterReceiver(mReceiver); 439 mVoiceConnector.destroy(); 440 LatinImeLogger.commit(); 441 LatinImeLogger.onDestroy(); 442 super.onDestroy(); 443 } 444 445 @Override 446 public void onConfigurationChanged(Configuration conf) { 447 mSubtypeSwitcher.onConfigurationChanged(conf); 448 // If orientation changed while predicting, commit the change 449 if (conf.orientation != mOrientation) { 450 InputConnection ic = getCurrentInputConnection(); 451 commitTyped(ic); 452 if (ic != null) ic.finishComposingText(); // For voice input 453 mOrientation = conf.orientation; 454 } 455 456 mConfigurationChanging = true; 457 super.onConfigurationChanged(conf); 458 mVoiceConnector.onConfigurationChanged(mConfigurationChanging); 459 mConfigurationChanging = false; 460 } 461 462 @Override 463 public View onCreateInputView() { 464 return mKeyboardSwitcher.onCreateInputView(); 465 } 466 467 @Override 468 public View onCreateCandidatesView() { 469 LayoutInflater inflater = getLayoutInflater(); 470 LinearLayout container = (LinearLayout)inflater.inflate(R.layout.candidates, null); 471 mCandidateViewContainer = container; 472 if (container.getPaddingRight() != 0) { 473 HorizontalScrollView scrollView = 474 (HorizontalScrollView) container.findViewById(R.id.candidates_scroll_view); 475 scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); 476 container.setGravity(Gravity.CENTER_HORIZONTAL); 477 } 478 mCandidateView = (CandidateView) container.findViewById(R.id.candidates); 479 mCandidateView.setService(this); 480 setCandidatesViewShown(true); 481 return container; 482 } 483 484 private static boolean isPasswordVariation(int variation) { 485 return variation == InputType.TYPE_TEXT_VARIATION_PASSWORD 486 || variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD 487 || variation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; 488 } 489 490 private static boolean isEmailVariation(int variation) { 491 return variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS 492 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; 493 } 494 495 @Override 496 public void onStartInputView(EditorInfo attribute, boolean restarting) { 497 final KeyboardSwitcher switcher = mKeyboardSwitcher; 498 LatinKeyboardView inputView = switcher.getInputView(); 499 500 // In landscape mode, this method gets called without the input view being created. 501 if (inputView == null) { 502 return; 503 } 504 505 mSubtypeSwitcher.updateParametersOnStartInputView(); 506 507 if (mRefreshKeyboardRequired) { 508 mRefreshKeyboardRequired = false; 509 onKeyboardLanguageChanged(); 510 } 511 512 TextEntryState.newSession(this); 513 514 // Most such things we decide below in the switch statement, but we need to know 515 // now whether this is a password text field, because we need to know now (before 516 // the switch statement) whether we want to enable the voice button. 517 int variation = attribute.inputType & InputType.TYPE_MASK_VARIATION; 518 mVoiceConnector.resetVoiceStates(isPasswordVariation(variation)); 519 mInputTypeNoAutoCorrect = false; 520 mIsSettingsSuggestionStripOn = false; 521 mApplicationSpecifiedCompletionOn = false; 522 mApplicationSpecifiedCompletions = null; 523 mEnteredText = null; 524 525 final int mode; 526 switch (attribute.inputType & InputType.TYPE_MASK_CLASS) { 527 case InputType.TYPE_CLASS_NUMBER: 528 case InputType.TYPE_CLASS_DATETIME: 529 mode = KeyboardId.MODE_NUMBER; 530 break; 531 case InputType.TYPE_CLASS_PHONE: 532 mode = KeyboardId.MODE_PHONE; 533 break; 534 case InputType.TYPE_CLASS_TEXT: 535 mIsSettingsSuggestionStripOn = true; 536 // Make sure that passwords are not displayed in candidate view 537 if (isPasswordVariation(variation)) { 538 mIsSettingsSuggestionStripOn = false; 539 } 540 if (isEmailVariation(variation) 541 || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { 542 mAutoSpace = false; 543 } else { 544 mAutoSpace = true; 545 } 546 if (isEmailVariation(variation)) { 547 mIsSettingsSuggestionStripOn = false; 548 mode = KeyboardId.MODE_EMAIL; 549 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 550 mIsSettingsSuggestionStripOn = false; 551 mode = KeyboardId.MODE_URL; 552 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { 553 mode = KeyboardId.MODE_IM; 554 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 555 mIsSettingsSuggestionStripOn = false; 556 mode = KeyboardId.MODE_TEXT; 557 } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { 558 mode = KeyboardId.MODE_WEB; 559 // If it's a browser edit field and auto correct is not ON explicitly, then 560 // disable auto correction, but keep suggestions on. 561 if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) { 562 mInputTypeNoAutoCorrect = true; 563 } 564 } else { 565 mode = KeyboardId.MODE_TEXT; 566 } 567 568 // If NO_SUGGESTIONS is set, don't do prediction. 569 if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) { 570 mIsSettingsSuggestionStripOn = false; 571 mInputTypeNoAutoCorrect = true; 572 } 573 // If it's not multiline and the autoCorrect flag is not set, then don't correct 574 if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 && 575 (attribute.inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) { 576 mInputTypeNoAutoCorrect = true; 577 } 578 if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { 579 mIsSettingsSuggestionStripOn = false; 580 mApplicationSpecifiedCompletionOn = isFullscreenMode(); 581 } 582 break; 583 default: 584 mode = KeyboardId.MODE_TEXT; 585 break; 586 } 587 inputView.closing(); 588 mComposing.setLength(0); 589 mHasValidSuggestions = false; 590 mDeleteCount = 0; 591 mJustAddedAutoSpace = false; 592 593 loadSettings(attribute); 594 if (mSubtypeSwitcher.isKeyboardMode()) { 595 switcher.loadKeyboard(mode, attribute.imeOptions, 596 mVoiceConnector.isVoiceButtonEnabled(), 597 mVoiceConnector.isVoiceButtonOnPrimary()); 598 switcher.updateShiftState(); 599 } 600 601 setCandidatesViewShownInternal(isCandidateStripVisible(), 602 false /* needsInputViewShown */ ); 603 // Delay updating suggestions because keyboard input view may not be shown at this point. 604 mHandler.postUpdateSuggestions(); 605 606 // If the dictionary is not big enough, don't auto correct 607 mHasDictionary = mSuggest.hasMainDictionary(); 608 609 updateCorrectionMode(); 610 611 inputView.setPreviewEnabled(mPopupOn); 612 inputView.setProximityCorrectionEnabled(true); 613 mIsSettingsSuggestionStripOn &= (mCorrectionMode > 0 || isShowingSuggestionsStrip()); 614 // If we just entered a text field, maybe it has some old text that requires correction 615 checkReCorrectionOnStart(); 616 inputView.setForeground(true); 617 618 mVoiceConnector.onStartInputView(inputView.getWindowToken()); 619 620 if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); 621 } 622 623 private void checkReCorrectionOnStart() { 624 if (!mReCorrectionEnabled) return; 625 626 final InputConnection ic = getCurrentInputConnection(); 627 if (ic == null) return; 628 // There could be a pending composing span. Clean it up first. 629 ic.finishComposingText(); 630 631 if (isShowingSuggestionsStrip() && isSuggestionsRequested()) { 632 // First get the cursor position. This is required by setOldSuggestions(), so that 633 // it can pass the correct range to setComposingRegion(). At this point, we don't 634 // have valid values for mLastSelectionStart/End because onUpdateSelection() has 635 // not been called yet. 636 ExtractedTextRequest etr = new ExtractedTextRequest(); 637 etr.token = 0; // anything is fine here 638 ExtractedText et = ic.getExtractedText(etr, 0); 639 if (et == null) return; 640 641 mLastSelectionStart = et.startOffset + et.selectionStart; 642 mLastSelectionEnd = et.startOffset + et.selectionEnd; 643 644 // Then look for possible corrections in a delayed fashion 645 if (!TextUtils.isEmpty(et.text) && isCursorTouchingWord()) { 646 mHandler.postUpdateOldSuggestions(); 647 } 648 } 649 } 650 651 @Override 652 public void onFinishInput() { 653 super.onFinishInput(); 654 655 LatinImeLogger.commit(); 656 mKeyboardSwitcher.onAutoCorrectionStateChanged(false); 657 658 mVoiceConnector.flushVoiceInputLogs(mConfigurationChanging); 659 660 KeyboardView inputView = mKeyboardSwitcher.getInputView(); 661 if (inputView != null) inputView.closing(); 662 if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites(); 663 if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); 664 } 665 666 @Override 667 public void onFinishInputView(boolean finishingInput) { 668 super.onFinishInputView(finishingInput); 669 KeyboardView inputView = mKeyboardSwitcher.getInputView(); 670 if (inputView != null) inputView.setForeground(false); 671 // Remove pending messages related to update suggestions 672 mHandler.cancelUpdateSuggestions(); 673 mHandler.cancelUpdateOldSuggestions(); 674 } 675 676 @Override 677 public void onUpdateExtractedText(int token, ExtractedText text) { 678 super.onUpdateExtractedText(token, text); 679 mVoiceConnector.showPunctuationHintIfNecessary(); 680 } 681 682 @Override 683 public void onUpdateSelection(int oldSelStart, int oldSelEnd, 684 int newSelStart, int newSelEnd, 685 int candidatesStart, int candidatesEnd) { 686 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 687 candidatesStart, candidatesEnd); 688 689 if (DEBUG) { 690 Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart 691 + ", ose=" + oldSelEnd 692 + ", nss=" + newSelStart 693 + ", nse=" + newSelEnd 694 + ", cs=" + candidatesStart 695 + ", ce=" + candidatesEnd); 696 } 697 698 mVoiceConnector.setCursorAndSelection(newSelEnd, newSelStart); 699 700 // If the current selection in the text view changes, we should 701 // clear whatever candidate text we have. 702 if ((((mComposing.length() > 0 && mHasValidSuggestions) 703 || mVoiceConnector.isVoiceInputHighlighted()) && (newSelStart != candidatesEnd 704 || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart)) { 705 mComposing.setLength(0); 706 mHasValidSuggestions = false; 707 mHandler.postUpdateSuggestions(); 708 TextEntryState.reset(); 709 InputConnection ic = getCurrentInputConnection(); 710 if (ic != null) { 711 ic.finishComposingText(); 712 } 713 mVoiceConnector.setVoiceInputHighlighted(false); 714 } else if (!mHasValidSuggestions && !mJustAccepted) { 715 switch (TextEntryState.getState()) { 716 case ACCEPTED_DEFAULT: 717 TextEntryState.reset(); 718 // $FALL-THROUGH$ 719 case SPACE_AFTER_PICKED: 720 mJustAddedAutoSpace = false; // The user moved the cursor. 721 break; 722 default: 723 break; 724 } 725 } 726 mJustAccepted = false; 727 mHandler.postUpdateShiftKeyState(); 728 729 // Make a note of the cursor position 730 mLastSelectionStart = newSelStart; 731 mLastSelectionEnd = newSelEnd; 732 733 if (mReCorrectionEnabled && isShowingSuggestionsStrip()) { 734 // Don't look for corrections if the keyboard is not visible 735 if (mKeyboardSwitcher.isInputViewShown()) { 736 // Check if we should go in or out of correction mode. 737 if (isSuggestionsRequested() && !mJustReverted 738 && (candidatesStart == candidatesEnd || newSelStart != oldSelStart 739 || TextEntryState.isCorrecting()) 740 && (newSelStart < newSelEnd - 1 || !mHasValidSuggestions)) { 741 if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) { 742 mHandler.postUpdateOldSuggestions(); 743 } else { 744 abortCorrection(false); 745 // Show the punctuation suggestions list if the current one is not 746 // and if not showing "Touch again to save". 747 if (mCandidateView != null && !isShowingPunctuationList() 748 && !mCandidateView.isShowingAddToDictionaryHint()) { 749 setPunctuationSuggestions(); 750 } 751 } 752 } 753 } 754 } 755 } 756 757 /** 758 * This is called when the user has clicked on the extracted text view, 759 * when running in fullscreen mode. The default implementation hides 760 * the candidates view when this happens, but only if the extracted text 761 * editor has a vertical scroll bar because its text doesn't fit. 762 * Here we override the behavior due to the possibility that a re-correction could 763 * cause the candidate strip to disappear and re-appear. 764 */ 765 @Override 766 public void onExtractedTextClicked() { 767 if (mReCorrectionEnabled && isSuggestionsRequested()) return; 768 769 super.onExtractedTextClicked(); 770 } 771 772 /** 773 * This is called when the user has performed a cursor movement in the 774 * extracted text view, when it is running in fullscreen mode. The default 775 * implementation hides the candidates view when a vertical movement 776 * happens, but only if the extracted text editor has a vertical scroll bar 777 * because its text doesn't fit. 778 * Here we override the behavior due to the possibility that a re-correction could 779 * cause the candidate strip to disappear and re-appear. 780 */ 781 @Override 782 public void onExtractedCursorMovement(int dx, int dy) { 783 if (mReCorrectionEnabled && isSuggestionsRequested()) return; 784 785 super.onExtractedCursorMovement(dx, dy); 786 } 787 788 @Override 789 public void hideWindow() { 790 LatinImeLogger.commit(); 791 mKeyboardSwitcher.onAutoCorrectionStateChanged(false); 792 793 if (TRACE) Debug.stopMethodTracing(); 794 if (mOptionsDialog != null && mOptionsDialog.isShowing()) { 795 mOptionsDialog.dismiss(); 796 mOptionsDialog = null; 797 } 798 mVoiceConnector.hideVoiceWindow(mConfigurationChanging); 799 mWordHistory.clear(); 800 super.hideWindow(); 801 TextEntryState.endSession(); 802 } 803 804 @Override 805 public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) { 806 if (DEBUG) { 807 Log.i(TAG, "Received completions:"); 808 final int count = (applicationSpecifiedCompletions != null) 809 ? applicationSpecifiedCompletions.length : 0; 810 for (int i = 0; i < count; i++) { 811 Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); 812 } 813 } 814 if (mApplicationSpecifiedCompletionOn) { 815 mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; 816 if (applicationSpecifiedCompletions == null) { 817 clearSuggestions(); 818 return; 819 } 820 821 SuggestedWords.Builder builder = new SuggestedWords.Builder() 822 .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions) 823 .setTypedWordValid(true) 824 .setHasMinimalSuggestion(true); 825 // When in fullscreen mode, show completions generated by the application 826 setSuggestions(builder.build()); 827 mBestWord = null; 828 setCandidatesViewShown(true); 829 } 830 } 831 832 private void setCandidatesViewShownInternal(boolean shown, boolean needsInputViewShown) { 833 // TODO: Remove this if we support candidates with hard keyboard 834 if (onEvaluateInputViewShown()) { 835 super.setCandidatesViewShown(shown 836 && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true)); 837 } 838 } 839 840 @Override 841 public void setCandidatesViewShown(boolean shown) { 842 setCandidatesViewShownInternal(shown, true /* needsInputViewShown */ ); 843 } 844 845 @Override 846 public void onComputeInsets(InputMethodService.Insets outInsets) { 847 super.onComputeInsets(outInsets); 848 if (!isFullscreenMode()) { 849 outInsets.contentTopInsets = outInsets.visibleTopInsets; 850 } 851 } 852 853 @Override 854 public boolean onEvaluateFullscreenMode() { 855 final Resources res = mResources; 856 DisplayMetrics dm = res.getDisplayMetrics(); 857 float displayHeight = dm.heightPixels; 858 // If the display is more than X inches high, don't go to fullscreen mode 859 float dimen = res.getDimension(R.dimen.max_height_for_fullscreen); 860 if (displayHeight > dimen) { 861 return false; 862 } else { 863 return super.onEvaluateFullscreenMode(); 864 } 865 } 866 867 @Override 868 public boolean onKeyDown(int keyCode, KeyEvent event) { 869 switch (keyCode) { 870 case KeyEvent.KEYCODE_BACK: 871 if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) { 872 if (mKeyboardSwitcher.getInputView().handleBack()) { 873 return true; 874 } 875 } 876 break; 877 } 878 return super.onKeyDown(keyCode, event); 879 } 880 881 @Override 882 public boolean onKeyUp(int keyCode, KeyEvent event) { 883 switch (keyCode) { 884 case KeyEvent.KEYCODE_DPAD_DOWN: 885 case KeyEvent.KEYCODE_DPAD_UP: 886 case KeyEvent.KEYCODE_DPAD_LEFT: 887 case KeyEvent.KEYCODE_DPAD_RIGHT: 888 // Enable shift key and DPAD to do selections 889 if (mKeyboardSwitcher.isInputViewShown() 890 && mKeyboardSwitcher.isShiftedOrShiftLocked()) { 891 KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(), 892 event.getAction(), event.getKeyCode(), event.getRepeatCount(), 893 event.getDeviceId(), event.getScanCode(), 894 KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); 895 InputConnection ic = getCurrentInputConnection(); 896 if (ic != null) 897 ic.sendKeyEvent(newEvent); 898 return true; 899 } 900 break; 901 } 902 return super.onKeyUp(keyCode, event); 903 } 904 905 public void commitTyped(InputConnection inputConnection) { 906 if (mHasValidSuggestions) { 907 mHasValidSuggestions = false; 908 if (mComposing.length() > 0) { 909 if (inputConnection != null) { 910 inputConnection.commitText(mComposing, 1); 911 } 912 mCommittedLength = mComposing.length(); 913 TextEntryState.acceptedTyped(mComposing); 914 addToDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED); 915 } 916 updateSuggestions(); 917 } 918 } 919 920 public boolean getCurrentAutoCapsState() { 921 InputConnection ic = getCurrentInputConnection(); 922 EditorInfo ei = getCurrentInputEditorInfo(); 923 if (mAutoCap && ic != null && ei != null && ei.inputType != InputType.TYPE_NULL) { 924 return ic.getCursorCapsMode(ei.inputType) != 0; 925 } 926 return false; 927 } 928 929 private void swapPunctuationAndSpace() { 930 final InputConnection ic = getCurrentInputConnection(); 931 if (ic == null) return; 932 CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); 933 if (lastTwo != null && lastTwo.length() == 2 934 && lastTwo.charAt(0) == Keyboard.CODE_SPACE 935 && isSentenceSeparator(lastTwo.charAt(1))) { 936 ic.beginBatchEdit(); 937 ic.deleteSurroundingText(2, 0); 938 ic.commitText(lastTwo.charAt(1) + " ", 1); 939 ic.endBatchEdit(); 940 mKeyboardSwitcher.updateShiftState(); 941 mJustAddedAutoSpace = true; 942 } 943 } 944 945 private void reswapPeriodAndSpace() { 946 final InputConnection ic = getCurrentInputConnection(); 947 if (ic == null) return; 948 CharSequence lastThree = ic.getTextBeforeCursor(3, 0); 949 if (lastThree != null && lastThree.length() == 3 950 && lastThree.charAt(0) == Keyboard.CODE_PERIOD 951 && lastThree.charAt(1) == Keyboard.CODE_SPACE 952 && lastThree.charAt(2) == Keyboard.CODE_PERIOD) { 953 ic.beginBatchEdit(); 954 ic.deleteSurroundingText(3, 0); 955 ic.commitText(" ..", 1); 956 ic.endBatchEdit(); 957 mKeyboardSwitcher.updateShiftState(); 958 } 959 } 960 961 private void doubleSpace() { 962 //if (!mAutoPunctuate) return; 963 if (mCorrectionMode == Suggest.CORRECTION_NONE) return; 964 final InputConnection ic = getCurrentInputConnection(); 965 if (ic == null) return; 966 CharSequence lastThree = ic.getTextBeforeCursor(3, 0); 967 if (lastThree != null && lastThree.length() == 3 968 && Character.isLetterOrDigit(lastThree.charAt(0)) 969 && lastThree.charAt(1) == Keyboard.CODE_SPACE 970 && lastThree.charAt(2) == Keyboard.CODE_SPACE) { 971 ic.beginBatchEdit(); 972 ic.deleteSurroundingText(2, 0); 973 ic.commitText(". ", 1); 974 ic.endBatchEdit(); 975 mKeyboardSwitcher.updateShiftState(); 976 mJustAddedAutoSpace = true; 977 } 978 } 979 980 private void maybeRemovePreviousPeriod(CharSequence text) { 981 final InputConnection ic = getCurrentInputConnection(); 982 if (ic == null) return; 983 984 // When the text's first character is '.', remove the previous period 985 // if there is one. 986 CharSequence lastOne = ic.getTextBeforeCursor(1, 0); 987 if (lastOne != null && lastOne.length() == 1 988 && lastOne.charAt(0) == Keyboard.CODE_PERIOD 989 && text.charAt(0) == Keyboard.CODE_PERIOD) { 990 ic.deleteSurroundingText(1, 0); 991 } 992 } 993 994 private void removeTrailingSpace() { 995 final InputConnection ic = getCurrentInputConnection(); 996 if (ic == null) return; 997 998 CharSequence lastOne = ic.getTextBeforeCursor(1, 0); 999 if (lastOne != null && lastOne.length() == 1 1000 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { 1001 ic.deleteSurroundingText(1, 0); 1002 } 1003 } 1004 1005 public boolean addWordToDictionary(String word) { 1006 mUserDictionary.addWord(word, 128); 1007 // Suggestion strip should be updated after the operation of adding word to the 1008 // user dictionary 1009 mHandler.postUpdateSuggestions(); 1010 return true; 1011 } 1012 1013 private boolean isAlphabet(int code) { 1014 if (Character.isLetter(code)) { 1015 return true; 1016 } else { 1017 return false; 1018 } 1019 } 1020 1021 private void onSettingsKeyPressed() { 1022 if (!isShowingOptionDialog()) { 1023 if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) { 1024 showOptionsMenu(); 1025 } else { 1026 launchSettings(); 1027 } 1028 } 1029 } 1030 1031 private void onSettingsKeyLongPressed() { 1032 if (!isShowingOptionDialog()) { 1033 if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) { 1034 mImm.showInputMethodPicker(); 1035 } else { 1036 launchSettings(); 1037 } 1038 } 1039 } 1040 1041 private boolean isShowingOptionDialog() { 1042 return mOptionsDialog != null && mOptionsDialog.isShowing(); 1043 } 1044 1045 // Implementation of {@link KeyboardActionListener}. 1046 @Override 1047 public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { 1048 long when = SystemClock.uptimeMillis(); 1049 if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { 1050 mDeleteCount = 0; 1051 } 1052 mLastKeyTime = when; 1053 KeyboardSwitcher switcher = mKeyboardSwitcher; 1054 final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); 1055 switch (primaryCode) { 1056 case Keyboard.CODE_DELETE: 1057 handleBackspace(); 1058 mDeleteCount++; 1059 LatinImeLogger.logOnDelete(); 1060 break; 1061 case Keyboard.CODE_SHIFT: 1062 // Shift key is handled in onPress() when device has distinct multi-touch panel. 1063 if (!distinctMultiTouch) 1064 switcher.toggleShift(); 1065 break; 1066 case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: 1067 // Symbol key is handled in onPress() when device has distinct multi-touch panel. 1068 if (!distinctMultiTouch) 1069 switcher.changeKeyboardMode(); 1070 break; 1071 case Keyboard.CODE_CANCEL: 1072 if (!isShowingOptionDialog()) { 1073 handleClose(); 1074 } 1075 break; 1076 case Keyboard.CODE_SETTINGS: 1077 onSettingsKeyPressed(); 1078 break; 1079 case Keyboard.CODE_SETTINGS_LONGPRESS: 1080 onSettingsKeyLongPressed(); 1081 break; 1082 case Keyboard.CODE_NEXT_LANGUAGE: 1083 toggleLanguage(false, true); 1084 break; 1085 case Keyboard.CODE_PREV_LANGUAGE: 1086 toggleLanguage(false, false); 1087 break; 1088 case Keyboard.CODE_CAPSLOCK: 1089 switcher.toggleCapsLock(); 1090 break; 1091 case Keyboard.CODE_VOICE: 1092 mSubtypeSwitcher.switchToShortcutIME(); 1093 break; 1094 case Keyboard.CODE_TAB: 1095 handleTab(); 1096 break; 1097 default: 1098 if (primaryCode != Keyboard.CODE_ENTER) { 1099 mJustAddedAutoSpace = false; 1100 } 1101 RingCharBuffer.getInstance().push((char)primaryCode, x, y); 1102 LatinImeLogger.logOnInputChar(); 1103 if (isWordSeparator(primaryCode)) { 1104 handleSeparator(primaryCode); 1105 } else { 1106 handleCharacter(primaryCode, keyCodes); 1107 } 1108 // Cancel the just reverted state 1109 mJustReverted = false; 1110 } 1111 switcher.onKey(primaryCode); 1112 // Reset after any single keystroke 1113 mEnteredText = null; 1114 } 1115 1116 @Override 1117 public void onTextInput(CharSequence text) { 1118 mVoiceConnector.commitVoiceInput(); 1119 InputConnection ic = getCurrentInputConnection(); 1120 if (ic == null) return; 1121 abortCorrection(false); 1122 ic.beginBatchEdit(); 1123 commitTyped(ic); 1124 maybeRemovePreviousPeriod(text); 1125 ic.commitText(text, 1); 1126 ic.endBatchEdit(); 1127 mKeyboardSwitcher.updateShiftState(); 1128 mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY); 1129 mJustReverted = false; 1130 mJustAddedAutoSpace = false; 1131 mEnteredText = text; 1132 } 1133 1134 @Override 1135 public void onCancelInput() { 1136 // User released a finger outside any key 1137 mKeyboardSwitcher.onCancelInput(); 1138 } 1139 1140 private void handleBackspace() { 1141 if (mVoiceConnector.logAndRevertVoiceInput()) return; 1142 boolean deleteChar = false; 1143 InputConnection ic = getCurrentInputConnection(); 1144 if (ic == null) return; 1145 1146 ic.beginBatchEdit(); 1147 1148 mVoiceConnector.handleBackspace(); 1149 1150 if (mHasValidSuggestions) { 1151 final int length = mComposing.length(); 1152 if (length > 0) { 1153 mComposing.delete(length - 1, length); 1154 mWord.deleteLast(); 1155 ic.setComposingText(mComposing, 1); 1156 if (mComposing.length() == 0) { 1157 mHasValidSuggestions = false; 1158 } 1159 mHandler.postUpdateSuggestions(); 1160 } else { 1161 ic.deleteSurroundingText(1, 0); 1162 } 1163 } else { 1164 deleteChar = true; 1165 } 1166 mHandler.postUpdateShiftKeyState(); 1167 TextEntryState.backspace(); 1168 if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) { 1169 revertLastWord(deleteChar); 1170 ic.endBatchEdit(); 1171 return; 1172 } else if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { 1173 ic.deleteSurroundingText(mEnteredText.length(), 0); 1174 } else if (deleteChar) { 1175 if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { 1176 // Go back to the suggestion mode if the user canceled the 1177 // "Touch again to save". 1178 // NOTE: In gerenal, we don't revert the word when backspacing 1179 // from a manual suggestion pick. We deliberately chose a 1180 // different behavior only in the case of picking the first 1181 // suggestion (typed word). It's intentional to have made this 1182 // inconsistent with backspacing after selecting other suggestions. 1183 revertLastWord(deleteChar); 1184 } else { 1185 sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); 1186 if (mDeleteCount > DELETE_ACCELERATE_AT) { 1187 sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); 1188 } 1189 } 1190 } 1191 mJustReverted = false; 1192 ic.endBatchEdit(); 1193 } 1194 1195 private void handleTab() { 1196 final int imeOptions = getCurrentInputEditorInfo().imeOptions; 1197 final int navigationFlags = 1198 EditorInfo.IME_FLAG_NAVIGATE_NEXT | EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS; 1199 if ((imeOptions & navigationFlags) == 0) { 1200 sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB); 1201 return; 1202 } 1203 1204 final InputConnection ic = getCurrentInputConnection(); 1205 if (ic == null) 1206 return; 1207 1208 // True if keyboard is in either chording shift or manual temporary upper case mode. 1209 final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase(); 1210 if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0 1211 && !isManualTemporaryUpperCase) { 1212 ic.performEditorAction(EditorInfo.IME_ACTION_NEXT); 1213 } else if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0 1214 && isManualTemporaryUpperCase) { 1215 ic.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 1216 } 1217 } 1218 1219 private void abortCorrection(boolean force) { 1220 if (force || TextEntryState.isCorrecting()) { 1221 TextEntryState.onAbortCorrection(); 1222 setCandidatesViewShown(isCandidateStripVisible()); 1223 getCurrentInputConnection().finishComposingText(); 1224 clearSuggestions(); 1225 } 1226 } 1227 1228 private void handleCharacter(int primaryCode, int[] keyCodes) { 1229 mVoiceConnector.handleCharacter(); 1230 1231 if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) { 1232 abortCorrection(false); 1233 } 1234 1235 int code = primaryCode; 1236 if (isAlphabet(code) && isSuggestionsRequested() && !isCursorTouchingWord()) { 1237 if (!mHasValidSuggestions) { 1238 mHasValidSuggestions = true; 1239 mComposing.setLength(0); 1240 saveWordInHistory(mBestWord); 1241 mWord.reset(); 1242 } 1243 } 1244 KeyboardSwitcher switcher = mKeyboardSwitcher; 1245 if (switcher.isShiftedOrShiftLocked()) { 1246 if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT 1247 || keyCodes[0] > Character.MAX_CODE_POINT) { 1248 return; 1249 } 1250 code = keyCodes[0]; 1251 if (switcher.isAlphabetMode() && Character.isLowerCase(code)) { 1252 int upperCaseCode = Character.toUpperCase(code); 1253 if (upperCaseCode != code) { 1254 code = upperCaseCode; 1255 } else { 1256 // Some keys, such as [eszett], have upper case as multi-characters. 1257 String upperCase = new String(new int[] {code}, 0, 1).toUpperCase(); 1258 onTextInput(upperCase); 1259 return; 1260 } 1261 } 1262 } 1263 if (mHasValidSuggestions) { 1264 if (mComposing.length() == 0 && switcher.isAlphabetMode() 1265 && switcher.isShiftedOrShiftLocked()) { 1266 mWord.setFirstCharCapitalized(true); 1267 } 1268 mComposing.append((char) code); 1269 mWord.add(code, keyCodes); 1270 InputConnection ic = getCurrentInputConnection(); 1271 if (ic != null) { 1272 // If it's the first letter, make note of auto-caps state 1273 if (mWord.size() == 1) { 1274 mWord.setAutoCapitalized(getCurrentAutoCapsState()); 1275 } 1276 ic.setComposingText(mComposing, 1); 1277 } 1278 mHandler.postUpdateSuggestions(); 1279 } else { 1280 sendKeyChar((char)code); 1281 } 1282 switcher.updateShiftState(); 1283 if (LatinIME.PERF_DEBUG) measureCps(); 1284 TextEntryState.typedCharacter((char) code, isWordSeparator(code)); 1285 } 1286 1287 private void handleSeparator(int primaryCode) { 1288 mVoiceConnector.handleSeparator(); 1289 1290 // Should dismiss the "Touch again to save" message when handling separator 1291 if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { 1292 mHandler.postUpdateSuggestions(); 1293 } 1294 1295 boolean pickedDefault = false; 1296 // Handle separator 1297 final InputConnection ic = getCurrentInputConnection(); 1298 if (ic != null) { 1299 ic.beginBatchEdit(); 1300 abortCorrection(false); 1301 } 1302 if (mHasValidSuggestions) { 1303 // In certain languages where single quote is a separator, it's better 1304 // not to auto correct, but accept the typed word. For instance, 1305 // in Italian dov' should not be expanded to dove' because the elision 1306 // requires the last vowel to be removed. 1307 if (mAutoCorrectOn && primaryCode != '\'' && !mJustReverted) { 1308 pickedDefault = pickDefaultSuggestion(); 1309 // Picked the suggestion by the space key. We consider this 1310 // as "added an auto space". 1311 if (primaryCode == Keyboard.CODE_SPACE) { 1312 mJustAddedAutoSpace = true; 1313 } 1314 } else { 1315 commitTyped(ic); 1316 } 1317 } 1318 if (mJustAddedAutoSpace && primaryCode == Keyboard.CODE_ENTER) { 1319 removeTrailingSpace(); 1320 mJustAddedAutoSpace = false; 1321 } 1322 sendKeyChar((char)primaryCode); 1323 1324 // Handle the case of ". ." -> " .." with auto-space if necessary 1325 // before changing the TextEntryState. 1326 if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED 1327 && primaryCode == Keyboard.CODE_PERIOD) { 1328 reswapPeriodAndSpace(); 1329 } 1330 1331 TextEntryState.typedCharacter((char) primaryCode, true); 1332 if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED 1333 && primaryCode != Keyboard.CODE_ENTER) { 1334 swapPunctuationAndSpace(); 1335 } else if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) { 1336 doubleSpace(); 1337 } 1338 if (pickedDefault) { 1339 CharSequence typedWord = mWord.getTypedWord(); 1340 TextEntryState.backToAcceptedDefault(typedWord); 1341 if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) { 1342 if (ic != null) { 1343 CorrectionInfo correctionInfo = new CorrectionInfo( 1344 mLastSelectionEnd - typedWord.length(), typedWord, mBestWord); 1345 ic.commitCorrection(correctionInfo); 1346 } 1347 if (mCandidateView != null) 1348 mCandidateView.onAutoCorrectionInverted(mBestWord); 1349 } 1350 setPunctuationSuggestions(); 1351 } 1352 mKeyboardSwitcher.updateShiftState(); 1353 if (ic != null) { 1354 ic.endBatchEdit(); 1355 } 1356 } 1357 1358 private void handleClose() { 1359 commitTyped(getCurrentInputConnection()); 1360 mVoiceConnector.handleClose(); 1361 requestHideSelf(0); 1362 LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); 1363 if (inputView != null) 1364 inputView.closing(); 1365 TextEntryState.endSession(); 1366 } 1367 1368 private void saveWordInHistory(CharSequence result) { 1369 if (mWord.size() <= 1) { 1370 mWord.reset(); 1371 return; 1372 } 1373 // Skip if result is null. It happens in some edge case. 1374 if (TextUtils.isEmpty(result)) { 1375 return; 1376 } 1377 1378 // Make a copy of the CharSequence, since it is/could be a mutable CharSequence 1379 final String resultCopy = result.toString(); 1380 TypedWordAlternatives entry = new TypedWordAlternatives(resultCopy, 1381 new WordComposer(mWord)); 1382 mWordHistory.add(entry); 1383 } 1384 1385 private boolean isSuggestionsRequested() { 1386 return mIsSettingsSuggestionStripOn; 1387 } 1388 1389 private boolean isShowingPunctuationList() { 1390 return mSuggestPuncList == mCandidateView.getSuggestions(); 1391 } 1392 1393 private boolean isShowingSuggestionsStrip() { 1394 return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE) 1395 || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE 1396 && mOrientation == Configuration.ORIENTATION_PORTRAIT); 1397 } 1398 1399 private boolean isCandidateStripVisible() { 1400 if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isCorrecting()) 1401 return true; 1402 if (!isShowingSuggestionsStrip()) 1403 return false; 1404 if (mApplicationSpecifiedCompletionOn) 1405 return true; 1406 return isSuggestionsRequested(); 1407 } 1408 1409 public void switchToKeyboardView() { 1410 mHandler.post(new Runnable() { 1411 @Override 1412 public void run() { 1413 if (DEBUG) { 1414 Log.d(TAG, "Switch to keyboard view."); 1415 } 1416 View v = mKeyboardSwitcher.getInputView(); 1417 if (v != null) { 1418 // Confirms that the keyboard view doesn't have parent view. 1419 ViewParent p = v.getParent(); 1420 if (p != null && p instanceof ViewGroup) { 1421 ((ViewGroup) p).removeView(v); 1422 } 1423 setInputView(v); 1424 } 1425 setCandidatesViewShown(isCandidateStripVisible()); 1426 updateInputViewShown(); 1427 mHandler.postUpdateSuggestions(); 1428 } 1429 }); 1430 } 1431 1432 public void clearSuggestions() { 1433 setSuggestions(SuggestedWords.EMPTY); 1434 } 1435 1436 public void setSuggestions(SuggestedWords words) { 1437 if (mVoiceConnector.getAndResetIsShowingHint()) { 1438 setCandidatesView(mCandidateViewContainer); 1439 } 1440 1441 if (mCandidateView != null) { 1442 mCandidateView.setSuggestions(words); 1443 if (mCandidateView.isConfigCandidateHighlightFontColorEnabled()) { 1444 mKeyboardSwitcher.onAutoCorrectionStateChanged( 1445 words.hasWordAboveAutoCorrectionScoreThreshold()); 1446 } 1447 } 1448 } 1449 1450 public void updateSuggestions() { 1451 mKeyboardSwitcher.setPreferredLetters(null); 1452 1453 // Check if we have a suggestion engine attached. 1454 if ((mSuggest == null || !isSuggestionsRequested()) 1455 && !mVoiceConnector.isVoiceInputHighlighted()) { 1456 return; 1457 } 1458 1459 if (!mHasValidSuggestions) { 1460 setPunctuationSuggestions(); 1461 return; 1462 } 1463 showSuggestions(mWord); 1464 } 1465 1466 private SuggestedWords.Builder getTypedSuggestions(WordComposer word) { 1467 return mSuggest.getSuggestedWordBuilder(mKeyboardSwitcher.getInputView(), word, null); 1468 } 1469 1470 private void showCorrections(WordAlternatives alternatives) { 1471 mKeyboardSwitcher.setPreferredLetters(null); 1472 SuggestedWords.Builder builder = alternatives.getAlternatives(); 1473 builder.setTypedWordValid(false).setHasMinimalSuggestion(false); 1474 showSuggestions(builder.build(), alternatives.getOriginalWord()); 1475 } 1476 1477 private void showSuggestions(WordComposer word) { 1478 // TODO Maybe need better way of retrieving previous word 1479 CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(), 1480 mWordSeparators); 1481 SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder( 1482 mKeyboardSwitcher.getInputView(), word, prevWord); 1483 1484 int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies(); 1485 mKeyboardSwitcher.setPreferredLetters(nextLettersFrequencies); 1486 1487 boolean correctionAvailable = !mInputTypeNoAutoCorrect && !mJustReverted 1488 && mSuggest.hasMinimalCorrection(); 1489 final CharSequence typedWord = word.getTypedWord(); 1490 // If we're in basic correct 1491 final boolean typedWordValid = mSuggest.isValidWord(typedWord) || 1492 (preferCapitalization() 1493 && mSuggest.isValidWord(typedWord.toString().toLowerCase())); 1494 if (mCorrectionMode == Suggest.CORRECTION_FULL 1495 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { 1496 correctionAvailable |= typedWordValid; 1497 } 1498 // Don't auto-correct words with multiple capital letter 1499 correctionAvailable &= !word.isMostlyCaps(); 1500 correctionAvailable &= !TextEntryState.isCorrecting(); 1501 1502 // Basically, we update the suggestion strip only when suggestion count > 1. However, 1503 // there is an exception: We update the suggestion strip whenever typed word's length 1504 // is 1, regardless of suggestion count. Actually, in most cases, suggestion count is 1 1505 // when typed word's length is 1, but we do always need to clear the previous state when 1506 // the user starts typing a word (i.e. typed word's length == 1). 1507 if (typedWord.length() == 1 || builder.size() > 1 1508 || mCandidateView.isShowingAddToDictionaryHint()) { 1509 builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion(correctionAvailable); 1510 } else { 1511 final SuggestedWords previousSuggestions = mCandidateView.getSuggestions(); 1512 if (previousSuggestions == mSuggestPuncList) 1513 return; 1514 builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions); 1515 } 1516 showSuggestions(builder.build(), typedWord); 1517 } 1518 1519 private void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) { 1520 setSuggestions(suggestedWords); 1521 if (suggestedWords.size() > 0) { 1522 if (suggestedWords.hasAutoCorrectionWord()) { 1523 mBestWord = suggestedWords.getWord(1); 1524 } else { 1525 mBestWord = typedWord; 1526 } 1527 } else { 1528 mBestWord = null; 1529 } 1530 setCandidatesViewShown(isCandidateStripVisible()); 1531 } 1532 1533 private boolean pickDefaultSuggestion() { 1534 // Complete any pending candidate query first 1535 if (mHandler.hasPendingUpdateSuggestions()) { 1536 mHandler.cancelUpdateSuggestions(); 1537 updateSuggestions(); 1538 } 1539 if (mBestWord != null && mBestWord.length() > 0) { 1540 TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); 1541 mJustAccepted = true; 1542 pickSuggestion(mBestWord); 1543 // Add the word to the auto dictionary if it's not a known word 1544 addToDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); 1545 return true; 1546 1547 } 1548 return false; 1549 } 1550 1551 public void pickSuggestionManually(int index, CharSequence suggestion) { 1552 SuggestedWords suggestions = mCandidateView.getSuggestions(); 1553 mVoiceConnector.flushAndLogAllTextModificationCounters(index, suggestion, mWordSeparators); 1554 1555 final boolean correcting = TextEntryState.isCorrecting(); 1556 InputConnection ic = getCurrentInputConnection(); 1557 if (ic != null) { 1558 ic.beginBatchEdit(); 1559 } 1560 if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null 1561 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 1562 CompletionInfo ci = mApplicationSpecifiedCompletions[index]; 1563 if (ic != null) { 1564 ic.commitCompletion(ci); 1565 } 1566 mCommittedLength = suggestion.length(); 1567 if (mCandidateView != null) { 1568 mCandidateView.clear(); 1569 } 1570 mKeyboardSwitcher.updateShiftState(); 1571 if (ic != null) { 1572 ic.endBatchEdit(); 1573 } 1574 return; 1575 } 1576 1577 // If this is a punctuation, apply it through the normal key press 1578 if (suggestion.length() == 1 && (isWordSeparator(suggestion.charAt(0)) 1579 || isSuggestedPunctuation(suggestion.charAt(0)))) { 1580 // Word separators are suggested before the user inputs something. 1581 // So, LatinImeLogger logs "" as a user's input. 1582 LatinImeLogger.logOnManualSuggestion( 1583 "", suggestion.toString(), index, suggestions.mWords); 1584 final char primaryCode = suggestion.charAt(0); 1585 onCodeInput(primaryCode, new int[] { primaryCode }, 1586 KeyboardActionListener.NOT_A_TOUCH_COORDINATE, 1587 KeyboardActionListener.NOT_A_TOUCH_COORDINATE); 1588 if (ic != null) { 1589 ic.endBatchEdit(); 1590 } 1591 return; 1592 } 1593 mJustAccepted = true; 1594 pickSuggestion(suggestion); 1595 // Add the word to the auto dictionary if it's not a known word 1596 if (index == 0) { 1597 addToDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); 1598 } else { 1599 addToBigramDictionary(suggestion, 1); 1600 } 1601 LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(), 1602 index, suggestions.mWords); 1603 TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); 1604 // Follow it with a space 1605 if (mAutoSpace && !correcting) { 1606 sendSpace(); 1607 mJustAddedAutoSpace = true; 1608 } 1609 1610 final boolean showingAddToDictionaryHint = index == 0 && mCorrectionMode > 0 1611 && !mSuggest.isValidWord(suggestion) 1612 && !mSuggest.isValidWord(suggestion.toString().toLowerCase()); 1613 1614 if (!correcting) { 1615 // Fool the state watcher so that a subsequent backspace will not do a revert, unless 1616 // we just did a correction, in which case we need to stay in 1617 // TextEntryState.State.PICKED_SUGGESTION state. 1618 TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true); 1619 setPunctuationSuggestions(); 1620 } else if (!showingAddToDictionaryHint) { 1621 // If we're not showing the "Touch again to save", then show corrections again. 1622 // In case the cursor position doesn't change, make sure we show the suggestions again. 1623 clearSuggestions(); 1624 mHandler.postUpdateOldSuggestions(); 1625 } 1626 if (showingAddToDictionaryHint) { 1627 mCandidateView.showAddToDictionaryHint(suggestion); 1628 } 1629 if (ic != null) { 1630 ic.endBatchEdit(); 1631 } 1632 } 1633 1634 /** 1635 * Commits the chosen word to the text field and saves it for later 1636 * retrieval. 1637 * @param suggestion the suggestion picked by the user to be committed to 1638 * the text field 1639 */ 1640 private void pickSuggestion(CharSequence suggestion) { 1641 KeyboardSwitcher switcher = mKeyboardSwitcher; 1642 if (!switcher.isKeyboardAvailable()) 1643 return; 1644 InputConnection ic = getCurrentInputConnection(); 1645 if (ic != null) { 1646 mVoiceConnector.rememberReplacedWord(suggestion, mWordSeparators); 1647 ic.commitText(suggestion, 1); 1648 } 1649 saveWordInHistory(suggestion); 1650 mHasValidSuggestions = false; 1651 mCommittedLength = suggestion.length(); 1652 switcher.setPreferredLetters(null); 1653 } 1654 1655 /** 1656 * Tries to apply any typed alternatives for the word if we have any cached alternatives, 1657 * otherwise tries to find new corrections and completions for the word. 1658 * @param touching The word that the cursor is touching, with position information 1659 * @return true if an alternative was found, false otherwise. 1660 */ 1661 private boolean applyTypedAlternatives(EditingUtils.SelectedWord touching) { 1662 // If we didn't find a match, search for result in typed word history 1663 WordComposer foundWord = null; 1664 WordAlternatives alternatives = null; 1665 for (WordAlternatives entry : mWordHistory) { 1666 if (TextUtils.equals(entry.getChosenWord(), touching.mWord)) { 1667 if (entry instanceof TypedWordAlternatives) { 1668 foundWord = ((TypedWordAlternatives) entry).word; 1669 } 1670 alternatives = entry; 1671 break; 1672 } 1673 } 1674 // If we didn't find a match, at least suggest corrections. 1675 if (foundWord == null 1676 && (mSuggest.isValidWord(touching.mWord) 1677 || mSuggest.isValidWord(touching.mWord.toString().toLowerCase()))) { 1678 foundWord = new WordComposer(); 1679 for (int i = 0; i < touching.mWord.length(); i++) { 1680 foundWord.add(touching.mWord.charAt(i), new int[] { 1681 touching.mWord.charAt(i) 1682 }); 1683 } 1684 foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.mWord.charAt(0))); 1685 } 1686 // Found a match, show suggestions 1687 if (foundWord != null || alternatives != null) { 1688 if (alternatives == null) { 1689 alternatives = new TypedWordAlternatives(touching.mWord, foundWord); 1690 } 1691 showCorrections(alternatives); 1692 if (foundWord != null) { 1693 mWord = new WordComposer(foundWord); 1694 } else { 1695 mWord.reset(); 1696 } 1697 return true; 1698 } 1699 return false; 1700 } 1701 1702 private void setOldSuggestions() { 1703 mVoiceConnector.setShowingVoiceSuggestions(false); 1704 if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) { 1705 return; 1706 } 1707 InputConnection ic = getCurrentInputConnection(); 1708 if (ic == null) return; 1709 if (!mHasValidSuggestions) { 1710 // Extract the selected or touching text 1711 EditingUtils.SelectedWord touching = EditingUtils.getWordAtCursorOrSelection(ic, 1712 mLastSelectionStart, mLastSelectionEnd, mWordSeparators); 1713 1714 if (touching != null && touching.mWord.length() > 1) { 1715 ic.beginBatchEdit(); 1716 1717 if (!mVoiceConnector.applyVoiceAlternatives(touching) 1718 && !applyTypedAlternatives(touching)) { 1719 abortCorrection(true); 1720 } else { 1721 TextEntryState.selectedForCorrection(); 1722 EditingUtils.underlineWord(ic, touching); 1723 } 1724 1725 ic.endBatchEdit(); 1726 } else { 1727 abortCorrection(true); 1728 setPunctuationSuggestions(); // Show the punctuation suggestions list 1729 } 1730 } else { 1731 abortCorrection(true); 1732 } 1733 } 1734 1735 private void setPunctuationSuggestions() { 1736 setSuggestions(mSuggestPuncList); 1737 setCandidatesViewShown(isCandidateStripVisible()); 1738 } 1739 1740 private void addToDictionaries(CharSequence suggestion, int frequencyDelta) { 1741 checkAddToDictionary(suggestion, frequencyDelta, false); 1742 } 1743 1744 private void addToBigramDictionary(CharSequence suggestion, int frequencyDelta) { 1745 checkAddToDictionary(suggestion, frequencyDelta, true); 1746 } 1747 1748 /** 1749 * Adds to the UserBigramDictionary and/or AutoDictionary 1750 * @param addToBigramDictionary true if it should be added to bigram dictionary if possible 1751 */ 1752 private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta, 1753 boolean addToBigramDictionary) { 1754 if (suggestion == null || suggestion.length() < 1) return; 1755 // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be 1756 // adding words in situations where the user or application really didn't 1757 // want corrections enabled or learned. 1758 if (!(mCorrectionMode == Suggest.CORRECTION_FULL 1759 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { 1760 return; 1761 } 1762 if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion) 1763 || (!mSuggest.isValidWord(suggestion.toString()) 1764 && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) { 1765 mAutoDictionary.addWord(suggestion.toString(), frequencyDelta); 1766 } 1767 1768 if (mUserBigramDictionary != null) { 1769 CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(), 1770 mSentenceSeparators); 1771 if (!TextUtils.isEmpty(prevWord)) { 1772 mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString()); 1773 } 1774 } 1775 } 1776 1777 private boolean isCursorTouchingWord() { 1778 InputConnection ic = getCurrentInputConnection(); 1779 if (ic == null) return false; 1780 CharSequence toLeft = ic.getTextBeforeCursor(1, 0); 1781 CharSequence toRight = ic.getTextAfterCursor(1, 0); 1782 if (!TextUtils.isEmpty(toLeft) 1783 && !isWordSeparator(toLeft.charAt(0)) 1784 && !isSuggestedPunctuation(toLeft.charAt(0))) { 1785 return true; 1786 } 1787 if (!TextUtils.isEmpty(toRight) 1788 && !isWordSeparator(toRight.charAt(0)) 1789 && !isSuggestedPunctuation(toRight.charAt(0))) { 1790 return true; 1791 } 1792 return false; 1793 } 1794 1795 private boolean sameAsTextBeforeCursor(InputConnection ic, CharSequence text) { 1796 CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0); 1797 return TextUtils.equals(text, beforeText); 1798 } 1799 1800 public void revertLastWord(boolean deleteChar) { 1801 final int length = mComposing.length(); 1802 if (!mHasValidSuggestions && length > 0) { 1803 final InputConnection ic = getCurrentInputConnection(); 1804 mHasValidSuggestions = true; 1805 mJustReverted = true; 1806 if (deleteChar) ic.deleteSurroundingText(1, 0); 1807 int toDelete = mCommittedLength; 1808 CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); 1809 if (toTheLeft != null && toTheLeft.length() > 0 1810 && isWordSeparator(toTheLeft.charAt(0))) { 1811 toDelete--; 1812 } 1813 ic.deleteSurroundingText(toDelete, 0); 1814 ic.setComposingText(mComposing, 1); 1815 TextEntryState.backspace(); 1816 mHandler.postUpdateSuggestions(); 1817 } else { 1818 sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); 1819 mJustReverted = false; 1820 } 1821 } 1822 1823 protected String getWordSeparators() { 1824 return mWordSeparators; 1825 } 1826 1827 public boolean isWordSeparator(int code) { 1828 String separators = getWordSeparators(); 1829 return separators.contains(String.valueOf((char)code)); 1830 } 1831 1832 private boolean isSentenceSeparator(int code) { 1833 return mSentenceSeparators.contains(String.valueOf((char)code)); 1834 } 1835 1836 private void sendSpace() { 1837 sendKeyChar((char)Keyboard.CODE_SPACE); 1838 mKeyboardSwitcher.updateShiftState(); 1839 } 1840 1841 public boolean preferCapitalization() { 1842 return mWord.isFirstCharCapitalized(); 1843 } 1844 1845 // Notify that Language has been changed and toggleLanguage will update KeyboaredID according 1846 // to new Language. 1847 public void onKeyboardLanguageChanged() { 1848 toggleLanguage(true, true); 1849 } 1850 1851 // "reset" and "next" are used only for USE_SPACEBAR_LANGUAGE_SWITCHER. 1852 private void toggleLanguage(boolean reset, boolean next) { 1853 if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()) { 1854 mSubtypeSwitcher.toggleLanguage(reset, next); 1855 } 1856 // Reload keyboard because the current language has been changed. 1857 KeyboardSwitcher switcher = mKeyboardSwitcher; 1858 final int mode = switcher.getKeyboardMode(); 1859 final EditorInfo attribute = getCurrentInputEditorInfo(); 1860 final int imeOptions = (attribute != null) ? attribute.imeOptions : 0; 1861 switcher.loadKeyboard(mode, imeOptions, mVoiceConnector.isVoiceButtonEnabled(), 1862 mVoiceConnector.isVoiceButtonOnPrimary()); 1863 initSuggest(); 1864 switcher.updateShiftState(); 1865 } 1866 1867 @Override 1868 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, 1869 String key) { 1870 mSubtypeSwitcher.onSharedPreferenceChanged(sharedPreferences, key); 1871 if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) { 1872 mRefreshKeyboardRequired = true; 1873 } else if (Settings.PREF_RECORRECTION_ENABLED.equals(key)) { 1874 mReCorrectionEnabled = sharedPreferences.getBoolean( 1875 Settings.PREF_RECORRECTION_ENABLED, 1876 mResources.getBoolean(R.bool.default_recorrection_enabled)); 1877 } 1878 } 1879 1880 @Override 1881 public void onSwipeDown() { 1882 if (mConfigSwipeDownDismissKeyboardEnabled) 1883 handleClose(); 1884 } 1885 1886 @Override 1887 public void onPress(int primaryCode) { 1888 if (mKeyboardSwitcher.isVibrateAndSoundFeedbackRequired()) { 1889 vibrate(); 1890 playKeyClick(primaryCode); 1891 } 1892 KeyboardSwitcher switcher = mKeyboardSwitcher; 1893 final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); 1894 if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { 1895 switcher.onPressShift(); 1896 } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { 1897 switcher.onPressSymbol(); 1898 } else { 1899 switcher.onOtherKeyPressed(); 1900 } 1901 } 1902 1903 @Override 1904 public void onRelease(int primaryCode) { 1905 KeyboardSwitcher switcher = mKeyboardSwitcher; 1906 // Reset any drag flags in the keyboard 1907 switcher.keyReleased(); 1908 final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); 1909 if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { 1910 switcher.onReleaseShift(); 1911 } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { 1912 switcher.onReleaseSymbol(); 1913 } 1914 } 1915 1916 1917 // receive ringer mode changes to detect silent mode 1918 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 1919 @Override 1920 public void onReceive(Context context, Intent intent) { 1921 updateRingerMode(); 1922 } 1923 }; 1924 1925 // update flags for silent mode 1926 private void updateRingerMode() { 1927 if (mAudioManager == null) { 1928 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 1929 } 1930 if (mAudioManager != null) { 1931 mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); 1932 } 1933 } 1934 1935 private void playKeyClick(int primaryCode) { 1936 // if mAudioManager is null, we don't have the ringer state yet 1937 // mAudioManager will be set by updateRingerMode 1938 if (mAudioManager == null) { 1939 if (mKeyboardSwitcher.getInputView() != null) { 1940 updateRingerMode(); 1941 } 1942 } 1943 if (mSoundOn && !mSilentMode) { 1944 // FIXME: Volume and enable should come from UI settings 1945 // FIXME: These should be triggered after auto-repeat logic 1946 int sound = AudioManager.FX_KEYPRESS_STANDARD; 1947 switch (primaryCode) { 1948 case Keyboard.CODE_DELETE: 1949 sound = AudioManager.FX_KEYPRESS_DELETE; 1950 break; 1951 case Keyboard.CODE_ENTER: 1952 sound = AudioManager.FX_KEYPRESS_RETURN; 1953 break; 1954 case Keyboard.CODE_SPACE: 1955 sound = AudioManager.FX_KEYPRESS_SPACEBAR; 1956 break; 1957 } 1958 mAudioManager.playSoundEffect(sound, FX_VOLUME); 1959 } 1960 } 1961 1962 public void vibrate() { 1963 if (!mVibrateOn) { 1964 return; 1965 } 1966 LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); 1967 if (inputView != null) { 1968 inputView.performHapticFeedback( 1969 HapticFeedbackConstants.KEYBOARD_TAP, 1970 HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); 1971 } 1972 } 1973 1974 public void promoteToUserDictionary(String word, int frequency) { 1975 if (mUserDictionary.isValidWord(word)) return; 1976 mUserDictionary.addWord(word, frequency); 1977 } 1978 1979 public WordComposer getCurrentWord() { 1980 return mWord; 1981 } 1982 1983 public boolean getPopupOn() { 1984 return mPopupOn; 1985 } 1986 1987 private void updateCorrectionMode() { 1988 mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false; 1989 mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes) 1990 && !mInputTypeNoAutoCorrect && mHasDictionary; 1991 mCorrectionMode = (mAutoCorrectOn && mAutoCorrectEnabled) 1992 ? Suggest.CORRECTION_FULL 1993 : (mAutoCorrectOn ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); 1994 mCorrectionMode = (mBigramSuggestionEnabled && mAutoCorrectOn && mAutoCorrectEnabled) 1995 ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; 1996 if (mSuggest != null) { 1997 mSuggest.setCorrectionMode(mCorrectionMode); 1998 } 1999 } 2000 2001 private void updateAutoTextEnabled() { 2002 if (mSuggest == null) return; 2003 mSuggest.setAutoTextEnabled(mQuickFixes 2004 && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage()); 2005 } 2006 2007 private void updateSuggestionVisibility(SharedPreferences prefs) { 2008 final Resources res = mResources; 2009 final String suggestionVisiblityStr = prefs.getString( 2010 Settings.PREF_SHOW_SUGGESTIONS_SETTING, 2011 res.getString(R.string.prefs_suggestion_visibility_default_value)); 2012 for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) { 2013 if (suggestionVisiblityStr.equals(res.getString(visibility))) { 2014 mSuggestionVisibility = visibility; 2015 break; 2016 } 2017 } 2018 } 2019 2020 protected void launchSettings() { 2021 launchSettings(Settings.class); 2022 } 2023 2024 public void launchDebugSettings() { 2025 launchSettings(DebugSettings.class); 2026 } 2027 2028 protected void launchSettings(Class<? extends PreferenceActivity> settingsClass) { 2029 handleClose(); 2030 Intent intent = new Intent(); 2031 intent.setClass(LatinIME.this, settingsClass); 2032 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 2033 startActivity(intent); 2034 } 2035 2036 private void loadSettings(EditorInfo attribute) { 2037 // Get the settings preferences 2038 final SharedPreferences prefs = mPrefs; 2039 Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); 2040 mVibrateOn = vibrator != null && vibrator.hasVibrator() 2041 && prefs.getBoolean(Settings.PREF_VIBRATE_ON, false); 2042 mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, false); 2043 mPopupOn = prefs.getBoolean(Settings.PREF_POPUP_ON, 2044 mResources.getBoolean(R.bool.config_default_popup_preview)); 2045 mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true); 2046 mQuickFixes = prefs.getBoolean(Settings.PREF_QUICK_FIXES, true); 2047 2048 mAutoCorrectEnabled = isAutoCorrectEnabled(prefs); 2049 mBigramSuggestionEnabled = mAutoCorrectEnabled && isBigramSuggestionEnabled(prefs); 2050 loadAndSetAutoCorrectionThreshold(prefs); 2051 2052 mVoiceConnector.loadSettings(attribute, prefs); 2053 2054 updateCorrectionMode(); 2055 updateAutoTextEnabled(); 2056 updateSuggestionVisibility(prefs); 2057 SubtypeSwitcher.getInstance().loadSettings(); 2058 } 2059 2060 /** 2061 * Load Auto correction threshold from SharedPreferences, and modify mSuggest's threshold. 2062 */ 2063 private void loadAndSetAutoCorrectionThreshold(SharedPreferences sp) { 2064 // When mSuggest is not initialized, cannnot modify mSuggest's threshold. 2065 if (mSuggest == null) return; 2066 // When auto correction setting is turned off, the threshold is ignored. 2067 if (!isAutoCorrectEnabled(sp)) return; 2068 2069 final String currentAutoCorrectionSetting = sp.getString( 2070 Settings.PREF_AUTO_CORRECTION_THRESHOLD, 2071 mResources.getString(R.string.auto_correction_threshold_mode_index_modest)); 2072 final String[] autoCorrectionThresholdValues = mResources.getStringArray( 2073 R.array.auto_correction_threshold_values); 2074 // When autoCrrectionThreshold is greater than 1.0, auto correction is virtually turned off. 2075 double autoCorrectionThreshold = Double.MAX_VALUE; 2076 try { 2077 final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting); 2078 if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) { 2079 autoCorrectionThreshold = Double.parseDouble( 2080 autoCorrectionThresholdValues[arrayIndex]); 2081 } 2082 } catch (NumberFormatException e) { 2083 // Whenever the threshold settings are correct, never come here. 2084 autoCorrectionThreshold = Double.MAX_VALUE; 2085 Log.w(TAG, "Cannot load auto correction threshold setting." 2086 + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting 2087 + ", autoCorrectionThresholdValues: " 2088 + Arrays.toString(autoCorrectionThresholdValues)); 2089 } 2090 // TODO: This should be refactored : 2091 // setAutoCorrectionThreshold should be called outside of this method. 2092 mSuggest.setAutoCorrectionThreshold(autoCorrectionThreshold); 2093 } 2094 2095 private boolean isAutoCorrectEnabled(SharedPreferences sp) { 2096 final String currentAutoCorrectionSetting = sp.getString( 2097 Settings.PREF_AUTO_CORRECTION_THRESHOLD, 2098 mResources.getString(R.string.auto_correction_threshold_mode_index_modest)); 2099 final String autoCorrectionOff = mResources.getString( 2100 R.string.auto_correction_threshold_mode_index_off); 2101 return !currentAutoCorrectionSetting.equals(autoCorrectionOff); 2102 } 2103 2104 private boolean isBigramSuggestionEnabled(SharedPreferences sp) { 2105 // TODO: Define default value instead of 'true'. 2106 return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTIONS, true); 2107 } 2108 2109 private void initSuggestPuncList() { 2110 if (mSuggestPuncs != null || mSuggestPuncList != null) 2111 return; 2112 SuggestedWords.Builder builder = new SuggestedWords.Builder(); 2113 String puncs = mResources.getString(R.string.suggested_punctuations); 2114 if (puncs != null) { 2115 for (int i = 0; i < puncs.length(); i++) { 2116 builder.addWord(puncs.subSequence(i, i + 1)); 2117 } 2118 } 2119 mSuggestPuncList = builder.build(); 2120 mSuggestPuncs = puncs; 2121 } 2122 2123 private boolean isSuggestedPunctuation(int code) { 2124 return mSuggestPuncs.contains(String.valueOf((char)code)); 2125 } 2126 2127 private void showOptionsMenu() { 2128 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2129 builder.setCancelable(true); 2130 builder.setIcon(R.drawable.ic_dialog_keyboard); 2131 builder.setNegativeButton(android.R.string.cancel, null); 2132 CharSequence itemSettings = getString(R.string.english_ime_settings); 2133 CharSequence itemInputMethod = getString(R.string.selectInputMethod); 2134 builder.setItems(new CharSequence[] { 2135 itemInputMethod, itemSettings}, 2136 new DialogInterface.OnClickListener() { 2137 2138 @Override 2139 public void onClick(DialogInterface di, int position) { 2140 di.dismiss(); 2141 switch (position) { 2142 case POS_SETTINGS: 2143 launchSettings(); 2144 break; 2145 case POS_METHOD: 2146 mImm.showInputMethodPicker(); 2147 break; 2148 } 2149 } 2150 }); 2151 builder.setTitle(mResources.getString(R.string.english_ime_input_options)); 2152 mOptionsDialog = builder.create(); 2153 Window window = mOptionsDialog.getWindow(); 2154 WindowManager.LayoutParams lp = window.getAttributes(); 2155 lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); 2156 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 2157 window.setAttributes(lp); 2158 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 2159 mOptionsDialog.show(); 2160 } 2161 2162 @Override 2163 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { 2164 super.dump(fd, fout, args); 2165 2166 final Printer p = new PrintWriterPrinter(fout); 2167 p.println("LatinIME state :"); 2168 p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode()); 2169 p.println(" mComposing=" + mComposing.toString()); 2170 p.println(" mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn); 2171 p.println(" mCorrectionMode=" + mCorrectionMode); 2172 p.println(" mHasValidSuggestions=" + mHasValidSuggestions); 2173 p.println(" mAutoCorrectOn=" + mAutoCorrectOn); 2174 p.println(" mAutoSpace=" + mAutoSpace); 2175 p.println(" mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn); 2176 p.println(" TextEntryState.state=" + TextEntryState.getState()); 2177 p.println(" mSoundOn=" + mSoundOn); 2178 p.println(" mVibrateOn=" + mVibrateOn); 2179 p.println(" mPopupOn=" + mPopupOn); 2180 } 2181 2182 // Characters per second measurement 2183 2184 private long mLastCpsTime; 2185 private static final int CPS_BUFFER_SIZE = 16; 2186 private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE]; 2187 private int mCpsIndex; 2188 2189 private void measureCps() { 2190 long now = System.currentTimeMillis(); 2191 if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial 2192 mCpsIntervals[mCpsIndex] = now - mLastCpsTime; 2193 mLastCpsTime = now; 2194 mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE; 2195 long total = 0; 2196 for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i]; 2197 System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total)); 2198 } 2199 2200 @Override 2201 public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) { 2202 SubtypeSwitcher.getInstance().updateSubtype(subtype); 2203 } 2204} 2205