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