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