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