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