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