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