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