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