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