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