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