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