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