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