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