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