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