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