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