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