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