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