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