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