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