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