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