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