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