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