LatinIME.java revision 406d192a9e8f07ed6c6a408650feb0a757ca388e
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.Message; 33import android.os.SystemClock; 34import android.preference.PreferenceActivity; 35import android.preference.PreferenceManager; 36import android.text.InputType; 37import android.text.TextUtils; 38import android.util.Log; 39import android.util.PrintWriterPrinter; 40import android.util.Printer; 41import android.view.HapticFeedbackConstants; 42import android.view.KeyEvent; 43import android.view.View; 44import android.view.ViewGroup; 45import android.view.ViewParent; 46import android.view.inputmethod.CompletionInfo; 47import android.view.inputmethod.EditorInfo; 48import android.view.inputmethod.ExtractedText; 49import android.view.inputmethod.InputConnection; 50 51import com.android.inputmethod.accessibility.AccessibilityUtils; 52import com.android.inputmethod.compat.CompatUtils; 53import com.android.inputmethod.compat.EditorInfoCompatUtils; 54import com.android.inputmethod.compat.InputConnectionCompatUtils; 55import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; 56import com.android.inputmethod.compat.InputMethodServiceCompatWrapper; 57import com.android.inputmethod.compat.InputTypeCompatUtils; 58import com.android.inputmethod.compat.SuggestionSpanUtils; 59import com.android.inputmethod.compat.VibratorCompatWrapper; 60import com.android.inputmethod.deprecated.LanguageSwitcherProxy; 61import com.android.inputmethod.deprecated.VoiceProxy; 62import com.android.inputmethod.keyboard.Key; 63import com.android.inputmethod.keyboard.Keyboard; 64import com.android.inputmethod.keyboard.KeyboardActionListener; 65import com.android.inputmethod.keyboard.KeyboardId; 66import com.android.inputmethod.keyboard.KeyboardSwitcher; 67import com.android.inputmethod.keyboard.KeyboardView; 68import com.android.inputmethod.keyboard.LatinKeyboard; 69import com.android.inputmethod.keyboard.LatinKeyboardView; 70 71import java.io.FileDescriptor; 72import java.io.PrintWriter; 73import java.util.Locale; 74 75/** 76 * Input method implementation for Qwerty'ish keyboard. 77 */ 78public class LatinIME extends InputMethodServiceCompatWrapper implements KeyboardActionListener, 79 SuggestionsView.Listener { 80 private static final String TAG = LatinIME.class.getSimpleName(); 81 private static final boolean TRACE = false; 82 private static boolean DEBUG; 83 84 /** 85 * The private IME option used to indicate that no microphone should be 86 * shown for a given text field. For instance, this is specified by the 87 * search dialog when the dialog is already showing a voice search button. 88 * 89 * @deprecated Use {@link LatinIME#IME_OPTION_NO_MICROPHONE} with package name prefixed. 90 */ 91 @SuppressWarnings("dep-ann") 92 public static final String IME_OPTION_NO_MICROPHONE_COMPAT = "nm"; 93 94 /** 95 * The private IME option used to indicate that no microphone should be 96 * shown for a given text field. For instance, this is specified by the 97 * search dialog when the dialog is already showing a voice search button. 98 */ 99 public static final String IME_OPTION_NO_MICROPHONE = "noMicrophoneKey"; 100 101 /** 102 * The private IME option used to indicate that no settings key should be 103 * shown for a given text field. 104 */ 105 public static final String IME_OPTION_NO_SETTINGS_KEY = "noSettingsKey"; 106 107 /** 108 * The private IME option used to indicate that the given text field needs 109 * ASCII code points input. 110 */ 111 public static final String IME_OPTION_FORCE_ASCII = "forceAscii"; 112 113 /** 114 * The subtype extra value used to indicate that the subtype keyboard layout is capable for 115 * typing ASCII characters. 116 */ 117 public static final String SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE = "AsciiCapable"; 118 119 /** 120 * The subtype extra value used to indicate that the subtype keyboard layout supports touch 121 * position correction. 122 */ 123 public static final String SUBTYPE_EXTRA_VALUE_SUPPORT_TOUCH_POSITION_CORRECTION = 124 "SupportTouchPositionCorrection"; 125 /** 126 * The subtype extra value used to indicate that the subtype keyboard layout should be loaded 127 * from the specified locale. 128 */ 129 public static final String SUBTYPE_EXTRA_VALUE_KEYBOARD_LOCALE = "KeyboardLocale"; 130 131 private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; 132 133 // How many continuous deletes at which to start deleting at a higher speed. 134 private static final int DELETE_ACCELERATE_AT = 20; 135 // Key events coming any faster than this are long-presses. 136 private static final int QUICK_PRESS = 200; 137 138 private static final int PENDING_IMS_CALLBACK_DURATION = 800; 139 140 /** 141 * The name of the scheme used by the Package Manager to warn of a new package installation, 142 * replacement or removal. 143 */ 144 private static final String SCHEME_PACKAGE = "package"; 145 146 // TODO: migrate this to SettingsValues 147 private int mSuggestionVisibility; 148 private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE 149 = R.string.prefs_suggestion_visibility_show_value; 150 private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE 151 = R.string.prefs_suggestion_visibility_show_only_portrait_value; 152 private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE 153 = R.string.prefs_suggestion_visibility_hide_value; 154 155 private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] { 156 SUGGESTION_VISIBILILTY_SHOW_VALUE, 157 SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE, 158 SUGGESTION_VISIBILILTY_HIDE_VALUE 159 }; 160 161 // Magic space: a space that should disappear on space/apostrophe insertion, move after the 162 // punctuation on punctuation insertion, and become a real space on alpha char insertion. 163 // Weak space: a space that should be swapped only by suggestion strip punctuation. 164 // Double space: the state where the user pressed space twice quickly, which LatinIME 165 // resolved as period-space. Undoing this converts the period to a space. 166 // Swap punctuation: the state where a (weak or magic) space and a punctuation from the 167 // suggestion strip have just been swapped. Undoing this swaps them back. 168 private static final int SPACE_STATE_NONE = 0; 169 private static final int SPACE_STATE_DOUBLE = 1; 170 private static final int SPACE_STATE_SWAP_PUNCTUATION = 2; 171 private static final int SPACE_STATE_MAGIC = 3; 172 private static final int SPACE_STATE_WEAK = 4; 173 174 // Current space state of the input method. This can be any of the above constants. 175 private int mSpaceState; 176 177 private SettingsValues mSettingsValues; 178 179 private View mExtractArea; 180 private View mKeyPreviewBackingView; 181 private View mSuggestionsContainer; 182 private SuggestionsView mSuggestionsView; 183 private Suggest mSuggest; 184 private CompletionInfo[] mApplicationSpecifiedCompletions; 185 186 private InputMethodManagerCompatWrapper mImm; 187 private Resources mResources; 188 private SharedPreferences mPrefs; 189 private String mInputMethodId; 190 private KeyboardSwitcher mKeyboardSwitcher; 191 private SubtypeSwitcher mSubtypeSwitcher; 192 private VoiceProxy mVoiceProxy; 193 194 private UserDictionary mUserDictionary; 195 private UserBigramDictionary mUserBigramDictionary; 196 private UserUnigramDictionary mUserUnigramDictionary; 197 private boolean mIsUserDictionaryAvailable; 198 199 // TODO: Create an inner class to group options and pseudo-options to improve readability. 200 // These variables are initialized according to the {@link EditorInfo#inputType}. 201 private boolean mInsertSpaceOnPickSuggestionManually; 202 private boolean mInputTypeNoAutoCorrect; 203 private boolean mIsSettingsSuggestionStripOn; 204 private boolean mApplicationSpecifiedCompletionOn; 205 206 private WordComposer mWordComposer = new WordComposer(); 207 private CharSequence mBestWord; 208 private boolean mHasUncommittedTypedChars; 209 210 private int mCorrectionMode; 211 private int mCommittedLength; 212 private String mWordSavedForAutoCorrectCancellation; 213 // Keep track of the last selection range to decide if we need to show word alternatives 214 private int mLastSelectionStart; 215 private int mLastSelectionEnd; 216 217 // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't 218 // "expect" it, it means the user actually moved the cursor. 219 private boolean mExpectingUpdateSelection; 220 private int mDeleteCount; 221 private long mLastKeyTime; 222 223 private AudioManager mAudioManager; 224 private boolean mSilentModeOn; // System-wide current configuration 225 226 private VibratorCompatWrapper mVibrator; 227 228 // TODO: Move this flag to VoiceProxy 229 private boolean mConfigurationChanging; 230 231 // Member variables for remembering the current device orientation. 232 private int mDisplayOrientation; 233 234 // Object for reacting to adding/removing a dictionary pack. 235 private BroadcastReceiver mDictionaryPackInstallReceiver = 236 new DictionaryPackInstallBroadcastReceiver(this); 237 238 // Keeps track of most recently inserted text (multi-character key) for reverting 239 private CharSequence mEnteredText; 240 241 private final ComposingStateManager mComposingStateManager = 242 ComposingStateManager.getInstance(); 243 244 public final UIHandler mHandler = new UIHandler(this); 245 246 public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> { 247 private static final int MSG_UPDATE_SUGGESTIONS = 0; 248 private static final int MSG_UPDATE_SHIFT_STATE = 1; 249 private static final int MSG_VOICE_RESULTS = 2; 250 private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 3; 251 private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 4; 252 private static final int MSG_SPACE_TYPED = 5; 253 private static final int MSG_SET_BIGRAM_PREDICTIONS = 6; 254 private static final int MSG_PENDING_IMS_CALLBACK = 7; 255 256 private int mDelayBeforeFadeoutLanguageOnSpacebar; 257 private int mDelayUpdateSuggestions; 258 private int mDelayUpdateShiftState; 259 private int mDurationOfFadeoutLanguageOnSpacebar; 260 private float mFinalFadeoutFactorOfLanguageOnSpacebar; 261 private long mDoubleSpacesTurnIntoPeriodTimeout; 262 263 public UIHandler(LatinIME outerInstance) { 264 super(outerInstance); 265 } 266 267 public void onCreate() { 268 final Resources res = getOuterInstance().getResources(); 269 mDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger( 270 R.integer.config_delay_before_fadeout_language_on_spacebar); 271 mDelayUpdateSuggestions = 272 res.getInteger(R.integer.config_delay_update_suggestions); 273 mDelayUpdateShiftState = 274 res.getInteger(R.integer.config_delay_update_shift_state); 275 mDurationOfFadeoutLanguageOnSpacebar = res.getInteger( 276 R.integer.config_duration_of_fadeout_language_on_spacebar); 277 mFinalFadeoutFactorOfLanguageOnSpacebar = res.getInteger( 278 R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f; 279 mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger( 280 R.integer.config_double_spaces_turn_into_period_timeout); 281 } 282 283 @Override 284 public void handleMessage(Message msg) { 285 final LatinIME latinIme = getOuterInstance(); 286 final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; 287 final LatinKeyboardView inputView = switcher.getKeyboardView(); 288 switch (msg.what) { 289 case MSG_UPDATE_SUGGESTIONS: 290 latinIme.updateSuggestions(); 291 break; 292 case MSG_UPDATE_SHIFT_STATE: 293 switcher.updateShiftState(); 294 break; 295 case MSG_SET_BIGRAM_PREDICTIONS: 296 latinIme.updateBigramPredictions(); 297 break; 298 case MSG_VOICE_RESULTS: 299 latinIme.mVoiceProxy.handleVoiceResults(latinIme.preferCapitalization() 300 || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked())); 301 break; 302 case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR: 303 if (inputView != null) { 304 inputView.setSpacebarTextFadeFactor( 305 (1.0f + mFinalFadeoutFactorOfLanguageOnSpacebar) / 2, 306 (LatinKeyboard)msg.obj); 307 } 308 sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj), 309 mDurationOfFadeoutLanguageOnSpacebar); 310 break; 311 case MSG_DISMISS_LANGUAGE_ON_SPACEBAR: 312 if (inputView != null) { 313 inputView.setSpacebarTextFadeFactor(mFinalFadeoutFactorOfLanguageOnSpacebar, 314 (LatinKeyboard)msg.obj); 315 } 316 break; 317 } 318 } 319 320 public void postUpdateSuggestions() { 321 removeMessages(MSG_UPDATE_SUGGESTIONS); 322 sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions); 323 } 324 325 public void cancelUpdateSuggestions() { 326 removeMessages(MSG_UPDATE_SUGGESTIONS); 327 } 328 329 public boolean hasPendingUpdateSuggestions() { 330 return hasMessages(MSG_UPDATE_SUGGESTIONS); 331 } 332 333 public void postUpdateShiftKeyState() { 334 removeMessages(MSG_UPDATE_SHIFT_STATE); 335 sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState); 336 } 337 338 public void cancelUpdateShiftState() { 339 removeMessages(MSG_UPDATE_SHIFT_STATE); 340 } 341 342 public void postUpdateBigramPredictions() { 343 removeMessages(MSG_SET_BIGRAM_PREDICTIONS); 344 sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions); 345 } 346 347 public void cancelUpdateBigramPredictions() { 348 removeMessages(MSG_SET_BIGRAM_PREDICTIONS); 349 } 350 351 public void updateVoiceResults() { 352 sendMessage(obtainMessage(MSG_VOICE_RESULTS)); 353 } 354 355 public void startDisplayLanguageOnSpacebar(boolean localeChanged) { 356 final LatinIME latinIme = getOuterInstance(); 357 removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR); 358 removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR); 359 final LatinKeyboardView inputView = latinIme.mKeyboardSwitcher.getKeyboardView(); 360 if (inputView != null) { 361 final LatinKeyboard keyboard = latinIme.mKeyboardSwitcher.getLatinKeyboard(); 362 // The language is always displayed when the delay is negative. 363 final boolean needsToDisplayLanguage = localeChanged 364 || mDelayBeforeFadeoutLanguageOnSpacebar < 0; 365 // The language is never displayed when the delay is zero. 366 if (mDelayBeforeFadeoutLanguageOnSpacebar != 0) { 367 inputView.setSpacebarTextFadeFactor(needsToDisplayLanguage ? 1.0f 368 : mFinalFadeoutFactorOfLanguageOnSpacebar, 369 keyboard); 370 } 371 // The fadeout animation will start when the delay is positive. 372 if (localeChanged && mDelayBeforeFadeoutLanguageOnSpacebar > 0) { 373 sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard), 374 mDelayBeforeFadeoutLanguageOnSpacebar); 375 } 376 } 377 } 378 379 public void startDoubleSpacesTimer() { 380 removeMessages(MSG_SPACE_TYPED); 381 sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), mDoubleSpacesTurnIntoPeriodTimeout); 382 } 383 384 public void cancelDoubleSpacesTimer() { 385 removeMessages(MSG_SPACE_TYPED); 386 } 387 388 public boolean isAcceptingDoubleSpaces() { 389 return hasMessages(MSG_SPACE_TYPED); 390 } 391 392 // Working variables for the following methods. 393 private boolean mIsOrientationChanging; 394 private boolean mPendingSuccesiveImsCallback; 395 private boolean mHasPendingStartInput; 396 private boolean mHasPendingFinishInputView; 397 private boolean mHasPendingFinishInput; 398 private EditorInfo mAppliedEditorInfo; 399 400 public void startOrientationChanging() { 401 removeMessages(MSG_PENDING_IMS_CALLBACK); 402 resetPendingImsCallback(); 403 mIsOrientationChanging = true; 404 final LatinIME latinIme = getOuterInstance(); 405 if (latinIme.isInputViewShown()) { 406 latinIme.mKeyboardSwitcher.saveKeyboardState(); 407 } 408 } 409 410 private void resetPendingImsCallback() { 411 mHasPendingFinishInputView = false; 412 mHasPendingFinishInput = false; 413 mHasPendingStartInput = false; 414 } 415 416 private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo, 417 boolean restarting) { 418 if (mHasPendingFinishInputView) 419 latinIme.onFinishInputViewInternal(mHasPendingFinishInput); 420 if (mHasPendingFinishInput) 421 latinIme.onFinishInputInternal(); 422 if (mHasPendingStartInput) 423 latinIme.onStartInputInternal(editorInfo, restarting); 424 resetPendingImsCallback(); 425 } 426 427 public void onStartInput(EditorInfo editorInfo, boolean restarting) { 428 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 429 // Typically this is the second onStartInput after orientation changed. 430 mHasPendingStartInput = true; 431 } else { 432 if (mIsOrientationChanging && restarting) { 433 // This is the first onStartInput after orientation changed. 434 mIsOrientationChanging = false; 435 mPendingSuccesiveImsCallback = true; 436 } 437 final LatinIME latinIme = getOuterInstance(); 438 executePendingImsCallback(latinIme, editorInfo, restarting); 439 latinIme.onStartInputInternal(editorInfo, restarting); 440 } 441 } 442 443 public void onStartInputView(EditorInfo editorInfo, boolean restarting) { 444 if (hasMessages(MSG_PENDING_IMS_CALLBACK) 445 && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { 446 // Typically this is the second onStartInputView after orientation changed. 447 resetPendingImsCallback(); 448 } else { 449 if (mPendingSuccesiveImsCallback) { 450 // This is the first onStartInputView after orientation changed. 451 mPendingSuccesiveImsCallback = false; 452 resetPendingImsCallback(); 453 sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), 454 PENDING_IMS_CALLBACK_DURATION); 455 } 456 final LatinIME latinIme = getOuterInstance(); 457 executePendingImsCallback(latinIme, editorInfo, restarting); 458 latinIme.onStartInputViewInternal(editorInfo, restarting); 459 mAppliedEditorInfo = editorInfo; 460 } 461 } 462 463 public void onFinishInputView(boolean finishingInput) { 464 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 465 // Typically this is the first onFinishInputView after orientation changed. 466 mHasPendingFinishInputView = true; 467 } else { 468 final LatinIME latinIme = getOuterInstance(); 469 latinIme.onFinishInputViewInternal(finishingInput); 470 mAppliedEditorInfo = null; 471 } 472 } 473 474 public void onFinishInput() { 475 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 476 // Typically this is the first onFinishInput after orientation changed. 477 mHasPendingFinishInput = true; 478 } else { 479 final LatinIME latinIme = getOuterInstance(); 480 executePendingImsCallback(latinIme, null, false); 481 latinIme.onFinishInputInternal(); 482 } 483 } 484 } 485 486 @Override 487 public void onCreate() { 488 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 489 mPrefs = prefs; 490 LatinImeLogger.init(this, prefs); 491 LanguageSwitcherProxy.init(this, prefs); 492 InputMethodManagerCompatWrapper.init(this); 493 SubtypeSwitcher.init(this); 494 KeyboardSwitcher.init(this, prefs); 495 AccessibilityUtils.init(this, prefs); 496 497 super.onCreate(); 498 499 mImm = InputMethodManagerCompatWrapper.getInstance(); 500 mInputMethodId = Utils.getInputMethodId(mImm, getPackageName()); 501 mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 502 mKeyboardSwitcher = KeyboardSwitcher.getInstance(); 503 mVibrator = VibratorCompatWrapper.getInstance(this); 504 mHandler.onCreate(); 505 DEBUG = LatinImeLogger.sDBG; 506 507 final Resources res = getResources(); 508 mResources = res; 509 510 loadSettings(); 511 512 Utils.GCUtils.getInstance().reset(); 513 boolean tryGC = true; 514 for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { 515 try { 516 initSuggest(); 517 tryGC = false; 518 } catch (OutOfMemoryError e) { 519 tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e); 520 } 521 } 522 523 mDisplayOrientation = res.getConfiguration().orientation; 524 525 // Register to receive ringer mode change and network state change. 526 // Also receive installation and removal of a dictionary pack. 527 final IntentFilter filter = new IntentFilter(); 528 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 529 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 530 registerReceiver(mReceiver, filter); 531 mVoiceProxy = VoiceProxy.init(this, prefs, mHandler); 532 533 final IntentFilter packageFilter = new IntentFilter(); 534 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 535 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 536 packageFilter.addDataScheme(SCHEME_PACKAGE); 537 registerReceiver(mDictionaryPackInstallReceiver, packageFilter); 538 539 final IntentFilter newDictFilter = new IntentFilter(); 540 newDictFilter.addAction( 541 DictionaryPackInstallBroadcastReceiver.NEW_DICTIONARY_INTENT_ACTION); 542 registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); 543 } 544 545 // Has to be package-visible for unit tests 546 /* package */ void loadSettings() { 547 if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 548 if (null == mSubtypeSwitcher) mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 549 mSettingsValues = new SettingsValues(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr()); 550 resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); 551 } 552 553 private void initSuggest() { 554 final String localeStr = mSubtypeSwitcher.getInputLocaleStr(); 555 final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr); 556 557 final Resources res = mResources; 558 final Locale savedLocale = LocaleUtils.setSystemLocale(res, keyboardLocale); 559 final ContactsDictionary oldContactsDictionary; 560 if (mSuggest != null) { 561 oldContactsDictionary = mSuggest.getContactsDictionary(); 562 mSuggest.close(); 563 } else { 564 oldContactsDictionary = null; 565 } 566 567 int mainDicResId = Utils.getMainDictionaryResourceId(res); 568 mSuggest = new Suggest(this, mainDicResId, keyboardLocale); 569 if (mSettingsValues.mAutoCorrectEnabled) { 570 mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold); 571 } 572 573 mUserDictionary = new UserDictionary(this, localeStr); 574 mSuggest.setUserDictionary(mUserDictionary); 575 mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); 576 577 resetContactsDictionary(oldContactsDictionary); 578 579 mUserUnigramDictionary 580 = new UserUnigramDictionary(this, this, localeStr, Suggest.DIC_USER_UNIGRAM); 581 mSuggest.setUserUnigramDictionary(mUserUnigramDictionary); 582 583 mUserBigramDictionary 584 = new UserBigramDictionary(this, this, localeStr, Suggest.DIC_USER_BIGRAM); 585 mSuggest.setUserBigramDictionary(mUserBigramDictionary); 586 587 updateCorrectionMode(); 588 589 LocaleUtils.setSystemLocale(res, savedLocale); 590 } 591 592 /** 593 * Resets the contacts dictionary in mSuggest according to the user settings. 594 * 595 * This method takes an optional contacts dictionary to use. Since the contacts dictionary 596 * does not depend on the locale, it can be reused across different instances of Suggest. 597 * The dictionary will also be opened or closed as necessary depending on the settings. 598 * 599 * @param oldContactsDictionary an optional dictionary to use, or null 600 */ 601 private void resetContactsDictionary(final ContactsDictionary oldContactsDictionary) { 602 final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict); 603 604 final ContactsDictionary dictionaryToUse; 605 if (!shouldSetDictionary) { 606 // Make sure the dictionary is closed. If it is already closed, this is a no-op, 607 // so it's safe to call it anyways. 608 if (null != oldContactsDictionary) oldContactsDictionary.close(); 609 dictionaryToUse = null; 610 } else if (null != oldContactsDictionary) { 611 // Make sure the old contacts dictionary is opened. If it is already open, this is a 612 // no-op, so it's safe to call it anyways. 613 oldContactsDictionary.reopen(this); 614 dictionaryToUse = oldContactsDictionary; 615 } else { 616 dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS); 617 } 618 619 if (null != mSuggest) { 620 mSuggest.setContactsDictionary(dictionaryToUse); 621 } 622 } 623 624 /* package private */ void resetSuggestMainDict() { 625 final String localeStr = mSubtypeSwitcher.getInputLocaleStr(); 626 final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr); 627 int mainDicResId = Utils.getMainDictionaryResourceId(mResources); 628 mSuggest.resetMainDict(this, mainDicResId, keyboardLocale); 629 } 630 631 @Override 632 public void onDestroy() { 633 if (mSuggest != null) { 634 mSuggest.close(); 635 mSuggest = null; 636 } 637 unregisterReceiver(mReceiver); 638 unregisterReceiver(mDictionaryPackInstallReceiver); 639 mVoiceProxy.destroy(); 640 LatinImeLogger.commit(); 641 LatinImeLogger.onDestroy(); 642 super.onDestroy(); 643 } 644 645 @Override 646 public void onConfigurationChanged(Configuration conf) { 647 mSubtypeSwitcher.onConfigurationChanged(conf); 648 mComposingStateManager.onFinishComposingText(); 649 // If orientation changed while predicting, commit the change 650 if (mDisplayOrientation != conf.orientation) { 651 mDisplayOrientation = conf.orientation; 652 mHandler.startOrientationChanging(); 653 final InputConnection ic = getCurrentInputConnection(); 654 commitTyped(ic); 655 if (ic != null) ic.finishComposingText(); // For voice input 656 if (isShowingOptionDialog()) 657 mOptionsDialog.dismiss(); 658 } 659 660 mConfigurationChanging = true; 661 super.onConfigurationChanged(conf); 662 mVoiceProxy.onConfigurationChanged(conf); 663 mConfigurationChanging = false; 664 665 // This will work only when the subtype is not supported. 666 LanguageSwitcherProxy.onConfigurationChanged(conf); 667 } 668 669 @Override 670 public View onCreateInputView() { 671 return mKeyboardSwitcher.onCreateInputView(); 672 } 673 674 @Override 675 public void setInputView(View view) { 676 super.setInputView(view); 677 mExtractArea = getWindow().getWindow().getDecorView() 678 .findViewById(android.R.id.extractArea); 679 mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); 680 mSuggestionsContainer = view.findViewById(R.id.suggestions_container); 681 mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view); 682 if (mSuggestionsView != null) 683 mSuggestionsView.setListener(this, view); 684 if (LatinImeLogger.sVISUALDEBUG) { 685 mKeyPreviewBackingView.setBackgroundColor(0x10FF0000); 686 } 687 } 688 689 @Override 690 public void setCandidatesView(View view) { 691 // To ensure that CandidatesView will never be set. 692 return; 693 } 694 695 @Override 696 public void onStartInput(EditorInfo editorInfo, boolean restarting) { 697 mHandler.onStartInput(editorInfo, restarting); 698 } 699 700 @Override 701 public void onStartInputView(EditorInfo editorInfo, boolean restarting) { 702 mHandler.onStartInputView(editorInfo, restarting); 703 } 704 705 @Override 706 public void onFinishInputView(boolean finishingInput) { 707 mHandler.onFinishInputView(finishingInput); 708 } 709 710 @Override 711 public void onFinishInput() { 712 mHandler.onFinishInput(); 713 } 714 715 private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) { 716 super.onStartInput(editorInfo, restarting); 717 } 718 719 private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) { 720 super.onStartInputView(editorInfo, restarting); 721 final KeyboardSwitcher switcher = mKeyboardSwitcher; 722 LatinKeyboardView inputView = switcher.getKeyboardView(); 723 724 if (DEBUG) { 725 Log.d(TAG, "onStartInputView: editorInfo:" + ((editorInfo == null) ? "none" 726 : String.format("inputType=0x%08x imeOptions=0x%08x", 727 editorInfo.inputType, editorInfo.imeOptions))); 728 } 729 // In landscape mode, this method gets called without the input view being created. 730 if (inputView == null) { 731 return; 732 } 733 734 // Forward this event to the accessibility utilities, if enabled. 735 final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); 736 if (accessUtils.isTouchExplorationEnabled()) { 737 accessUtils.onStartInputViewInternal(editorInfo, restarting); 738 } 739 740 mSubtypeSwitcher.updateParametersOnStartInputView(); 741 742 TextEntryState.reset(); 743 744 // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to 745 // know now whether this is a password text field, because we need to know now whether we 746 // want to enable the voice button. 747 final VoiceProxy voiceIme = mVoiceProxy; 748 final int inputType = (editorInfo != null) ? editorInfo.inputType : 0; 749 voiceIme.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(inputType) 750 || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)); 751 752 // The EditorInfo might have a flag that affects fullscreen mode. 753 // Note: This call should be done by InputMethodService? 754 updateFullscreenMode(); 755 initializeInputAttributes(editorInfo); 756 757 inputView.closing(); 758 mEnteredText = null; 759 mWordComposer.reset(); 760 mHasUncommittedTypedChars = false; 761 mDeleteCount = 0; 762 mSpaceState = SPACE_STATE_NONE; 763 764 loadSettings(); 765 updateCorrectionMode(); 766 updateSuggestionVisibility(mPrefs, mResources); 767 768 if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) { 769 mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold); 770 } 771 mVoiceProxy.loadSettings(editorInfo, mPrefs); 772 // This will work only when the subtype is not supported. 773 LanguageSwitcherProxy.loadSettings(); 774 775 if (mSubtypeSwitcher.isKeyboardMode()) { 776 switcher.loadKeyboard(editorInfo, mSettingsValues); 777 } 778 779 if (mSuggestionsView != null) 780 mSuggestionsView.clear(); 781 setSuggestionStripShownInternal( 782 isSuggestionsStripVisible(), /* needsInputViewShown */ false); 783 // Delay updating suggestions because keyboard input view may not be shown at this point. 784 mHandler.postUpdateSuggestions(); 785 mHandler.cancelDoubleSpacesTimer(); 786 787 inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn, 788 mSettingsValues.mKeyPreviewPopupDismissDelay); 789 inputView.setProximityCorrectionEnabled(true); 790 791 voiceIme.onStartInputView(inputView.getWindowToken()); 792 793 if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); 794 } 795 796 private void initializeInputAttributes(EditorInfo editorInfo) { 797 if (editorInfo == null) 798 return; 799 final int inputType = editorInfo.inputType; 800 if (inputType == InputType.TYPE_NULL) { 801 // TODO: We should honor TYPE_NULL specification. 802 Log.i(TAG, "InputType.TYPE_NULL is specified"); 803 } 804 final int inputClass = inputType & InputType.TYPE_MASK_CLASS; 805 final int variation = inputType & InputType.TYPE_MASK_VARIATION; 806 if (inputClass == 0) { 807 Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x imeOptions=0x%08x", 808 inputType, editorInfo.imeOptions)); 809 } 810 811 mInsertSpaceOnPickSuggestionManually = false; 812 mInputTypeNoAutoCorrect = false; 813 mIsSettingsSuggestionStripOn = false; 814 mApplicationSpecifiedCompletionOn = false; 815 mApplicationSpecifiedCompletions = null; 816 817 if (inputClass == InputType.TYPE_CLASS_TEXT) { 818 mIsSettingsSuggestionStripOn = true; 819 // Make sure that passwords are not displayed in {@link SuggestionsView}. 820 if (InputTypeCompatUtils.isPasswordInputType(inputType) 821 || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)) { 822 mIsSettingsSuggestionStripOn = false; 823 } 824 if (InputTypeCompatUtils.isEmailVariation(variation) 825 || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { 826 // The point in turning this off is that we don't want to insert a space after 827 // a name when filling a form: we can't delete trailing spaces when changing fields 828 mInsertSpaceOnPickSuggestionManually = false; 829 } else { 830 mInsertSpaceOnPickSuggestionManually = true; 831 } 832 if (InputTypeCompatUtils.isEmailVariation(variation)) { 833 mIsSettingsSuggestionStripOn = false; 834 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 835 mIsSettingsSuggestionStripOn = false; 836 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 837 mIsSettingsSuggestionStripOn = false; 838 } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { 839 // If it's a browser edit field and auto correct is not ON explicitly, then 840 // disable auto correction, but keep suggestions on. 841 if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) { 842 mInputTypeNoAutoCorrect = true; 843 } 844 } 845 846 // If NO_SUGGESTIONS is set, don't do prediction. 847 if ((inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) { 848 mIsSettingsSuggestionStripOn = false; 849 mInputTypeNoAutoCorrect = true; 850 } 851 // If it's not multiline and the autoCorrect flag is not set, then don't correct 852 if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 853 && (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) { 854 mInputTypeNoAutoCorrect = true; 855 } 856 if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { 857 mIsSettingsSuggestionStripOn = false; 858 mApplicationSpecifiedCompletionOn = isFullscreenMode(); 859 } 860 } 861 } 862 863 @Override 864 public void onWindowHidden() { 865 super.onWindowHidden(); 866 KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 867 if (inputView != null) inputView.closing(); 868 } 869 870 private void onFinishInputInternal() { 871 super.onFinishInput(); 872 873 LatinImeLogger.commit(); 874 875 mVoiceProxy.flushVoiceInputLogs(mConfigurationChanging); 876 877 KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 878 if (inputView != null) inputView.closing(); 879 if (mUserUnigramDictionary != null) mUserUnigramDictionary.flushPendingWrites(); 880 if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); 881 } 882 883 private void onFinishInputViewInternal(boolean finishingInput) { 884 super.onFinishInputView(finishingInput); 885 mKeyboardSwitcher.onFinishInputView(); 886 KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 887 if (inputView != null) inputView.cancelAllMessages(); 888 // Remove pending messages related to update suggestions 889 mHandler.cancelUpdateSuggestions(); 890 } 891 892 @Override 893 public void onUpdateExtractedText(int token, ExtractedText text) { 894 super.onUpdateExtractedText(token, text); 895 mVoiceProxy.showPunctuationHintIfNecessary(); 896 } 897 898 @Override 899 public void onUpdateSelection(int oldSelStart, int oldSelEnd, 900 int newSelStart, int newSelEnd, 901 int candidatesStart, int candidatesEnd) { 902 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 903 candidatesStart, candidatesEnd); 904 905 if (DEBUG) { 906 Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart 907 + ", ose=" + oldSelEnd 908 + ", lss=" + mLastSelectionStart 909 + ", lse=" + mLastSelectionEnd 910 + ", nss=" + newSelStart 911 + ", nse=" + newSelEnd 912 + ", cs=" + candidatesStart 913 + ", ce=" + candidatesEnd); 914 } 915 916 mVoiceProxy.setCursorAndSelection(newSelEnd, newSelStart); 917 918 // If the current selection in the text view changes, we should 919 // clear whatever candidate text we have. 920 final boolean selectionChanged = (newSelStart != candidatesEnd 921 || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart; 922 final boolean candidatesCleared = candidatesStart == -1 && candidatesEnd == -1; 923 if (!mExpectingUpdateSelection) { 924 if (SPACE_STATE_WEAK == mSpaceState) { 925 // Test for no WEAK_SPACE action because there is a race condition that may end up 926 // in coming here on a normal key press. We set this to NONE because after 927 // a cursor move, we don't want the suggestion strip to swap the space with the 928 // newly inserted punctuation. 929 mSpaceState = SPACE_STATE_NONE; 930 } 931 if (((mWordComposer.size() > 0 && mHasUncommittedTypedChars) 932 || mVoiceProxy.isVoiceInputHighlighted()) 933 && (selectionChanged || candidatesCleared)) { 934 mWordComposer.reset(); 935 mHasUncommittedTypedChars = false; 936 TextEntryState.reset(); 937 updateSuggestions(); 938 final InputConnection ic = getCurrentInputConnection(); 939 if (ic != null) { 940 ic.finishComposingText(); 941 } 942 mComposingStateManager.onFinishComposingText(); 943 mVoiceProxy.setVoiceInputHighlighted(false); 944 } else if (!mHasUncommittedTypedChars) { 945 TextEntryState.reset(); 946 updateSuggestions(); 947 } 948 } 949 mExpectingUpdateSelection = false; 950 mHandler.postUpdateShiftKeyState(); 951 // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not 952 // here. It would probably be too expensive to call directly here but we may want to post a 953 // message to delay it. The point would be to unify behavior between backspace to the 954 // end of a word and manually put the pointer at the end of the word. 955 956 // Make a note of the cursor position 957 mLastSelectionStart = newSelStart; 958 mLastSelectionEnd = newSelEnd; 959 } 960 961 /** 962 * This is called when the user has clicked on the extracted text view, 963 * when running in fullscreen mode. The default implementation hides 964 * the suggestions view when this happens, but only if the extracted text 965 * editor has a vertical scroll bar because its text doesn't fit. 966 * Here we override the behavior due to the possibility that a re-correction could 967 * cause the suggestions strip to disappear and re-appear. 968 */ 969 @Override 970 public void onExtractedTextClicked() { 971 if (isSuggestionsRequested()) return; 972 973 super.onExtractedTextClicked(); 974 } 975 976 /** 977 * This is called when the user has performed a cursor movement in the 978 * extracted text view, when it is running in fullscreen mode. The default 979 * implementation hides the suggestions view when a vertical movement 980 * happens, but only if the extracted text editor has a vertical scroll bar 981 * because its text doesn't fit. 982 * Here we override the behavior due to the possibility that a re-correction could 983 * cause the suggestions strip to disappear and re-appear. 984 */ 985 @Override 986 public void onExtractedCursorMovement(int dx, int dy) { 987 if (isSuggestionsRequested()) return; 988 989 super.onExtractedCursorMovement(dx, dy); 990 } 991 992 @Override 993 public void hideWindow() { 994 LatinImeLogger.commit(); 995 mKeyboardSwitcher.onHideWindow(); 996 997 if (TRACE) Debug.stopMethodTracing(); 998 if (mOptionsDialog != null && mOptionsDialog.isShowing()) { 999 mOptionsDialog.dismiss(); 1000 mOptionsDialog = null; 1001 } 1002 mVoiceProxy.hideVoiceWindow(mConfigurationChanging); 1003 super.hideWindow(); 1004 } 1005 1006 @Override 1007 public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) { 1008 if (DEBUG) { 1009 Log.i(TAG, "Received completions:"); 1010 if (applicationSpecifiedCompletions != null) { 1011 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { 1012 Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); 1013 } 1014 } 1015 } 1016 if (mApplicationSpecifiedCompletionOn) { 1017 mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; 1018 if (applicationSpecifiedCompletions == null) { 1019 clearSuggestions(); 1020 return; 1021 } 1022 1023 SuggestedWords.Builder builder = new SuggestedWords.Builder() 1024 .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions) 1025 .setTypedWordValid(false) 1026 .setHasMinimalSuggestion(false); 1027 // When in fullscreen mode, show completions generated by the application 1028 setSuggestions(builder.build()); 1029 mBestWord = null; 1030 setSuggestionStripShown(true); 1031 } 1032 } 1033 1034 private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) { 1035 // TODO: Modify this if we support suggestions with hard keyboard 1036 if (onEvaluateInputViewShown() && mSuggestionsContainer != null) { 1037 final boolean shouldShowSuggestions = shown 1038 && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true); 1039 if (isFullscreenMode()) { 1040 mSuggestionsContainer.setVisibility( 1041 shouldShowSuggestions ? View.VISIBLE : View.GONE); 1042 } else { 1043 mSuggestionsContainer.setVisibility( 1044 shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); 1045 } 1046 } 1047 } 1048 1049 private void setSuggestionStripShown(boolean shown) { 1050 setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); 1051 } 1052 1053 @Override 1054 public void onComputeInsets(InputMethodService.Insets outInsets) { 1055 super.onComputeInsets(outInsets); 1056 final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 1057 if (inputView == null || mSuggestionsContainer == null) 1058 return; 1059 // In fullscreen mode, the height of the extract area managed by InputMethodService should 1060 // be considered. 1061 // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. 1062 final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; 1063 final int backingHeight = (mKeyPreviewBackingView.getVisibility() == View.GONE) ? 0 1064 : mKeyPreviewBackingView.getHeight(); 1065 final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0 1066 : mSuggestionsContainer.getHeight(); 1067 final int extraHeight = extractHeight + backingHeight + suggestionsHeight; 1068 int touchY = extraHeight; 1069 // Need to set touchable region only if input view is being shown 1070 if (mKeyboardSwitcher.isInputViewShown()) { 1071 if (mSuggestionsContainer.getVisibility() == View.VISIBLE) { 1072 touchY -= suggestionsHeight; 1073 } 1074 final int touchWidth = inputView.getWidth(); 1075 final int touchHeight = inputView.getHeight() + extraHeight 1076 // Extend touchable region below the keyboard. 1077 + EXTENDED_TOUCHABLE_REGION_HEIGHT; 1078 if (DEBUG) { 1079 Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth 1080 + " height=" + touchHeight); 1081 } 1082 setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight); 1083 } 1084 outInsets.contentTopInsets = touchY; 1085 outInsets.visibleTopInsets = touchY; 1086 } 1087 1088 @Override 1089 public boolean onEvaluateFullscreenMode() { 1090 return super.onEvaluateFullscreenMode() 1091 && mResources.getBoolean(R.bool.config_use_fullscreen_mode); 1092 } 1093 1094 @Override 1095 public void updateFullscreenMode() { 1096 super.updateFullscreenMode(); 1097 1098 if (mKeyPreviewBackingView == null) return; 1099 // In fullscreen mode, no need to have extra space to show the key preview. 1100 // If not, we should have extra space above the keyboard to show the key preview. 1101 mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); 1102 } 1103 1104 @Override 1105 public boolean onKeyDown(int keyCode, KeyEvent event) { 1106 switch (keyCode) { 1107 case KeyEvent.KEYCODE_BACK: 1108 if (event.getRepeatCount() == 0) { 1109 if (mSuggestionsView != null && mSuggestionsView.handleBack()) { 1110 return true; 1111 } 1112 final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); 1113 if (keyboardView != null && keyboardView.handleBack()) { 1114 return true; 1115 } 1116 } 1117 break; 1118 } 1119 return super.onKeyDown(keyCode, event); 1120 } 1121 1122 @Override 1123 public boolean onKeyUp(int keyCode, KeyEvent event) { 1124 switch (keyCode) { 1125 case KeyEvent.KEYCODE_DPAD_DOWN: 1126 case KeyEvent.KEYCODE_DPAD_UP: 1127 case KeyEvent.KEYCODE_DPAD_LEFT: 1128 case KeyEvent.KEYCODE_DPAD_RIGHT: 1129 // Enable shift key and DPAD to do selections 1130 if (mKeyboardSwitcher.isInputViewShown() 1131 && mKeyboardSwitcher.isShiftedOrShiftLocked()) { 1132 KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(), 1133 event.getAction(), event.getKeyCode(), event.getRepeatCount(), 1134 event.getDeviceId(), event.getScanCode(), 1135 KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); 1136 final InputConnection ic = getCurrentInputConnection(); 1137 if (ic != null) 1138 ic.sendKeyEvent(newEvent); 1139 return true; 1140 } 1141 break; 1142 } 1143 return super.onKeyUp(keyCode, event); 1144 } 1145 1146 public void commitTyped(final InputConnection ic) { 1147 if (!mHasUncommittedTypedChars) return; 1148 mHasUncommittedTypedChars = false; 1149 final CharSequence typedWord = mWordComposer.getTypedWord(); 1150 if (typedWord.length() > 0) { 1151 if (ic != null) { 1152 ic.commitText(typedWord, 1); 1153 } 1154 mCommittedLength = typedWord.length(); 1155 TextEntryState.acceptedTyped(typedWord); 1156 addToUserUnigramAndBigramDictionaries(typedWord, 1157 UserUnigramDictionary.FREQUENCY_FOR_TYPED); 1158 } 1159 updateSuggestions(); 1160 } 1161 1162 public boolean getCurrentAutoCapsState() { 1163 final InputConnection ic = getCurrentInputConnection(); 1164 EditorInfo ei = getCurrentInputEditorInfo(); 1165 if (mSettingsValues.mAutoCap && ic != null && ei != null 1166 && ei.inputType != InputType.TYPE_NULL) { 1167 return ic.getCursorCapsMode(ei.inputType) != 0; 1168 } 1169 return false; 1170 } 1171 1172 // "ic" may be null 1173 private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) { 1174 if (null == ic) return; 1175 CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); 1176 // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. 1177 if (lastTwo != null && lastTwo.length() == 2 1178 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) { 1179 ic.deleteSurroundingText(2, 0); 1180 ic.commitText(lastTwo.charAt(1) + " ", 1); 1181 mKeyboardSwitcher.updateShiftState(); 1182 } 1183 } 1184 1185 private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) { 1186 if (mCorrectionMode == Suggest.CORRECTION_NONE) return false; 1187 if (ic == null) return false; 1188 final CharSequence lastThree = ic.getTextBeforeCursor(3, 0); 1189 if (lastThree != null && lastThree.length() == 3 1190 && Utils.canBeFollowedByPeriod(lastThree.charAt(0)) 1191 && lastThree.charAt(1) == Keyboard.CODE_SPACE 1192 && lastThree.charAt(2) == Keyboard.CODE_SPACE 1193 && mHandler.isAcceptingDoubleSpaces()) { 1194 mHandler.cancelDoubleSpacesTimer(); 1195 ic.deleteSurroundingText(2, 0); 1196 ic.commitText(". ", 1); 1197 mKeyboardSwitcher.updateShiftState(); 1198 return true; 1199 } 1200 return false; 1201 } 1202 1203 // "ic" must not be null 1204 private static void maybeRemovePreviousPeriod(final InputConnection ic, CharSequence text) { 1205 // When the text's first character is '.', remove the previous period 1206 // if there is one. 1207 final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); 1208 if (lastOne != null && lastOne.length() == 1 1209 && lastOne.charAt(0) == Keyboard.CODE_PERIOD 1210 && text.charAt(0) == Keyboard.CODE_PERIOD) { 1211 ic.deleteSurroundingText(1, 0); 1212 } 1213 } 1214 1215 // "ic" may be null 1216 private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) { 1217 if (ic == null) return; 1218 final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); 1219 if (lastOne != null && lastOne.length() == 1 1220 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { 1221 ic.deleteSurroundingText(1, 0); 1222 } 1223 } 1224 1225 @Override 1226 public boolean addWordToDictionary(String word) { 1227 mUserDictionary.addWord(word, 128); 1228 // Suggestion strip should be updated after the operation of adding word to the 1229 // user dictionary 1230 mHandler.postUpdateSuggestions(); 1231 return true; 1232 } 1233 1234 private static boolean isAlphabet(int code) { 1235 return Character.isLetter(code); 1236 } 1237 1238 private void onSettingsKeyPressed() { 1239 if (isShowingOptionDialog()) return; 1240 if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) { 1241 showSubtypeSelectorAndSettings(); 1242 } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm, false /* exclude aux subtypes */)) { 1243 showOptionsMenu(); 1244 } else { 1245 launchSettings(); 1246 } 1247 } 1248 1249 // Virtual codes representing custom requests. These are used in onCustomRequest() below. 1250 public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1; 1251 public static final int CODE_HAPTIC_AND_AUDIO_FEEDBACK = 2; 1252 1253 @Override 1254 public boolean onCustomRequest(int requestCode) { 1255 if (isShowingOptionDialog()) return false; 1256 switch (requestCode) { 1257 case CODE_SHOW_INPUT_METHOD_PICKER: 1258 if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm, true /* include aux subtypes */)) { 1259 mImm.showInputMethodPicker(); 1260 return true; 1261 } 1262 return false; 1263 case CODE_HAPTIC_AND_AUDIO_FEEDBACK: 1264 hapticAndAudioFeedback(Keyboard.CODE_UNSPECIFIED); 1265 return true; 1266 } 1267 return false; 1268 } 1269 1270 private boolean isShowingOptionDialog() { 1271 return mOptionsDialog != null && mOptionsDialog.isShowing(); 1272 } 1273 1274 private void insertPunctuationFromSuggestionStrip(final InputConnection ic, final int code) { 1275 final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : null; 1276 final int toLeft = TextUtils.isEmpty(beforeText) ? 0 : beforeText.charAt(0); 1277 final boolean shouldRegisterSwapPunctuation; 1278 // If we have a space left of the cursor and it's a weak or a magic space, then we should 1279 // swap it, and override the space state with SPACESTATE_SWAP_PUNCTUATION. 1280 // To swap it, we fool handleSeparator to think the previous space state was a 1281 // magic space. 1282 if (Keyboard.CODE_SPACE == toLeft && mSpaceState == SPACE_STATE_WEAK) { 1283 mSpaceState = SPACE_STATE_MAGIC; 1284 shouldRegisterSwapPunctuation = true; 1285 } else { 1286 shouldRegisterSwapPunctuation = false; 1287 } 1288 onCodeInput(code, new int[] { code }, 1289 KeyboardActionListener.NOT_A_TOUCH_COORDINATE, 1290 KeyboardActionListener.NOT_A_TOUCH_COORDINATE); 1291 if (shouldRegisterSwapPunctuation) { 1292 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; 1293 } 1294 } 1295 1296 // Implementation of {@link KeyboardActionListener}. 1297 @Override 1298 public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { 1299 final long when = SystemClock.uptimeMillis(); 1300 if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { 1301 mDeleteCount = 0; 1302 } 1303 mLastKeyTime = when; 1304 final KeyboardSwitcher switcher = mKeyboardSwitcher; 1305 final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); 1306 // The space state depends only on the last character pressed and its own previous 1307 // state. Here, we revert the space state to neutral if the key is actually modifying 1308 // the input contents (any non-shift key), which is what we should do for 1309 // all inputs that do not result in a special state. Each character handling is then 1310 // free to override the state as they see fit. 1311 final int spaceState = mSpaceState; 1312 1313 // TODO: Consolidate the double space timer, mLastKeyTime, and the space state. 1314 if (primaryCode != Keyboard.CODE_SPACE) { 1315 mHandler.cancelDoubleSpacesTimer(); 1316 } 1317 1318 switch (primaryCode) { 1319 case Keyboard.CODE_DELETE: 1320 mSpaceState = SPACE_STATE_NONE; 1321 handleBackspace(spaceState); 1322 mDeleteCount++; 1323 mExpectingUpdateSelection = true; 1324 LatinImeLogger.logOnDelete(); 1325 break; 1326 case Keyboard.CODE_SHIFT: 1327 // Shift key is handled in onPress() when device has distinct multi-touch panel. 1328 if (!distinctMultiTouch) { 1329 switcher.toggleShift(); 1330 } 1331 break; 1332 case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: 1333 // Symbol key is handled in onPress() when device has distinct multi-touch panel. 1334 if (!distinctMultiTouch) { 1335 switcher.toggleAlphabetAndSymbols(); 1336 } 1337 break; 1338 case Keyboard.CODE_SETTINGS: 1339 onSettingsKeyPressed(); 1340 break; 1341 case Keyboard.CODE_CAPSLOCK: 1342 switcher.toggleCapsLock(); 1343 hapticAndAudioFeedback(primaryCode); 1344 break; 1345 case Keyboard.CODE_SHORTCUT: 1346 mSubtypeSwitcher.switchToShortcutIME(); 1347 break; 1348 case Keyboard.CODE_TAB: 1349 handleTab(); 1350 // There are two cases for tab. Either we send a "next" event, that may change the 1351 // focus but will never move the cursor. Or, we send a real tab keycode, which some 1352 // applications may accept or ignore, and we don't know whether this will move the 1353 // cursor or not. So actually, we don't really know. 1354 // So to go with the safer option, we'd rather behave as if the user moved the 1355 // cursor when they didn't than the opposite. We also expect that most applications 1356 // will actually use tab only for focus movement. 1357 // To sum it up: do not update mExpectingUpdateSelection here. 1358 break; 1359 default: 1360 mSpaceState = SPACE_STATE_NONE; 1361 if (mSettingsValues.isWordSeparator(primaryCode)) { 1362 handleSeparator(primaryCode, x, y, spaceState); 1363 } else { 1364 handleCharacter(primaryCode, keyCodes, x, y, spaceState); 1365 } 1366 mExpectingUpdateSelection = true; 1367 break; 1368 } 1369 switcher.onCodeInput(primaryCode); 1370 // Reset after any single keystroke 1371 mEnteredText = null; 1372 } 1373 1374 @Override 1375 public void onTextInput(CharSequence text) { 1376 mVoiceProxy.commitVoiceInput(); 1377 final InputConnection ic = getCurrentInputConnection(); 1378 if (ic == null) return; 1379 ic.beginBatchEdit(); 1380 commitTyped(ic); 1381 maybeRemovePreviousPeriod(ic, text); 1382 ic.commitText(text, 1); 1383 ic.endBatchEdit(); 1384 mKeyboardSwitcher.updateShiftState(); 1385 mKeyboardSwitcher.onCodeInput(Keyboard.CODE_DUMMY); 1386 mSpaceState = SPACE_STATE_NONE; 1387 mEnteredText = text; 1388 } 1389 1390 @Override 1391 public void onCancelInput() { 1392 // User released a finger outside any key 1393 mKeyboardSwitcher.onCancelInput(); 1394 } 1395 1396 private void handleBackspace(final int spaceState) { 1397 if (mVoiceProxy.logAndRevertVoiceInput()) return; 1398 1399 final InputConnection ic = getCurrentInputConnection(); 1400 if (ic == null) return; 1401 ic.beginBatchEdit(); 1402 1403 mVoiceProxy.handleBackspace(); 1404 1405 final boolean deleteChar = !mHasUncommittedTypedChars; 1406 if (mHasUncommittedTypedChars) { 1407 final int length = mWordComposer.size(); 1408 if (length > 0) { 1409 mWordComposer.deleteLast(); 1410 final CharSequence textWithUnderline = 1411 mComposingStateManager.isAutoCorrectionIndicatorOn() 1412 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline( 1413 this, mWordComposer.getTypedWord()) 1414 : mWordComposer.getTypedWord(); 1415 ic.setComposingText(textWithUnderline, 1); 1416 if (mWordComposer.size() == 0) { 1417 mHasUncommittedTypedChars = false; 1418 } 1419 if (1 == length) { 1420 // 1 == length means we are about to erase the last character of the word, 1421 // so we can show bigrams. 1422 mHandler.postUpdateBigramPredictions(); 1423 } else { 1424 // length > 1, so we still have letters to deduce a suggestion from. 1425 mHandler.postUpdateSuggestions(); 1426 } 1427 } else { 1428 ic.deleteSurroundingText(1, 0); 1429 } 1430 } 1431 mHandler.postUpdateShiftKeyState(); 1432 1433 // TODO: Merge space state with TextEntryState 1434 TextEntryState.backspace(); 1435 if (null != mWordSavedForAutoCorrectCancellation) { 1436 Utils.Stats.onAutoCorrectionCancellation(); 1437 cancelAutoCorrect(ic); 1438 mWordSavedForAutoCorrectCancellation = null; 1439 ic.endBatchEdit(); 1440 return; 1441 } else { 1442 mWordSavedForAutoCorrectCancellation = null; 1443 } 1444 1445 if (SPACE_STATE_DOUBLE == spaceState) { 1446 if (revertDoubleSpace(ic)) { 1447 ic.endBatchEdit(); 1448 // No need to reset mSpaceState, it has already be done (that's why we 1449 // receive it as a parameter) 1450 return; 1451 } 1452 } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1453 if (revertSwapPunctuation(ic)) { 1454 ic.endBatchEdit(); 1455 // Likewise 1456 return; 1457 } 1458 } 1459 1460 if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { 1461 // Cancel multi-character input: remove the text we just entered. 1462 // This is triggered on backspace after a key that inputs multiple characters, 1463 // like the smiley key or the .com key. 1464 ic.deleteSurroundingText(mEnteredText.length(), 0); 1465 } else if (deleteChar) { 1466 if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) { 1467 // Go back to the suggestion mode if the user canceled the 1468 // "Touch again to save". 1469 // NOTE: In gerenal, we don't revert the word when backspacing 1470 // from a manual suggestion pick. We deliberately chose a 1471 // different behavior only in the case of picking the first 1472 // suggestion (typed word). It's intentional to have made this 1473 // inconsistent with backspacing after selecting other suggestions. 1474 restartSuggestionsOnManuallyPickedTypedWord(ic); 1475 } else { 1476 ic.deleteSurroundingText(1, 0); 1477 if (mDeleteCount > DELETE_ACCELERATE_AT) { 1478 ic.deleteSurroundingText(1, 0); 1479 } 1480 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic); 1481 } 1482 } 1483 ic.endBatchEdit(); 1484 } 1485 1486 private void handleTab() { 1487 final int imeOptions = getCurrentInputEditorInfo().imeOptions; 1488 if (!EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions) 1489 && !EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)) { 1490 sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB); 1491 return; 1492 } 1493 1494 final InputConnection ic = getCurrentInputConnection(); 1495 if (ic == null) 1496 return; 1497 1498 // True if keyboard is in either chording shift or manual temporary upper case mode. 1499 final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase(); 1500 if (EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions) 1501 && !isManualTemporaryUpperCase) { 1502 EditorInfoCompatUtils.performEditorActionNext(ic); 1503 } else if (EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions) 1504 && isManualTemporaryUpperCase) { 1505 EditorInfoCompatUtils.performEditorActionPrevious(ic); 1506 } 1507 } 1508 1509 private void handleCharacter(final int primaryCode, final int[] keyCodes, final int x, 1510 final int y, final int spaceState) { 1511 mVoiceProxy.handleCharacter(); 1512 1513 final InputConnection ic = getCurrentInputConnection(); 1514 if (ic != null) ic.beginBatchEdit(); 1515 if (SPACE_STATE_MAGIC == spaceState 1516 && mSettingsValues.isMagicSpaceStripper(primaryCode)) { 1517 removeTrailingSpaceWhileInBatchEdit(ic); 1518 } 1519 1520 int code = primaryCode; 1521 if ((isAlphabet(code) || mSettingsValues.isSymbolExcludedFromWordSeparators(code)) 1522 && isSuggestionsRequested() && !isCursorTouchingWord()) { 1523 if (!mHasUncommittedTypedChars) { 1524 // Reset entirely the composing state anyway, then start composing a new word unless 1525 // the character is a single quote. 1526 mHasUncommittedTypedChars = (Keyboard.CODE_SINGLE_QUOTE != code); 1527 mWordComposer.reset(); 1528 clearSuggestions(); 1529 mComposingStateManager.onFinishComposingText(); 1530 } 1531 } 1532 final KeyboardSwitcher switcher = mKeyboardSwitcher; 1533 if (switcher.isShiftedOrShiftLocked()) { 1534 if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT 1535 || keyCodes[0] > Character.MAX_CODE_POINT) { 1536 if (null != ic) ic.endBatchEdit(); 1537 return; 1538 } 1539 code = keyCodes[0]; 1540 if (switcher.isAlphabetMode() && Character.isLowerCase(code)) { 1541 // In some locales, such as Turkish, Character.toUpperCase() may return a wrong 1542 // character because it doesn't take care of locale. 1543 final String upperCaseString = new String(new int[] {code}, 0, 1) 1544 .toUpperCase(mSubtypeSwitcher.getInputLocale()); 1545 if (upperCaseString.codePointCount(0, upperCaseString.length()) == 1) { 1546 code = upperCaseString.codePointAt(0); 1547 } else { 1548 // Some keys, such as [eszett], have upper case as multi-characters. 1549 onTextInput(upperCaseString); 1550 if (null != ic) ic.endBatchEdit(); 1551 return; 1552 } 1553 } 1554 } 1555 if (mHasUncommittedTypedChars) { 1556 mWordComposer.add(code, keyCodes, x, y); 1557 if (ic != null) { 1558 // If it's the first letter, make note of auto-caps state 1559 if (mWordComposer.size() == 1) { 1560 mWordComposer.setAutoCapitalized(getCurrentAutoCapsState()); 1561 mComposingStateManager.onStartComposingText(); 1562 } 1563 final CharSequence textWithUnderline = 1564 mComposingStateManager.isAutoCorrectionIndicatorOn() 1565 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline( 1566 this, mWordComposer.getTypedWord()) 1567 : mWordComposer.getTypedWord(); 1568 ic.setComposingText(textWithUnderline, 1); 1569 } 1570 mHandler.postUpdateSuggestions(); 1571 } else { 1572 sendKeyChar((char)code); 1573 } 1574 if (SPACE_STATE_MAGIC == spaceState 1575 && mSettingsValues.isMagicSpaceSwapper(primaryCode)) { 1576 if (null != ic) swapSwapperAndSpaceWhileInBatchEdit(ic); 1577 } 1578 1579 switcher.updateShiftState(); 1580 if (mSettingsValues.isWordSeparator(code)) { 1581 TextEntryState.typedCharacter((char) code, true, x, y); 1582 Utils.Stats.onSeparator((char)code, x, y); 1583 } else { 1584 TextEntryState.typedCharacter((char) code, false, x, y); 1585 Utils.Stats.onNonSeparator((char)code, x, y); 1586 } 1587 if (null != ic) ic.endBatchEdit(); 1588 } 1589 1590 private void handleSeparator(final int primaryCode, final int x, final int y, 1591 final int spaceState) { 1592 mVoiceProxy.handleSeparator(); 1593 mComposingStateManager.onFinishComposingText(); 1594 1595 // Should dismiss the "Touch again to save" message when handling separator 1596 if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) { 1597 mHandler.cancelUpdateBigramPredictions(); 1598 mHandler.postUpdateSuggestions(); 1599 } 1600 1601 boolean pickedDefault = false; 1602 // Handle separator 1603 final InputConnection ic = getCurrentInputConnection(); 1604 if (ic != null) { 1605 ic.beginBatchEdit(); 1606 } 1607 if (mHasUncommittedTypedChars) { 1608 // In certain languages where single quote is a separator, it's better 1609 // not to auto correct, but accept the typed word. For instance, 1610 // in Italian dov' should not be expanded to dove' because the elision 1611 // requires the last vowel to be removed. 1612 final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled 1613 && !mInputTypeNoAutoCorrect; 1614 if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) { 1615 pickedDefault = pickDefaultSuggestion(primaryCode); 1616 } else { 1617 commitTyped(ic); 1618 } 1619 } else { 1620 mWordSavedForAutoCorrectCancellation = null; 1621 } 1622 1623 final boolean swapMagicSpace; 1624 if (Keyboard.CODE_ENTER == primaryCode && (SPACE_STATE_MAGIC == spaceState 1625 || SPACE_STATE_SWAP_PUNCTUATION == spaceState)) { 1626 removeTrailingSpaceWhileInBatchEdit(ic); 1627 swapMagicSpace = false; 1628 } else if (SPACE_STATE_MAGIC == spaceState) { 1629 if (mSettingsValues.isMagicSpaceSwapper(primaryCode)) { 1630 swapMagicSpace = true; 1631 } else { 1632 swapMagicSpace = false; 1633 if (mSettingsValues.isMagicSpaceStripper(primaryCode)) { 1634 removeTrailingSpaceWhileInBatchEdit(ic); 1635 } 1636 } 1637 } else { 1638 swapMagicSpace = false; 1639 } 1640 1641 sendKeyChar((char)primaryCode); 1642 1643 if (Keyboard.CODE_SPACE == primaryCode) { 1644 if (isSuggestionsRequested()) { 1645 if (maybeDoubleSpaceWhileInBatchEdit(ic)) { 1646 mSpaceState = SPACE_STATE_DOUBLE; 1647 } else if (!isShowingPunctuationList()) { 1648 mSpaceState = SPACE_STATE_WEAK; 1649 } 1650 } 1651 1652 mHandler.startDoubleSpacesTimer(); 1653 if (!isCursorTouchingWord()) { 1654 mHandler.cancelUpdateSuggestions(); 1655 mHandler.postUpdateBigramPredictions(); 1656 } 1657 } else { 1658 if (swapMagicSpace) { 1659 swapSwapperAndSpaceWhileInBatchEdit(ic); 1660 mSpaceState = SPACE_STATE_MAGIC; 1661 } 1662 1663 // Set punctuation right away. onUpdateSelection will fire but tests whether it is 1664 // already displayed or not, so it's okay. 1665 setPunctuationSuggestions(); 1666 } 1667 1668 TextEntryState.typedCharacter((char) primaryCode, true, x, y); 1669 Utils.Stats.onSeparator((char)primaryCode, x, y); 1670 1671 if (pickedDefault) { 1672 CharSequence typedWord = mWordComposer.getTypedWord(); 1673 TextEntryState.backToAcceptedDefault(typedWord); 1674 if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) { 1675 InputConnectionCompatUtils.commitCorrection( 1676 ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord); 1677 } 1678 } 1679 mKeyboardSwitcher.updateShiftState(); 1680 if (ic != null) { 1681 ic.endBatchEdit(); 1682 } 1683 } 1684 1685 private void handleClose() { 1686 commitTyped(getCurrentInputConnection()); 1687 mVoiceProxy.handleClose(); 1688 requestHideSelf(0); 1689 LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 1690 if (inputView != null) 1691 inputView.closing(); 1692 } 1693 1694 public boolean isSuggestionsRequested() { 1695 return mIsSettingsSuggestionStripOn 1696 && (mCorrectionMode > 0 || isShowingSuggestionsStrip()); 1697 } 1698 1699 public boolean isShowingPunctuationList() { 1700 if (mSuggestionsView == null) return false; 1701 return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions(); 1702 } 1703 1704 public boolean isShowingSuggestionsStrip() { 1705 return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE) 1706 || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE 1707 && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT); 1708 } 1709 1710 public boolean isSuggestionsStripVisible() { 1711 if (mSuggestionsView == null) 1712 return false; 1713 if (mSuggestionsView.isShowingAddToDictionaryHint()) 1714 return true; 1715 if (!isShowingSuggestionsStrip()) 1716 return false; 1717 if (mApplicationSpecifiedCompletionOn) 1718 return true; 1719 return isSuggestionsRequested(); 1720 } 1721 1722 public void switchToKeyboardView() { 1723 if (DEBUG) { 1724 Log.d(TAG, "Switch to keyboard view."); 1725 } 1726 View v = mKeyboardSwitcher.getKeyboardView(); 1727 if (v != null) { 1728 // Confirms that the keyboard view doesn't have parent view. 1729 ViewParent p = v.getParent(); 1730 if (p != null && p instanceof ViewGroup) { 1731 ((ViewGroup) p).removeView(v); 1732 } 1733 setInputView(v); 1734 } 1735 setSuggestionStripShown(isSuggestionsStripVisible()); 1736 updateInputViewShown(); 1737 mHandler.postUpdateSuggestions(); 1738 } 1739 1740 public void clearSuggestions() { 1741 setSuggestions(SuggestedWords.EMPTY); 1742 } 1743 1744 public void setSuggestions(SuggestedWords words) { 1745 if (mSuggestionsView != null) { 1746 mSuggestionsView.setSuggestions(words); 1747 mKeyboardSwitcher.onAutoCorrectionStateChanged( 1748 words.hasWordAboveAutoCorrectionScoreThreshold()); 1749 } 1750 1751 // Put a blue underline to a word in TextView which will be auto-corrected. 1752 final InputConnection ic = getCurrentInputConnection(); 1753 if (ic != null) { 1754 final boolean oldAutoCorrectionIndicator = 1755 mComposingStateManager.isAutoCorrectionIndicatorOn(); 1756 final boolean newAutoCorrectionIndicator = Utils.willAutoCorrect(words); 1757 if (oldAutoCorrectionIndicator != newAutoCorrectionIndicator) { 1758 if (LatinImeLogger.sDBG) { 1759 Log.d(TAG, "Flip the indicator. " + oldAutoCorrectionIndicator 1760 + " -> " + newAutoCorrectionIndicator); 1761 } 1762 final CharSequence textWithUnderline = newAutoCorrectionIndicator 1763 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline( 1764 this, mWordComposer.getTypedWord()) 1765 : mWordComposer.getTypedWord(); 1766 if (!TextUtils.isEmpty(textWithUnderline)) { 1767 ic.setComposingText(textWithUnderline, 1); 1768 } 1769 mComposingStateManager.setAutoCorrectionIndicatorOn(newAutoCorrectionIndicator); 1770 } 1771 } 1772 } 1773 1774 public void updateSuggestions() { 1775 // Check if we have a suggestion engine attached. 1776 if ((mSuggest == null || !isSuggestionsRequested()) 1777 && !mVoiceProxy.isVoiceInputHighlighted()) { 1778 return; 1779 } 1780 1781 mHandler.cancelUpdateSuggestions(); 1782 mHandler.cancelUpdateBigramPredictions(); 1783 1784 if (!mHasUncommittedTypedChars) { 1785 setPunctuationSuggestions(); 1786 return; 1787 } 1788 1789 final WordComposer wordComposer = mWordComposer; 1790 // TODO: May need a better way of retrieving previous word 1791 final InputConnection ic = getCurrentInputConnection(); 1792 final CharSequence prevWord; 1793 if (null == ic) { 1794 prevWord = null; 1795 } else { 1796 prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators); 1797 } 1798 // getSuggestedWordBuilder handles gracefully a null value of prevWord 1799 final SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(wordComposer, 1800 prevWord, mKeyboardSwitcher.getLatinKeyboard().getProximityInfo(), mCorrectionMode); 1801 1802 boolean autoCorrectionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection(); 1803 final CharSequence typedWord = wordComposer.getTypedWord(); 1804 // Here, we want to promote a whitelisted word if exists. 1805 // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid" 1806 // but still autocorrected from - in the case the whitelist only capitalizes the word. 1807 // The whitelist should be case-insensitive, so it's not possible to be consistent with 1808 // a boolean flag. Right now this is handled with a slight hack in 1809 // WhitelistDictionary#shouldForciblyAutoCorrectFrom. 1810 final int quotesCount = wordComposer.trailingSingleQuotesCount(); 1811 final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected( 1812 mSuggest.getUnigramDictionaries(), 1813 // If the typed string ends with a single quote, for dictionary lookup purposes 1814 // we behave as if the single quote was not here. Here, we are looking up the 1815 // typed string in the dictionary (to avoid autocorrecting from an existing 1816 // word, so for consistency this lookup should be made WITHOUT the trailing 1817 // single quote. 1818 quotesCount > 0 1819 ? typedWord.subSequence(0, typedWord.length() - quotesCount) : typedWord, 1820 preferCapitalization()); 1821 if (mCorrectionMode == Suggest.CORRECTION_FULL 1822 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { 1823 autoCorrectionAvailable |= (!allowsToBeAutoCorrected); 1824 } 1825 // Don't auto-correct words with multiple capital letter 1826 autoCorrectionAvailable &= !wordComposer.isMostlyCaps(); 1827 1828 // Basically, we update the suggestion strip only when suggestion count > 1. However, 1829 // there is an exception: We update the suggestion strip whenever typed word's length 1830 // is 1 or typed word is found in dictionary, regardless of suggestion count. Actually, 1831 // in most cases, suggestion count is 1 when typed word's length is 1, but we do always 1832 // need to clear the previous state when the user starts typing a word (i.e. typed word's 1833 // length == 1). 1834 if (typedWord != null) { 1835 if (builder.size() > 1 || typedWord.length() == 1 || (!allowsToBeAutoCorrected) 1836 || mSuggestionsView.isShowingAddToDictionaryHint()) { 1837 builder.setTypedWordValid(!allowsToBeAutoCorrected).setHasMinimalSuggestion( 1838 autoCorrectionAvailable); 1839 } else { 1840 SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions(); 1841 if (previousSuggestions == mSettingsValues.mSuggestPuncList) { 1842 if (builder.size() == 0) { 1843 return; 1844 } 1845 previousSuggestions = SuggestedWords.EMPTY; 1846 } 1847 builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions); 1848 } 1849 } 1850 showSuggestions(builder.build(), typedWord); 1851 } 1852 1853 public void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) { 1854 final boolean shouldBlockAutoCorrectionBySafetyNet = 1855 Utils.shouldBlockAutoCorrectionBySafetyNet(suggestedWords, mSuggest); 1856 if (shouldBlockAutoCorrectionBySafetyNet) { 1857 suggestedWords.setShouldBlockAutoCorrection(); 1858 } 1859 setSuggestions(suggestedWords); 1860 if (suggestedWords.size() > 0) { 1861 if (shouldBlockAutoCorrectionBySafetyNet) { 1862 mBestWord = typedWord; 1863 } else if (suggestedWords.hasAutoCorrectionWord()) { 1864 mBestWord = suggestedWords.getWord(1); 1865 } else { 1866 mBestWord = typedWord; 1867 } 1868 } else { 1869 mBestWord = null; 1870 } 1871 setSuggestionStripShown(isSuggestionsStripVisible()); 1872 } 1873 1874 private boolean pickDefaultSuggestion(int separatorCode) { 1875 // Complete any pending suggestions query first 1876 if (mHandler.hasPendingUpdateSuggestions()) { 1877 mHandler.cancelUpdateSuggestions(); 1878 updateSuggestions(); 1879 } 1880 if (mBestWord != null && mBestWord.length() > 0) { 1881 Utils.Stats.onAutoCorrection(mWordComposer.getTypedWord(), mBestWord.toString(), 1882 separatorCode); 1883 TextEntryState.acceptedDefault(mWordComposer.getTypedWord(), mBestWord, 1884 separatorCode); 1885 mExpectingUpdateSelection = true; 1886 commitBestWord(mBestWord); 1887 if (!mBestWord.equals(mWordComposer.getTypedWord())) { 1888 mWordSavedForAutoCorrectCancellation = mBestWord.toString(); 1889 } 1890 // Add the word to the user unigram dictionary if it's not a known word 1891 addToUserUnigramAndBigramDictionaries(mBestWord, 1892 UserUnigramDictionary.FREQUENCY_FOR_TYPED); 1893 return true; 1894 } 1895 return false; 1896 } 1897 1898 @Override 1899 public void pickSuggestionManually(int index, CharSequence suggestion) { 1900 mComposingStateManager.onFinishComposingText(); 1901 SuggestedWords suggestions = mSuggestionsView.getSuggestions(); 1902 mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion, 1903 mSettingsValues.mWordSeparators); 1904 1905 final InputConnection ic = getCurrentInputConnection(); 1906 if (ic != null) { 1907 ic.beginBatchEdit(); 1908 } 1909 if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null 1910 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 1911 if (ic != null) { 1912 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; 1913 ic.commitCompletion(completionInfo); 1914 } 1915 mCommittedLength = suggestion.length(); 1916 if (mSuggestionsView != null) { 1917 mSuggestionsView.clear(); 1918 } 1919 mKeyboardSwitcher.updateShiftState(); 1920 if (ic != null) { 1921 ic.endBatchEdit(); 1922 } 1923 return; 1924 } 1925 1926 // If this is a punctuation, apply it through the normal key press 1927 if (suggestion.length() == 1 && (mSettingsValues.isWordSeparator(suggestion.charAt(0)) 1928 || mSettingsValues.isSuggestedPunctuation(suggestion.charAt(0)))) { 1929 // Word separators are suggested before the user inputs something. 1930 // So, LatinImeLogger logs "" as a user's input. 1931 LatinImeLogger.logOnManualSuggestion( 1932 "", suggestion.toString(), index, suggestions.mWords); 1933 // Find out whether the previous character is a space. If it is, as a special case 1934 // for punctuation entered through the suggestion strip, it should be swapped 1935 // if it was a magic or a weak space. This is meant to help in case the user 1936 // pressed space on purpose of displaying the suggestion strip punctuation. 1937 final int rawPrimaryCode = suggestion.charAt(0); 1938 // Maybe apply the "bidi mirrored" conversions for parentheses 1939 final LatinKeyboard keyboard = mKeyboardSwitcher.getLatinKeyboard(); 1940 final boolean isRtl = keyboard != null && keyboard.mIsRtlKeyboard; 1941 final int primaryCode = Key.getRtlParenthesisCode(rawPrimaryCode, isRtl); 1942 1943 insertPunctuationFromSuggestionStrip(ic, primaryCode); 1944 // TODO: the following endBatchEdit seems useless, check 1945 if (ic != null) { 1946 ic.endBatchEdit(); 1947 } 1948 return; 1949 } 1950 if (!mHasUncommittedTypedChars) { 1951 // If we are not composing a word, then it was a suggestion inferred from 1952 // context - no user input. We should reset the word composer. 1953 mWordComposer.reset(); 1954 } 1955 mExpectingUpdateSelection = true; 1956 commitBestWord(suggestion); 1957 // Add the word to the auto dictionary if it's not a known word 1958 if (index == 0) { 1959 addToUserUnigramAndBigramDictionaries(suggestion, 1960 UserUnigramDictionary.FREQUENCY_FOR_PICKED); 1961 } else { 1962 addToOnlyBigramDictionary(suggestion, 1); 1963 } 1964 LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(), 1965 suggestion.toString(), index, suggestions.mWords); 1966 TextEntryState.acceptedSuggestion(mWordComposer.getTypedWord().toString(), suggestion); 1967 // Follow it with a space 1968 if (mInsertSpaceOnPickSuggestionManually) { 1969 sendMagicSpace(); 1970 } 1971 1972 // We should show the "Touch again to save" hint if the user pressed the first entry 1973 // AND either: 1974 // - There is no dictionary (we know that because we tried to load it => null != mSuggest 1975 // AND mSuggest.hasMainDictionary() is false) 1976 // - There is a dictionary and the word is not in it 1977 // Please note that if mSuggest is null, it means that everything is off: suggestion 1978 // and correction, so we shouldn't try to show the hint 1979 // We used to look at mCorrectionMode here, but showing the hint should have nothing 1980 // to do with the autocorrection setting. 1981 final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null 1982 // If there is no dictionary the hint should be shown. 1983 && (!mSuggest.hasMainDictionary() 1984 // If "suggestion" is not in the dictionary, the hint should be shown. 1985 || !AutoCorrection.isValidWord( 1986 mSuggest.getUnigramDictionaries(), suggestion, true)); 1987 1988 // Fool the state watcher so that a subsequent backspace will not do a revert, unless 1989 // we just did a correction, in which case we need to stay in 1990 // TextEntryState.State.PICKED_SUGGESTION state. 1991 TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true, 1992 WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); 1993 Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE, 1994 WordComposer.NOT_A_COORDINATE); 1995 if (!showingAddToDictionaryHint) { 1996 // If we're not showing the "Touch again to save", then show corrections again. 1997 // In case the cursor position doesn't change, make sure we show the suggestions again. 1998 updateBigramPredictions(); 1999 // Updating the predictions right away may be slow and feel unresponsive on slower 2000 // terminals. On the other hand if we just postUpdateBigramPredictions() it will 2001 // take a noticeable delay to update them which may feel uneasy. 2002 } 2003 if (showingAddToDictionaryHint) { 2004 if (mIsUserDictionaryAvailable) { 2005 mSuggestionsView.showAddToDictionaryHint( 2006 suggestion, mSettingsValues.mHintToSaveText); 2007 } else { 2008 mHandler.postUpdateSuggestions(); 2009 } 2010 } 2011 if (ic != null) { 2012 ic.endBatchEdit(); 2013 } 2014 } 2015 2016 /** 2017 * Commits the chosen word to the text field and saves it for later retrieval. 2018 */ 2019 private void commitBestWord(CharSequence bestWord) { 2020 final KeyboardSwitcher switcher = mKeyboardSwitcher; 2021 if (!switcher.isKeyboardAvailable()) 2022 return; 2023 final InputConnection ic = getCurrentInputConnection(); 2024 if (ic != null) { 2025 mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators); 2026 if (mSettingsValues.mEnableSuggestionSpanInsertion) { 2027 final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions(); 2028 ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( 2029 this, bestWord, suggestedWords), 1); 2030 } else { 2031 ic.commitText(bestWord, 1); 2032 } 2033 } 2034 mHasUncommittedTypedChars = false; 2035 mCommittedLength = bestWord.length(); 2036 } 2037 2038 private static final WordComposer sEmptyWordComposer = new WordComposer(); 2039 public void updateBigramPredictions() { 2040 if (mSuggest == null || !isSuggestionsRequested()) 2041 return; 2042 2043 if (!mSettingsValues.mBigramPredictionEnabled) { 2044 setPunctuationSuggestions(); 2045 return; 2046 } 2047 2048 final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(), 2049 mSettingsValues.mWordSeparators); 2050 SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(sEmptyWordComposer, 2051 prevWord, mKeyboardSwitcher.getLatinKeyboard().getProximityInfo(), mCorrectionMode); 2052 2053 if (builder.size() > 0) { 2054 // Explicitly supply an empty typed word (the no-second-arg version of 2055 // showSuggestions will retrieve the word near the cursor, we don't want that here) 2056 showSuggestions(builder.build(), ""); 2057 } else { 2058 if (!isShowingPunctuationList()) setPunctuationSuggestions(); 2059 } 2060 } 2061 2062 public void setPunctuationSuggestions() { 2063 setSuggestions(mSettingsValues.mSuggestPuncList); 2064 setSuggestionStripShown(isSuggestionsStripVisible()); 2065 } 2066 2067 private void addToUserUnigramAndBigramDictionaries(CharSequence suggestion, 2068 int frequencyDelta) { 2069 checkAddToDictionary(suggestion, frequencyDelta, false); 2070 } 2071 2072 private void addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta) { 2073 checkAddToDictionary(suggestion, frequencyDelta, true); 2074 } 2075 2076 /** 2077 * Adds to the UserBigramDictionary and/or UserUnigramDictionary 2078 * @param selectedANotTypedWord true if it should be added to bigram dictionary if possible 2079 */ 2080 private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta, 2081 boolean selectedANotTypedWord) { 2082 if (suggestion == null || suggestion.length() < 1) return; 2083 2084 // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be 2085 // adding words in situations where the user or application really didn't 2086 // want corrections enabled or learned. 2087 if (!(mCorrectionMode == Suggest.CORRECTION_FULL 2088 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { 2089 return; 2090 } 2091 2092 if (null != mSuggest && null != mUserUnigramDictionary) { 2093 final boolean selectedATypedWordAndItsInUserUnigramDic = 2094 !selectedANotTypedWord && mUserUnigramDictionary.isValidWord(suggestion); 2095 final boolean isValidWord = AutoCorrection.isValidWord( 2096 mSuggest.getUnigramDictionaries(), suggestion, true); 2097 final boolean needsToAddToUserUnigramDictionary = 2098 selectedATypedWordAndItsInUserUnigramDic || !isValidWord; 2099 if (needsToAddToUserUnigramDictionary) { 2100 mUserUnigramDictionary.addWord(suggestion.toString(), frequencyDelta); 2101 } 2102 } 2103 2104 if (mUserBigramDictionary != null) { 2105 // We don't want to register as bigrams words separated by a separator. 2106 // For example "I will, and you too" : we don't want the pair ("will" "and") to be 2107 // a bigram. 2108 final InputConnection ic = getCurrentInputConnection(); 2109 if (null != ic) { 2110 final CharSequence prevWord = 2111 EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators); 2112 if (!TextUtils.isEmpty(prevWord)) { 2113 mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString()); 2114 } 2115 } 2116 } 2117 } 2118 2119 public boolean isCursorTouchingWord() { 2120 final InputConnection ic = getCurrentInputConnection(); 2121 if (ic == null) return false; 2122 CharSequence toLeft = ic.getTextBeforeCursor(1, 0); 2123 CharSequence toRight = ic.getTextAfterCursor(1, 0); 2124 if (!TextUtils.isEmpty(toLeft) 2125 && !mSettingsValues.isWordSeparator(toLeft.charAt(0)) 2126 && !mSettingsValues.isSuggestedPunctuation(toLeft.charAt(0))) { 2127 return true; 2128 } 2129 if (!TextUtils.isEmpty(toRight) 2130 && !mSettingsValues.isWordSeparator(toRight.charAt(0)) 2131 && !mSettingsValues.isSuggestedPunctuation(toRight.charAt(0))) { 2132 return true; 2133 } 2134 return false; 2135 } 2136 2137 // "ic" must not be null 2138 private static boolean sameAsTextBeforeCursor(final InputConnection ic, CharSequence text) { 2139 CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0); 2140 return TextUtils.equals(text, beforeText); 2141 } 2142 2143 // "ic" must not be null 2144 /** 2145 * Check if the cursor is actually at the end of a word. If so, restart suggestions on this 2146 * word, else do nothing. 2147 */ 2148 private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord( 2149 final InputConnection ic) { 2150 // Bail out if the cursor is not at the end of a word (cursor must be preceded by 2151 // non-whitespace, non-separator, non-start-of-text) 2152 // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. 2153 final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0); 2154 if (TextUtils.isEmpty(textBeforeCursor) 2155 || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return; 2156 2157 // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, 2158 // separator or end of line/text) 2159 // Example: "test|"<EOL> "te|st" get rejected here 2160 final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0); 2161 if (!TextUtils.isEmpty(textAfterCursor) 2162 && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return; 2163 2164 // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) 2165 // Example: " '|" gets rejected here but "I'|" and "I|" are okay 2166 final CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators); 2167 if (TextUtils.isEmpty(word)) return; 2168 if (word.length() == 1 && !Character.isLetter(word.charAt(0))) return; 2169 2170 // Okay, we are at the end of a word. Restart suggestions. 2171 restartSuggestionsOnWordBeforeCursor(ic, word); 2172 } 2173 2174 // "ic" must not be null 2175 private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic, 2176 final CharSequence word) { 2177 mWordComposer.setComposingWord(word, mKeyboardSwitcher.getLatinKeyboard()); 2178 // mBestWord will be set appropriately by updateSuggestions() called by the handler 2179 mBestWord = null; 2180 mHasUncommittedTypedChars = true; 2181 mComposingStateManager.onStartComposingText(); 2182 TextEntryState.restartSuggestionsOnWordBeforeCursor(); 2183 ic.deleteSurroundingText(word.length(), 0); 2184 ic.setComposingText(word, 1); 2185 mHandler.postUpdateSuggestions(); 2186 } 2187 2188 // "ic" must not be null 2189 private void cancelAutoCorrect(final InputConnection ic) { 2190 final int cancelLength = mWordSavedForAutoCorrectCancellation.length(); 2191 final CharSequence separator = ic.getTextBeforeCursor(1, 0); 2192 if (DEBUG) { 2193 final String wordBeforeCursor = 2194 ic.getTextBeforeCursor(cancelLength + 1, 0).subSequence(0, cancelLength) 2195 .toString(); 2196 if (!mWordSavedForAutoCorrectCancellation.equals(wordBeforeCursor)) { 2197 throw new RuntimeException("cancelAutoCorrect check failed: we thought we were " 2198 + "reverting \"" + mWordSavedForAutoCorrectCancellation 2199 + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); 2200 } 2201 if (mWordComposer.getTypedWord().equals(wordBeforeCursor)) { 2202 throw new RuntimeException("cancelAutoCorrect check failed: we wanted to cancel " 2203 + "auto correction and revert to \"" + mWordComposer.getTypedWord() 2204 + "\" but we found this very string before the cursor"); 2205 } 2206 } 2207 ic.deleteSurroundingText(cancelLength + 1, 0); 2208 2209 // Re-insert the separator 2210 ic.commitText(mWordComposer.getTypedWord(), 1); 2211 TextEntryState.acceptedTyped(mWordComposer.getTypedWord()); 2212 ic.commitText(separator, 1); 2213 TextEntryState.typedCharacter(separator.charAt(0), true, 2214 WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); 2215 Utils.Stats.onSeparator(separator.charAt(0), WordComposer.NOT_A_COORDINATE, 2216 WordComposer.NOT_A_COORDINATE); 2217 mHandler.cancelUpdateBigramPredictions(); 2218 mHandler.postUpdateSuggestions(); 2219 } 2220 2221 // "ic" must not be null 2222 private void restartSuggestionsOnManuallyPickedTypedWord(final InputConnection ic) { 2223 final CharSequence separator = ic.getTextBeforeCursor(1, 0); 2224 final int restartLength = mCommittedLength; 2225 if (DEBUG) { 2226 final String wordBeforeCursor = 2227 ic.getTextBeforeCursor(restartLength + 1, 0).subSequence(0, restartLength) 2228 .toString(); 2229 if (!mWordComposer.getTypedWord().equals(wordBeforeCursor)) { 2230 throw new RuntimeException("restartSuggestionsOnManuallyPickedTypedWord " 2231 + "check failed: we thought we were reverting \"" 2232 + mWordComposer.getTypedWord() 2233 + "\", but before the cursor we found \"" 2234 + wordBeforeCursor + "\""); 2235 } 2236 } 2237 ic.deleteSurroundingText(restartLength + 1, 0); 2238 2239 // Note: this relies on the last word still being held in the WordComposer 2240 // Note: in the interest of code simplicity, we may want to just call 2241 // restartSuggestionsOnWordBeforeCursorIfAtEndOfWord instead, but retrieving 2242 // the old WordComposer allows to reuse the actual typed coordinates. 2243 mHasUncommittedTypedChars = true; 2244 ic.setComposingText(mWordComposer.getTypedWord(), 1); 2245 TextEntryState.backspace(); 2246 mHandler.cancelUpdateBigramPredictions(); 2247 mHandler.postUpdateSuggestions(); 2248 } 2249 2250 // "ic" must not be null 2251 private boolean revertDoubleSpace(final InputConnection ic) { 2252 mHandler.cancelDoubleSpacesTimer(); 2253 // Here we test whether we indeed have a period and a space before us. This should not 2254 // be needed, but it's there just in case something went wrong. 2255 final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); 2256 if (!". ".equals(textBeforeCursor)) 2257 return false; 2258 ic.beginBatchEdit(); 2259 ic.deleteSurroundingText(2, 0); 2260 ic.commitText(" ", 1); 2261 ic.endBatchEdit(); 2262 return true; 2263 } 2264 2265 private static boolean revertSwapPunctuation(final InputConnection ic) { 2266 // Here we test whether we indeed have a space and something else before us. This should not 2267 // be needed, but it's there just in case something went wrong. 2268 final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); 2269 // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to 2270 // enter surrogate pairs this code will have been removed. 2271 if (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1)) 2272 return false; 2273 ic.beginBatchEdit(); 2274 ic.deleteSurroundingText(2, 0); 2275 ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1); 2276 ic.endBatchEdit(); 2277 return true; 2278 } 2279 2280 public boolean isWordSeparator(int code) { 2281 return mSettingsValues.isWordSeparator(code); 2282 } 2283 2284 private void sendMagicSpace() { 2285 sendKeyChar((char)Keyboard.CODE_SPACE); 2286 mSpaceState = SPACE_STATE_MAGIC; 2287 mKeyboardSwitcher.updateShiftState(); 2288 } 2289 2290 public boolean preferCapitalization() { 2291 return mWordComposer.isFirstCharCapitalized(); 2292 } 2293 2294 // Notify that language or mode have been changed and toggleLanguage will update KeyboardID 2295 // according to new language or mode. 2296 public void onRefreshKeyboard() { 2297 if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) { 2298 // Before Honeycomb, Voice IME is in LatinIME and it changes the current input view, 2299 // so that we need to re-create the keyboard input view here. 2300 setInputView(mKeyboardSwitcher.onCreateInputView()); 2301 } 2302 // When the device locale is changed in SetupWizard etc., this method may get called via 2303 // onConfigurationChanged before SoftInputWindow is shown. 2304 if (mKeyboardSwitcher.getKeyboardView() != null) { 2305 // Reload keyboard because the current language has been changed. 2306 mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues); 2307 } 2308 initSuggest(); 2309 loadSettings(); 2310 } 2311 2312 private void hapticAndAudioFeedback(int primaryCode) { 2313 vibrate(); 2314 playKeyClick(primaryCode); 2315 } 2316 2317 @Override 2318 public void onPress(int primaryCode, boolean withSliding) { 2319 final KeyboardSwitcher switcher = mKeyboardSwitcher; 2320 if (switcher.isVibrateAndSoundFeedbackRequired()) { 2321 hapticAndAudioFeedback(primaryCode); 2322 } 2323 final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); 2324 if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { 2325 switcher.onPressShift(withSliding); 2326 } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { 2327 switcher.onPressSymbol(); 2328 } else { 2329 switcher.onOtherKeyPressed(); 2330 } 2331 } 2332 2333 @Override 2334 public void onRelease(int primaryCode, boolean withSliding) { 2335 KeyboardSwitcher switcher = mKeyboardSwitcher; 2336 // Reset any drag flags in the keyboard 2337 final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); 2338 if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { 2339 switcher.onReleaseShift(withSliding); 2340 } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { 2341 switcher.onReleaseSymbol(); 2342 } 2343 } 2344 2345 2346 // receive ringer mode change and network state change. 2347 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 2348 @Override 2349 public void onReceive(Context context, Intent intent) { 2350 final String action = intent.getAction(); 2351 if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { 2352 updateRingerMode(); 2353 } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 2354 mSubtypeSwitcher.onNetworkStateChanged(intent); 2355 } 2356 } 2357 }; 2358 2359 // update flags for silent mode 2360 private void updateRingerMode() { 2361 if (mAudioManager == null) { 2362 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 2363 if (mAudioManager == null) return; 2364 } 2365 mSilentModeOn = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); 2366 } 2367 2368 private void playKeyClick(int primaryCode) { 2369 // if mAudioManager is null, we don't have the ringer state yet 2370 // mAudioManager will be set by updateRingerMode 2371 if (mAudioManager == null) { 2372 if (mKeyboardSwitcher.getKeyboardView() != null) { 2373 updateRingerMode(); 2374 } 2375 } 2376 if (isSoundOn()) { 2377 final int sound; 2378 switch (primaryCode) { 2379 case Keyboard.CODE_DELETE: 2380 sound = AudioManager.FX_KEYPRESS_DELETE; 2381 break; 2382 case Keyboard.CODE_ENTER: 2383 sound = AudioManager.FX_KEYPRESS_RETURN; 2384 break; 2385 case Keyboard.CODE_SPACE: 2386 sound = AudioManager.FX_KEYPRESS_SPACEBAR; 2387 break; 2388 default: 2389 sound = AudioManager.FX_KEYPRESS_STANDARD; 2390 break; 2391 } 2392 mAudioManager.playSoundEffect(sound, mSettingsValues.mFxVolume); 2393 } 2394 } 2395 2396 public void vibrate() { 2397 if (!mSettingsValues.mVibrateOn) { 2398 return; 2399 } 2400 if (mSettingsValues.mKeypressVibrationDuration < 0) { 2401 // Go ahead with the system default 2402 LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 2403 if (inputView != null) { 2404 inputView.performHapticFeedback( 2405 HapticFeedbackConstants.KEYBOARD_TAP, 2406 HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); 2407 } 2408 } else if (mVibrator != null) { 2409 mVibrator.vibrate(mSettingsValues.mKeypressVibrationDuration); 2410 } 2411 } 2412 2413 public WordComposer getCurrentWord() { 2414 return mWordComposer; 2415 } 2416 2417 boolean isSoundOn() { 2418 return mSettingsValues.mSoundOn && !mSilentModeOn; 2419 } 2420 2421 private void updateCorrectionMode() { 2422 // TODO: cleanup messy flags 2423 final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled 2424 && !mInputTypeNoAutoCorrect; 2425 mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE; 2426 mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect) 2427 ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; 2428 } 2429 2430 private void updateSuggestionVisibility(final SharedPreferences prefs, final Resources res) { 2431 final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting; 2432 for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) { 2433 if (suggestionVisiblityStr.equals(res.getString(visibility))) { 2434 mSuggestionVisibility = visibility; 2435 break; 2436 } 2437 } 2438 } 2439 2440 protected void launchSettings() { 2441 launchSettingsClass(Settings.class); 2442 } 2443 2444 public void launchDebugSettings() { 2445 launchSettingsClass(DebugSettings.class); 2446 } 2447 2448 protected void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) { 2449 handleClose(); 2450 Intent intent = new Intent(); 2451 intent.setClass(LatinIME.this, settingsClass); 2452 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 2453 startActivity(intent); 2454 } 2455 2456 private void showSubtypeSelectorAndSettings() { 2457 final CharSequence title = getString(R.string.english_ime_input_options); 2458 final CharSequence[] items = new CharSequence[] { 2459 // TODO: Should use new string "Select active input modes". 2460 getString(R.string.language_selection_title), 2461 getString(R.string.english_ime_settings), 2462 }; 2463 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 2464 @Override 2465 public void onClick(DialogInterface di, int position) { 2466 di.dismiss(); 2467 switch (position) { 2468 case 0: 2469 Intent intent = CompatUtils.getInputLanguageSelectionIntent( 2470 mInputMethodId, Intent.FLAG_ACTIVITY_NEW_TASK 2471 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2472 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2473 startActivity(intent); 2474 break; 2475 case 1: 2476 launchSettings(); 2477 break; 2478 } 2479 } 2480 }; 2481 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 2482 .setItems(items, listener) 2483 .setTitle(title); 2484 showOptionDialogInternal(builder.create()); 2485 } 2486 2487 private void showOptionsMenu() { 2488 final CharSequence title = getString(R.string.english_ime_input_options); 2489 final CharSequence[] items = new CharSequence[] { 2490 getString(R.string.selectInputMethod), 2491 getString(R.string.english_ime_settings), 2492 }; 2493 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 2494 @Override 2495 public void onClick(DialogInterface di, int position) { 2496 di.dismiss(); 2497 switch (position) { 2498 case 0: 2499 mImm.showInputMethodPicker(); 2500 break; 2501 case 1: 2502 launchSettings(); 2503 break; 2504 } 2505 } 2506 }; 2507 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 2508 .setItems(items, listener) 2509 .setTitle(title); 2510 showOptionDialogInternal(builder.create()); 2511 } 2512 2513 @Override 2514 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { 2515 super.dump(fd, fout, args); 2516 2517 final Printer p = new PrintWriterPrinter(fout); 2518 p.println("LatinIME state :"); 2519 final Keyboard keyboard = mKeyboardSwitcher.getLatinKeyboard(); 2520 final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; 2521 p.println(" Keyboard mode = " + keyboardMode); 2522 p.println(" mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn); 2523 p.println(" mCorrectionMode=" + mCorrectionMode); 2524 p.println(" mHasUncommittedTypedChars=" + mHasUncommittedTypedChars); 2525 p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled); 2526 p.println(" mInsertSpaceOnPickSuggestionManually=" + mInsertSpaceOnPickSuggestionManually); 2527 p.println(" mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn); 2528 p.println(" TextEntryState.state=" + TextEntryState.getState()); 2529 p.println(" mSoundOn=" + mSettingsValues.mSoundOn); 2530 p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn); 2531 p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn); 2532 } 2533} 2534