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