LatinIME.java revision f960eb186d63aa4f9fecd22f036fc8724d39d949
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII; 20import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; 21import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; 22 23import android.app.Activity; 24import android.app.AlertDialog; 25import android.content.BroadcastReceiver; 26import android.content.Context; 27import android.content.DialogInterface; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.content.SharedPreferences; 31import android.content.pm.ApplicationInfo; 32import android.content.res.Configuration; 33import android.content.res.Resources; 34import android.graphics.Rect; 35import android.inputmethodservice.InputMethodService; 36import android.media.AudioManager; 37import android.net.ConnectivityManager; 38import android.os.Debug; 39import android.os.Handler; 40import android.os.HandlerThread; 41import android.os.IBinder; 42import android.os.Message; 43import android.os.SystemClock; 44import android.preference.PreferenceManager; 45import android.text.InputType; 46import android.text.TextUtils; 47import android.util.Log; 48import android.util.PrintWriterPrinter; 49import android.util.Printer; 50import android.view.KeyCharacterMap; 51import android.view.KeyEvent; 52import android.view.View; 53import android.view.ViewGroup.LayoutParams; 54import android.view.Window; 55import android.view.WindowManager; 56import android.view.inputmethod.CompletionInfo; 57import android.view.inputmethod.CorrectionInfo; 58import android.view.inputmethod.EditorInfo; 59import android.view.inputmethod.InputMethodSubtype; 60 61import com.android.inputmethod.accessibility.AccessibilityUtils; 62import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; 63import com.android.inputmethod.compat.CompatUtils; 64import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; 65import com.android.inputmethod.compat.InputMethodServiceCompatUtils; 66import com.android.inputmethod.compat.SuggestionSpanUtils; 67import com.android.inputmethod.keyboard.KeyDetector; 68import com.android.inputmethod.keyboard.Keyboard; 69import com.android.inputmethod.keyboard.KeyboardActionListener; 70import com.android.inputmethod.keyboard.KeyboardId; 71import com.android.inputmethod.keyboard.KeyboardSwitcher; 72import com.android.inputmethod.keyboard.KeyboardView; 73import com.android.inputmethod.keyboard.MainKeyboardView; 74import com.android.inputmethod.latin.LocaleUtils.RunInLocale; 75import com.android.inputmethod.latin.Utils.Stats; 76import com.android.inputmethod.latin.define.ProductionFlag; 77import com.android.inputmethod.latin.suggestions.SuggestionStripView; 78import com.android.inputmethod.research.ResearchLogger; 79 80import java.io.FileDescriptor; 81import java.io.PrintWriter; 82import java.util.ArrayList; 83import java.util.Locale; 84 85/** 86 * Input method implementation for Qwerty'ish keyboard. 87 */ 88public 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 } 736 setSuggestionStripShownInternal( 737 isSuggestionsStripVisible(), /* needsInputViewShown */ false); 738 739 mLastSelectionStart = editorInfo.initialSelStart; 740 mLastSelectionEnd = editorInfo.initialSelEnd; 741 // If we come here something in the text state is very likely to have changed. 742 // We should update the shift state regardless of whether we are restarting or not, because 743 // this is not perceived as a layout change that may be disruptive like we may have with 744 // switcher.loadKeyboard; in apps like Talk, we come here when the text is sent and the 745 // field gets emptied and we need to re-evaluate the shift state, but not the whole layout 746 // which would be disruptive. 747 // Space state must be updated before calling updateShiftState 748 mKeyboardSwitcher.updateShiftState(); 749 750 mHandler.cancelUpdateSuggestionStrip(); 751 mHandler.cancelDoubleSpacesTimer(); 752 753 mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable); 754 mainKeyboardView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn, 755 mCurrentSettings.mKeyPreviewPopupDismissDelay); 756 mainKeyboardView.setGestureHandlingEnabledByUser(mCurrentSettings.mGestureInputEnabled); 757 mainKeyboardView.setGesturePreviewMode(mCurrentSettings.mGesturePreviewTrailEnabled, 758 mCurrentSettings.mGestureFloatingPreviewTextEnabled); 759 760 if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); 761 } 762 763 // Callback for the TargetApplicationGetter 764 @Override 765 public void onTargetApplicationKnown(final ApplicationInfo info) { 766 mTargetApplicationInfo = info; 767 } 768 769 @Override 770 public void onWindowHidden() { 771 if (ProductionFlag.IS_EXPERIMENTAL) { 772 ResearchLogger.latinIME_onWindowHidden(mLastSelectionStart, mLastSelectionEnd, 773 getCurrentInputConnection()); 774 } 775 super.onWindowHidden(); 776 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 777 if (mainKeyboardView != null) { 778 mainKeyboardView.closing(); 779 } 780 } 781 782 private void onFinishInputInternal() { 783 super.onFinishInput(); 784 785 LatinImeLogger.commit(); 786 if (ProductionFlag.IS_EXPERIMENTAL) { 787 ResearchLogger.getInstance().latinIME_onFinishInputInternal(); 788 } 789 790 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 791 if (mainKeyboardView != null) { 792 mainKeyboardView.closing(); 793 } 794 } 795 796 private void onFinishInputViewInternal(final boolean finishingInput) { 797 super.onFinishInputView(finishingInput); 798 mKeyboardSwitcher.onFinishInputView(); 799 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 800 if (mainKeyboardView != null) { 801 mainKeyboardView.cancelAllMessages(); 802 } 803 // Remove pending messages related to update suggestions 804 mHandler.cancelUpdateSuggestionStrip(); 805 } 806 807 @Override 808 public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, 809 final int newSelStart, final int newSelEnd, 810 final int composingSpanStart, final int composingSpanEnd) { 811 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 812 composingSpanStart, composingSpanEnd); 813 if (DEBUG) { 814 Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart 815 + ", ose=" + oldSelEnd 816 + ", lss=" + mLastSelectionStart 817 + ", lse=" + mLastSelectionEnd 818 + ", nss=" + newSelStart 819 + ", nse=" + newSelEnd 820 + ", cs=" + composingSpanStart 821 + ", ce=" + composingSpanEnd); 822 } 823 if (ProductionFlag.IS_EXPERIMENTAL) { 824 final boolean expectingUpdateSelectionFromLogger = 825 ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection(); 826 ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd, 827 oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, 828 composingSpanEnd, mExpectingUpdateSelection, 829 expectingUpdateSelectionFromLogger, mConnection); 830 if (expectingUpdateSelectionFromLogger) { 831 // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work 832 return; 833 } 834 } 835 836 // TODO: refactor the following code to be less contrived. 837 // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means 838 // that the cursor is not at the end of the composing span, or there is a selection. 839 // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place 840 // as last time we were called (if there is a selection, it means the start hasn't 841 // changed, so it's the end that did). 842 final boolean selectionChanged = (newSelStart != composingSpanEnd 843 || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart; 844 // if composingSpanStart and composingSpanEnd are -1, it means there is no composing 845 // span in the view - we can use that to narrow down whether the cursor was moved 846 // by us or not. If we are composing a word but there is no composing span, then 847 // we know for sure the cursor moved while we were composing and we should reset 848 // the state. 849 final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; 850 if (!mExpectingUpdateSelection 851 && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) { 852 // TAKE CARE: there is a race condition when we enter this test even when the user 853 // did not explicitly move the cursor. This happens when typing fast, where two keys 854 // turn this flag on in succession and both onUpdateSelection() calls arrive after 855 // the second one - the first call successfully avoids this test, but the second one 856 // enters. For the moment we rely on noComposingSpan to further reduce the impact. 857 858 // TODO: the following is probably better done in resetEntireInputState(). 859 // it should only happen when the cursor moved, and the very purpose of the 860 // test below is to narrow down whether this happened or not. Likewise with 861 // the call to updateShiftState. 862 // We set this to NONE because after a cursor move, we don't want the space 863 // state-related special processing to kick in. 864 mSpaceState = SPACE_STATE_NONE; 865 866 if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) { 867 // If we are composing a word and moving the cursor, we would want to set a 868 // suggestion span for recorrection to work correctly. Unfortunately, that 869 // would involve the keyboard committing some new text, which would move the 870 // cursor back to where it was. Latin IME could then fix the position of the cursor 871 // again, but the asynchronous nature of the calls results in this wreaking havoc 872 // with selection on double tap and the like. 873 // Another option would be to send suggestions each time we set the composing 874 // text, but that is probably too expensive to do, so we decided to leave things 875 // as is. 876 resetEntireInputState(newSelStart); 877 } 878 879 mKeyboardSwitcher.updateShiftState(); 880 } 881 mExpectingUpdateSelection = false; 882 // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not 883 // here. It would probably be too expensive to call directly here but we may want to post a 884 // message to delay it. The point would be to unify behavior between backspace to the 885 // end of a word and manually put the pointer at the end of the word. 886 887 // Make a note of the cursor position 888 mLastSelectionStart = newSelStart; 889 mLastSelectionEnd = newSelEnd; 890 } 891 892 /** 893 * This is called when the user has clicked on the extracted text view, 894 * when running in fullscreen mode. The default implementation hides 895 * the suggestions view when this happens, but only if the extracted text 896 * editor has a vertical scroll bar because its text doesn't fit. 897 * Here we override the behavior due to the possibility that a re-correction could 898 * cause the suggestions strip to disappear and re-appear. 899 */ 900 @Override 901 public void onExtractedTextClicked() { 902 if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return; 903 904 super.onExtractedTextClicked(); 905 } 906 907 /** 908 * This is called when the user has performed a cursor movement in the 909 * extracted text view, when it is running in fullscreen mode. The default 910 * implementation hides the suggestions view when a vertical movement 911 * happens, but only if the extracted text editor has a vertical scroll bar 912 * because its text doesn't fit. 913 * Here we override the behavior due to the possibility that a re-correction could 914 * cause the suggestions strip to disappear and re-appear. 915 */ 916 @Override 917 public void onExtractedCursorMovement(final int dx, final int dy) { 918 if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return; 919 920 super.onExtractedCursorMovement(dx, dy); 921 } 922 923 @Override 924 public void hideWindow() { 925 LatinImeLogger.commit(); 926 mKeyboardSwitcher.onHideWindow(); 927 928 if (TRACE) Debug.stopMethodTracing(); 929 if (mOptionsDialog != null && mOptionsDialog.isShowing()) { 930 mOptionsDialog.dismiss(); 931 mOptionsDialog = null; 932 } 933 super.hideWindow(); 934 } 935 936 @Override 937 public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) { 938 if (DEBUG) { 939 Log.i(TAG, "Received completions:"); 940 if (applicationSpecifiedCompletions != null) { 941 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { 942 Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); 943 } 944 } 945 } 946 if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return; 947 mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; 948 if (applicationSpecifiedCompletions == null) { 949 clearSuggestionStrip(); 950 if (ProductionFlag.IS_EXPERIMENTAL) { 951 ResearchLogger.latinIME_onDisplayCompletions(null); 952 } 953 return; 954 } 955 956 final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = 957 SuggestedWords.getFromApplicationSpecifiedCompletions( 958 applicationSpecifiedCompletions); 959 final SuggestedWords suggestedWords = new SuggestedWords( 960 applicationSuggestedWords, 961 false /* typedWordValid */, 962 false /* hasAutoCorrectionCandidate */, 963 false /* isPunctuationSuggestions */, 964 false /* isObsoleteSuggestions */, 965 false /* isPrediction */); 966 // When in fullscreen mode, show completions generated by the application 967 final boolean isAutoCorrection = false; 968 setSuggestionStrip(suggestedWords, isAutoCorrection); 969 setAutoCorrectionIndicator(isAutoCorrection); 970 // TODO: is this the right thing to do? What should we auto-correct to in 971 // this case? This says to keep whatever the user typed. 972 mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); 973 setSuggestionStripShown(true); 974 if (ProductionFlag.IS_EXPERIMENTAL) { 975 ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); 976 } 977 } 978 979 private void setSuggestionStripShownInternal(final boolean shown, 980 final boolean needsInputViewShown) { 981 // TODO: Modify this if we support suggestions with hard keyboard 982 if (onEvaluateInputViewShown() && mSuggestionsContainer != null) { 983 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 984 final boolean inputViewShown = (mainKeyboardView != null) 985 ? mainKeyboardView.isShown() : false; 986 final boolean shouldShowSuggestions = shown 987 && (needsInputViewShown ? inputViewShown : true); 988 if (isFullscreenMode()) { 989 mSuggestionsContainer.setVisibility( 990 shouldShowSuggestions ? View.VISIBLE : View.GONE); 991 } else { 992 mSuggestionsContainer.setVisibility( 993 shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); 994 } 995 } 996 } 997 998 private void setSuggestionStripShown(final boolean shown) { 999 setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); 1000 } 1001 1002 private int getAdjustedBackingViewHeight() { 1003 final int currentHeight = mKeyPreviewBackingView.getHeight(); 1004 if (currentHeight > 0) { 1005 return currentHeight; 1006 } 1007 1008 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1009 if (mainKeyboardView == null) { 1010 return 0; 1011 } 1012 final int keyboardHeight = mainKeyboardView.getHeight(); 1013 final int suggestionsHeight = mSuggestionsContainer.getHeight(); 1014 final int displayHeight = mResources.getDisplayMetrics().heightPixels; 1015 final Rect rect = new Rect(); 1016 mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect); 1017 final int notificationBarHeight = rect.top; 1018 final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight 1019 - keyboardHeight; 1020 1021 final LayoutParams params = mKeyPreviewBackingView.getLayoutParams(); 1022 params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); 1023 mKeyPreviewBackingView.setLayoutParams(params); 1024 return params.height; 1025 } 1026 1027 @Override 1028 public void onComputeInsets(final InputMethodService.Insets outInsets) { 1029 super.onComputeInsets(outInsets); 1030 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1031 if (mainKeyboardView == null || mSuggestionsContainer == null) { 1032 return; 1033 } 1034 final int adjustedBackingHeight = getAdjustedBackingViewHeight(); 1035 final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE); 1036 final int backingHeight = backingGone ? 0 : adjustedBackingHeight; 1037 // In fullscreen mode, the height of the extract area managed by InputMethodService should 1038 // be considered. 1039 // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. 1040 final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; 1041 final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0 1042 : mSuggestionsContainer.getHeight(); 1043 final int extraHeight = extractHeight + backingHeight + suggestionsHeight; 1044 int touchY = extraHeight; 1045 // Need to set touchable region only if input view is being shown 1046 if (mainKeyboardView.isShown()) { 1047 if (mSuggestionsContainer.getVisibility() == View.VISIBLE) { 1048 touchY -= suggestionsHeight; 1049 } 1050 final int touchWidth = mainKeyboardView.getWidth(); 1051 final int touchHeight = mainKeyboardView.getHeight() + extraHeight 1052 // Extend touchable region below the keyboard. 1053 + EXTENDED_TOUCHABLE_REGION_HEIGHT; 1054 outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; 1055 outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight); 1056 } 1057 outInsets.contentTopInsets = touchY; 1058 outInsets.visibleTopInsets = touchY; 1059 } 1060 1061 @Override 1062 public boolean onEvaluateFullscreenMode() { 1063 // Reread resource value here, because this method is called by framework anytime as needed. 1064 final boolean isFullscreenModeAllowed = 1065 mCurrentSettings.isFullscreenModeAllowed(getResources()); 1066 return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed; 1067 } 1068 1069 @Override 1070 public void updateFullscreenMode() { 1071 super.updateFullscreenMode(); 1072 1073 if (mKeyPreviewBackingView == null) return; 1074 // In fullscreen mode, no need to have extra space to show the key preview. 1075 // If not, we should have extra space above the keyboard to show the key preview. 1076 mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); 1077 } 1078 1079 // This will reset the whole input state to the starting state. It will clear 1080 // the composing word, reset the last composed word, tell the inputconnection about it. 1081 private void resetEntireInputState(final int newCursorPosition) { 1082 resetComposingState(true /* alsoResetLastComposedWord */); 1083 if (mCurrentSettings.mBigramPredictionEnabled) { 1084 clearSuggestionStrip(); 1085 } else { 1086 setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false); 1087 } 1088 mConnection.resetCachesUponCursorMove(newCursorPosition); 1089 } 1090 1091 private void resetComposingState(final boolean alsoResetLastComposedWord) { 1092 mWordComposer.reset(); 1093 if (alsoResetLastComposedWord) 1094 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 1095 } 1096 1097 private void commitTyped(final String separatorString) { 1098 if (!mWordComposer.isComposingWord()) return; 1099 final CharSequence typedWord = mWordComposer.getTypedWord(); 1100 if (typedWord.length() > 0) { 1101 commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, 1102 separatorString); 1103 } 1104 } 1105 1106 // Called from the KeyboardSwitcher which needs to know auto caps state to display 1107 // the right layout. 1108 public int getCurrentAutoCapsState() { 1109 if (!mCurrentSettings.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; 1110 1111 final EditorInfo ei = getCurrentInputEditorInfo(); 1112 if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; 1113 final int inputType = ei.inputType; 1114 // Warning: this depends on mSpaceState, which may not be the most current value. If 1115 // mSpaceState gets updated later, whoever called this may need to be told about it. 1116 return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(), 1117 SPACE_STATE_PHANTOM == mSpaceState); 1118 } 1119 1120 // Factor in auto-caps and manual caps and compute the current caps mode. 1121 private int getActualCapsMode() { 1122 final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode(); 1123 if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode; 1124 final int auto = getCurrentAutoCapsState(); 1125 if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { 1126 return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; 1127 } 1128 if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED; 1129 return WordComposer.CAPS_MODE_OFF; 1130 } 1131 1132 private void swapSwapperAndSpace() { 1133 CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); 1134 // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. 1135 if (lastTwo != null && lastTwo.length() == 2 1136 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) { 1137 mConnection.deleteSurroundingText(2, 0); 1138 mConnection.commitText(lastTwo.charAt(1) + " ", 1); 1139 if (ProductionFlag.IS_EXPERIMENTAL) { 1140 ResearchLogger.latinIME_swapSwapperAndSpace(); 1141 } 1142 mKeyboardSwitcher.updateShiftState(); 1143 } 1144 } 1145 1146 private boolean maybeDoubleSpace() { 1147 if (!mCurrentSettings.mCorrectionEnabled) return false; 1148 if (!mHandler.isAcceptingDoubleSpaces()) return false; 1149 final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0); 1150 if (lastThree != null && lastThree.length() == 3 1151 && canBeFollowedByPeriod(lastThree.charAt(0)) 1152 && lastThree.charAt(1) == Keyboard.CODE_SPACE 1153 && lastThree.charAt(2) == Keyboard.CODE_SPACE) { 1154 mHandler.cancelDoubleSpacesTimer(); 1155 mConnection.deleteSurroundingText(2, 0); 1156 mConnection.commitText(". ", 1); 1157 mKeyboardSwitcher.updateShiftState(); 1158 return true; 1159 } 1160 return false; 1161 } 1162 1163 private static boolean canBeFollowedByPeriod(final int codePoint) { 1164 // TODO: Check again whether there really ain't a better way to check this. 1165 // TODO: This should probably be language-dependant... 1166 return Character.isLetterOrDigit(codePoint) 1167 || codePoint == Keyboard.CODE_SINGLE_QUOTE 1168 || codePoint == Keyboard.CODE_DOUBLE_QUOTE 1169 || codePoint == Keyboard.CODE_CLOSING_PARENTHESIS 1170 || codePoint == Keyboard.CODE_CLOSING_SQUARE_BRACKET 1171 || codePoint == Keyboard.CODE_CLOSING_CURLY_BRACKET 1172 || codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET; 1173 } 1174 1175 // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is 1176 // pressed. 1177 @Override 1178 public boolean addWordToUserDictionary(final String word) { 1179 mUserDictionary.addWordToUserDictionary(word, 128); 1180 return true; 1181 } 1182 1183 private static boolean isAlphabet(final int code) { 1184 return Character.isLetter(code); 1185 } 1186 1187 private void onSettingsKeyPressed() { 1188 if (isShowingOptionDialog()) return; 1189 showSubtypeSelectorAndSettings(); 1190 } 1191 1192 // Virtual codes representing custom requests. These are used in onCustomRequest() below. 1193 public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1; 1194 1195 @Override 1196 public boolean onCustomRequest(final int requestCode) { 1197 if (isShowingOptionDialog()) return false; 1198 switch (requestCode) { 1199 case CODE_SHOW_INPUT_METHOD_PICKER: 1200 if (ImfUtils.hasMultipleEnabledIMEsOrSubtypes( 1201 this, true /* include aux subtypes */)) { 1202 mImm.showInputMethodPicker(); 1203 return true; 1204 } 1205 return false; 1206 } 1207 return false; 1208 } 1209 1210 private boolean isShowingOptionDialog() { 1211 return mOptionsDialog != null && mOptionsDialog.isShowing(); 1212 } 1213 1214 private static int getActionId(final Keyboard keyboard) { 1215 return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE; 1216 } 1217 1218 private void performEditorAction(final int actionId) { 1219 mConnection.performEditorAction(actionId); 1220 } 1221 1222 // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. 1223 private void handleLanguageSwitchKey() { 1224 final IBinder token = getWindow().getWindow().getAttributes().token; 1225 if (mCurrentSettings.mIncludesOtherImesInLanguageSwitchList) { 1226 mImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); 1227 return; 1228 } 1229 if (mShouldSwitchToLastSubtype) { 1230 final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype(); 1231 final boolean lastSubtypeBelongsToThisIme = 1232 ImfUtils.checkIfSubtypeBelongsToThisImeAndEnabled(this, lastSubtype); 1233 if (lastSubtypeBelongsToThisIme && mImm.switchToLastInputMethod(token)) { 1234 mShouldSwitchToLastSubtype = false; 1235 } else { 1236 mImm.switchToNextInputMethod(token, true /* onlyCurrentIme */); 1237 mShouldSwitchToLastSubtype = true; 1238 } 1239 } else { 1240 mImm.switchToNextInputMethod(token, true /* onlyCurrentIme */); 1241 } 1242 } 1243 1244 private void sendDownUpKeyEventForBackwardCompatibility(final int code) { 1245 final long eventTime = SystemClock.uptimeMillis(); 1246 mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, 1247 KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1248 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1249 mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, 1250 KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1251 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1252 } 1253 1254 private void sendKeyCodePoint(final int code) { 1255 // TODO: Remove this special handling of digit letters. 1256 // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. 1257 if (code >= '0' && code <= '9') { 1258 sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0); 1259 if (ProductionFlag.IS_EXPERIMENTAL) { 1260 ResearchLogger.latinIME_sendKeyCodePoint(code); 1261 } 1262 return; 1263 } 1264 1265 // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because 1266 // we want to be able to compile against the Ice Cream Sandwich SDK. 1267 if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null 1268 && mTargetApplicationInfo.targetSdkVersion < 16) { 1269 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate 1270 // a hardware keyboard event on pressing enter or delete. This is bad for many 1271 // reasons (there are race conditions with commits) but some applications are 1272 // relying on this behavior so we continue to support it for older apps. 1273 sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER); 1274 } else { 1275 final String text = new String(new int[] { code }, 0, 1); 1276 mConnection.commitText(text, text.length()); 1277 } 1278 } 1279 1280 // Implementation of {@link KeyboardActionListener}. 1281 @Override 1282 public void onCodeInput(final int primaryCode, final int x, final int y) { 1283 final long when = SystemClock.uptimeMillis(); 1284 if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { 1285 mDeleteCount = 0; 1286 } 1287 mLastKeyTime = when; 1288 mConnection.beginBatchEdit(); 1289 final KeyboardSwitcher switcher = mKeyboardSwitcher; 1290 // The space state depends only on the last character pressed and its own previous 1291 // state. Here, we revert the space state to neutral if the key is actually modifying 1292 // the input contents (any non-shift key), which is what we should do for 1293 // all inputs that do not result in a special state. Each character handling is then 1294 // free to override the state as they see fit. 1295 final int spaceState = mSpaceState; 1296 if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false; 1297 1298 // TODO: Consolidate the double space timer, mLastKeyTime, and the space state. 1299 if (primaryCode != Keyboard.CODE_SPACE) { 1300 mHandler.cancelDoubleSpacesTimer(); 1301 } 1302 1303 boolean didAutoCorrect = false; 1304 switch (primaryCode) { 1305 case Keyboard.CODE_DELETE: 1306 mSpaceState = SPACE_STATE_NONE; 1307 handleBackspace(spaceState); 1308 mDeleteCount++; 1309 mExpectingUpdateSelection = true; 1310 mShouldSwitchToLastSubtype = true; 1311 LatinImeLogger.logOnDelete(x, y); 1312 break; 1313 case Keyboard.CODE_SHIFT: 1314 case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: 1315 // Shift and symbol key is handled in onPressKey() and onReleaseKey(). 1316 break; 1317 case Keyboard.CODE_SETTINGS: 1318 onSettingsKeyPressed(); 1319 break; 1320 case Keyboard.CODE_SHORTCUT: 1321 mSubtypeSwitcher.switchToShortcutIME(this); 1322 break; 1323 case Keyboard.CODE_ACTION_ENTER: 1324 performEditorAction(getActionId(switcher.getKeyboard())); 1325 break; 1326 case Keyboard.CODE_ACTION_NEXT: 1327 performEditorAction(EditorInfo.IME_ACTION_NEXT); 1328 break; 1329 case Keyboard.CODE_ACTION_PREVIOUS: 1330 performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 1331 break; 1332 case Keyboard.CODE_LANGUAGE_SWITCH: 1333 handleLanguageSwitchKey(); 1334 break; 1335 case Keyboard.CODE_RESEARCH: 1336 if (ProductionFlag.IS_EXPERIMENTAL) { 1337 ResearchLogger.getInstance().onResearchKeySelected(this); 1338 } 1339 break; 1340 default: 1341 mSpaceState = SPACE_STATE_NONE; 1342 if (mCurrentSettings.isWordSeparator(primaryCode)) { 1343 didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); 1344 } else { 1345 if (SPACE_STATE_PHANTOM == spaceState) { 1346 if (ProductionFlag.IS_INTERNAL) { 1347 if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { 1348 Stats.onAutoCorrection( 1349 "", mWordComposer.getTypedWord(), " ", mWordComposer); 1350 } 1351 } 1352 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 1353 } 1354 final int keyX, keyY; 1355 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1356 if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { 1357 keyX = x; 1358 keyY = y; 1359 } else { 1360 keyX = Constants.NOT_A_COORDINATE; 1361 keyY = Constants.NOT_A_COORDINATE; 1362 } 1363 handleCharacter(primaryCode, keyX, keyY, spaceState); 1364 } 1365 mExpectingUpdateSelection = true; 1366 mShouldSwitchToLastSubtype = true; 1367 break; 1368 } 1369 switcher.onCodeInput(primaryCode); 1370 // Reset after any single keystroke, except shift and symbol-shift 1371 if (!didAutoCorrect && primaryCode != Keyboard.CODE_SHIFT 1372 && primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL) 1373 mLastComposedWord.deactivate(); 1374 if (Keyboard.CODE_DELETE != primaryCode) { 1375 mEnteredText = null; 1376 } 1377 mConnection.endBatchEdit(); 1378 if (ProductionFlag.IS_EXPERIMENTAL) { 1379 ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); 1380 } 1381 } 1382 1383 // Called from PointerTracker through the KeyboardActionListener interface 1384 @Override 1385 public void onTextInput(final CharSequence rawText) { 1386 mConnection.beginBatchEdit(); 1387 if (mWordComposer.isComposingWord()) { 1388 commitCurrentAutoCorrection(rawText.toString()); 1389 } else { 1390 resetComposingState(true /* alsoResetLastComposedWord */); 1391 } 1392 mHandler.postUpdateSuggestionStrip(); 1393 final CharSequence text = specificTldProcessingOnTextInput(rawText); 1394 if (SPACE_STATE_PHANTOM == mSpaceState) { 1395 sendKeyCodePoint(Keyboard.CODE_SPACE); 1396 } 1397 mConnection.commitText(text, 1); 1398 mConnection.endBatchEdit(); 1399 // Space state must be updated before calling updateShiftState 1400 mSpaceState = SPACE_STATE_NONE; 1401 mKeyboardSwitcher.updateShiftState(); 1402 mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT); 1403 mEnteredText = text; 1404 } 1405 1406 @Override 1407 public void onStartBatchInput() { 1408 BatchInputUpdater.getInstance().onStartBatchInput(); 1409 mConnection.beginBatchEdit(); 1410 if (mWordComposer.isComposingWord()) { 1411 if (ProductionFlag.IS_INTERNAL) { 1412 if (mWordComposer.isBatchMode()) { 1413 Stats.onAutoCorrection("", mWordComposer.getTypedWord(), " ", mWordComposer); 1414 } 1415 } 1416 if (mWordComposer.size() <= 1) { 1417 // We auto-correct the previous (typed, not gestured) string iff it's one character 1418 // long. The reason for this is, even in the middle of gesture typing, you'll still 1419 // tap one-letter words and you want them auto-corrected (typically, "i" in English 1420 // should become "I"). However for any longer word, we assume that the reason for 1421 // tapping probably is that the word you intend to type is not in the dictionary, 1422 // so we do not attempt to correct, on the assumption that if that was a dictionary 1423 // word, the user would probably have gestured instead. 1424 commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); 1425 } else { 1426 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 1427 } 1428 mExpectingUpdateSelection = true; 1429 // The following is necessary for the case where the user typed something but didn't 1430 // manual pick it and didn't input any separator. 1431 mSpaceState = SPACE_STATE_PHANTOM; 1432 } else { 1433 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 1434 // TODO: reverse this logic. We should have the means to determine whether a character 1435 // should usually be followed by a space, and it should be more readable. 1436 if (Constants.NOT_A_CODE != codePointBeforeCursor 1437 && !Character.isWhitespace(codePointBeforeCursor) 1438 && !mCurrentSettings.isPhantomSpacePromotingSymbol(codePointBeforeCursor) 1439 && !mCurrentSettings.isWeakSpaceStripper(codePointBeforeCursor)) { 1440 mSpaceState = SPACE_STATE_PHANTOM; 1441 } 1442 } 1443 mConnection.endBatchEdit(); 1444 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); 1445 } 1446 1447 private static final class BatchInputUpdater implements Handler.Callback { 1448 private final Handler mHandler; 1449 private LatinIME mLatinIme; 1450 private boolean mInBatchInput; // synchornized using "this". 1451 1452 private BatchInputUpdater() { 1453 final HandlerThread handlerThread = new HandlerThread( 1454 BatchInputUpdater.class.getSimpleName()); 1455 handlerThread.start(); 1456 mHandler = new Handler(handlerThread.getLooper(), this); 1457 } 1458 1459 // Initialization-on-demand holder 1460 private static final class OnDemandInitializationHolder { 1461 public static final BatchInputUpdater sInstance = new BatchInputUpdater(); 1462 } 1463 1464 public static BatchInputUpdater getInstance() { 1465 return OnDemandInitializationHolder.sInstance; 1466 } 1467 1468 private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; 1469 1470 @Override 1471 public boolean handleMessage(final Message msg) { 1472 switch (msg.what) { 1473 case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: 1474 updateBatchInput((InputPointers)msg.obj, mLatinIme); 1475 break; 1476 } 1477 return true; 1478 } 1479 1480 // Run in the UI thread. 1481 public synchronized void onStartBatchInput() { 1482 mInBatchInput = true; 1483 } 1484 1485 // Run in the Handler thread. 1486 private synchronized void updateBatchInput(final InputPointers batchPointers, 1487 final LatinIME latinIme) { 1488 if (!mInBatchInput) { 1489 // Batch input has ended while the message was being delivered. 1490 return; 1491 } 1492 final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked( 1493 batchPointers, latinIme); 1494 latinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1495 suggestedWords, false /* dismissGestureFloatingPreviewText */); 1496 } 1497 1498 // Run in the UI thread. 1499 public void onUpdateBatchInput(final InputPointers batchPointers, final LatinIME latinIme) { 1500 mLatinIme = latinIme; 1501 if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) { 1502 return; 1503 } 1504 mHandler.obtainMessage( 1505 MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers) 1506 .sendToTarget(); 1507 } 1508 1509 // Run in the UI thread. 1510 public synchronized SuggestedWords onEndBatchInput(final InputPointers batchPointers, 1511 final LatinIME latinIme) { 1512 mInBatchInput = false; 1513 final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked( 1514 batchPointers, latinIme); 1515 latinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1516 suggestedWords, true /* dismissGestureFloatingPreviewText */); 1517 return suggestedWords; 1518 } 1519 1520 // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to 1521 // be synchronized. 1522 private static SuggestedWords getSuggestedWordsGestureLocked( 1523 final InputPointers batchPointers, final LatinIME latinIme) { 1524 latinIme.mWordComposer.setBatchInputPointers(batchPointers); 1525 return latinIme.getSuggestedWords(Suggest.SESSION_GESTURE); 1526 } 1527 } 1528 1529 private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, 1530 final boolean dismissGestureFloatingPreviewText) { 1531 final String batchInputText = (suggestedWords.size() > 0) 1532 ? suggestedWords.getWord(0) : null; 1533 final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1534 mainKeyboardView.showGestureFloatingPreviewText(batchInputText); 1535 showSuggestionStrip(suggestedWords, null); 1536 if (dismissGestureFloatingPreviewText) { 1537 mainKeyboardView.dismissGestureFloatingPreviewText(); 1538 } 1539 } 1540 1541 @Override 1542 public void onUpdateBatchInput(final InputPointers batchPointers) { 1543 BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers, this); 1544 } 1545 1546 @Override 1547 public void onEndBatchInput(final InputPointers batchPointers) { 1548 final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput( 1549 batchPointers, this); 1550 final String batchInputText = (suggestedWords.size() > 0) 1551 ? suggestedWords.getWord(0) : null; 1552 if (TextUtils.isEmpty(batchInputText)) { 1553 return; 1554 } 1555 mWordComposer.setBatchInputWord(batchInputText); 1556 mConnection.beginBatchEdit(); 1557 if (SPACE_STATE_PHANTOM == mSpaceState) { 1558 sendKeyCodePoint(Keyboard.CODE_SPACE); 1559 } 1560 mConnection.setComposingText(batchInputText, 1); 1561 mExpectingUpdateSelection = true; 1562 mConnection.endBatchEdit(); 1563 // Space state must be updated before calling updateShiftState 1564 mSpaceState = SPACE_STATE_PHANTOM; 1565 mKeyboardSwitcher.updateShiftState(); 1566 } 1567 1568 private CharSequence specificTldProcessingOnTextInput(final CharSequence text) { 1569 if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD 1570 || !Character.isLetter(text.charAt(1))) { 1571 // Not a tld: do nothing. 1572 return text; 1573 } 1574 // We have a TLD (or something that looks like this): make sure we don't add 1575 // a space even if currently in phantom mode. 1576 mSpaceState = SPACE_STATE_NONE; 1577 // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code 1578 final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0); 1579 if (lastOne != null && lastOne.length() == 1 1580 && lastOne.charAt(0) == Keyboard.CODE_PERIOD) { 1581 return text.subSequence(1, text.length()); 1582 } else { 1583 return text; 1584 } 1585 } 1586 1587 // Called from PointerTracker through the KeyboardActionListener interface 1588 @Override 1589 public void onCancelInput() { 1590 // User released a finger outside any key 1591 mKeyboardSwitcher.onCancelInput(); 1592 } 1593 1594 private void handleBackspace(final int spaceState) { 1595 // In many cases, we may have to put the keyboard in auto-shift state again. However 1596 // we want to wait a few milliseconds before doing it to avoid the keyboard flashing 1597 // during key repeat. 1598 mHandler.postUpdateShiftState(); 1599 1600 if (mWordComposer.isComposingWord()) { 1601 final int length = mWordComposer.size(); 1602 if (length > 0) { 1603 // Immediately after a batch input. 1604 if (SPACE_STATE_PHANTOM == spaceState) { 1605 mWordComposer.reset(); 1606 } else { 1607 mWordComposer.deleteLast(); 1608 } 1609 mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1610 mHandler.postUpdateSuggestionStrip(); 1611 } else { 1612 mConnection.deleteSurroundingText(1, 0); 1613 } 1614 } else { 1615 if (mLastComposedWord.canRevertCommit()) { 1616 if (ProductionFlag.IS_INTERNAL) { 1617 Stats.onAutoCorrectionCancellation(); 1618 } 1619 revertCommit(); 1620 return; 1621 } 1622 if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { 1623 // Cancel multi-character input: remove the text we just entered. 1624 // This is triggered on backspace after a key that inputs multiple characters, 1625 // like the smiley key or the .com key. 1626 final int length = mEnteredText.length(); 1627 mConnection.deleteSurroundingText(length, 0); 1628 mEnteredText = null; 1629 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. 1630 // In addition we know that spaceState is false, and that we should not be 1631 // reverting any autocorrect at this point. So we can safely return. 1632 return; 1633 } 1634 if (SPACE_STATE_DOUBLE == spaceState) { 1635 mHandler.cancelDoubleSpacesTimer(); 1636 if (mConnection.revertDoubleSpace()) { 1637 // No need to reset mSpaceState, it has already be done (that's why we 1638 // receive it as a parameter) 1639 return; 1640 } 1641 } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1642 if (mConnection.revertSwapPunctuation()) { 1643 // Likewise 1644 return; 1645 } 1646 } 1647 1648 // No cancelling of commit/double space/swap: we have a regular backspace. 1649 // We should backspace one char and restart suggestion if at the end of a word. 1650 if (mLastSelectionStart != mLastSelectionEnd) { 1651 // If there is a selection, remove it. 1652 final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart; 1653 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); 1654 mConnection.deleteSurroundingText(lengthToDelete, 0); 1655 } else { 1656 // There is no selection, just delete one character. 1657 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { 1658 // This should never happen. 1659 Log.e(TAG, "Backspace when we don't know the selection position"); 1660 } 1661 // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because 1662 // we want to be able to compile against the Ice Cream Sandwich SDK. 1663 if (mTargetApplicationInfo != null 1664 && mTargetApplicationInfo.targetSdkVersion < 16) { 1665 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate 1666 // a hardware keyboard event on pressing enter or delete. This is bad for many 1667 // reasons (there are race conditions with commits) but some applications are 1668 // relying on this behavior so we continue to support it for older apps. 1669 sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL); 1670 } else { 1671 mConnection.deleteSurroundingText(1, 0); 1672 } 1673 if (mDeleteCount > DELETE_ACCELERATE_AT) { 1674 mConnection.deleteSurroundingText(1, 0); 1675 } 1676 } 1677 if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) { 1678 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(); 1679 } 1680 } 1681 } 1682 1683 private boolean maybeStripSpace(final int code, 1684 final int spaceState, final boolean isFromSuggestionStrip) { 1685 if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1686 mConnection.removeTrailingSpace(); 1687 return false; 1688 } else if ((SPACE_STATE_WEAK == spaceState 1689 || SPACE_STATE_SWAP_PUNCTUATION == spaceState) 1690 && isFromSuggestionStrip) { 1691 if (mCurrentSettings.isWeakSpaceSwapper(code)) { 1692 return true; 1693 } else { 1694 if (mCurrentSettings.isWeakSpaceStripper(code)) { 1695 mConnection.removeTrailingSpace(); 1696 } 1697 return false; 1698 } 1699 } else { 1700 return false; 1701 } 1702 } 1703 1704 private void handleCharacter(final int primaryCode, final int x, 1705 final int y, final int spaceState) { 1706 boolean isComposingWord = mWordComposer.isComposingWord(); 1707 1708 if (SPACE_STATE_PHANTOM == spaceState && 1709 !mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode)) { 1710 if (isComposingWord) { 1711 // Sanity check 1712 throw new RuntimeException("Should not be composing here"); 1713 } 1714 sendKeyCodePoint(Keyboard.CODE_SPACE); 1715 } 1716 1717 // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several 1718 // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI 1719 // thread here. 1720 if (!isComposingWord && (isAlphabet(primaryCode) 1721 || mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode)) 1722 && mCurrentSettings.isSuggestionsRequested(mDisplayOrientation) && 1723 !mConnection.isCursorTouchingWord(mCurrentSettings)) { 1724 // Reset entirely the composing state anyway, then start composing a new word unless 1725 // the character is a single quote. The idea here is, single quote is not a 1726 // separator and it should be treated as a normal character, except in the first 1727 // position where it should not start composing a word. 1728 isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode); 1729 // Here we don't need to reset the last composed word. It will be reset 1730 // when we commit this one, if we ever do; if on the other hand we backspace 1731 // it entirely and resume suggestions on the previous word, we'd like to still 1732 // have touch coordinates for it. 1733 resetComposingState(false /* alsoResetLastComposedWord */); 1734 } 1735 if (isComposingWord) { 1736 final int keyX, keyY; 1737 if (KeyboardActionListener.Adapter.isInvalidCoordinate(x) 1738 || KeyboardActionListener.Adapter.isInvalidCoordinate(y)) { 1739 keyX = x; 1740 keyY = y; 1741 } else { 1742 final KeyDetector keyDetector = 1743 mKeyboardSwitcher.getMainKeyboardView().getKeyDetector(); 1744 keyX = keyDetector.getTouchX(x); 1745 keyY = keyDetector.getTouchY(y); 1746 } 1747 mWordComposer.add(primaryCode, keyX, keyY); 1748 // If it's the first letter, make note of auto-caps state 1749 if (mWordComposer.size() == 1) { 1750 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); 1751 } 1752 mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1753 } else { 1754 final boolean swapWeakSpace = maybeStripSpace(primaryCode, 1755 spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x); 1756 1757 sendKeyCodePoint(primaryCode); 1758 1759 if (swapWeakSpace) { 1760 swapSwapperAndSpace(); 1761 mSpaceState = SPACE_STATE_WEAK; 1762 } 1763 // In case the "add to dictionary" hint was still displayed. 1764 if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint(); 1765 } 1766 mHandler.postUpdateSuggestionStrip(); 1767 if (ProductionFlag.IS_INTERNAL) { 1768 Utils.Stats.onNonSeparator((char)primaryCode, x, y); 1769 } 1770 } 1771 1772 // Returns true if we did an autocorrection, false otherwise. 1773 private boolean handleSeparator(final int primaryCode, final int x, final int y, 1774 final int spaceState) { 1775 boolean didAutoCorrect = false; 1776 // Handle separator 1777 if (mWordComposer.isComposingWord()) { 1778 if (mCurrentSettings.mCorrectionEnabled) { 1779 // TODO: maybe cache Strings in an <String> sparse array or something 1780 commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1)); 1781 didAutoCorrect = true; 1782 } else { 1783 commitTyped(new String(new int[]{primaryCode}, 0, 1)); 1784 } 1785 } 1786 1787 final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, 1788 Constants.SUGGESTION_STRIP_COORDINATE == x); 1789 1790 if (SPACE_STATE_PHANTOM == spaceState && 1791 mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) { 1792 sendKeyCodePoint(Keyboard.CODE_SPACE); 1793 } 1794 sendKeyCodePoint(primaryCode); 1795 1796 if (Keyboard.CODE_SPACE == primaryCode) { 1797 if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) { 1798 if (maybeDoubleSpace()) { 1799 mSpaceState = SPACE_STATE_DOUBLE; 1800 } else if (!isShowingPunctuationList()) { 1801 mSpaceState = SPACE_STATE_WEAK; 1802 } 1803 } 1804 1805 mHandler.startDoubleSpacesTimer(); 1806 if (!mConnection.isCursorTouchingWord(mCurrentSettings)) { 1807 mHandler.postUpdateSuggestionStrip(); 1808 } 1809 } else { 1810 if (swapWeakSpace) { 1811 swapSwapperAndSpace(); 1812 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; 1813 } else if (SPACE_STATE_PHANTOM == spaceState 1814 && !mCurrentSettings.isWeakSpaceStripper(primaryCode) 1815 && !mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) { 1816 // If we are in phantom space state, and the user presses a separator, we want to 1817 // stay in phantom space state so that the next keypress has a chance to add the 1818 // space. For example, if I type "Good dat", pick "day" from the suggestion strip 1819 // then insert a comma and go on to typing the next word, I want the space to be 1820 // inserted automatically before the next word, the same way it is when I don't 1821 // input the comma. 1822 // The case is a little different if the separator is a space stripper. Such a 1823 // separator does not normally need a space on the right (that's the difference 1824 // between swappers and strippers), so we should not stay in phantom space state if 1825 // the separator is a stripper. Hence the additional test above. 1826 mSpaceState = SPACE_STATE_PHANTOM; 1827 } 1828 1829 // Set punctuation right away. onUpdateSelection will fire but tests whether it is 1830 // already displayed or not, so it's okay. 1831 setPunctuationSuggestions(); 1832 } 1833 if (ProductionFlag.IS_INTERNAL) { 1834 Utils.Stats.onSeparator((char)primaryCode, x, y); 1835 } 1836 1837 mKeyboardSwitcher.updateShiftState(); 1838 return didAutoCorrect; 1839 } 1840 1841 private CharSequence getTextWithUnderline(final CharSequence text) { 1842 return mIsAutoCorrectionIndicatorOn 1843 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) 1844 : text; 1845 } 1846 1847 private void handleClose() { 1848 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 1849 requestHideSelf(0); 1850 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1851 if (mainKeyboardView != null) { 1852 mainKeyboardView.closing(); 1853 } 1854 } 1855 1856 // TODO: make this private 1857 // Outside LatinIME, only used by the test suite. 1858 /* package for tests */ 1859 boolean isShowingPunctuationList() { 1860 if (mSuggestionStripView == null) return false; 1861 return mCurrentSettings.mSuggestPuncList == mSuggestionStripView.getSuggestions(); 1862 } 1863 1864 private boolean isSuggestionsStripVisible() { 1865 if (mSuggestionStripView == null) 1866 return false; 1867 if (mSuggestionStripView.isShowingAddToDictionaryHint()) 1868 return true; 1869 if (!mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation)) 1870 return false; 1871 if (mCurrentSettings.isApplicationSpecifiedCompletionsOn()) 1872 return true; 1873 return mCurrentSettings.isSuggestionsRequested(mDisplayOrientation); 1874 } 1875 1876 private void clearSuggestionStrip() { 1877 setSuggestionStrip(SuggestedWords.EMPTY, false); 1878 setAutoCorrectionIndicator(false); 1879 } 1880 1881 private void setSuggestionStrip(final SuggestedWords words, final boolean isAutoCorrection) { 1882 if (mSuggestionStripView != null) { 1883 mSuggestionStripView.setSuggestions(words); 1884 mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); 1885 } 1886 } 1887 1888 private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { 1889 // Put a blue underline to a word in TextView which will be auto-corrected. 1890 if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator 1891 && mWordComposer.isComposingWord()) { 1892 mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; 1893 final CharSequence textWithUnderline = 1894 getTextWithUnderline(mWordComposer.getTypedWord()); 1895 // TODO: when called from an updateSuggestionStrip() call that results from a posted 1896 // message, this is called outside any batch edit. Potentially, this may result in some 1897 // janky flickering of the screen, although the display speed makes it unlikely in 1898 // the practice. 1899 mConnection.setComposingText(textWithUnderline, 1); 1900 } 1901 } 1902 1903 private void updateSuggestionStrip() { 1904 mHandler.cancelUpdateSuggestionStrip(); 1905 1906 // Check if we have a suggestion engine attached. 1907 if (mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) { 1908 if (mWordComposer.isComposingWord()) { 1909 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " 1910 + "requested!"); 1911 mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); 1912 } 1913 return; 1914 } 1915 1916 if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) { 1917 setPunctuationSuggestions(); 1918 return; 1919 } 1920 1921 final SuggestedWords suggestedWords = getSuggestedWords(Suggest.SESSION_TYPING); 1922 final String typedWord = mWordComposer.getTypedWord(); 1923 showSuggestionStrip(suggestedWords, typedWord); 1924 } 1925 1926 private SuggestedWords getSuggestedWords(final int sessionId) { 1927 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1928 if (keyboard == null) { 1929 return SuggestedWords.EMPTY; 1930 } 1931 final String typedWord = mWordComposer.getTypedWord(); 1932 // Get the word on which we should search the bigrams. If we are composing a word, it's 1933 // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we 1934 // should just skip whitespace if any, so 1. 1935 // TODO: this is slow (2-way IPC) - we should probably cache this instead. 1936 final CharSequence prevWord = 1937 mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 1938 mWordComposer.isComposingWord() ? 2 : 1); 1939 final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer, 1940 prevWord, keyboard.getProximityInfo(), mCurrentSettings.mCorrectionEnabled, 1941 sessionId); 1942 return maybeRetrieveOlderSuggestions(typedWord, suggestedWords); 1943 } 1944 1945 private SuggestedWords maybeRetrieveOlderSuggestions(final CharSequence typedWord, 1946 final SuggestedWords suggestedWords) { 1947 // TODO: consolidate this into getSuggestedWords 1948 // We update the suggestion strip only when we have some suggestions to show, i.e. when 1949 // the suggestion count is > 1; else, we leave the old suggestions, with the typed word 1950 // replaced with the new one. However, when the word is a dictionary word, or when the 1951 // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the 1952 // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to 1953 // revert to suggestions - although it is unclear how we can come here if it's displayed. 1954 if (suggestedWords.size() > 1 || typedWord.length() <= 1 1955 || !suggestedWords.mTypedWordValid 1956 || mSuggestionStripView.isShowingAddToDictionaryHint()) { 1957 return suggestedWords; 1958 } else { 1959 SuggestedWords previousSuggestions = mSuggestionStripView.getSuggestions(); 1960 if (previousSuggestions == mCurrentSettings.mSuggestPuncList) { 1961 previousSuggestions = SuggestedWords.EMPTY; 1962 } 1963 final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = 1964 SuggestedWords.getTypedWordAndPreviousSuggestions( 1965 typedWord, previousSuggestions); 1966 return new SuggestedWords(typedWordAndPreviousSuggestions, 1967 false /* typedWordValid */, 1968 false /* hasAutoCorrectionCandidate */, 1969 false /* isPunctuationSuggestions */, 1970 true /* isObsoleteSuggestions */, 1971 false /* isPrediction */); 1972 } 1973 } 1974 1975 private void showSuggestionStrip(final SuggestedWords suggestedWords, 1976 final CharSequence typedWord) { 1977 if (null == suggestedWords || suggestedWords.size() <= 0) { 1978 clearSuggestionStrip(); 1979 return; 1980 } 1981 final CharSequence autoCorrection; 1982 if (suggestedWords.size() > 0) { 1983 if (suggestedWords.mWillAutoCorrect) { 1984 autoCorrection = suggestedWords.getWord(1); 1985 } else { 1986 autoCorrection = typedWord; 1987 } 1988 } else { 1989 autoCorrection = null; 1990 } 1991 mWordComposer.setAutoCorrection(autoCorrection); 1992 final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); 1993 setSuggestionStrip(suggestedWords, isAutoCorrection); 1994 setAutoCorrectionIndicator(isAutoCorrection); 1995 setSuggestionStripShown(isSuggestionsStripVisible()); 1996 } 1997 1998 private void commitCurrentAutoCorrection(final String separatorString) { 1999 // Complete any pending suggestions query first 2000 if (mHandler.hasPendingUpdateSuggestions()) { 2001 updateSuggestionStrip(); 2002 } 2003 final CharSequence typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); 2004 final String typedWord = mWordComposer.getTypedWord(); 2005 final CharSequence autoCorrection = (typedAutoCorrection != null) 2006 ? typedAutoCorrection : typedWord; 2007 if (autoCorrection != null) { 2008 if (TextUtils.isEmpty(typedWord)) { 2009 throw new RuntimeException("We have an auto-correction but the typed word " 2010 + "is empty? Impossible! I must commit suicide."); 2011 } 2012 if (ProductionFlag.IS_INTERNAL) { 2013 Stats.onAutoCorrection( 2014 typedWord, autoCorrection.toString(), separatorString, mWordComposer); 2015 } 2016 mExpectingUpdateSelection = true; 2017 commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, 2018 separatorString); 2019 if (!typedWord.equals(autoCorrection)) { 2020 // This will make the correction flash for a short while as a visual clue 2021 // to the user that auto-correction happened. It has no other effect; in particular 2022 // note that this won't affect the text inside the text field AT ALL: it only makes 2023 // the segment of text starting at the supplied index and running for the length 2024 // of the auto-correction flash. At this moment, the "typedWord" argument is 2025 // ignored by TextView. 2026 mConnection.commitCorrection( 2027 new CorrectionInfo(mLastSelectionEnd - typedWord.length(), 2028 typedWord, autoCorrection)); 2029 } 2030 } 2031 } 2032 2033 // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} 2034 // interface 2035 @Override 2036 public void pickSuggestionManually(final int index, final CharSequence suggestion) { 2037 final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions(); 2038 // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput 2039 if (suggestion.length() == 1 && isShowingPunctuationList()) { 2040 // Word separators are suggested before the user inputs something. 2041 // So, LatinImeLogger logs "" as a user's input. 2042 LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords); 2043 // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. 2044 final int primaryCode = suggestion.charAt(0); 2045 onCodeInput(primaryCode, 2046 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); 2047 if (ProductionFlag.IS_EXPERIMENTAL) { 2048 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion); 2049 } 2050 return; 2051 } 2052 2053 mConnection.beginBatchEdit(); 2054 if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0 2055 // In the batch input mode, a manually picked suggested word should just replace 2056 // the current batch input text and there is no need for a phantom space. 2057 && !mWordComposer.isBatchMode()) { 2058 int firstChar = Character.codePointAt(suggestion, 0); 2059 if ((!mCurrentSettings.isWeakSpaceStripper(firstChar)) 2060 && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) { 2061 sendKeyCodePoint(Keyboard.CODE_SPACE); 2062 } 2063 } 2064 2065 if (mCurrentSettings.isApplicationSpecifiedCompletionsOn() 2066 && mApplicationSpecifiedCompletions != null 2067 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 2068 if (mSuggestionStripView != null) { 2069 mSuggestionStripView.clear(); 2070 } 2071 mKeyboardSwitcher.updateShiftState(); 2072 resetComposingState(true /* alsoResetLastComposedWord */); 2073 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; 2074 mConnection.commitCompletion(completionInfo); 2075 mConnection.endBatchEdit(); 2076 return; 2077 } 2078 2079 // We need to log before we commit, because the word composer will store away the user 2080 // typed word. 2081 final String replacedWord = mWordComposer.getTypedWord().toString(); 2082 LatinImeLogger.logOnManualSuggestion(replacedWord, 2083 suggestion.toString(), index, suggestedWords); 2084 mExpectingUpdateSelection = true; 2085 commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, 2086 LastComposedWord.NOT_A_SEPARATOR); 2087 if (ProductionFlag.IS_EXPERIMENTAL) { 2088 ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion); 2089 } 2090 mConnection.endBatchEdit(); 2091 // Don't allow cancellation of manual pick 2092 mLastComposedWord.deactivate(); 2093 // Space state must be updated before calling updateShiftState 2094 mSpaceState = SPACE_STATE_PHANTOM; 2095 mKeyboardSwitcher.updateShiftState(); 2096 2097 // We should show the "Touch again to save" hint if the user pressed the first entry 2098 // AND it's in none of our current dictionaries (main, user or otherwise). 2099 // Please note that if mSuggest is null, it means that everything is off: suggestion 2100 // and correction, so we shouldn't try to show the hint 2101 final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null 2102 // If the suggestion is not in the dictionary, the hint should be shown. 2103 && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true); 2104 2105 if (ProductionFlag.IS_INTERNAL) { 2106 Stats.onSeparator((char)Keyboard.CODE_SPACE, 2107 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 2108 } 2109 if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) { 2110 mSuggestionStripView.showAddToDictionaryHint( 2111 suggestion, mCurrentSettings.mHintToSaveText); 2112 } else { 2113 // If we're not showing the "Touch again to save", then update the suggestion strip. 2114 mHandler.postUpdateSuggestionStrip(); 2115 } 2116 } 2117 2118 /** 2119 * Commits the chosen word to the text field and saves it for later retrieval. 2120 */ 2121 private void commitChosenWord(final CharSequence chosenWord, final int commitType, 2122 final String separatorString) { 2123 final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions(); 2124 mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( 2125 this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); 2126 // Add the word to the user history dictionary 2127 final CharSequence prevWord = addToUserHistoryDictionary(chosenWord); 2128 // TODO: figure out here if this is an auto-correct or if the best word is actually 2129 // what user typed. Note: currently this is done much later in 2130 // LastComposedWord#didCommitTypedWord by string equality of the remembered 2131 // strings. 2132 mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord.toString(), 2133 separatorString, prevWord); 2134 } 2135 2136 private void setPunctuationSuggestions() { 2137 if (mCurrentSettings.mBigramPredictionEnabled) { 2138 clearSuggestionStrip(); 2139 } else { 2140 setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false); 2141 } 2142 setAutoCorrectionIndicator(false); 2143 setSuggestionStripShown(isSuggestionsStripVisible()); 2144 } 2145 2146 private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) { 2147 if (TextUtils.isEmpty(suggestion)) return null; 2148 if (mSuggest == null) return null; 2149 2150 // If correction is not enabled, we don't add words to the user history dictionary. 2151 // That's to avoid unintended additions in some sensitive fields, or fields that 2152 // expect to receive non-words. 2153 if (!mCurrentSettings.mCorrectionEnabled) return null; 2154 2155 final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary; 2156 if (userHistoryDictionary != null) { 2157 final CharSequence prevWord 2158 = mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2); 2159 final String secondWord; 2160 if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { 2161 secondWord = suggestion.toString().toLowerCase( 2162 mSubtypeSwitcher.getCurrentSubtypeLocale()); 2163 } else { 2164 secondWord = suggestion.toString(); 2165 } 2166 // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". 2167 // We don't add words with 0-frequency (assuming they would be profanity etc.). 2168 final int maxFreq = AutoCorrection.getMaxFrequency( 2169 mSuggest.getUnigramDictionaries(), suggestion); 2170 if (maxFreq == 0) return null; 2171 userHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(), 2172 secondWord, maxFreq > 0); 2173 return prevWord; 2174 } 2175 return null; 2176 } 2177 2178 /** 2179 * Check if the cursor is actually at the end of a word. If so, restart suggestions on this 2180 * word, else do nothing. 2181 */ 2182 private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() { 2183 final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mCurrentSettings); 2184 if (null != word) { 2185 restartSuggestionsOnWordBeforeCursor(word); 2186 } 2187 } 2188 2189 private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) { 2190 mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); 2191 final int length = word.length(); 2192 mConnection.deleteSurroundingText(length, 0); 2193 mConnection.setComposingText(word, 1); 2194 mHandler.postUpdateSuggestionStrip(); 2195 } 2196 2197 private void revertCommit() { 2198 final CharSequence previousWord = mLastComposedWord.mPrevWord; 2199 final String originallyTypedWord = mLastComposedWord.mTypedWord; 2200 final CharSequence committedWord = mLastComposedWord.mCommittedWord; 2201 final int cancelLength = committedWord.length(); 2202 final int separatorLength = LastComposedWord.getSeparatorLength( 2203 mLastComposedWord.mSeparatorString); 2204 // TODO: should we check our saved separator against the actual contents of the text view? 2205 final int deleteLength = cancelLength + separatorLength; 2206 if (DEBUG) { 2207 if (mWordComposer.isComposingWord()) { 2208 throw new RuntimeException("revertCommit, but we are composing a word"); 2209 } 2210 final String wordBeforeCursor = 2211 mConnection.getTextBeforeCursor(deleteLength, 0) 2212 .subSequence(0, cancelLength).toString(); 2213 if (!TextUtils.equals(committedWord, wordBeforeCursor)) { 2214 throw new RuntimeException("revertCommit check failed: we thought we were " 2215 + "reverting \"" + committedWord 2216 + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); 2217 } 2218 } 2219 mConnection.deleteSurroundingText(deleteLength, 0); 2220 if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { 2221 mUserHistoryDictionary.cancelAddingUserHistory( 2222 previousWord.toString(), committedWord.toString()); 2223 } 2224 mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1); 2225 if (ProductionFlag.IS_INTERNAL) { 2226 Stats.onSeparator(mLastComposedWord.mSeparatorString, 2227 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 2228 } 2229 if (ProductionFlag.IS_EXPERIMENTAL) { 2230 ResearchLogger.latinIME_revertCommit(originallyTypedWord); 2231 } 2232 // Don't restart suggestion yet. We'll restart if the user deletes the 2233 // separator. 2234 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 2235 // We have a separator between the word and the cursor: we should show predictions. 2236 mHandler.postUpdateSuggestionStrip(); 2237 } 2238 2239 // Used by the RingCharBuffer 2240 public boolean isWordSeparator(final int code) { 2241 return mCurrentSettings.isWordSeparator(code); 2242 } 2243 2244 // TODO: Make this private 2245 // Outside LatinIME, only used by the {@link InputTestsBase} test suite. 2246 /* package for test */ 2247 void loadKeyboard() { 2248 // When the device locale is changed in SetupWizard etc., this method may get called via 2249 // onConfigurationChanged before SoftInputWindow is shown. 2250 initSuggest(); 2251 loadSettings(); 2252 if (mKeyboardSwitcher.getMainKeyboardView() != null) { 2253 // Reload keyboard because the current language has been changed. 2254 mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings); 2255 } 2256 // Since we just changed languages, we should re-evaluate suggestions with whatever word 2257 // we are currently composing. If we are not composing anything, we may want to display 2258 // predictions or punctuation signs (which is done by the updateSuggestionStrip anyway). 2259 mHandler.postUpdateSuggestionStrip(); 2260 } 2261 2262 // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to 2263 // {@link KeyboardSwitcher}. Called from KeyboardSwitcher 2264 public void hapticAndAudioFeedback(final int primaryCode) { 2265 mFeedbackManager.hapticAndAudioFeedback( 2266 primaryCode, mKeyboardSwitcher.getMainKeyboardView()); 2267 } 2268 2269 // Callback called by PointerTracker through the KeyboardActionListener. This is called when a 2270 // key is depressed; release matching call is onReleaseKey below. 2271 @Override 2272 public void onPressKey(final int primaryCode) { 2273 mKeyboardSwitcher.onPressKey(primaryCode); 2274 } 2275 2276 // Callback by PointerTracker through the KeyboardActionListener. This is called when a key 2277 // is released; press matching call is onPressKey above. 2278 @Override 2279 public void onReleaseKey(final int primaryCode, final boolean withSliding) { 2280 mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); 2281 2282 // If accessibility is on, ensure the user receives keyboard state updates. 2283 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 2284 switch (primaryCode) { 2285 case Keyboard.CODE_SHIFT: 2286 AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); 2287 break; 2288 case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: 2289 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); 2290 break; 2291 } 2292 } 2293 2294 if (Keyboard.CODE_DELETE == primaryCode) { 2295 // This is a stopgap solution to avoid leaving a high surrogate alone in a text view. 2296 // In the future, we need to deprecate deteleSurroundingText() and have a surrogate 2297 // pair-friendly way of deleting characters in InputConnection. 2298 // TODO: use getCodePointBeforeCursor instead to improve performance 2299 final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0); 2300 if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) { 2301 mConnection.deleteSurroundingText(1, 0); 2302 } 2303 } 2304 } 2305 2306 // receive ringer mode change and network state change. 2307 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 2308 @Override 2309 public void onReceive(final Context context, final Intent intent) { 2310 final String action = intent.getAction(); 2311 if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 2312 mSubtypeSwitcher.onNetworkStateChanged(intent); 2313 } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { 2314 mFeedbackManager.onRingerModeChanged(); 2315 } 2316 } 2317 }; 2318 2319 private void launchSettings() { 2320 handleClose(); 2321 launchSubActivity(SettingsActivity.class); 2322 } 2323 2324 // Called from debug code only 2325 public void launchDebugSettings() { 2326 handleClose(); 2327 launchSubActivity(DebugSettingsActivity.class); 2328 } 2329 2330 public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) { 2331 // Put the text in the attached EditText into a safe, saved state before switching to a 2332 // new activity that will also use the soft keyboard. 2333 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 2334 launchSubActivity(activityClass); 2335 } 2336 2337 private void launchSubActivity(final Class<? extends Activity> activityClass) { 2338 Intent intent = new Intent(); 2339 intent.setClass(LatinIME.this, activityClass); 2340 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 2341 startActivity(intent); 2342 } 2343 2344 private void showSubtypeSelectorAndSettings() { 2345 final CharSequence title = getString(R.string.english_ime_input_options); 2346 final CharSequence[] items = new CharSequence[] { 2347 // TODO: Should use new string "Select active input modes". 2348 getString(R.string.language_selection_title), 2349 getString(R.string.english_ime_settings), 2350 }; 2351 final Context context = this; 2352 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 2353 @Override 2354 public void onClick(DialogInterface di, int position) { 2355 di.dismiss(); 2356 switch (position) { 2357 case 0: 2358 Intent intent = CompatUtils.getInputLanguageSelectionIntent( 2359 ImfUtils.getInputMethodIdOfThisIme(context), 2360 Intent.FLAG_ACTIVITY_NEW_TASK 2361 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2362 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2363 startActivity(intent); 2364 break; 2365 case 1: 2366 launchSettings(); 2367 break; 2368 } 2369 } 2370 }; 2371 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 2372 .setItems(items, listener) 2373 .setTitle(title); 2374 showOptionDialog(builder.create()); 2375 } 2376 2377 public void showOptionDialog(final AlertDialog dialog) { 2378 final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); 2379 if (windowToken == null) { 2380 return; 2381 } 2382 2383 dialog.setCancelable(true); 2384 dialog.setCanceledOnTouchOutside(true); 2385 2386 final Window window = dialog.getWindow(); 2387 final WindowManager.LayoutParams lp = window.getAttributes(); 2388 lp.token = windowToken; 2389 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 2390 window.setAttributes(lp); 2391 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 2392 2393 mOptionsDialog = dialog; 2394 dialog.show(); 2395 } 2396 2397 public void debugDumpStateAndCrashWithException(final String context) { 2398 final StringBuilder s = new StringBuilder(); 2399 s.append("Target application : ").append(mTargetApplicationInfo.name) 2400 .append("\nPackage : ").append(mTargetApplicationInfo.packageName) 2401 .append("\nTarget app sdk version : ") 2402 .append(mTargetApplicationInfo.targetSdkVersion) 2403 .append("\nAttributes : ").append(mCurrentSettings.getInputAttributesDebugString()) 2404 .append("\nContext : ").append(context); 2405 throw new RuntimeException(s.toString()); 2406 } 2407 2408 @Override 2409 protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) { 2410 super.dump(fd, fout, args); 2411 2412 final Printer p = new PrintWriterPrinter(fout); 2413 p.println("LatinIME state :"); 2414 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 2415 final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; 2416 p.println(" Keyboard mode = " + keyboardMode); 2417 p.println(" mIsSuggestionsSuggestionsRequested = " 2418 + mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)); 2419 p.println(" mCorrectionEnabled=" + mCurrentSettings.mCorrectionEnabled); 2420 p.println(" isComposingWord=" + mWordComposer.isComposingWord()); 2421 p.println(" mSoundOn=" + mCurrentSettings.mSoundOn); 2422 p.println(" mVibrateOn=" + mCurrentSettings.mVibrateOn); 2423 p.println(" mKeyPreviewPopupOn=" + mCurrentSettings.mKeyPreviewPopupOn); 2424 p.println(" inputAttributes=" + mCurrentSettings.getInputAttributesDebugString()); 2425 } 2426} 2427