LatinIME.java revision e8c4b99e5665c6c19793df6c2490ee4bf281bf63
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII; 20import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; 21import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; 22 23import android.app.Activity; 24import android.app.AlertDialog; 25import android.content.BroadcastReceiver; 26import android.content.Context; 27import android.content.DialogInterface; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.content.SharedPreferences; 31import android.content.pm.PackageInfo; 32import android.content.res.Configuration; 33import android.content.res.Resources; 34import android.graphics.Rect; 35import android.inputmethodservice.InputMethodService; 36import android.media.AudioManager; 37import android.net.ConnectivityManager; 38import android.os.Debug; 39import android.os.Handler; 40import android.os.HandlerThread; 41import android.os.IBinder; 42import android.os.Message; 43import android.os.SystemClock; 44import android.preference.PreferenceManager; 45import android.text.InputType; 46import android.text.SpannableString; 47import android.text.TextUtils; 48import android.text.style.SuggestionSpan; 49import android.util.Log; 50import android.util.PrintWriterPrinter; 51import android.util.Printer; 52import android.view.KeyCharacterMap; 53import android.view.KeyEvent; 54import android.view.View; 55import android.view.ViewGroup.LayoutParams; 56import android.view.Window; 57import android.view.WindowManager; 58import android.view.inputmethod.CompletionInfo; 59import android.view.inputmethod.CorrectionInfo; 60import android.view.inputmethod.EditorInfo; 61import android.view.inputmethod.InputMethodSubtype; 62 63import com.android.inputmethod.accessibility.AccessibilityUtils; 64import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; 65import com.android.inputmethod.annotations.UsedForTesting; 66import com.android.inputmethod.compat.AppWorkaroundsUtils; 67import com.android.inputmethod.compat.InputMethodServiceCompatUtils; 68import com.android.inputmethod.compat.SuggestionSpanUtils; 69import com.android.inputmethod.dictionarypack.DictionaryPackConstants; 70import com.android.inputmethod.event.EventInterpreter; 71import com.android.inputmethod.keyboard.KeyDetector; 72import com.android.inputmethod.keyboard.Keyboard; 73import com.android.inputmethod.keyboard.KeyboardActionListener; 74import com.android.inputmethod.keyboard.KeyboardId; 75import com.android.inputmethod.keyboard.KeyboardSwitcher; 76import com.android.inputmethod.keyboard.MainKeyboardView; 77import com.android.inputmethod.latin.RichInputConnection.Range; 78import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 79import com.android.inputmethod.latin.Utils.Stats; 80import com.android.inputmethod.latin.define.ProductionFlag; 81import com.android.inputmethod.latin.suggestions.SuggestionStripView; 82import com.android.inputmethod.research.ResearchLogger; 83 84import java.io.FileDescriptor; 85import java.io.PrintWriter; 86import java.util.ArrayList; 87import java.util.Locale; 88import java.util.TreeSet; 89 90/** 91 * Input method implementation for Qwerty'ish keyboard. 92 */ 93public class LatinIME extends InputMethodService implements KeyboardActionListener, 94 SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener, 95 Suggest.SuggestInitializationListener { 96 private static final String TAG = LatinIME.class.getSimpleName(); 97 private static final boolean TRACE = false; 98 private static boolean DEBUG; 99 100 private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; 101 102 // How many continuous deletes at which to start deleting at a higher speed. 103 private static final int DELETE_ACCELERATE_AT = 20; 104 // Key events coming any faster than this are long-presses. 105 private static final int QUICK_PRESS = 200; 106 107 private static final int PENDING_IMS_CALLBACK_DURATION = 800; 108 109 /** 110 * The name of the scheme used by the Package Manager to warn of a new package installation, 111 * replacement or removal. 112 */ 113 private static final String SCHEME_PACKAGE = "package"; 114 115 private static final int SPACE_STATE_NONE = 0; 116 // Double space: the state where the user pressed space twice quickly, which LatinIME 117 // resolved as period-space. Undoing this converts the period to a space. 118 private static final int SPACE_STATE_DOUBLE = 1; 119 // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip 120 // have just been swapped. Undoing this swaps them back; the space is still considered weak. 121 private static final int SPACE_STATE_SWAP_PUNCTUATION = 2; 122 // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak 123 // spaces happen when the user presses space, accepting the current suggestion (whether 124 // it's an auto-correction or not). 125 private static final int SPACE_STATE_WEAK = 3; 126 // Phantom space: a not-yet-inserted space that should get inserted on the next input, 127 // character provided it's not a separator. If it's a separator, the phantom space is dropped. 128 // Phantom spaces happen when a user chooses a word from the suggestion strip. 129 private static final int SPACE_STATE_PHANTOM = 4; 130 131 // Current space state of the input method. This can be any of the above constants. 132 private int mSpaceState; 133 134 private final Settings mSettings; 135 136 private View mExtractArea; 137 private View mKeyPreviewBackingView; 138 private View mSuggestionsContainer; 139 private SuggestionStripView mSuggestionStripView; 140 // Never null 141 private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; 142 @UsedForTesting Suggest mSuggest; 143 private CompletionInfo[] mApplicationSpecifiedCompletions; 144 private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils(); 145 146 private RichInputMethodManager mRichImm; 147 @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; 148 private final SubtypeSwitcher mSubtypeSwitcher; 149 private final SubtypeState mSubtypeState = new SubtypeState(); 150 // At start, create a default event interpreter that does nothing by passing it no decoder spec. 151 // The event interpreter should never be null. 152 private EventInterpreter mEventInterpreter = new EventInterpreter(this); 153 154 private boolean mIsMainDictionaryAvailable; 155 private UserBinaryDictionary mUserDictionary; 156 private UserHistoryDictionary mUserHistoryDictionary; 157 private boolean mIsUserDictionaryAvailable; 158 159 private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 160 private PositionalInfoForUserDictPendingAddition 161 mPositionalInfoForUserDictPendingAddition = null; 162 private final WordComposer mWordComposer = new WordComposer(); 163 private final RichInputConnection mConnection = new RichInputConnection(this); 164 private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); 165 166 // Keep track of the last selection range to decide if we need to show word alternatives 167 private static final int NOT_A_CURSOR_POSITION = -1; 168 private int mLastSelectionStart = NOT_A_CURSOR_POSITION; 169 private int mLastSelectionEnd = NOT_A_CURSOR_POSITION; 170 171 // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't 172 // "expect" it, it means the user actually moved the cursor. 173 private boolean mExpectingUpdateSelection; 174 private int mDeleteCount; 175 private long mLastKeyTime; 176 private TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); 177 178 // Member variables for remembering the current device orientation. 179 private int mDisplayOrientation; 180 181 // Object for reacting to adding/removing a dictionary pack. 182 private BroadcastReceiver mDictionaryPackInstallReceiver = 183 new DictionaryPackInstallBroadcastReceiver(this); 184 185 // Keeps track of most recently inserted text (multi-character key) for reverting 186 private String mEnteredText; 187 188 // TODO: This boolean is persistent state and causes large side effects at unexpected times. 189 // Find a way to remove it for readability. 190 private boolean mIsAutoCorrectionIndicatorOn; 191 192 private AlertDialog mOptionsDialog; 193 194 private final boolean mIsHardwareAcceleratedDrawingEnabled; 195 196 public final UIHandler mHandler = new UIHandler(this); 197 198 public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> { 199 private static final int MSG_UPDATE_SHIFT_STATE = 0; 200 private static final int MSG_PENDING_IMS_CALLBACK = 1; 201 private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; 202 private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; 203 private static final int MSG_RESUME_SUGGESTIONS = 4; 204 private static final int MSG_REOPEN_DICTIONARIES = 5; 205 206 private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; 207 208 private int mDelayUpdateSuggestions; 209 private int mDelayUpdateShiftState; 210 private long mDoubleSpacePeriodTimeout; 211 private long mDoubleSpacePeriodTimerStart; 212 213 public UIHandler(final LatinIME outerInstance) { 214 super(outerInstance); 215 } 216 217 public void onCreate() { 218 final Resources res = getOuterInstance().getResources(); 219 mDelayUpdateSuggestions = 220 res.getInteger(R.integer.config_delay_update_suggestions); 221 mDelayUpdateShiftState = 222 res.getInteger(R.integer.config_delay_update_shift_state); 223 mDoubleSpacePeriodTimeout = 224 res.getInteger(R.integer.config_double_space_period_timeout); 225 } 226 227 @Override 228 public void handleMessage(final Message msg) { 229 final LatinIME latinIme = getOuterInstance(); 230 final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; 231 switch (msg.what) { 232 case MSG_UPDATE_SUGGESTION_STRIP: 233 latinIme.updateSuggestionStrip(); 234 break; 235 case MSG_UPDATE_SHIFT_STATE: 236 switcher.updateShiftState(); 237 break; 238 case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: 239 latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj, 240 msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); 241 break; 242 case MSG_RESUME_SUGGESTIONS: 243 latinIme.restartSuggestionsOnWordTouchedByCursor(); 244 break; 245 case MSG_REOPEN_DICTIONARIES: 246 latinIme.initSuggest(); 247 // In theory we could call latinIme.updateSuggestionStrip() right away, but 248 // in the practice, the dictionary is not finished opening yet so we wouldn't 249 // get any suggestions. Wait one frame. 250 postUpdateSuggestionStrip(); 251 break; 252 } 253 } 254 255 public void postUpdateSuggestionStrip() { 256 sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions); 257 } 258 259 public void postReopenDictionaries() { 260 sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)); 261 } 262 263 public void postResumeSuggestions() { 264 removeMessages(MSG_RESUME_SUGGESTIONS); 265 sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); 266 } 267 268 public void cancelUpdateSuggestionStrip() { 269 removeMessages(MSG_UPDATE_SUGGESTION_STRIP); 270 } 271 272 public boolean hasPendingUpdateSuggestions() { 273 return hasMessages(MSG_UPDATE_SUGGESTION_STRIP); 274 } 275 276 public boolean hasPendingReopenDictionaries() { 277 return hasMessages(MSG_REOPEN_DICTIONARIES); 278 } 279 280 public void postUpdateShiftState() { 281 removeMessages(MSG_UPDATE_SHIFT_STATE); 282 sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState); 283 } 284 285 public void cancelUpdateShiftState() { 286 removeMessages(MSG_UPDATE_SHIFT_STATE); 287 } 288 289 public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, 290 final boolean dismissGestureFloatingPreviewText) { 291 removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 292 final int arg1 = dismissGestureFloatingPreviewText 293 ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : 0; 294 obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords) 295 .sendToTarget(); 296 } 297 298 public void startDoubleSpacePeriodTimer() { 299 mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis(); 300 } 301 302 public void cancelDoubleSpacePeriodTimer() { 303 mDoubleSpacePeriodTimerStart = 0; 304 } 305 306 public boolean isAcceptingDoubleSpacePeriod() { 307 return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart 308 < mDoubleSpacePeriodTimeout; 309 } 310 311 // Working variables for the following methods. 312 private boolean mIsOrientationChanging; 313 private boolean mPendingSuccessiveImsCallback; 314 private boolean mHasPendingStartInput; 315 private boolean mHasPendingFinishInputView; 316 private boolean mHasPendingFinishInput; 317 private EditorInfo mAppliedEditorInfo; 318 319 public void startOrientationChanging() { 320 removeMessages(MSG_PENDING_IMS_CALLBACK); 321 resetPendingImsCallback(); 322 mIsOrientationChanging = true; 323 final LatinIME latinIme = getOuterInstance(); 324 if (latinIme.isInputViewShown()) { 325 latinIme.mKeyboardSwitcher.saveKeyboardState(); 326 } 327 } 328 329 private void resetPendingImsCallback() { 330 mHasPendingFinishInputView = false; 331 mHasPendingFinishInput = false; 332 mHasPendingStartInput = false; 333 } 334 335 private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, 336 boolean restarting) { 337 if (mHasPendingFinishInputView) 338 latinIme.onFinishInputViewInternal(mHasPendingFinishInput); 339 if (mHasPendingFinishInput) 340 latinIme.onFinishInputInternal(); 341 if (mHasPendingStartInput) 342 latinIme.onStartInputInternal(editorInfo, restarting); 343 resetPendingImsCallback(); 344 } 345 346 public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { 347 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 348 // Typically this is the second onStartInput after orientation changed. 349 mHasPendingStartInput = true; 350 } else { 351 if (mIsOrientationChanging && restarting) { 352 // This is the first onStartInput after orientation changed. 353 mIsOrientationChanging = false; 354 mPendingSuccessiveImsCallback = true; 355 } 356 final LatinIME latinIme = getOuterInstance(); 357 executePendingImsCallback(latinIme, editorInfo, restarting); 358 latinIme.onStartInputInternal(editorInfo, restarting); 359 } 360 } 361 362 public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { 363 if (hasMessages(MSG_PENDING_IMS_CALLBACK) 364 && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { 365 // Typically this is the second onStartInputView after orientation changed. 366 resetPendingImsCallback(); 367 } else { 368 if (mPendingSuccessiveImsCallback) { 369 // This is the first onStartInputView after orientation changed. 370 mPendingSuccessiveImsCallback = false; 371 resetPendingImsCallback(); 372 sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), 373 PENDING_IMS_CALLBACK_DURATION); 374 } 375 final LatinIME latinIme = getOuterInstance(); 376 executePendingImsCallback(latinIme, editorInfo, restarting); 377 latinIme.onStartInputViewInternal(editorInfo, restarting); 378 mAppliedEditorInfo = editorInfo; 379 } 380 } 381 382 public void onFinishInputView(final boolean finishingInput) { 383 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 384 // Typically this is the first onFinishInputView after orientation changed. 385 mHasPendingFinishInputView = true; 386 } else { 387 final LatinIME latinIme = getOuterInstance(); 388 latinIme.onFinishInputViewInternal(finishingInput); 389 mAppliedEditorInfo = null; 390 } 391 } 392 393 public void onFinishInput() { 394 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 395 // Typically this is the first onFinishInput after orientation changed. 396 mHasPendingFinishInput = true; 397 } else { 398 final LatinIME latinIme = getOuterInstance(); 399 executePendingImsCallback(latinIme, null, false); 400 latinIme.onFinishInputInternal(); 401 } 402 } 403 } 404 405 static final class SubtypeState { 406 private InputMethodSubtype mLastActiveSubtype; 407 private boolean mCurrentSubtypeUsed; 408 409 public void currentSubtypeUsed() { 410 mCurrentSubtypeUsed = true; 411 } 412 413 public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) { 414 final InputMethodSubtype currentSubtype = richImm.getInputMethodManager() 415 .getCurrentInputMethodSubtype(); 416 final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; 417 final boolean currentSubtypeUsed = mCurrentSubtypeUsed; 418 if (currentSubtypeUsed) { 419 mLastActiveSubtype = currentSubtype; 420 mCurrentSubtypeUsed = false; 421 } 422 if (currentSubtypeUsed 423 && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype) 424 && !currentSubtype.equals(lastActiveSubtype)) { 425 richImm.setInputMethodAndSubtype(token, lastActiveSubtype); 426 return; 427 } 428 richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */); 429 } 430 } 431 432 public LatinIME() { 433 super(); 434 mSettings = Settings.getInstance(); 435 mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 436 mKeyboardSwitcher = KeyboardSwitcher.getInstance(); 437 mIsHardwareAcceleratedDrawingEnabled = 438 InputMethodServiceCompatUtils.enableHardwareAcceleration(this); 439 Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled); 440 } 441 442 @Override 443 public void onCreate() { 444 Settings.init(this); 445 LatinImeLogger.init(this); 446 RichInputMethodManager.init(this); 447 mRichImm = RichInputMethodManager.getInstance(); 448 SubtypeSwitcher.init(this); 449 KeyboardSwitcher.init(this); 450 AudioAndHapticFeedbackManager.init(this); 451 AccessibilityUtils.init(this); 452 453 super.onCreate(); 454 455 mHandler.onCreate(); 456 DEBUG = LatinImeLogger.sDBG; 457 458 // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}. 459 loadSettings(); 460 initSuggest(); 461 462 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 463 ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest); 464 } 465 mDisplayOrientation = getResources().getConfiguration().orientation; 466 467 // Register to receive ringer mode change and network state change. 468 // Also receive installation and removal of a dictionary pack. 469 final IntentFilter filter = new IntentFilter(); 470 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 471 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 472 registerReceiver(mReceiver, filter); 473 474 final IntentFilter packageFilter = new IntentFilter(); 475 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 476 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 477 packageFilter.addDataScheme(SCHEME_PACKAGE); 478 registerReceiver(mDictionaryPackInstallReceiver, packageFilter); 479 480 final IntentFilter newDictFilter = new IntentFilter(); 481 newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); 482 registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); 483 } 484 485 // Has to be package-visible for unit tests 486 @UsedForTesting 487 void loadSettings() { 488 final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 489 final InputAttributes inputAttributes = 490 new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode()); 491 mSettings.loadSettings(locale, inputAttributes); 492 AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent()); 493 // To load the keyboard we need to load all the settings once, but resetting the 494 // contacts dictionary should be deferred until after the new layout has been displayed 495 // to improve responsivity. In the language switching process, we post a reopenDictionaries 496 // message, then come here to read the settings for the new language before we change 497 // the layout; at this time, we need to skip resetting the contacts dictionary. It will 498 // be done later inside {@see #initSuggest()} when the reopenDictionaries message is 499 // processed. 500 if (!mHandler.hasPendingReopenDictionaries()) { 501 // May need to reset the contacts dictionary depending on the user settings. 502 resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); 503 } 504 } 505 506 // Note that this method is called from a non-UI thread. 507 @Override 508 public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) { 509 mIsMainDictionaryAvailable = isMainDictionaryAvailable; 510 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 511 if (mainKeyboardView != null) { 512 mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable); 513 } 514 } 515 516 private void initSuggest() { 517 final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 518 final String localeStr = subtypeLocale.toString(); 519 520 final ContactsBinaryDictionary oldContactsDictionary; 521 if (mSuggest != null) { 522 oldContactsDictionary = mSuggest.getContactsDictionary(); 523 mSuggest.close(); 524 } else { 525 oldContactsDictionary = null; 526 } 527 mSuggest = new Suggest(this /* Context */, subtypeLocale, 528 this /* SuggestInitializationListener */); 529 if (mSettings.getCurrent().mCorrectionEnabled) { 530 mSuggest.setAutoCorrectionThreshold(mSettings.getCurrent().mAutoCorrectionThreshold); 531 } 532 533 mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); 534 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 535 ResearchLogger.getInstance().initSuggest(mSuggest); 536 } 537 538 mUserDictionary = new UserBinaryDictionary(this, localeStr); 539 mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); 540 mSuggest.setUserDictionary(mUserDictionary); 541 542 resetContactsDictionary(oldContactsDictionary); 543 544 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 545 mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, prefs); 546 mSuggest.setUserHistoryDictionary(mUserHistoryDictionary); 547 } 548 549 /** 550 * Resets the contacts dictionary in mSuggest according to the user settings. 551 * 552 * This method takes an optional contacts dictionary to use when the locale hasn't changed 553 * since the contacts dictionary can be opened or closed as necessary depending on the settings. 554 * 555 * @param oldContactsDictionary an optional dictionary to use, or null 556 */ 557 private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) { 558 final boolean shouldSetDictionary = 559 (null != mSuggest && mSettings.getCurrent().mUseContactsDict); 560 561 final ContactsBinaryDictionary dictionaryToUse; 562 if (!shouldSetDictionary) { 563 // Make sure the dictionary is closed. If it is already closed, this is a no-op, 564 // so it's safe to call it anyways. 565 if (null != oldContactsDictionary) oldContactsDictionary.close(); 566 dictionaryToUse = null; 567 } else { 568 final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 569 if (null != oldContactsDictionary) { 570 if (!oldContactsDictionary.mLocale.equals(locale)) { 571 // If the locale has changed then recreate the contacts dictionary. This 572 // allows locale dependent rules for handling bigram name predictions. 573 oldContactsDictionary.close(); 574 dictionaryToUse = new ContactsBinaryDictionary(this, locale); 575 } else { 576 // Make sure the old contacts dictionary is opened. If it is already open, 577 // this is a no-op, so it's safe to call it anyways. 578 oldContactsDictionary.reopen(this); 579 dictionaryToUse = oldContactsDictionary; 580 } 581 } else { 582 dictionaryToUse = new ContactsBinaryDictionary(this, locale); 583 } 584 } 585 586 if (null != mSuggest) { 587 mSuggest.setContactsDictionary(dictionaryToUse); 588 } 589 } 590 591 /* package private */ void resetSuggestMainDict() { 592 final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 593 mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */); 594 mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); 595 } 596 597 @Override 598 public void onDestroy() { 599 if (mSuggest != null) { 600 mSuggest.close(); 601 mSuggest = null; 602 } 603 mSettings.onDestroy(); 604 unregisterReceiver(mReceiver); 605 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 606 ResearchLogger.getInstance().onDestroy(); 607 } 608 unregisterReceiver(mDictionaryPackInstallReceiver); 609 LatinImeLogger.commit(); 610 LatinImeLogger.onDestroy(); 611 super.onDestroy(); 612 } 613 614 @Override 615 public void onConfigurationChanged(final Configuration conf) { 616 // If orientation changed while predicting, commit the change 617 if (mDisplayOrientation != conf.orientation) { 618 mDisplayOrientation = conf.orientation; 619 mHandler.startOrientationChanging(); 620 mConnection.beginBatchEdit(); 621 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 622 mConnection.finishComposingText(); 623 mConnection.endBatchEdit(); 624 if (isShowingOptionDialog()) { 625 mOptionsDialog.dismiss(); 626 } 627 } 628 super.onConfigurationChanged(conf); 629 } 630 631 @Override 632 public View onCreateInputView() { 633 return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); 634 } 635 636 @Override 637 public void setInputView(final View view) { 638 super.setInputView(view); 639 mExtractArea = getWindow().getWindow().getDecorView() 640 .findViewById(android.R.id.extractArea); 641 mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); 642 mSuggestionsContainer = view.findViewById(R.id.suggestions_container); 643 mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); 644 if (mSuggestionStripView != null) 645 mSuggestionStripView.setListener(this, view); 646 if (LatinImeLogger.sVISUALDEBUG) { 647 mKeyPreviewBackingView.setBackgroundColor(0x10FF0000); 648 } 649 } 650 651 @Override 652 public void setCandidatesView(final View view) { 653 // To ensure that CandidatesView will never be set. 654 return; 655 } 656 657 @Override 658 public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { 659 mHandler.onStartInput(editorInfo, restarting); 660 } 661 662 @Override 663 public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { 664 mHandler.onStartInputView(editorInfo, restarting); 665 } 666 667 @Override 668 public void onFinishInputView(final boolean finishingInput) { 669 mHandler.onFinishInputView(finishingInput); 670 } 671 672 @Override 673 public void onFinishInput() { 674 mHandler.onFinishInput(); 675 } 676 677 @Override 678 public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { 679 // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() 680 // is not guaranteed. It may even be called at the same time on a different thread. 681 mSubtypeSwitcher.onSubtypeChanged(subtype); 682 loadKeyboard(); 683 } 684 685 private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { 686 super.onStartInput(editorInfo, restarting); 687 } 688 689 @SuppressWarnings("deprecation") 690 private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) { 691 super.onStartInputView(editorInfo, restarting); 692 final KeyboardSwitcher switcher = mKeyboardSwitcher; 693 final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView(); 694 final SettingsValues currentSettings = mSettings.getCurrent(); 695 696 if (editorInfo == null) { 697 Log.e(TAG, "Null EditorInfo in onStartInputView()"); 698 if (LatinImeLogger.sDBG) { 699 throw new NullPointerException("Null EditorInfo in onStartInputView()"); 700 } 701 return; 702 } 703 if (DEBUG) { 704 Log.d(TAG, "onStartInputView: editorInfo:" 705 + String.format("inputType=0x%08x imeOptions=0x%08x", 706 editorInfo.inputType, editorInfo.imeOptions)); 707 Log.d(TAG, "All caps = " 708 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) 709 + ", sentence caps = " 710 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) 711 + ", word caps = " 712 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); 713 } 714 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 715 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 716 ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs); 717 } 718 if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) { 719 Log.w(TAG, "Deprecated private IME option specified: " 720 + editorInfo.privateImeOptions); 721 Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead"); 722 } 723 if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) { 724 Log.w(TAG, "Deprecated private IME option specified: " 725 + editorInfo.privateImeOptions); 726 Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead"); 727 } 728 729 final PackageInfo packageInfo = 730 TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName); 731 mAppWorkAroundsUtils.setPackageInfo(packageInfo); 732 if (null == packageInfo) { 733 new TargetPackageInfoGetterTask(this /* context */, this /* listener */) 734 .execute(editorInfo.packageName); 735 } 736 737 LatinImeLogger.onStartInputView(editorInfo); 738 // In landscape mode, this method gets called without the input view being created. 739 if (mainKeyboardView == null) { 740 return; 741 } 742 743 // Forward this event to the accessibility utilities, if enabled. 744 final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); 745 if (accessUtils.isTouchExplorationEnabled()) { 746 accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting); 747 } 748 749 final boolean inputTypeChanged = !currentSettings.isSameInputType(editorInfo); 750 final boolean isDifferentTextField = !restarting || inputTypeChanged; 751 if (isDifferentTextField) { 752 mSubtypeSwitcher.updateParametersOnStartInputView(); 753 } 754 755 // The EditorInfo might have a flag that affects fullscreen mode. 756 // Note: This call should be done by InputMethodService? 757 updateFullscreenMode(); 758 mApplicationSpecifiedCompletions = null; 759 760 // The app calling setText() has the effect of clearing the composing 761 // span, so we should reset our state unconditionally, even if restarting is true. 762 mEnteredText = null; 763 resetComposingState(true /* alsoResetLastComposedWord */); 764 mDeleteCount = 0; 765 mSpaceState = SPACE_STATE_NONE; 766 mRecapitalizeStatus.deactivate(); 767 mCurrentlyPressedHardwareKeys.clear(); 768 769 // Note: the following does a round-trip IPC on the main thread: be careful 770 final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 771 if (null != mSuggest && null != currentLocale && !currentLocale.equals(mSuggest.mLocale)) { 772 initSuggest(); 773 } 774 if (mSuggestionStripView != null) { 775 // This will set the punctuation suggestions if next word suggestion is off; 776 // otherwise it will clear the suggestion strip. 777 setPunctuationSuggestions(); 778 } 779 mSuggestedWords = SuggestedWords.EMPTY; 780 781 mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart, 782 false /* shouldFinishComposition */); 783 784 if (isDifferentTextField) { 785 mainKeyboardView.closing(); 786 loadSettings(); 787 788 if (mSuggest != null && currentSettings.mCorrectionEnabled) { 789 mSuggest.setAutoCorrectionThreshold(currentSettings.mAutoCorrectionThreshold); 790 } 791 792 switcher.loadKeyboard(editorInfo, currentSettings); 793 } else if (restarting) { 794 // TODO: Come up with a more comprehensive way to reset the keyboard layout when 795 // a keyboard layout set doesn't get reloaded in this method. 796 switcher.resetKeyboardStateToAlphabet(); 797 // In apps like Talk, we come here when the text is sent and the field gets emptied and 798 // we need to re-evaluate the shift state, but not the whole layout which would be 799 // disruptive. 800 // Space state must be updated before calling updateShiftState 801 switcher.updateShiftState(); 802 } 803 setSuggestionStripShownInternal( 804 isSuggestionsStripVisible(), /* needsInputViewShown */ false); 805 806 mLastSelectionStart = editorInfo.initialSelStart; 807 mLastSelectionEnd = editorInfo.initialSelEnd; 808 809 mHandler.cancelUpdateSuggestionStrip(); 810 mHandler.cancelDoubleSpacePeriodTimer(); 811 812 mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable); 813 mainKeyboardView.setKeyPreviewPopupEnabled(currentSettings.mKeyPreviewPopupOn, 814 currentSettings.mKeyPreviewPopupDismissDelay); 815 mainKeyboardView.setSlidingKeyInputPreviewEnabled( 816 currentSettings.mSlidingKeyInputPreviewEnabled); 817 mainKeyboardView.setGestureHandlingEnabledByUser( 818 currentSettings.mGestureInputEnabled); 819 mainKeyboardView.setGesturePreviewMode(currentSettings.mGesturePreviewTrailEnabled, 820 currentSettings.mGestureFloatingPreviewTextEnabled); 821 822 // If we have a user dictionary addition in progress, we should check now if we should 823 // replace the previously committed string with the word that has actually been added 824 // to the user dictionary. 825 if (null != mPositionalInfoForUserDictPendingAddition 826 && mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( 827 mConnection, editorInfo, mLastSelectionEnd, currentLocale)) { 828 mPositionalInfoForUserDictPendingAddition = null; 829 } 830 // If tryReplaceWithActualWord returns false, we don't know what word was 831 // added to the user dictionary yet, so we keep the data and defer processing. The word will 832 // be replaced when the user dictionary reports back with the actual word, which ends 833 // up calling #onWordAddedToUserDictionary() in this class. 834 835 if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); 836 } 837 838 // Callback for the TargetPackageInfoGetterTask 839 @Override 840 public void onTargetPackageInfoKnown(final PackageInfo info) { 841 mAppWorkAroundsUtils.setPackageInfo(info); 842 } 843 844 @Override 845 public void onWindowHidden() { 846 super.onWindowHidden(); 847 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 848 if (mainKeyboardView != null) { 849 mainKeyboardView.closing(); 850 } 851 } 852 853 private void onFinishInputInternal() { 854 super.onFinishInput(); 855 856 LatinImeLogger.commit(); 857 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 858 if (mainKeyboardView != null) { 859 mainKeyboardView.closing(); 860 } 861 } 862 863 private void onFinishInputViewInternal(final boolean finishingInput) { 864 super.onFinishInputView(finishingInput); 865 mKeyboardSwitcher.onFinishInputView(); 866 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 867 if (mainKeyboardView != null) { 868 mainKeyboardView.cancelAllMessages(); 869 } 870 // Remove pending messages related to update suggestions 871 mHandler.cancelUpdateSuggestionStrip(); 872 // Should do the following in onFinishInputInternal but until JB MR2 it's not called :( 873 if (mWordComposer.isComposingWord()) mConnection.finishComposingText(); 874 resetComposingState(true /* alsoResetLastComposedWord */); 875 mRichImm.clearSubtypeCaches(); 876 // Notify ResearchLogger 877 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 878 ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart, 879 mLastSelectionEnd, getCurrentInputConnection()); 880 } 881 } 882 883 @Override 884 public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, 885 final int newSelStart, final int newSelEnd, 886 final int composingSpanStart, final int composingSpanEnd) { 887 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 888 composingSpanStart, composingSpanEnd); 889 if (DEBUG) { 890 Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart 891 + ", ose=" + oldSelEnd 892 + ", lss=" + mLastSelectionStart 893 + ", lse=" + mLastSelectionEnd 894 + ", nss=" + newSelStart 895 + ", nse=" + newSelEnd 896 + ", cs=" + composingSpanStart 897 + ", ce=" + composingSpanEnd); 898 } 899 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 900 final boolean expectingUpdateSelectionFromLogger = 901 ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection(); 902 ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd, 903 oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, 904 composingSpanEnd, mExpectingUpdateSelection, 905 expectingUpdateSelectionFromLogger, mConnection); 906 if (expectingUpdateSelectionFromLogger) { 907 // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work 908 return; 909 } 910 } 911 912 // TODO: refactor the following code to be less contrived. 913 // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means 914 // that the cursor is not at the end of the composing span, or there is a selection. 915 // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place 916 // as last time we were called (if there is a selection, it means the start hasn't 917 // changed, so it's the end that did). 918 final boolean selectionChanged = (newSelStart != composingSpanEnd 919 || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart; 920 // if composingSpanStart and composingSpanEnd are -1, it means there is no composing 921 // span in the view - we can use that to narrow down whether the cursor was moved 922 // by us or not. If we are composing a word but there is no composing span, then 923 // we know for sure the cursor moved while we were composing and we should reset 924 // the state. 925 final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; 926 // If the keyboard is not visible, we don't need to do all the housekeeping work, as it 927 // will be reset when the keyboard shows up anyway. 928 // TODO: revisit this when LatinIME supports hardware keyboards. 929 // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown(). 930 // TODO: find a better way to simulate actual execution. 931 if (isInputViewShown() && !mExpectingUpdateSelection 932 && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) { 933 // TAKE CARE: there is a race condition when we enter this test even when the user 934 // did not explicitly move the cursor. This happens when typing fast, where two keys 935 // turn this flag on in succession and both onUpdateSelection() calls arrive after 936 // the second one - the first call successfully avoids this test, but the second one 937 // enters. For the moment we rely on noComposingSpan to further reduce the impact. 938 939 // TODO: the following is probably better done in resetEntireInputState(). 940 // it should only happen when the cursor moved, and the very purpose of the 941 // test below is to narrow down whether this happened or not. Likewise with 942 // the call to updateShiftState. 943 // We set this to NONE because after a cursor move, we don't want the space 944 // state-related special processing to kick in. 945 mSpaceState = SPACE_STATE_NONE; 946 947 if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) { 948 // If we are composing a word and moving the cursor, we would want to set a 949 // suggestion span for recorrection to work correctly. Unfortunately, that 950 // would involve the keyboard committing some new text, which would move the 951 // cursor back to where it was. Latin IME could then fix the position of the cursor 952 // again, but the asynchronous nature of the calls results in this wreaking havoc 953 // with selection on double tap and the like. 954 // Another option would be to send suggestions each time we set the composing 955 // text, but that is probably too expensive to do, so we decided to leave things 956 // as is. 957 resetEntireInputState(newSelStart); 958 } 959 960 // We moved the cursor. If we are touching a word, we need to resume suggestion, 961 // unless suggestions are off. 962 if (isSuggestionsStripVisible()) { 963 mHandler.postResumeSuggestions(); 964 } 965 // Reset the last recapitalization. 966 mRecapitalizeStatus.deactivate(); 967 mKeyboardSwitcher.updateShiftState(); 968 } 969 mExpectingUpdateSelection = false; 970 971 // Make a note of the cursor position 972 mLastSelectionStart = newSelStart; 973 mLastSelectionEnd = newSelEnd; 974 mSubtypeState.currentSubtypeUsed(); 975 } 976 977 /** 978 * This is called when the user has clicked on the extracted text view, 979 * when running in fullscreen mode. The default implementation hides 980 * the suggestions view when this happens, but only if the extracted text 981 * editor has a vertical scroll bar because its text doesn't fit. 982 * Here we override the behavior due to the possibility that a re-correction could 983 * cause the suggestions strip to disappear and re-appear. 984 */ 985 @Override 986 public void onExtractedTextClicked() { 987 if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; 988 989 super.onExtractedTextClicked(); 990 } 991 992 /** 993 * This is called when the user has performed a cursor movement in the 994 * extracted text view, when it is running in fullscreen mode. The default 995 * implementation hides the suggestions view when a vertical movement 996 * happens, but only if the extracted text editor has a vertical scroll bar 997 * because its text doesn't fit. 998 * Here we override the behavior due to the possibility that a re-correction could 999 * cause the suggestions strip to disappear and re-appear. 1000 */ 1001 @Override 1002 public void onExtractedCursorMovement(final int dx, final int dy) { 1003 if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; 1004 1005 super.onExtractedCursorMovement(dx, dy); 1006 } 1007 1008 @Override 1009 public void hideWindow() { 1010 LatinImeLogger.commit(); 1011 mKeyboardSwitcher.onHideWindow(); 1012 1013 if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { 1014 AccessibleKeyboardViewProxy.getInstance().onHideWindow(); 1015 } 1016 1017 if (TRACE) Debug.stopMethodTracing(); 1018 if (mOptionsDialog != null && mOptionsDialog.isShowing()) { 1019 mOptionsDialog.dismiss(); 1020 mOptionsDialog = null; 1021 } 1022 super.hideWindow(); 1023 } 1024 1025 @Override 1026 public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) { 1027 if (DEBUG) { 1028 Log.i(TAG, "Received completions:"); 1029 if (applicationSpecifiedCompletions != null) { 1030 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { 1031 Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); 1032 } 1033 } 1034 } 1035 if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return; 1036 if (applicationSpecifiedCompletions == null) { 1037 clearSuggestionStrip(); 1038 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1039 ResearchLogger.latinIME_onDisplayCompletions(null); 1040 } 1041 return; 1042 } 1043 mApplicationSpecifiedCompletions = 1044 CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions); 1045 1046 final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = 1047 SuggestedWords.getFromApplicationSpecifiedCompletions( 1048 applicationSpecifiedCompletions); 1049 final SuggestedWords suggestedWords = new SuggestedWords( 1050 applicationSuggestedWords, 1051 false /* typedWordValid */, 1052 false /* hasAutoCorrectionCandidate */, 1053 false /* isPunctuationSuggestions */, 1054 false /* isObsoleteSuggestions */, 1055 false /* isPrediction */); 1056 // When in fullscreen mode, show completions generated by the application 1057 final boolean isAutoCorrection = false; 1058 setSuggestedWords(suggestedWords, isAutoCorrection); 1059 setAutoCorrectionIndicator(isAutoCorrection); 1060 setSuggestionStripShown(true); 1061 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1062 ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); 1063 } 1064 } 1065 1066 private void setSuggestionStripShownInternal(final boolean shown, 1067 final boolean needsInputViewShown) { 1068 // TODO: Modify this if we support suggestions with hard keyboard 1069 if (onEvaluateInputViewShown() && mSuggestionsContainer != null) { 1070 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1071 final boolean inputViewShown = (mainKeyboardView != null) 1072 ? mainKeyboardView.isShown() : false; 1073 final boolean shouldShowSuggestions = shown 1074 && (needsInputViewShown ? inputViewShown : true); 1075 if (isFullscreenMode()) { 1076 mSuggestionsContainer.setVisibility( 1077 shouldShowSuggestions ? View.VISIBLE : View.GONE); 1078 } else { 1079 mSuggestionsContainer.setVisibility( 1080 shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); 1081 } 1082 } 1083 } 1084 1085 private void setSuggestionStripShown(final boolean shown) { 1086 setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); 1087 } 1088 1089 private int getAdjustedBackingViewHeight() { 1090 final int currentHeight = mKeyPreviewBackingView.getHeight(); 1091 if (currentHeight > 0) { 1092 return currentHeight; 1093 } 1094 1095 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1096 if (mainKeyboardView == null) { 1097 return 0; 1098 } 1099 final int keyboardHeight = mainKeyboardView.getHeight(); 1100 final int suggestionsHeight = mSuggestionsContainer.getHeight(); 1101 final int displayHeight = getResources().getDisplayMetrics().heightPixels; 1102 final Rect rect = new Rect(); 1103 mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect); 1104 final int notificationBarHeight = rect.top; 1105 final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight 1106 - keyboardHeight; 1107 1108 final LayoutParams params = mKeyPreviewBackingView.getLayoutParams(); 1109 params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); 1110 mKeyPreviewBackingView.setLayoutParams(params); 1111 return params.height; 1112 } 1113 1114 @Override 1115 public void onComputeInsets(final InputMethodService.Insets outInsets) { 1116 super.onComputeInsets(outInsets); 1117 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1118 if (mainKeyboardView == null || mSuggestionsContainer == null) { 1119 return; 1120 } 1121 final int adjustedBackingHeight = getAdjustedBackingViewHeight(); 1122 final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE); 1123 final int backingHeight = backingGone ? 0 : adjustedBackingHeight; 1124 // In fullscreen mode, the height of the extract area managed by InputMethodService should 1125 // be considered. 1126 // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. 1127 final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; 1128 final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0 1129 : mSuggestionsContainer.getHeight(); 1130 final int extraHeight = extractHeight + backingHeight + suggestionsHeight; 1131 int visibleTopY = extraHeight; 1132 // Need to set touchable region only if input view is being shown 1133 if (mainKeyboardView.isShown()) { 1134 if (mSuggestionsContainer.getVisibility() == View.VISIBLE) { 1135 visibleTopY -= suggestionsHeight; 1136 } 1137 final int touchY = mainKeyboardView.isShowingMoreKeysPanel() ? 0 : visibleTopY; 1138 final int touchWidth = mainKeyboardView.getWidth(); 1139 final int touchHeight = mainKeyboardView.getHeight() + extraHeight 1140 // Extend touchable region below the keyboard. 1141 + EXTENDED_TOUCHABLE_REGION_HEIGHT; 1142 outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; 1143 outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight); 1144 } 1145 outInsets.contentTopInsets = visibleTopY; 1146 outInsets.visibleTopInsets = visibleTopY; 1147 } 1148 1149 @Override 1150 public boolean onEvaluateFullscreenMode() { 1151 // Reread resource value here, because this method is called by framework anytime as needed. 1152 final boolean isFullscreenModeAllowed = 1153 Settings.readUseFullscreenMode(getResources()); 1154 if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) { 1155 // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI 1156 // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI 1157 // without NO_FULLSCREEN doesn't work as expected. Because of this we need this 1158 // hack for now. Let's get rid of this once the framework gets fixed. 1159 final EditorInfo ei = getCurrentInputEditorInfo(); 1160 return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0)); 1161 } else { 1162 return false; 1163 } 1164 } 1165 1166 @Override 1167 public void updateFullscreenMode() { 1168 super.updateFullscreenMode(); 1169 1170 if (mKeyPreviewBackingView == null) return; 1171 // In fullscreen mode, no need to have extra space to show the key preview. 1172 // If not, we should have extra space above the keyboard to show the key preview. 1173 mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); 1174 } 1175 1176 // This will reset the whole input state to the starting state. It will clear 1177 // the composing word, reset the last composed word, tell the inputconnection about it. 1178 private void resetEntireInputState(final int newCursorPosition) { 1179 final boolean shouldFinishComposition = mWordComposer.isComposingWord(); 1180 resetComposingState(true /* alsoResetLastComposedWord */); 1181 if (mSettings.getCurrent().mBigramPredictionEnabled) { 1182 clearSuggestionStrip(); 1183 } else { 1184 setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false); 1185 } 1186 mConnection.resetCachesUponCursorMove(newCursorPosition, shouldFinishComposition); 1187 } 1188 1189 private void resetComposingState(final boolean alsoResetLastComposedWord) { 1190 mWordComposer.reset(); 1191 if (alsoResetLastComposedWord) 1192 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 1193 } 1194 1195 private void commitTyped(final String separatorString) { 1196 if (!mWordComposer.isComposingWord()) return; 1197 final String typedWord = mWordComposer.getTypedWord(); 1198 if (typedWord.length() > 0) { 1199 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1200 ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); 1201 } 1202 commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, 1203 separatorString); 1204 } 1205 } 1206 1207 // Called from the KeyboardSwitcher which needs to know auto caps state to display 1208 // the right layout. 1209 public int getCurrentAutoCapsState() { 1210 if (!mSettings.getCurrent().mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; 1211 1212 final EditorInfo ei = getCurrentInputEditorInfo(); 1213 if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; 1214 final int inputType = ei.inputType; 1215 // Warning: this depends on mSpaceState, which may not be the most current value. If 1216 // mSpaceState gets updated later, whoever called this may need to be told about it. 1217 return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(), 1218 SPACE_STATE_PHANTOM == mSpaceState); 1219 } 1220 1221 public int getCurrentRecapitalizeState() { 1222 if (!mRecapitalizeStatus.isActive() 1223 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { 1224 // Not recapitalizing at the moment 1225 return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; 1226 } 1227 return mRecapitalizeStatus.getCurrentMode(); 1228 } 1229 1230 // Factor in auto-caps and manual caps and compute the current caps mode. 1231 private int getActualCapsMode() { 1232 final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode(); 1233 if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode; 1234 final int auto = getCurrentAutoCapsState(); 1235 if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { 1236 return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; 1237 } 1238 if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED; 1239 return WordComposer.CAPS_MODE_OFF; 1240 } 1241 1242 private void swapSwapperAndSpace() { 1243 final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); 1244 // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. 1245 if (lastTwo != null && lastTwo.length() == 2 1246 && lastTwo.charAt(0) == Constants.CODE_SPACE) { 1247 mConnection.deleteSurroundingText(2, 0); 1248 final String text = lastTwo.charAt(1) + " "; 1249 mConnection.commitText(text, 1); 1250 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1251 ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text); 1252 } 1253 mKeyboardSwitcher.updateShiftState(); 1254 } 1255 } 1256 1257 private boolean maybeDoubleSpacePeriod() { 1258 if (!mSettings.getCurrent().mCorrectionEnabled) return false; 1259 if (!mSettings.getCurrent().mUseDoubleSpacePeriod) return false; 1260 if (!mHandler.isAcceptingDoubleSpacePeriod()) return false; 1261 final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0); 1262 if (lastThree != null && lastThree.length() == 3 1263 && canBeFollowedByDoubleSpacePeriod(lastThree.charAt(0)) 1264 && lastThree.charAt(1) == Constants.CODE_SPACE 1265 && lastThree.charAt(2) == Constants.CODE_SPACE) { 1266 mHandler.cancelDoubleSpacePeriodTimer(); 1267 mConnection.deleteSurroundingText(2, 0); 1268 final String textToInsert = ". "; 1269 mConnection.commitText(textToInsert, 1); 1270 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1271 ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert, 1272 false /* isBatchMode */); 1273 } 1274 mKeyboardSwitcher.updateShiftState(); 1275 return true; 1276 } 1277 return false; 1278 } 1279 1280 private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { 1281 // TODO: Check again whether there really ain't a better way to check this. 1282 // TODO: This should probably be language-dependant... 1283 return Character.isLetterOrDigit(codePoint) 1284 || codePoint == Constants.CODE_SINGLE_QUOTE 1285 || codePoint == Constants.CODE_DOUBLE_QUOTE 1286 || codePoint == Constants.CODE_CLOSING_PARENTHESIS 1287 || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET 1288 || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET 1289 || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET; 1290 } 1291 1292 // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is 1293 // pressed. 1294 @Override 1295 public void addWordToUserDictionary(final String word) { 1296 if (TextUtils.isEmpty(word)) { 1297 // Probably never supposed to happen, but just in case. 1298 mPositionalInfoForUserDictPendingAddition = null; 1299 return; 1300 } 1301 final String wordToEdit; 1302 if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) { 1303 wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); 1304 } else { 1305 wordToEdit = word; 1306 } 1307 mUserDictionary.addWordToUserDictionary(wordToEdit); 1308 } 1309 1310 public void onWordAddedToUserDictionary(final String newSpelling) { 1311 // If word was added but not by us, bail out 1312 if (null == mPositionalInfoForUserDictPendingAddition) return; 1313 if (mWordComposer.isComposingWord()) { 1314 // We are late... give up and return 1315 mPositionalInfoForUserDictPendingAddition = null; 1316 return; 1317 } 1318 mPositionalInfoForUserDictPendingAddition.setActualWordBeingAdded(newSpelling); 1319 if (mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( 1320 mConnection, getCurrentInputEditorInfo(), mLastSelectionEnd, 1321 mSubtypeSwitcher.getCurrentSubtypeLocale())) { 1322 mPositionalInfoForUserDictPendingAddition = null; 1323 } 1324 } 1325 1326 private static boolean isAlphabet(final int code) { 1327 return Character.isLetter(code); 1328 } 1329 1330 private void onSettingsKeyPressed() { 1331 if (isShowingOptionDialog()) return; 1332 showSubtypeSelectorAndSettings(); 1333 } 1334 1335 // Virtual codes representing custom requests. These are used in onCustomRequest() below. 1336 public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1; 1337 1338 @Override 1339 public boolean onCustomRequest(final int requestCode) { 1340 if (isShowingOptionDialog()) return false; 1341 switch (requestCode) { 1342 case CODE_SHOW_INPUT_METHOD_PICKER: 1343 if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { 1344 mRichImm.getInputMethodManager().showInputMethodPicker(); 1345 return true; 1346 } 1347 return false; 1348 } 1349 return false; 1350 } 1351 1352 private boolean isShowingOptionDialog() { 1353 return mOptionsDialog != null && mOptionsDialog.isShowing(); 1354 } 1355 1356 private void performEditorAction(final int actionId) { 1357 mConnection.performEditorAction(actionId); 1358 } 1359 1360 // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. 1361 private void handleLanguageSwitchKey() { 1362 final IBinder token = getWindow().getWindow().getAttributes().token; 1363 if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) { 1364 mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); 1365 return; 1366 } 1367 mSubtypeState.switchSubtype(token, mRichImm); 1368 } 1369 1370 private void sendDownUpKeyEventForBackwardCompatibility(final int code) { 1371 final long eventTime = SystemClock.uptimeMillis(); 1372 mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, 1373 KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1374 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1375 mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, 1376 KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1377 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1378 } 1379 1380 private void sendKeyCodePoint(final int code) { 1381 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1382 ResearchLogger.latinIME_sendKeyCodePoint(code); 1383 } 1384 // TODO: Remove this special handling of digit letters. 1385 // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. 1386 if (code >= '0' && code <= '9') { 1387 sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0); 1388 return; 1389 } 1390 1391 if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) { 1392 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate 1393 // a hardware keyboard event on pressing enter or delete. This is bad for many 1394 // reasons (there are race conditions with commits) but some applications are 1395 // relying on this behavior so we continue to support it for older apps. 1396 sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER); 1397 } else { 1398 final String text = new String(new int[] { code }, 0, 1); 1399 mConnection.commitText(text, text.length()); 1400 } 1401 } 1402 1403 // Implementation of {@link KeyboardActionListener}. 1404 @Override 1405 public void onCodeInput(final int primaryCode, final int x, final int y) { 1406 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1407 ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); 1408 } 1409 final long when = SystemClock.uptimeMillis(); 1410 if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { 1411 mDeleteCount = 0; 1412 } 1413 mLastKeyTime = when; 1414 mConnection.beginBatchEdit(); 1415 final KeyboardSwitcher switcher = mKeyboardSwitcher; 1416 // The space state depends only on the last character pressed and its own previous 1417 // state. Here, we revert the space state to neutral if the key is actually modifying 1418 // the input contents (any non-shift key), which is what we should do for 1419 // all inputs that do not result in a special state. Each character handling is then 1420 // free to override the state as they see fit. 1421 final int spaceState = mSpaceState; 1422 if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false; 1423 1424 // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. 1425 if (primaryCode != Constants.CODE_SPACE) { 1426 mHandler.cancelDoubleSpacePeriodTimer(); 1427 } 1428 1429 boolean didAutoCorrect = false; 1430 switch (primaryCode) { 1431 case Constants.CODE_DELETE: 1432 mSpaceState = SPACE_STATE_NONE; 1433 handleBackspace(spaceState); 1434 mDeleteCount++; 1435 mExpectingUpdateSelection = true; 1436 LatinImeLogger.logOnDelete(x, y); 1437 break; 1438 case Constants.CODE_SHIFT: 1439 // Note: Calling back to the keyboard on Shift key is handled in 1440 // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}. 1441 final Keyboard currentKeyboard = switcher.getKeyboard(); 1442 if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { 1443 // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for 1444 // alphabetic shift and shift while in symbol layout. 1445 handleRecapitalize(); 1446 } 1447 break; 1448 case Constants.CODE_CAPSLOCK: 1449 // Note: Changing keyboard to shift lock state is handled in 1450 // {@link KeyboardSwitcher#onCodeInput(int)}. 1451 break; 1452 case Constants.CODE_SWITCH_ALPHA_SYMBOL: 1453 // Note: Calling back to the keyboard on symbol key is handled in 1454 // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}. 1455 break; 1456 case Constants.CODE_SETTINGS: 1457 onSettingsKeyPressed(); 1458 break; 1459 case Constants.CODE_SHORTCUT: 1460 mSubtypeSwitcher.switchToShortcutIME(this); 1461 break; 1462 case Constants.CODE_ACTION_NEXT: 1463 performEditorAction(EditorInfo.IME_ACTION_NEXT); 1464 break; 1465 case Constants.CODE_ACTION_PREVIOUS: 1466 performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 1467 break; 1468 case Constants.CODE_LANGUAGE_SWITCH: 1469 handleLanguageSwitchKey(); 1470 break; 1471 case Constants.CODE_RESEARCH: 1472 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1473 ResearchLogger.getInstance().onResearchKeySelected(this); 1474 } 1475 break; 1476 case Constants.CODE_ENTER: 1477 final EditorInfo editorInfo = getCurrentInputEditorInfo(); 1478 final int imeOptionsActionId = 1479 InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); 1480 if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { 1481 // Either we have an actionLabel and we should performEditorAction with actionId 1482 // regardless of its value. 1483 performEditorAction(editorInfo.actionId); 1484 } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { 1485 // We didn't have an actionLabel, but we had another action to execute. 1486 // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, 1487 // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it 1488 // means there should be an action and the app didn't bother to set a specific 1489 // code for it - presumably it only handles one. It does not have to be treated 1490 // in any specific way: anything that is not IME_ACTION_NONE should be sent to 1491 // performEditorAction. 1492 performEditorAction(imeOptionsActionId); 1493 } else { 1494 // No action label, and the action from imeOptions is NONE: this is a regular 1495 // enter key that should input a carriage return. 1496 didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); 1497 } 1498 break; 1499 case Constants.CODE_SHIFT_ENTER: 1500 didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); 1501 break; 1502 default: 1503 didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState); 1504 break; 1505 } 1506 switcher.onCodeInput(primaryCode); 1507 // Reset after any single keystroke, except shift, capslock, and symbol-shift 1508 if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT 1509 && primaryCode != Constants.CODE_CAPSLOCK 1510 && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) 1511 mLastComposedWord.deactivate(); 1512 if (Constants.CODE_DELETE != primaryCode) { 1513 mEnteredText = null; 1514 } 1515 mConnection.endBatchEdit(); 1516 } 1517 1518 private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y, 1519 final int spaceState) { 1520 mSpaceState = SPACE_STATE_NONE; 1521 final boolean didAutoCorrect; 1522 if (mSettings.getCurrent().isWordSeparator(primaryCode)) { 1523 didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); 1524 } else { 1525 didAutoCorrect = false; 1526 if (SPACE_STATE_PHANTOM == spaceState) { 1527 if (mSettings.isInternal()) { 1528 if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { 1529 Stats.onAutoCorrection( 1530 "", mWordComposer.getTypedWord(), " ", mWordComposer); 1531 } 1532 } 1533 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 1534 // If we are in the middle of a recorrection, we need to commit the recorrection 1535 // first so that we can insert the character at the current cursor position. 1536 resetEntireInputState(mLastSelectionStart); 1537 } else { 1538 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 1539 } 1540 } 1541 final int keyX, keyY; 1542 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1543 if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { 1544 keyX = x; 1545 keyY = y; 1546 } else { 1547 keyX = Constants.NOT_A_COORDINATE; 1548 keyY = Constants.NOT_A_COORDINATE; 1549 } 1550 handleCharacter(primaryCode, keyX, keyY, spaceState); 1551 } 1552 mExpectingUpdateSelection = true; 1553 return didAutoCorrect; 1554 } 1555 1556 // Called from PointerTracker through the KeyboardActionListener interface 1557 @Override 1558 public void onTextInput(final String rawText) { 1559 mConnection.beginBatchEdit(); 1560 if (mWordComposer.isComposingWord()) { 1561 commitCurrentAutoCorrection(rawText); 1562 } else { 1563 resetComposingState(true /* alsoResetLastComposedWord */); 1564 } 1565 mHandler.postUpdateSuggestionStrip(); 1566 final String text = specificTldProcessingOnTextInput(rawText); 1567 if (SPACE_STATE_PHANTOM == mSpaceState) { 1568 promotePhantomSpace(); 1569 } 1570 mConnection.commitText(text, 1); 1571 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1572 ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */); 1573 } 1574 mConnection.endBatchEdit(); 1575 // Space state must be updated before calling updateShiftState 1576 mSpaceState = SPACE_STATE_NONE; 1577 mKeyboardSwitcher.updateShiftState(); 1578 mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT); 1579 mEnteredText = text; 1580 } 1581 1582 @Override 1583 public void onStartBatchInput() { 1584 BatchInputUpdater.getInstance().onStartBatchInput(this); 1585 mHandler.cancelUpdateSuggestionStrip(); 1586 mConnection.beginBatchEdit(); 1587 if (mWordComposer.isComposingWord()) { 1588 if (mSettings.isInternal()) { 1589 if (mWordComposer.isBatchMode()) { 1590 Stats.onAutoCorrection("", mWordComposer.getTypedWord(), " ", mWordComposer); 1591 } 1592 } 1593 final int wordComposerSize = mWordComposer.size(); 1594 // Since isComposingWord() is true, the size is at least 1. 1595 final int lastChar = mWordComposer.getCodeBeforeCursor(); 1596 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 1597 // If we are in the middle of a recorrection, we need to commit the recorrection 1598 // first so that we can insert the batch input at the current cursor position. 1599 resetEntireInputState(mLastSelectionStart); 1600 } else if (wordComposerSize <= 1) { 1601 // We auto-correct the previous (typed, not gestured) string iff it's one character 1602 // long. The reason for this is, even in the middle of gesture typing, you'll still 1603 // tap one-letter words and you want them auto-corrected (typically, "i" in English 1604 // should become "I"). However for any longer word, we assume that the reason for 1605 // tapping probably is that the word you intend to type is not in the dictionary, 1606 // so we do not attempt to correct, on the assumption that if that was a dictionary 1607 // word, the user would probably have gestured instead. 1608 commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); 1609 } else { 1610 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 1611 } 1612 mExpectingUpdateSelection = true; 1613 } 1614 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 1615 if (Character.isLetterOrDigit(codePointBeforeCursor) 1616 || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) { 1617 mSpaceState = SPACE_STATE_PHANTOM; 1618 } 1619 mConnection.endBatchEdit(); 1620 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); 1621 } 1622 1623 private static final class BatchInputUpdater implements Handler.Callback { 1624 private final Handler mHandler; 1625 private LatinIME mLatinIme; 1626 private final Object mLock = new Object(); 1627 private boolean mInBatchInput; // synchronized using {@link #mLock}. 1628 1629 private BatchInputUpdater() { 1630 final HandlerThread handlerThread = new HandlerThread( 1631 BatchInputUpdater.class.getSimpleName()); 1632 handlerThread.start(); 1633 mHandler = new Handler(handlerThread.getLooper(), this); 1634 } 1635 1636 // Initialization-on-demand holder 1637 private static final class OnDemandInitializationHolder { 1638 public static final BatchInputUpdater sInstance = new BatchInputUpdater(); 1639 } 1640 1641 public static BatchInputUpdater getInstance() { 1642 return OnDemandInitializationHolder.sInstance; 1643 } 1644 1645 private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; 1646 1647 @Override 1648 public boolean handleMessage(final Message msg) { 1649 switch (msg.what) { 1650 case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: 1651 updateBatchInput((InputPointers)msg.obj); 1652 break; 1653 } 1654 return true; 1655 } 1656 1657 // Run in the UI thread. 1658 public void onStartBatchInput(final LatinIME latinIme) { 1659 synchronized (mLock) { 1660 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 1661 mInBatchInput = true; 1662 mLatinIme = latinIme; 1663 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1664 SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); 1665 } 1666 } 1667 1668 // Run in the Handler thread. 1669 private void updateBatchInput(final InputPointers batchPointers) { 1670 synchronized (mLock) { 1671 if (!mInBatchInput) { 1672 // Batch input has ended or canceled while the message was being delivered. 1673 return; 1674 } 1675 final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); 1676 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1677 suggestedWords, false /* dismissGestureFloatingPreviewText */); 1678 } 1679 } 1680 1681 // Run in the UI thread. 1682 public void onUpdateBatchInput(final InputPointers batchPointers) { 1683 if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) { 1684 return; 1685 } 1686 mHandler.obtainMessage( 1687 MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers) 1688 .sendToTarget(); 1689 } 1690 1691 public void onCancelBatchInput() { 1692 synchronized (mLock) { 1693 mInBatchInput = false; 1694 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1695 SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); 1696 } 1697 } 1698 1699 // Run in the UI thread. 1700 public SuggestedWords onEndBatchInput(final InputPointers batchPointers) { 1701 synchronized (mLock) { 1702 mInBatchInput = false; 1703 final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); 1704 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1705 suggestedWords, true /* dismissGestureFloatingPreviewText */); 1706 return suggestedWords; 1707 } 1708 } 1709 1710 // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to 1711 // be synchronized. 1712 private SuggestedWords getSuggestedWordsGestureLocked(final InputPointers batchPointers) { 1713 mLatinIme.mWordComposer.setBatchInputPointers(batchPointers); 1714 final SuggestedWords suggestedWords = 1715 mLatinIme.getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_GESTURE); 1716 final int suggestionCount = suggestedWords.size(); 1717 if (suggestionCount <= 1) { 1718 final String mostProbableSuggestion = (suggestionCount == 0) ? null 1719 : suggestedWords.getWord(0); 1720 return mLatinIme.getOlderSuggestions(mostProbableSuggestion); 1721 } 1722 return suggestedWords; 1723 } 1724 } 1725 1726 private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, 1727 final boolean dismissGestureFloatingPreviewText) { 1728 showSuggestionStrip(suggestedWords, null); 1729 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1730 mainKeyboardView.showGestureFloatingPreviewText(suggestedWords); 1731 if (dismissGestureFloatingPreviewText) { 1732 mainKeyboardView.dismissGestureFloatingPreviewText(); 1733 } 1734 } 1735 1736 @Override 1737 public void onUpdateBatchInput(final InputPointers batchPointers) { 1738 BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers); 1739 } 1740 1741 @Override 1742 public void onEndBatchInput(final InputPointers batchPointers) { 1743 final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput( 1744 batchPointers); 1745 final String batchInputText = suggestedWords.isEmpty() 1746 ? null : suggestedWords.getWord(0); 1747 if (TextUtils.isEmpty(batchInputText)) { 1748 return; 1749 } 1750 mWordComposer.setBatchInputWord(batchInputText); 1751 mConnection.beginBatchEdit(); 1752 if (SPACE_STATE_PHANTOM == mSpaceState) { 1753 promotePhantomSpace(); 1754 } 1755 mConnection.setComposingText(batchInputText, 1); 1756 mExpectingUpdateSelection = true; 1757 mConnection.endBatchEdit(); 1758 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1759 ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); 1760 } 1761 // Space state must be updated before calling updateShiftState 1762 mSpaceState = SPACE_STATE_PHANTOM; 1763 mKeyboardSwitcher.updateShiftState(); 1764 } 1765 1766 private String specificTldProcessingOnTextInput(final String text) { 1767 if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD 1768 || !Character.isLetter(text.charAt(1))) { 1769 // Not a tld: do nothing. 1770 return text; 1771 } 1772 // We have a TLD (or something that looks like this): make sure we don't add 1773 // a space even if currently in phantom mode. 1774 mSpaceState = SPACE_STATE_NONE; 1775 // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code 1776 final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0); 1777 if (lastOne != null && lastOne.length() == 1 1778 && lastOne.charAt(0) == Constants.CODE_PERIOD) { 1779 return text.substring(1); 1780 } else { 1781 return text; 1782 } 1783 } 1784 1785 // Called from PointerTracker through the KeyboardActionListener interface 1786 @Override 1787 public void onFinishSlidingInput() { 1788 // User finished sliding input. 1789 mKeyboardSwitcher.onFinishSlidingInput(); 1790 } 1791 1792 // Called from PointerTracker through the KeyboardActionListener interface 1793 @Override 1794 public void onCancelInput() { 1795 // User released a finger outside any key 1796 // Nothing to do so far. 1797 } 1798 1799 @Override 1800 public void onCancelBatchInput() { 1801 BatchInputUpdater.getInstance().onCancelBatchInput(); 1802 } 1803 1804 private void handleBackspace(final int spaceState) { 1805 // In many cases, we may have to put the keyboard in auto-shift state again. However 1806 // we want to wait a few milliseconds before doing it to avoid the keyboard flashing 1807 // during key repeat. 1808 mHandler.postUpdateShiftState(); 1809 1810 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 1811 // If we are in the middle of a recorrection, we need to commit the recorrection 1812 // first so that we can remove the character at the current cursor position. 1813 resetEntireInputState(mLastSelectionStart); 1814 // When we exit this if-clause, mWordComposer.isComposingWord() will return false. 1815 } 1816 if (mWordComposer.isComposingWord()) { 1817 final int length = mWordComposer.size(); 1818 if (length > 0) { 1819 if (mWordComposer.isBatchMode()) { 1820 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1821 final String word = mWordComposer.getTypedWord(); 1822 ResearchLogger.latinIME_handleBackspace_batch(word, 1); 1823 } 1824 final String rejectedSuggestion = mWordComposer.getTypedWord(); 1825 mWordComposer.reset(); 1826 mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); 1827 } else { 1828 mWordComposer.deleteLast(); 1829 } 1830 mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1831 mHandler.postUpdateSuggestionStrip(); 1832 } else { 1833 mConnection.deleteSurroundingText(1, 0); 1834 } 1835 } else { 1836 if (mLastComposedWord.canRevertCommit()) { 1837 if (mSettings.isInternal()) { 1838 Stats.onAutoCorrectionCancellation(); 1839 } 1840 revertCommit(); 1841 return; 1842 } 1843 if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { 1844 // Cancel multi-character input: remove the text we just entered. 1845 // This is triggered on backspace after a key that inputs multiple characters, 1846 // like the smiley key or the .com key. 1847 final int length = mEnteredText.length(); 1848 mConnection.deleteSurroundingText(length, 0); 1849 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1850 ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText); 1851 } 1852 mEnteredText = null; 1853 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. 1854 // In addition we know that spaceState is false, and that we should not be 1855 // reverting any autocorrect at this point. So we can safely return. 1856 return; 1857 } 1858 if (SPACE_STATE_DOUBLE == spaceState) { 1859 mHandler.cancelDoubleSpacePeriodTimer(); 1860 if (mConnection.revertDoubleSpacePeriod()) { 1861 // No need to reset mSpaceState, it has already be done (that's why we 1862 // receive it as a parameter) 1863 return; 1864 } 1865 } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1866 if (mConnection.revertSwapPunctuation()) { 1867 // Likewise 1868 return; 1869 } 1870 } 1871 1872 // No cancelling of commit/double space/swap: we have a regular backspace. 1873 // We should backspace one char and restart suggestion if at the end of a word. 1874 if (mLastSelectionStart != mLastSelectionEnd) { 1875 // If there is a selection, remove it. 1876 final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; 1877 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); 1878 // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to 1879 // happen, and if it's wrong, the next call to onUpdateSelection will correct it, 1880 // but we want to set it right away to avoid it being used with the wrong values 1881 // later (typically, in a subsequent press on backspace). 1882 mLastSelectionEnd = mLastSelectionStart; 1883 mConnection.deleteSurroundingText(numCharsDeleted, 0); 1884 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1885 ResearchLogger.latinIME_handleBackspace(numCharsDeleted, 1886 false /* shouldUncommitLogUnit */); 1887 } 1888 } else { 1889 // There is no selection, just delete one character. 1890 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { 1891 // This should never happen. 1892 Log.e(TAG, "Backspace when we don't know the selection position"); 1893 } 1894 if (mAppWorkAroundsUtils.isBeforeJellyBean()) { 1895 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate 1896 // a hardware keyboard event on pressing enter or delete. This is bad for many 1897 // reasons (there are race conditions with commits) but some applications are 1898 // relying on this behavior so we continue to support it for older apps. 1899 sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL); 1900 } else { 1901 mConnection.deleteSurroundingText(1, 0); 1902 } 1903 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1904 ResearchLogger.latinIME_handleBackspace(1, true /* shouldUncommitLogUnit */); 1905 } 1906 if (mDeleteCount > DELETE_ACCELERATE_AT) { 1907 mConnection.deleteSurroundingText(1, 0); 1908 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1909 ResearchLogger.latinIME_handleBackspace(1, 1910 true /* shouldUncommitLogUnit */); 1911 } 1912 } 1913 } 1914 if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) { 1915 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(); 1916 } 1917 } 1918 } 1919 1920 /* 1921 * Strip a trailing space if necessary and returns whether it's a swap weak space situation. 1922 */ 1923 private boolean maybeStripSpace(final int code, 1924 final int spaceState, final boolean isFromSuggestionStrip) { 1925 if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1926 mConnection.removeTrailingSpace(); 1927 return false; 1928 } 1929 if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState) 1930 && isFromSuggestionStrip) { 1931 if (mSettings.getCurrent().isUsuallyPrecededBySpace(code)) return false; 1932 if (mSettings.getCurrent().isUsuallyFollowedBySpace(code)) return true; 1933 mConnection.removeTrailingSpace(); 1934 } 1935 return false; 1936 } 1937 1938 private void handleCharacter(final int primaryCode, final int x, 1939 final int y, final int spaceState) { 1940 boolean isComposingWord = mWordComposer.isComposingWord(); 1941 1942 // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. 1943 // See onStartBatchInput() to see how to do it. 1944 if (SPACE_STATE_PHANTOM == spaceState && 1945 !mSettings.getCurrent().isWordConnector(primaryCode)) { 1946 if (isComposingWord) { 1947 // Sanity check 1948 throw new RuntimeException("Should not be composing here"); 1949 } 1950 promotePhantomSpace(); 1951 } 1952 1953 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 1954 // If we are in the middle of a recorrection, we need to commit the recorrection 1955 // first so that we can insert the character at the current cursor position. 1956 resetEntireInputState(mLastSelectionStart); 1957 isComposingWord = false; 1958 } 1959 // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several 1960 // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI 1961 // thread here. 1962 if (!isComposingWord && (isAlphabet(primaryCode) 1963 || mSettings.getCurrent().isWordConnector(primaryCode)) 1964 && mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation) && 1965 !mConnection.isCursorTouchingWord(mSettings.getCurrent())) { 1966 // Reset entirely the composing state anyway, then start composing a new word unless 1967 // the character is a single quote. The idea here is, single quote is not a 1968 // separator and it should be treated as a normal character, except in the first 1969 // position where it should not start composing a word. 1970 isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode); 1971 // Here we don't need to reset the last composed word. It will be reset 1972 // when we commit this one, if we ever do; if on the other hand we backspace 1973 // it entirely and resume suggestions on the previous word, we'd like to still 1974 // have touch coordinates for it. 1975 resetComposingState(false /* alsoResetLastComposedWord */); 1976 } 1977 if (isComposingWord) { 1978 final int keyX, keyY; 1979 if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) { 1980 final KeyDetector keyDetector = 1981 mKeyboardSwitcher.getMainKeyboardView().getKeyDetector(); 1982 keyX = keyDetector.getTouchX(x); 1983 keyY = keyDetector.getTouchY(y); 1984 } else { 1985 keyX = x; 1986 keyY = y; 1987 } 1988 mWordComposer.add(primaryCode, keyX, keyY); 1989 // If it's the first letter, make note of auto-caps state 1990 if (mWordComposer.size() == 1) { 1991 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); 1992 } 1993 mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1994 } else { 1995 final boolean swapWeakSpace = maybeStripSpace(primaryCode, 1996 spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x); 1997 1998 sendKeyCodePoint(primaryCode); 1999 2000 if (swapWeakSpace) { 2001 swapSwapperAndSpace(); 2002 mSpaceState = SPACE_STATE_WEAK; 2003 } 2004 // In case the "add to dictionary" hint was still displayed. 2005 if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint(); 2006 } 2007 mHandler.postUpdateSuggestionStrip(); 2008 if (mSettings.isInternal()) { 2009 Utils.Stats.onNonSeparator((char)primaryCode, x, y); 2010 } 2011 } 2012 2013 private void handleRecapitalize() { 2014 if (mLastSelectionStart == mLastSelectionEnd) return; // No selection 2015 // If we have a recapitalize in progress, use it; otherwise, create a new one. 2016 if (!mRecapitalizeStatus.isActive() 2017 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { 2018 final CharSequence selectedText = 2019 mConnection.getSelectedText(0 /* flags, 0 for no styles */); 2020 if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection 2021 mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd, 2022 selectedText.toString(), mSettings.getCurrentLocale(), 2023 mSettings.getWordSeparators()); 2024 // We trim leading and trailing whitespace. 2025 mRecapitalizeStatus.trim(); 2026 // Trimming the object may have changed the length of the string, and we need to 2027 // reposition the selection handles accordingly. As this result in an IPC call, 2028 // only do it if it's actually necessary, in other words if the recapitalize status 2029 // is not set at the same place as before. 2030 if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { 2031 mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); 2032 mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); 2033 mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); 2034 } 2035 } 2036 mRecapitalizeStatus.rotate(); 2037 final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; 2038 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); 2039 mConnection.deleteSurroundingText(numCharsDeleted, 0); 2040 mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); 2041 mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); 2042 mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); 2043 mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); 2044 // Match the keyboard to the new state. 2045 mKeyboardSwitcher.updateShiftState(); 2046 } 2047 2048 // Returns true if we did an autocorrection, false otherwise. 2049 private boolean handleSeparator(final int primaryCode, final int x, final int y, 2050 final int spaceState) { 2051 boolean didAutoCorrect = false; 2052 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 2053 // If we are in the middle of a recorrection, we need to commit the recorrection 2054 // first so that we can insert the separator at the current cursor position. 2055 resetEntireInputState(mLastSelectionStart); 2056 } 2057 if (mWordComposer.isComposingWord()) { 2058 if (mSettings.getCurrent().mCorrectionEnabled) { 2059 // TODO: maybe cache Strings in an <String> sparse array or something 2060 commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1)); 2061 didAutoCorrect = true; 2062 } else { 2063 commitTyped(new String(new int[]{primaryCode}, 0, 1)); 2064 } 2065 } 2066 2067 final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, 2068 Constants.SUGGESTION_STRIP_COORDINATE == x); 2069 2070 if (SPACE_STATE_PHANTOM == spaceState && 2071 mSettings.getCurrent().isUsuallyPrecededBySpace(primaryCode)) { 2072 promotePhantomSpace(); 2073 } 2074 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2075 ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); 2076 } 2077 sendKeyCodePoint(primaryCode); 2078 2079 if (Constants.CODE_SPACE == primaryCode) { 2080 if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) { 2081 if (maybeDoubleSpacePeriod()) { 2082 mSpaceState = SPACE_STATE_DOUBLE; 2083 } else if (!isShowingPunctuationList()) { 2084 mSpaceState = SPACE_STATE_WEAK; 2085 } 2086 } 2087 2088 mHandler.startDoubleSpacePeriodTimer(); 2089 mHandler.postUpdateSuggestionStrip(); 2090 } else { 2091 if (swapWeakSpace) { 2092 swapSwapperAndSpace(); 2093 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; 2094 } else if (SPACE_STATE_PHANTOM == spaceState 2095 && mSettings.getCurrent().isUsuallyFollowedBySpace(primaryCode)) { 2096 // If we are in phantom space state, and the user presses a separator, we want to 2097 // stay in phantom space state so that the next keypress has a chance to add the 2098 // space. For example, if I type "Good dat", pick "day" from the suggestion strip 2099 // then insert a comma and go on to typing the next word, I want the space to be 2100 // inserted automatically before the next word, the same way it is when I don't 2101 // input the comma. 2102 // The case is a little different if the separator is a space stripper. Such a 2103 // separator does not normally need a space on the right (that's the difference 2104 // between swappers and strippers), so we should not stay in phantom space state if 2105 // the separator is a stripper. Hence the additional test above. 2106 mSpaceState = SPACE_STATE_PHANTOM; 2107 } 2108 2109 // Set punctuation right away. onUpdateSelection will fire but tests whether it is 2110 // already displayed or not, so it's okay. 2111 setPunctuationSuggestions(); 2112 } 2113 if (mSettings.isInternal()) { 2114 Utils.Stats.onSeparator((char)primaryCode, x, y); 2115 } 2116 2117 mKeyboardSwitcher.updateShiftState(); 2118 return didAutoCorrect; 2119 } 2120 2121 private CharSequence getTextWithUnderline(final String text) { 2122 return mIsAutoCorrectionIndicatorOn 2123 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) 2124 : text; 2125 } 2126 2127 private void handleClose() { 2128 // TODO: Verify that words are logged properly when IME is closed. 2129 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 2130 requestHideSelf(0); 2131 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 2132 if (mainKeyboardView != null) { 2133 mainKeyboardView.closing(); 2134 } 2135 } 2136 2137 // TODO: make this private 2138 // Outside LatinIME, only used by the test suite. 2139 @UsedForTesting 2140 boolean isShowingPunctuationList() { 2141 if (mSuggestedWords == null) return false; 2142 return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords; 2143 } 2144 2145 private boolean isSuggestionsStripVisible() { 2146 if (mSuggestionStripView == null) 2147 return false; 2148 if (mSuggestionStripView.isShowingAddToDictionaryHint()) 2149 return true; 2150 if (null == mSettings.getCurrent()) 2151 return false; 2152 if (!mSettings.getCurrent().isSuggestionStripVisibleInOrientation(mDisplayOrientation)) 2153 return false; 2154 if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) 2155 return true; 2156 return mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation); 2157 } 2158 2159 private void clearSuggestionStrip() { 2160 setSuggestedWords(SuggestedWords.EMPTY, false); 2161 setAutoCorrectionIndicator(false); 2162 } 2163 2164 private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { 2165 mSuggestedWords = words; 2166 if (mSuggestionStripView != null) { 2167 mSuggestionStripView.setSuggestions(words); 2168 mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); 2169 } 2170 } 2171 2172 private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { 2173 // Put a blue underline to a word in TextView which will be auto-corrected. 2174 if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator 2175 && mWordComposer.isComposingWord()) { 2176 mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; 2177 final CharSequence textWithUnderline = 2178 getTextWithUnderline(mWordComposer.getTypedWord()); 2179 // TODO: when called from an updateSuggestionStrip() call that results from a posted 2180 // message, this is called outside any batch edit. Potentially, this may result in some 2181 // janky flickering of the screen, although the display speed makes it unlikely in 2182 // the practice. 2183 mConnection.setComposingText(textWithUnderline, 1); 2184 } 2185 } 2186 2187 private void updateSuggestionStrip() { 2188 mHandler.cancelUpdateSuggestionStrip(); 2189 2190 // Check if we have a suggestion engine attached. 2191 if (mSuggest == null 2192 || !mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) { 2193 if (mWordComposer.isComposingWord()) { 2194 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " 2195 + "requested!"); 2196 } 2197 return; 2198 } 2199 2200 if (!mWordComposer.isComposingWord() && !mSettings.getCurrent().mBigramPredictionEnabled) { 2201 setPunctuationSuggestions(); 2202 return; 2203 } 2204 2205 final SuggestedWords suggestedWords = 2206 getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_TYPING); 2207 final String typedWord = mWordComposer.getTypedWord(); 2208 showSuggestionStrip(suggestedWords, typedWord); 2209 } 2210 2211 private SuggestedWords getSuggestedWords(final int sessionId) { 2212 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 2213 if (keyboard == null || mSuggest == null) { 2214 return SuggestedWords.EMPTY; 2215 } 2216 // Get the word on which we should search the bigrams. If we are composing a word, it's 2217 // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we 2218 // should just skip whitespace if any, so 1. 2219 // TODO: this is slow (2-way IPC) - we should probably cache this instead. 2220 final String prevWord = 2221 mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators, 2222 mWordComposer.isComposingWord() ? 2 : 1); 2223 return mSuggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(), 2224 mSettings.getBlockPotentiallyOffensive(), 2225 mSettings.getCurrent().mCorrectionEnabled, sessionId); 2226 } 2227 2228 private SuggestedWords getSuggestedWordsOrOlderSuggestions(final int sessionId) { 2229 return maybeRetrieveOlderSuggestions(mWordComposer.getTypedWord(), 2230 getSuggestedWords(sessionId)); 2231 } 2232 2233 private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord, 2234 final SuggestedWords suggestedWords) { 2235 // TODO: consolidate this into getSuggestedWords 2236 // We update the suggestion strip only when we have some suggestions to show, i.e. when 2237 // the suggestion count is > 1; else, we leave the old suggestions, with the typed word 2238 // replaced with the new one. However, when the word is a dictionary word, or when the 2239 // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the 2240 // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to 2241 // revert to suggestions - although it is unclear how we can come here if it's displayed. 2242 if (suggestedWords.size() > 1 || typedWord.length() <= 1 2243 || suggestedWords.mTypedWordValid || null == mSuggestionStripView 2244 || mSuggestionStripView.isShowingAddToDictionaryHint()) { 2245 return suggestedWords; 2246 } else { 2247 return getOlderSuggestions(typedWord); 2248 } 2249 } 2250 2251 private SuggestedWords getOlderSuggestions(final String typedWord) { 2252 SuggestedWords previousSuggestedWords = mSuggestedWords; 2253 if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) { 2254 previousSuggestedWords = SuggestedWords.EMPTY; 2255 } 2256 if (typedWord == null) { 2257 return previousSuggestedWords; 2258 } 2259 final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = 2260 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, 2261 previousSuggestedWords); 2262 return new SuggestedWords(typedWordAndPreviousSuggestions, 2263 false /* typedWordValid */, 2264 false /* hasAutoCorrectionCandidate */, 2265 false /* isPunctuationSuggestions */, 2266 true /* isObsoleteSuggestions */, 2267 false /* isPrediction */); 2268 } 2269 2270 private void showSuggestionStrip(final SuggestedWords suggestedWords, final String typedWord) { 2271 if (suggestedWords.isEmpty()) { 2272 clearSuggestionStrip(); 2273 return; 2274 } 2275 final String autoCorrection; 2276 if (suggestedWords.mWillAutoCorrect) { 2277 autoCorrection = suggestedWords.getWord(1); 2278 } else { 2279 autoCorrection = typedWord; 2280 } 2281 mWordComposer.setAutoCorrection(autoCorrection); 2282 final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); 2283 setSuggestedWords(suggestedWords, isAutoCorrection); 2284 setAutoCorrectionIndicator(isAutoCorrection); 2285 setSuggestionStripShown(isSuggestionsStripVisible()); 2286 } 2287 2288 private void commitCurrentAutoCorrection(final String separatorString) { 2289 // Complete any pending suggestions query first 2290 if (mHandler.hasPendingUpdateSuggestions()) { 2291 updateSuggestionStrip(); 2292 } 2293 final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); 2294 final String typedWord = mWordComposer.getTypedWord(); 2295 final String autoCorrection = (typedAutoCorrection != null) 2296 ? typedAutoCorrection : typedWord; 2297 if (autoCorrection != null) { 2298 if (TextUtils.isEmpty(typedWord)) { 2299 throw new RuntimeException("We have an auto-correction but the typed word " 2300 + "is empty? Impossible! I must commit suicide."); 2301 } 2302 if (mSettings.isInternal()) { 2303 Stats.onAutoCorrection(typedWord, autoCorrection, separatorString, mWordComposer); 2304 } 2305 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2306 final SuggestedWords suggestedWords = mSuggestedWords; 2307 ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, 2308 separatorString, mWordComposer.isBatchMode(), suggestedWords); 2309 } 2310 mExpectingUpdateSelection = true; 2311 commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, 2312 separatorString); 2313 if (!typedWord.equals(autoCorrection)) { 2314 // This will make the correction flash for a short while as a visual clue 2315 // to the user that auto-correction happened. It has no other effect; in particular 2316 // note that this won't affect the text inside the text field AT ALL: it only makes 2317 // the segment of text starting at the supplied index and running for the length 2318 // of the auto-correction flash. At this moment, the "typedWord" argument is 2319 // ignored by TextView. 2320 mConnection.commitCorrection( 2321 new CorrectionInfo(mLastSelectionEnd - typedWord.length(), 2322 typedWord, autoCorrection)); 2323 } 2324 } 2325 } 2326 2327 // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} 2328 // interface 2329 @Override 2330 public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { 2331 final SuggestedWords suggestedWords = mSuggestedWords; 2332 final String suggestion = suggestionInfo.mWord; 2333 // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput 2334 if (suggestion.length() == 1 && isShowingPunctuationList()) { 2335 // Word separators are suggested before the user inputs something. 2336 // So, LatinImeLogger logs "" as a user's input. 2337 LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); 2338 // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. 2339 final int primaryCode = suggestion.charAt(0); 2340 onCodeInput(primaryCode, 2341 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); 2342 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2343 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, 2344 false /* isBatchMode */, suggestedWords.mIsPrediction); 2345 } 2346 return; 2347 } 2348 2349 mConnection.beginBatchEdit(); 2350 if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0 2351 // In the batch input mode, a manually picked suggested word should just replace 2352 // the current batch input text and there is no need for a phantom space. 2353 && !mWordComposer.isBatchMode()) { 2354 final int firstChar = Character.codePointAt(suggestion, 0); 2355 if (!mSettings.getCurrent().isWordSeparator(firstChar) 2356 || mSettings.getCurrent().isUsuallyPrecededBySpace(firstChar)) { 2357 promotePhantomSpace(); 2358 } 2359 } 2360 2361 if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn() 2362 && mApplicationSpecifiedCompletions != null 2363 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 2364 mSuggestedWords = SuggestedWords.EMPTY; 2365 if (mSuggestionStripView != null) { 2366 mSuggestionStripView.clear(); 2367 } 2368 mKeyboardSwitcher.updateShiftState(); 2369 resetComposingState(true /* alsoResetLastComposedWord */); 2370 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; 2371 mConnection.commitCompletion(completionInfo); 2372 mConnection.endBatchEdit(); 2373 return; 2374 } 2375 2376 // We need to log before we commit, because the word composer will store away the user 2377 // typed word. 2378 final String replacedWord = mWordComposer.getTypedWord(); 2379 LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); 2380 mExpectingUpdateSelection = true; 2381 commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, 2382 LastComposedWord.NOT_A_SEPARATOR); 2383 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2384 ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, 2385 mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind, 2386 suggestionInfo.mSourceDict); 2387 } 2388 mConnection.endBatchEdit(); 2389 // Don't allow cancellation of manual pick 2390 mLastComposedWord.deactivate(); 2391 // Space state must be updated before calling updateShiftState 2392 mSpaceState = SPACE_STATE_PHANTOM; 2393 mKeyboardSwitcher.updateShiftState(); 2394 2395 // We should show the "Touch again to save" hint if the user pressed the first entry 2396 // AND it's in none of our current dictionaries (main, user or otherwise). 2397 // Please note that if mSuggest is null, it means that everything is off: suggestion 2398 // and correction, so we shouldn't try to show the hint 2399 final boolean showingAddToDictionaryHint = 2400 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind 2401 || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) 2402 && mSuggest != null 2403 // If the suggestion is not in the dictionary, the hint should be shown. 2404 && !AutoCorrection.isValidWord(mSuggest, suggestion, true); 2405 2406 if (mSettings.isInternal()) { 2407 Stats.onSeparator((char)Constants.CODE_SPACE, 2408 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 2409 } 2410 if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) { 2411 mSuggestionStripView.showAddToDictionaryHint( 2412 suggestion, mSettings.getCurrent().mHintToSaveText); 2413 } else { 2414 // If we're not showing the "Touch again to save", then update the suggestion strip. 2415 mHandler.postUpdateSuggestionStrip(); 2416 } 2417 } 2418 2419 /** 2420 * Commits the chosen word to the text field and saves it for later retrieval. 2421 */ 2422 private void commitChosenWord(final String chosenWord, final int commitType, 2423 final String separatorString) { 2424 final SuggestedWords suggestedWords = mSuggestedWords; 2425 mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( 2426 this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); 2427 // Add the word to the user history dictionary 2428 final String prevWord = addToUserHistoryDictionary(chosenWord); 2429 // TODO: figure out here if this is an auto-correct or if the best word is actually 2430 // what user typed. Note: currently this is done much later in 2431 // LastComposedWord#didCommitTypedWord by string equality of the remembered 2432 // strings. 2433 mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString, 2434 prevWord); 2435 } 2436 2437 private void setPunctuationSuggestions() { 2438 if (mSettings.getCurrent().mBigramPredictionEnabled) { 2439 clearSuggestionStrip(); 2440 } else { 2441 setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false); 2442 } 2443 setAutoCorrectionIndicator(false); 2444 setSuggestionStripShown(isSuggestionsStripVisible()); 2445 } 2446 2447 private String addToUserHistoryDictionary(final String suggestion) { 2448 if (TextUtils.isEmpty(suggestion)) return null; 2449 if (mSuggest == null) return null; 2450 2451 // If correction is not enabled, we don't add words to the user history dictionary. 2452 // That's to avoid unintended additions in some sensitive fields, or fields that 2453 // expect to receive non-words. 2454 if (!mSettings.getCurrent().mCorrectionEnabled) return null; 2455 2456 final Suggest suggest = mSuggest; 2457 final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary; 2458 if (suggest == null || userHistoryDictionary == null) { 2459 // Avoid concurrent issue 2460 return null; 2461 } 2462 final String prevWord 2463 = mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators, 2); 2464 final String secondWord; 2465 if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { 2466 secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); 2467 } else { 2468 secondWord = suggestion; 2469 } 2470 // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". 2471 // We don't add words with 0-frequency (assuming they would be profanity etc.). 2472 final int maxFreq = AutoCorrection.getMaxFrequency( 2473 suggest.getUnigramDictionaries(), suggestion); 2474 if (maxFreq == 0) return null; 2475 userHistoryDictionary.addToUserHistory(prevWord, secondWord, maxFreq > 0); 2476 return prevWord; 2477 } 2478 2479 /** 2480 * Check if the cursor is touching a word. If so, restart suggestions on this word, else 2481 * do nothing. 2482 */ 2483 private void restartSuggestionsOnWordTouchedByCursor() { 2484 // HACK: We may want to special-case some apps that exhibit bad behavior in case of 2485 // recorrection. This is a temporary, stopgap measure that will be removed later. 2486 // TODO: remove this. 2487 if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return; 2488 // If the cursor is not touching a word, or if there is a selection, return right away. 2489 if (mLastSelectionStart != mLastSelectionEnd) return; 2490 // If we don't know the cursor location, return. 2491 if (mLastSelectionStart < 0) return; 2492 if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return; 2493 final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(), 2494 0 /* additionalPrecedingWordsCount */); 2495 if (null == range) return; // Happens if we don't have an input connection at all 2496 // If for some strange reason (editor bug or so) we measure the text before the cursor as 2497 // longer than what the entire text is supposed to be, the safe thing to do is bail out. 2498 final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); 2499 if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return; 2500 final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); 2501 final CharSequence word = range.mWord; 2502 final String typedWord = word.toString(); 2503 if (word instanceof SpannableString) { 2504 final SpannableString spannableString = (SpannableString)word; 2505 int i = 0; 2506 for (Object object : spannableString.getSpans(0, spannableString.length(), 2507 SuggestionSpan.class)) { 2508 SuggestionSpan span = (SuggestionSpan)object; 2509 for (String s : span.getSuggestions()) { 2510 ++i; 2511 if (!TextUtils.equals(s, typedWord)) { 2512 suggestions.add(new SuggestedWordInfo(s, 2513 SuggestionStripView.MAX_SUGGESTIONS - i, 2514 SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED)); 2515 } 2516 } 2517 } 2518 } 2519 mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard()); 2520 mWordComposer.setCursorPositionWithinWord(numberOfCharsInWordBeforeCursor); 2521 mConnection.setComposingRegion( 2522 mLastSelectionStart - numberOfCharsInWordBeforeCursor, 2523 mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor()); 2524 final SuggestedWords suggestedWords; 2525 if (suggestions.isEmpty()) { 2526 // We come here if there weren't any suggestion spans on this word. We will try to 2527 // compute suggestions for it instead. 2528 final SuggestedWords suggestedWordsIncludingTypedWord = 2529 getSuggestedWords(Suggest.SESSION_TYPING); 2530 if (suggestedWordsIncludingTypedWord.size() > 1) { 2531 // We were able to compute new suggestions for this word. 2532 // Remove the typed word, since we don't want to display it in this case. 2533 // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false. 2534 suggestedWords = 2535 suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord(); 2536 } else { 2537 // No saved suggestions, and we were unable to compute any good one either. 2538 // Rather than displaying an empty suggestion strip, we'll display the original 2539 // word alone in the middle. 2540 // Since there is only one word, willAutoCorrect is false. 2541 suggestedWords = suggestedWordsIncludingTypedWord; 2542 } 2543 } else { 2544 // We found suggestion spans in the word. We'll create the SuggestedWords out of 2545 // them, and make willAutoCorrect false. 2546 suggestedWords = new SuggestedWords(suggestions, 2547 true /* typedWordValid */, false /* willAutoCorrect */, 2548 false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, 2549 false /* isPrediction */); 2550 } 2551 2552 // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. 2553 // We never want to auto-correct on a resumed suggestion. Please refer to the three 2554 // places above where suggestedWords is affected. We also need to reset 2555 // mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching the text to adapt it. 2556 // TODO: remove mIsAutoCorrectionIndicator on (see comment on definition) 2557 mIsAutoCorrectionIndicatorOn = false; 2558 showSuggestionStrip(suggestedWords, typedWord); 2559 } 2560 2561 /** 2562 * Check if the cursor is actually at the end of a word. If so, restart suggestions on this 2563 * word, else do nothing. 2564 */ 2565 private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() { 2566 final CharSequence word = 2567 mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent()); 2568 if (null != word) { 2569 final String wordString = word.toString(); 2570 restartSuggestionsOnWordBeforeCursor(wordString); 2571 // TODO: Handle the case where the user manually moves the cursor and then backs up over 2572 // a separator. In that case, the current log unit should not be uncommitted. 2573 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2574 ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString, 2575 true /* dumpCurrentLogUnit */); 2576 } 2577 } 2578 } 2579 2580 private void restartSuggestionsOnWordBeforeCursor(final String word) { 2581 mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); 2582 final int length = word.length(); 2583 mConnection.deleteSurroundingText(length, 0); 2584 mConnection.setComposingText(word, 1); 2585 mHandler.postUpdateSuggestionStrip(); 2586 } 2587 2588 private void revertCommit() { 2589 final String previousWord = mLastComposedWord.mPrevWord; 2590 final String originallyTypedWord = mLastComposedWord.mTypedWord; 2591 final String committedWord = mLastComposedWord.mCommittedWord; 2592 final int cancelLength = committedWord.length(); 2593 final int separatorLength = LastComposedWord.getSeparatorLength( 2594 mLastComposedWord.mSeparatorString); 2595 // TODO: should we check our saved separator against the actual contents of the text view? 2596 final int deleteLength = cancelLength + separatorLength; 2597 if (DEBUG) { 2598 if (mWordComposer.isComposingWord()) { 2599 throw new RuntimeException("revertCommit, but we are composing a word"); 2600 } 2601 final CharSequence wordBeforeCursor = 2602 mConnection.getTextBeforeCursor(deleteLength, 0) 2603 .subSequence(0, cancelLength); 2604 if (!TextUtils.equals(committedWord, wordBeforeCursor)) { 2605 throw new RuntimeException("revertCommit check failed: we thought we were " 2606 + "reverting \"" + committedWord 2607 + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); 2608 } 2609 } 2610 mConnection.deleteSurroundingText(deleteLength, 0); 2611 if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { 2612 mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord); 2613 } 2614 mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1); 2615 if (mSettings.isInternal()) { 2616 Stats.onSeparator(mLastComposedWord.mSeparatorString, 2617 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 2618 } 2619 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2620 ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord, 2621 mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString); 2622 } 2623 // Don't restart suggestion yet. We'll restart if the user deletes the 2624 // separator. 2625 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 2626 // We have a separator between the word and the cursor: we should show predictions. 2627 mHandler.postUpdateSuggestionStrip(); 2628 } 2629 2630 // This essentially inserts a space, and that's it. 2631 public void promotePhantomSpace() { 2632 if (mSettings.getCurrent().shouldInsertSpacesAutomatically() 2633 && !mConnection.textBeforeCursorLooksLikeURL()) { 2634 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2635 ResearchLogger.latinIME_promotePhantomSpace(); 2636 } 2637 sendKeyCodePoint(Constants.CODE_SPACE); 2638 } 2639 } 2640 2641 // Used by the RingCharBuffer 2642 public boolean isWordSeparator(final int code) { 2643 return mSettings.getCurrent().isWordSeparator(code); 2644 } 2645 2646 // TODO: Make this private 2647 // Outside LatinIME, only used by the {@link InputTestsBase} test suite. 2648 @UsedForTesting 2649 void loadKeyboard() { 2650 // Since we are switching languages, the most urgent thing is to let the keyboard graphics 2651 // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on 2652 // the screen. Anything we do right now will delay this, so wait until the next frame 2653 // before we do the rest, like reopening dictionaries and updating suggestions. So we 2654 // post a message. 2655 mHandler.postReopenDictionaries(); 2656 loadSettings(); 2657 if (mKeyboardSwitcher.getMainKeyboardView() != null) { 2658 // Reload keyboard because the current language has been changed. 2659 mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); 2660 } 2661 } 2662 2663 // Callback called by PointerTracker through the KeyboardActionListener. This is called when a 2664 // key is depressed; release matching call is onReleaseKey below. 2665 @Override 2666 public void onPressKey(final int primaryCode, final boolean isSinglePointer) { 2667 mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer); 2668 } 2669 2670 // Callback by PointerTracker through the KeyboardActionListener. This is called when a key 2671 // is released; press matching call is onPressKey above. 2672 @Override 2673 public void onReleaseKey(final int primaryCode, final boolean withSliding) { 2674 mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); 2675 2676 // If accessibility is on, ensure the user receives keyboard state updates. 2677 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 2678 switch (primaryCode) { 2679 case Constants.CODE_SHIFT: 2680 AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); 2681 break; 2682 case Constants.CODE_SWITCH_ALPHA_SYMBOL: 2683 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); 2684 break; 2685 } 2686 } 2687 2688 if (Constants.CODE_DELETE == primaryCode) { 2689 // This is a stopgap solution to avoid leaving a high surrogate alone in a text view. 2690 // In the future, we need to deprecate deteleSurroundingText() and have a surrogate 2691 // pair-friendly way of deleting characters in InputConnection. 2692 // TODO: use getCodePointBeforeCursor instead to improve performance 2693 final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0); 2694 if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) { 2695 mConnection.deleteSurroundingText(1, 0); 2696 } 2697 } 2698 } 2699 2700 // Hooks for hardware keyboard 2701 @Override 2702 public boolean onKeyDown(final int keyCode, final KeyEvent event) { 2703 if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event); 2704 // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if 2705 // it doesn't know what to do with it and leave it to the application. For example, 2706 // hardware key events for adjusting the screen's brightness are passed as is. 2707 if (mEventInterpreter.onHardwareKeyEvent(event)) { 2708 final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); 2709 mCurrentlyPressedHardwareKeys.add(keyIdentifier); 2710 return true; 2711 } 2712 return super.onKeyDown(keyCode, event); 2713 } 2714 2715 @Override 2716 public boolean onKeyUp(final int keyCode, final KeyEvent event) { 2717 final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); 2718 if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { 2719 return true; 2720 } 2721 return super.onKeyUp(keyCode, event); 2722 } 2723 2724 // onKeyDown and onKeyUp are the main events we are interested in. There are two more events 2725 // related to handling of hardware key events that we may want to implement in the future: 2726 // boolean onKeyLongPress(final int keyCode, final KeyEvent event); 2727 // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event); 2728 2729 // receive ringer mode change and network state change. 2730 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 2731 @Override 2732 public void onReceive(final Context context, final Intent intent) { 2733 final String action = intent.getAction(); 2734 if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 2735 mSubtypeSwitcher.onNetworkStateChanged(intent); 2736 } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { 2737 AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged(); 2738 } 2739 } 2740 }; 2741 2742 private void launchSettings() { 2743 handleClose(); 2744 launchSubActivity(SettingsActivity.class); 2745 } 2746 2747 public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) { 2748 // Put the text in the attached EditText into a safe, saved state before switching to a 2749 // new activity that will also use the soft keyboard. 2750 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 2751 launchSubActivity(activityClass); 2752 } 2753 2754 private void launchSubActivity(final Class<? extends Activity> activityClass) { 2755 Intent intent = new Intent(); 2756 intent.setClass(LatinIME.this, activityClass); 2757 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 2758 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2759 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2760 startActivity(intent); 2761 } 2762 2763 private void showSubtypeSelectorAndSettings() { 2764 final CharSequence title = getString(R.string.english_ime_input_options); 2765 final CharSequence[] items = new CharSequence[] { 2766 // TODO: Should use new string "Select active input modes". 2767 getString(R.string.language_selection_title), 2768 getString(Utils.getAcitivityTitleResId(this, SettingsActivity.class)), 2769 }; 2770 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 2771 @Override 2772 public void onClick(DialogInterface di, int position) { 2773 di.dismiss(); 2774 switch (position) { 2775 case 0: 2776 final Intent intent = IntentUtils.getInputLanguageSelectionIntent( 2777 mRichImm.getInputMethodIdOfThisIme(), 2778 Intent.FLAG_ACTIVITY_NEW_TASK 2779 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2780 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2781 startActivity(intent); 2782 break; 2783 case 1: 2784 launchSettings(); 2785 break; 2786 } 2787 } 2788 }; 2789 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 2790 .setItems(items, listener) 2791 .setTitle(title); 2792 showOptionDialog(builder.create()); 2793 } 2794 2795 public void showOptionDialog(final AlertDialog dialog) { 2796 final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); 2797 if (windowToken == null) { 2798 return; 2799 } 2800 2801 dialog.setCancelable(true); 2802 dialog.setCanceledOnTouchOutside(true); 2803 2804 final Window window = dialog.getWindow(); 2805 final WindowManager.LayoutParams lp = window.getAttributes(); 2806 lp.token = windowToken; 2807 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 2808 window.setAttributes(lp); 2809 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 2810 2811 mOptionsDialog = dialog; 2812 dialog.show(); 2813 } 2814 2815 // TODO: can this be removed somehow without breaking the tests? 2816 @UsedForTesting 2817 /* package for test */ String getFirstSuggestedWord() { 2818 return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null; 2819 } 2820 2821 public void debugDumpStateAndCrashWithException(final String context) { 2822 final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString()); 2823 s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes) 2824 .append("\nContext : ").append(context); 2825 throw new RuntimeException(s.toString()); 2826 } 2827 2828 @Override 2829 protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) { 2830 super.dump(fd, fout, args); 2831 2832 final Printer p = new PrintWriterPrinter(fout); 2833 p.println("LatinIME state :"); 2834 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 2835 final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; 2836 p.println(" Keyboard mode = " + keyboardMode); 2837 final SettingsValues settingsValues = mSettings.getCurrent(); 2838 p.println(" mIsSuggestionsSuggestionsRequested = " 2839 + settingsValues.isSuggestionsRequested(mDisplayOrientation)); 2840 p.println(" mCorrectionEnabled=" + settingsValues.mCorrectionEnabled); 2841 p.println(" isComposingWord=" + mWordComposer.isComposingWord()); 2842 p.println(" mSoundOn=" + settingsValues.mSoundOn); 2843 p.println(" mVibrateOn=" + settingsValues.mVibrateOn); 2844 p.println(" mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn); 2845 p.println(" inputAttributes=" + settingsValues.mInputAttributes); 2846 } 2847} 2848