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