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