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