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