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