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