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