LatinIME.java revision aabc97233826da69edbadfc7c1465a505a1fe2ec
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under 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.PackageInfo; 32import android.content.res.Configuration; 33import android.content.res.Resources; 34import android.inputmethodservice.InputMethodService; 35import android.media.AudioManager; 36import android.net.ConnectivityManager; 37import android.os.Debug; 38import android.os.Handler; 39import android.os.HandlerThread; 40import android.os.IBinder; 41import android.os.Message; 42import android.os.SystemClock; 43import android.preference.PreferenceManager; 44import android.text.InputType; 45import android.text.TextUtils; 46import android.text.style.SuggestionSpan; 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.Window; 54import android.view.WindowManager; 55import android.view.inputmethod.CompletionInfo; 56import android.view.inputmethod.CorrectionInfo; 57import android.view.inputmethod.EditorInfo; 58import android.view.inputmethod.InputMethodSubtype; 59 60import com.android.inputmethod.accessibility.AccessibilityUtils; 61import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; 62import com.android.inputmethod.annotations.UsedForTesting; 63import com.android.inputmethod.compat.AppWorkaroundsUtils; 64import com.android.inputmethod.compat.InputMethodServiceCompatUtils; 65import com.android.inputmethod.compat.SuggestionSpanUtils; 66import com.android.inputmethod.dictionarypack.DictionaryPackConstants; 67import com.android.inputmethod.event.EventInterpreter; 68import com.android.inputmethod.keyboard.KeyDetector; 69import com.android.inputmethod.keyboard.Keyboard; 70import com.android.inputmethod.keyboard.KeyboardActionListener; 71import com.android.inputmethod.keyboard.KeyboardId; 72import com.android.inputmethod.keyboard.KeyboardSwitcher; 73import com.android.inputmethod.keyboard.MainKeyboardView; 74import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 75import com.android.inputmethod.latin.define.ProductionFlag; 76import com.android.inputmethod.latin.personalization.PersonalizationDictionary; 77import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister; 78import com.android.inputmethod.latin.personalization.PersonalizationHelper; 79import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary; 80import com.android.inputmethod.latin.personalization.UserHistoryPredictionDictionary; 81import com.android.inputmethod.latin.settings.Settings; 82import com.android.inputmethod.latin.settings.SettingsActivity; 83import com.android.inputmethod.latin.settings.SettingsValues; 84import com.android.inputmethod.latin.suggestions.SuggestionStripView; 85import com.android.inputmethod.latin.utils.ApplicationUtils; 86import com.android.inputmethod.latin.utils.AutoCorrectionUtils; 87import com.android.inputmethod.latin.utils.CapsModeUtils; 88import com.android.inputmethod.latin.utils.CollectionUtils; 89import com.android.inputmethod.latin.utils.CompletionInfoUtils; 90import com.android.inputmethod.latin.utils.InputTypeUtils; 91import com.android.inputmethod.latin.utils.IntentUtils; 92import com.android.inputmethod.latin.utils.JniUtils; 93import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; 94import com.android.inputmethod.latin.utils.PositionalInfoForUserDictPendingAddition; 95import com.android.inputmethod.latin.utils.RecapitalizeStatus; 96import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; 97import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; 98import com.android.inputmethod.latin.utils.TextRange; 99import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils; 100import com.android.inputmethod.research.ResearchLogger; 101 102import java.io.FileDescriptor; 103import java.io.PrintWriter; 104import java.util.ArrayList; 105import java.util.Locale; 106import java.util.TreeSet; 107 108/** 109 * Input method implementation for Qwerty'ish keyboard. 110 */ 111public class LatinIME extends InputMethodService implements KeyboardActionListener, 112 SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener, 113 Suggest.SuggestInitializationListener { 114 private static final String TAG = LatinIME.class.getSimpleName(); 115 private static final boolean TRACE = false; 116 private static boolean DEBUG; 117 118 private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; 119 120 // How many continuous deletes at which to start deleting at a higher speed. 121 private static final int DELETE_ACCELERATE_AT = 20; 122 // Key events coming any faster than this are long-presses. 123 private static final int QUICK_PRESS = 200; 124 125 private static final int PENDING_IMS_CALLBACK_DURATION = 800; 126 127 private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; 128 129 /** 130 * The name of the scheme used by the Package Manager to warn of a new package installation, 131 * replacement or removal. 132 */ 133 private static final String SCHEME_PACKAGE = "package"; 134 135 private static final int SPACE_STATE_NONE = 0; 136 // Double space: the state where the user pressed space twice quickly, which LatinIME 137 // resolved as period-space. Undoing this converts the period to a space. 138 private static final int SPACE_STATE_DOUBLE = 1; 139 // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip 140 // have just been swapped. Undoing this swaps them back; the space is still considered weak. 141 private static final int SPACE_STATE_SWAP_PUNCTUATION = 2; 142 // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak 143 // spaces happen when the user presses space, accepting the current suggestion (whether 144 // it's an auto-correction or not). 145 private static final int SPACE_STATE_WEAK = 3; 146 // Phantom space: a not-yet-inserted space that should get inserted on the next input, 147 // character provided it's not a separator. If it's a separator, the phantom space is dropped. 148 // Phantom spaces happen when a user chooses a word from the suggestion strip. 149 private static final int SPACE_STATE_PHANTOM = 4; 150 151 // Current space state of the input method. This can be any of the above constants. 152 private int mSpaceState; 153 154 private final Settings mSettings; 155 156 private View mInputView; 157 private int mInputViewMinHeight; 158 private SuggestionStripView mSuggestionStripView; 159 // Never null 160 private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; 161 private Suggest mSuggest; 162 private CompletionInfo[] mApplicationSpecifiedCompletions; 163 private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils(); 164 165 private RichInputMethodManager mRichImm; 166 @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; 167 private final SubtypeSwitcher mSubtypeSwitcher; 168 private final SubtypeState mSubtypeState = new SubtypeState(); 169 // At start, create a default event interpreter that does nothing by passing it no decoder spec. 170 // The event interpreter should never be null. 171 private EventInterpreter mEventInterpreter = new EventInterpreter(this); 172 173 private boolean mIsMainDictionaryAvailable; 174 private UserBinaryDictionary mUserDictionary; 175 private UserHistoryPredictionDictionary mUserHistoryPredictionDictionary; 176 private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary; 177 private PersonalizationDictionary mPersonalizationDictionary; 178 private boolean mIsUserDictionaryAvailable; 179 180 private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 181 private PositionalInfoForUserDictPendingAddition 182 mPositionalInfoForUserDictPendingAddition = null; 183 private final WordComposer mWordComposer = new WordComposer(); 184 private final RichInputConnection mConnection = new RichInputConnection(this); 185 private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); 186 187 // Keep track of the last selection range to decide if we need to show word alternatives 188 private static final int NOT_A_CURSOR_POSITION = -1; 189 private int mLastSelectionStart = NOT_A_CURSOR_POSITION; 190 private int mLastSelectionEnd = NOT_A_CURSOR_POSITION; 191 192 // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't 193 // "expect" it, it means the user actually moved the cursor. 194 private boolean mExpectingUpdateSelection; 195 private int mDeleteCount; 196 private long mLastKeyTime; 197 private final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); 198 // Personalization debugging params 199 private boolean mUseOnlyPersonalizationDictionaryForDebug = false; 200 private boolean mBoostPersonalizationDictionaryForDebug = false; 201 202 // Member variables for remembering the current device orientation. 203 private int mDisplayOrientation; 204 205 // Object for reacting to adding/removing a dictionary pack. 206 private BroadcastReceiver mDictionaryPackInstallReceiver = 207 new DictionaryPackInstallBroadcastReceiver(this); 208 209 // Keeps track of most recently inserted text (multi-character key) for reverting 210 private String mEnteredText; 211 212 // TODO: This boolean is persistent state and causes large side effects at unexpected times. 213 // Find a way to remove it for readability. 214 private boolean mIsAutoCorrectionIndicatorOn; 215 216 private AlertDialog mOptionsDialog; 217 218 private final boolean mIsHardwareAcceleratedDrawingEnabled; 219 220 public final UIHandler mHandler = new UIHandler(this); 221 222 public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> { 223 private static final int MSG_UPDATE_SHIFT_STATE = 0; 224 private static final int MSG_PENDING_IMS_CALLBACK = 1; 225 private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; 226 private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; 227 private static final int MSG_RESUME_SUGGESTIONS = 4; 228 private static final int MSG_REOPEN_DICTIONARIES = 5; 229 230 private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; 231 232 private int mDelayUpdateSuggestions; 233 private int mDelayUpdateShiftState; 234 private long mDoubleSpacePeriodTimeout; 235 private long mDoubleSpacePeriodTimerStart; 236 237 public UIHandler(final LatinIME outerInstance) { 238 super(outerInstance); 239 } 240 241 public void onCreate() { 242 final Resources res = getOuterInstance().getResources(); 243 mDelayUpdateSuggestions = 244 res.getInteger(R.integer.config_delay_update_suggestions); 245 mDelayUpdateShiftState = 246 res.getInteger(R.integer.config_delay_update_shift_state); 247 mDoubleSpacePeriodTimeout = 248 res.getInteger(R.integer.config_double_space_period_timeout); 249 } 250 251 @Override 252 public void handleMessage(final Message msg) { 253 final LatinIME latinIme = getOuterInstance(); 254 final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; 255 switch (msg.what) { 256 case MSG_UPDATE_SUGGESTION_STRIP: 257 latinIme.updateSuggestionStrip(); 258 break; 259 case MSG_UPDATE_SHIFT_STATE: 260 switcher.updateShiftState(); 261 break; 262 case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: 263 latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj, 264 msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); 265 break; 266 case MSG_RESUME_SUGGESTIONS: 267 latinIme.restartSuggestionsOnWordTouchedByCursor(); 268 break; 269 case MSG_REOPEN_DICTIONARIES: 270 latinIme.initSuggest(); 271 // In theory we could call latinIme.updateSuggestionStrip() right away, but 272 // in the practice, the dictionary is not finished opening yet so we wouldn't 273 // get any suggestions. Wait one frame. 274 postUpdateSuggestionStrip(); 275 break; 276 } 277 } 278 279 public void postUpdateSuggestionStrip() { 280 sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions); 281 } 282 283 public void postReopenDictionaries() { 284 sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)); 285 } 286 287 public void postResumeSuggestions() { 288 removeMessages(MSG_RESUME_SUGGESTIONS); 289 sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); 290 } 291 292 public void cancelUpdateSuggestionStrip() { 293 removeMessages(MSG_UPDATE_SUGGESTION_STRIP); 294 } 295 296 public boolean hasPendingUpdateSuggestions() { 297 return hasMessages(MSG_UPDATE_SUGGESTION_STRIP); 298 } 299 300 public boolean hasPendingReopenDictionaries() { 301 return hasMessages(MSG_REOPEN_DICTIONARIES); 302 } 303 304 public void postUpdateShiftState() { 305 removeMessages(MSG_UPDATE_SHIFT_STATE); 306 sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState); 307 } 308 309 public void cancelUpdateShiftState() { 310 removeMessages(MSG_UPDATE_SHIFT_STATE); 311 } 312 313 public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, 314 final boolean dismissGestureFloatingPreviewText) { 315 removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 316 final int arg1 = dismissGestureFloatingPreviewText 317 ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : 0; 318 obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords) 319 .sendToTarget(); 320 } 321 322 public void startDoubleSpacePeriodTimer() { 323 mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis(); 324 } 325 326 public void cancelDoubleSpacePeriodTimer() { 327 mDoubleSpacePeriodTimerStart = 0; 328 } 329 330 public boolean isAcceptingDoubleSpacePeriod() { 331 return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart 332 < mDoubleSpacePeriodTimeout; 333 } 334 335 // Working variables for the following methods. 336 private boolean mIsOrientationChanging; 337 private boolean mPendingSuccessiveImsCallback; 338 private boolean mHasPendingStartInput; 339 private boolean mHasPendingFinishInputView; 340 private boolean mHasPendingFinishInput; 341 private EditorInfo mAppliedEditorInfo; 342 343 public void startOrientationChanging() { 344 removeMessages(MSG_PENDING_IMS_CALLBACK); 345 resetPendingImsCallback(); 346 mIsOrientationChanging = true; 347 final LatinIME latinIme = getOuterInstance(); 348 if (latinIme.isInputViewShown()) { 349 latinIme.mKeyboardSwitcher.saveKeyboardState(); 350 } 351 } 352 353 private void resetPendingImsCallback() { 354 mHasPendingFinishInputView = false; 355 mHasPendingFinishInput = false; 356 mHasPendingStartInput = false; 357 } 358 359 private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, 360 boolean restarting) { 361 if (mHasPendingFinishInputView) 362 latinIme.onFinishInputViewInternal(mHasPendingFinishInput); 363 if (mHasPendingFinishInput) 364 latinIme.onFinishInputInternal(); 365 if (mHasPendingStartInput) 366 latinIme.onStartInputInternal(editorInfo, restarting); 367 resetPendingImsCallback(); 368 } 369 370 public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { 371 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 372 // Typically this is the second onStartInput after orientation changed. 373 mHasPendingStartInput = true; 374 } else { 375 if (mIsOrientationChanging && restarting) { 376 // This is the first onStartInput after orientation changed. 377 mIsOrientationChanging = false; 378 mPendingSuccessiveImsCallback = true; 379 } 380 final LatinIME latinIme = getOuterInstance(); 381 executePendingImsCallback(latinIme, editorInfo, restarting); 382 latinIme.onStartInputInternal(editorInfo, restarting); 383 } 384 } 385 386 public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { 387 if (hasMessages(MSG_PENDING_IMS_CALLBACK) 388 && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { 389 // Typically this is the second onStartInputView after orientation changed. 390 resetPendingImsCallback(); 391 } else { 392 if (mPendingSuccessiveImsCallback) { 393 // This is the first onStartInputView after orientation changed. 394 mPendingSuccessiveImsCallback = false; 395 resetPendingImsCallback(); 396 sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), 397 PENDING_IMS_CALLBACK_DURATION); 398 } 399 final LatinIME latinIme = getOuterInstance(); 400 executePendingImsCallback(latinIme, editorInfo, restarting); 401 latinIme.onStartInputViewInternal(editorInfo, restarting); 402 mAppliedEditorInfo = editorInfo; 403 } 404 } 405 406 public void onFinishInputView(final boolean finishingInput) { 407 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 408 // Typically this is the first onFinishInputView after orientation changed. 409 mHasPendingFinishInputView = true; 410 } else { 411 final LatinIME latinIme = getOuterInstance(); 412 latinIme.onFinishInputViewInternal(finishingInput); 413 mAppliedEditorInfo = null; 414 } 415 } 416 417 public void onFinishInput() { 418 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 419 // Typically this is the first onFinishInput after orientation changed. 420 mHasPendingFinishInput = true; 421 } else { 422 final LatinIME latinIme = getOuterInstance(); 423 executePendingImsCallback(latinIme, null, false); 424 latinIme.onFinishInputInternal(); 425 } 426 } 427 } 428 429 static final class SubtypeState { 430 private InputMethodSubtype mLastActiveSubtype; 431 private boolean mCurrentSubtypeUsed; 432 433 public void currentSubtypeUsed() { 434 mCurrentSubtypeUsed = true; 435 } 436 437 public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) { 438 final InputMethodSubtype currentSubtype = richImm.getInputMethodManager() 439 .getCurrentInputMethodSubtype(); 440 final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; 441 final boolean currentSubtypeUsed = mCurrentSubtypeUsed; 442 if (currentSubtypeUsed) { 443 mLastActiveSubtype = currentSubtype; 444 mCurrentSubtypeUsed = false; 445 } 446 if (currentSubtypeUsed 447 && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype) 448 && !currentSubtype.equals(lastActiveSubtype)) { 449 richImm.setInputMethodAndSubtype(token, lastActiveSubtype); 450 return; 451 } 452 richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */); 453 } 454 } 455 456 // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial 457 // JNI call as much as possible. 458 static { 459 JniUtils.loadNativeLibrary(); 460 } 461 462 public LatinIME() { 463 super(); 464 mSettings = Settings.getInstance(); 465 mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 466 mKeyboardSwitcher = KeyboardSwitcher.getInstance(); 467 mIsHardwareAcceleratedDrawingEnabled = 468 InputMethodServiceCompatUtils.enableHardwareAcceleration(this); 469 Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled); 470 } 471 472 @Override 473 public void onCreate() { 474 Settings.init(this); 475 LatinImeLogger.init(this); 476 RichInputMethodManager.init(this); 477 mRichImm = RichInputMethodManager.getInstance(); 478 SubtypeSwitcher.init(this); 479 KeyboardSwitcher.init(this); 480 AudioAndHapticFeedbackManager.init(this); 481 AccessibilityUtils.init(this); 482 PersonalizationDictionarySessionRegister.init(this); 483 484 super.onCreate(); 485 486 mHandler.onCreate(); 487 DEBUG = LatinImeLogger.sDBG; 488 489 // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}. 490 loadSettings(); 491 initSuggest(); 492 493 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 494 ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest); 495 } 496 mDisplayOrientation = getResources().getConfiguration().orientation; 497 498 // Register to receive ringer mode change and network state change. 499 // Also receive installation and removal of a dictionary pack. 500 final IntentFilter filter = new IntentFilter(); 501 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 502 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 503 registerReceiver(mReceiver, filter); 504 505 final IntentFilter packageFilter = new IntentFilter(); 506 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 507 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 508 packageFilter.addDataScheme(SCHEME_PACKAGE); 509 registerReceiver(mDictionaryPackInstallReceiver, packageFilter); 510 511 final IntentFilter newDictFilter = new IntentFilter(); 512 newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); 513 registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); 514 } 515 516 // Has to be package-visible for unit tests 517 @UsedForTesting 518 void loadSettings() { 519 final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 520 final InputAttributes inputAttributes = 521 new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode()); 522 mSettings.loadSettings(locale, inputAttributes); 523 AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent()); 524 // To load the keyboard we need to load all the settings once, but resetting the 525 // contacts dictionary should be deferred until after the new layout has been displayed 526 // to improve responsivity. In the language switching process, we post a reopenDictionaries 527 // message, then come here to read the settings for the new language before we change 528 // the layout; at this time, we need to skip resetting the contacts dictionary. It will 529 // be done later inside {@see #initSuggest()} when the reopenDictionaries message is 530 // processed. 531 if (!mHandler.hasPendingReopenDictionaries()) { 532 // May need to reset the contacts dictionary depending on the user settings. 533 resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); 534 } 535 } 536 537 // Note that this method is called from a non-UI thread. 538 @Override 539 public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) { 540 mIsMainDictionaryAvailable = isMainDictionaryAvailable; 541 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 542 if (mainKeyboardView != null) { 543 mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable); 544 } 545 } 546 547 private void initSuggest() { 548 final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 549 final String localeStr = subtypeLocale.toString(); 550 551 final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale, 552 this /* SuggestInitializationListener */); 553 final SettingsValues settingsValues = mSettings.getCurrent(); 554 if (settingsValues.mCorrectionEnabled) { 555 newSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold); 556 } 557 558 mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); 559 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 560 ResearchLogger.getInstance().initSuggest(newSuggest); 561 } 562 563 mUserDictionary = new UserBinaryDictionary(this, localeStr); 564 mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); 565 newSuggest.setUserDictionary(mUserDictionary); 566 567 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 568 569 mUserHistoryPredictionDictionary = PersonalizationHelper 570 .getUserHistoryPredictionDictionary(this, localeStr, prefs); 571 newSuggest.setUserHistoryPredictionDictionary(mUserHistoryPredictionDictionary); 572 mPersonalizationDictionary = PersonalizationHelper 573 .getPersonalizationDictionary(this, localeStr, prefs); 574 newSuggest.setPersonalizationDictionary(mPersonalizationDictionary); 575 mPersonalizationPredictionDictionary = PersonalizationHelper 576 .getPersonalizationPredictionDictionary(this, localeStr, prefs); 577 newSuggest.setPersonalizationPredictionDictionary(mPersonalizationPredictionDictionary); 578 579 final Suggest oldSuggest = mSuggest; 580 resetContactsDictionary(null != oldSuggest ? oldSuggest.getContactsDictionary() : null); 581 mSuggest = newSuggest; 582 if (oldSuggest != null) oldSuggest.close(); 583 } 584 585 /** 586 * Resets the contacts dictionary in mSuggest according to the user settings. 587 * 588 * This method takes an optional contacts dictionary to use when the locale hasn't changed 589 * since the contacts dictionary can be opened or closed as necessary depending on the settings. 590 * 591 * @param oldContactsDictionary an optional dictionary to use, or null 592 */ 593 private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) { 594 final Suggest suggest = mSuggest; 595 final boolean shouldSetDictionary = 596 (null != suggest && mSettings.getCurrent().mUseContactsDict); 597 598 final ContactsBinaryDictionary dictionaryToUse; 599 if (!shouldSetDictionary) { 600 // Make sure the dictionary is closed. If it is already closed, this is a no-op, 601 // so it's safe to call it anyways. 602 if (null != oldContactsDictionary) oldContactsDictionary.close(); 603 dictionaryToUse = null; 604 } else { 605 final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 606 if (null != oldContactsDictionary) { 607 if (!oldContactsDictionary.mLocale.equals(locale)) { 608 // If the locale has changed then recreate the contacts dictionary. This 609 // allows locale dependent rules for handling bigram name predictions. 610 oldContactsDictionary.close(); 611 dictionaryToUse = new ContactsBinaryDictionary(this, locale); 612 } else { 613 // Make sure the old contacts dictionary is opened. If it is already open, 614 // this is a no-op, so it's safe to call it anyways. 615 oldContactsDictionary.reopen(this); 616 dictionaryToUse = oldContactsDictionary; 617 } 618 } else { 619 dictionaryToUse = new ContactsBinaryDictionary(this, locale); 620 } 621 } 622 623 if (null != suggest) { 624 suggest.setContactsDictionary(dictionaryToUse); 625 } 626 } 627 628 /* package private */ void resetSuggestMainDict() { 629 final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 630 mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */); 631 mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); 632 } 633 634 @Override 635 public void onDestroy() { 636 final Suggest suggest = mSuggest; 637 if (suggest != null) { 638 suggest.close(); 639 mSuggest = null; 640 } 641 mSettings.onDestroy(); 642 unregisterReceiver(mReceiver); 643 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 644 ResearchLogger.getInstance().onDestroy(); 645 } 646 unregisterReceiver(mDictionaryPackInstallReceiver); 647 PersonalizationDictionarySessionRegister.onDestroy(this); 648 LatinImeLogger.commit(); 649 LatinImeLogger.onDestroy(); 650 super.onDestroy(); 651 } 652 653 @Override 654 public void onConfigurationChanged(final Configuration conf) { 655 // If orientation changed while predicting, commit the change 656 if (mDisplayOrientation != conf.orientation) { 657 mDisplayOrientation = conf.orientation; 658 mHandler.startOrientationChanging(); 659 mConnection.beginBatchEdit(); 660 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 661 mConnection.finishComposingText(); 662 mConnection.endBatchEdit(); 663 if (isShowingOptionDialog()) { 664 mOptionsDialog.dismiss(); 665 } 666 } 667 PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf); 668 super.onConfigurationChanged(conf); 669 } 670 671 @Override 672 public View onCreateInputView() { 673 return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); 674 } 675 676 private void setInputViewMinHeight(final int minHeight) { 677 if (mInputView != null && mInputViewMinHeight != minHeight) { 678 mInputView.setMinimumHeight(minHeight); 679 mInputViewMinHeight = minHeight; 680 } 681 } 682 683 @Override 684 public void setInputView(final View inputView) { 685 super.setInputView(inputView); 686 mInputView = inputView; 687 setInputViewMinHeight(0); 688 mSuggestionStripView = (SuggestionStripView)inputView.findViewById( 689 R.id.suggestion_strip_view); 690 if (mSuggestionStripView != null) { 691 mSuggestionStripView.setListener(this, inputView); 692 } 693 if (LatinImeLogger.sVISUALDEBUG) { 694 inputView.setBackgroundColor(0x10FF0000); 695 } 696 } 697 698 @Override 699 public void setCandidatesView(final View view) { 700 // To ensure that CandidatesView will never be set. 701 return; 702 } 703 704 @Override 705 public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { 706 mHandler.onStartInput(editorInfo, restarting); 707 } 708 709 @Override 710 public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { 711 mHandler.onStartInputView(editorInfo, restarting); 712 } 713 714 @Override 715 public void onFinishInputView(final boolean finishingInput) { 716 mHandler.onFinishInputView(finishingInput); 717 } 718 719 @Override 720 public void onFinishInput() { 721 mHandler.onFinishInput(); 722 } 723 724 @Override 725 public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { 726 // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() 727 // is not guaranteed. It may even be called at the same time on a different thread. 728 mSubtypeSwitcher.onSubtypeChanged(subtype); 729 loadKeyboard(); 730 } 731 732 private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { 733 super.onStartInput(editorInfo, restarting); 734 } 735 736 @SuppressWarnings("deprecation") 737 private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) { 738 super.onStartInputView(editorInfo, restarting); 739 final KeyboardSwitcher switcher = mKeyboardSwitcher; 740 final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView(); 741 // If we are starting input in a different text field from before, we'll have to reload 742 // settings, so currentSettingsValues can't be final. 743 SettingsValues currentSettingsValues = mSettings.getCurrent(); 744 745 if (editorInfo == null) { 746 Log.e(TAG, "Null EditorInfo in onStartInputView()"); 747 if (LatinImeLogger.sDBG) { 748 throw new NullPointerException("Null EditorInfo in onStartInputView()"); 749 } 750 return; 751 } 752 if (DEBUG) { 753 Log.d(TAG, "onStartInputView: editorInfo:" 754 + String.format("inputType=0x%08x imeOptions=0x%08x", 755 editorInfo.inputType, editorInfo.imeOptions)); 756 Log.d(TAG, "All caps = " 757 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) 758 + ", sentence caps = " 759 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) 760 + ", word caps = " 761 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); 762 } 763 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 764 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 765 ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs); 766 } 767 if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) { 768 Log.w(TAG, "Deprecated private IME option specified: " 769 + editorInfo.privateImeOptions); 770 Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead"); 771 } 772 if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) { 773 Log.w(TAG, "Deprecated private IME option specified: " 774 + editorInfo.privateImeOptions); 775 Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead"); 776 } 777 778 final PackageInfo packageInfo = 779 TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName); 780 mAppWorkAroundsUtils.setPackageInfo(packageInfo); 781 if (null == packageInfo) { 782 new TargetPackageInfoGetterTask(this /* context */, this /* listener */) 783 .execute(editorInfo.packageName); 784 } 785 786 LatinImeLogger.onStartInputView(editorInfo); 787 // In landscape mode, this method gets called without the input view being created. 788 if (mainKeyboardView == null) { 789 return; 790 } 791 792 // Forward this event to the accessibility utilities, if enabled. 793 final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); 794 if (accessUtils.isTouchExplorationEnabled()) { 795 accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting); 796 } 797 798 final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); 799 final boolean isDifferentTextField = !restarting || inputTypeChanged; 800 if (isDifferentTextField) { 801 mSubtypeSwitcher.updateParametersOnStartInputView(); 802 } 803 804 // The EditorInfo might have a flag that affects fullscreen mode. 805 // Note: This call should be done by InputMethodService? 806 updateFullscreenMode(); 807 mApplicationSpecifiedCompletions = null; 808 809 // The app calling setText() has the effect of clearing the composing 810 // span, so we should reset our state unconditionally, even if restarting is true. 811 mEnteredText = null; 812 resetComposingState(true /* alsoResetLastComposedWord */); 813 if (isDifferentTextField) mHandler.postResumeSuggestions(); 814 mDeleteCount = 0; 815 mSpaceState = SPACE_STATE_NONE; 816 mRecapitalizeStatus.deactivate(); 817 mCurrentlyPressedHardwareKeys.clear(); 818 819 // Note: the following does a round-trip IPC on the main thread: be careful 820 final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 821 final Suggest suggest = mSuggest; 822 if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) { 823 initSuggest(); 824 } 825 if (mSuggestionStripView != null) { 826 // This will set the punctuation suggestions if next word suggestion is off; 827 // otherwise it will clear the suggestion strip. 828 setPunctuationSuggestions(); 829 } 830 mSuggestedWords = SuggestedWords.EMPTY; 831 832 mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart, 833 false /* shouldFinishComposition */); 834 835 if (isDifferentTextField) { 836 mainKeyboardView.closing(); 837 loadSettings(); 838 currentSettingsValues = mSettings.getCurrent(); 839 840 if (suggest != null && currentSettingsValues.mCorrectionEnabled) { 841 suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold); 842 } 843 844 switcher.loadKeyboard(editorInfo, currentSettingsValues); 845 } else if (restarting) { 846 // TODO: Come up with a more comprehensive way to reset the keyboard layout when 847 // a keyboard layout set doesn't get reloaded in this method. 848 switcher.resetKeyboardStateToAlphabet(); 849 // In apps like Talk, we come here when the text is sent and the field gets emptied and 850 // we need to re-evaluate the shift state, but not the whole layout which would be 851 // disruptive. 852 // Space state must be updated before calling updateShiftState 853 switcher.updateShiftState(); 854 } 855 setSuggestionStripShownInternal( 856 isSuggestionsStripVisible(), /* needsInputViewShown */ false); 857 858 mLastSelectionStart = editorInfo.initialSelStart; 859 mLastSelectionEnd = editorInfo.initialSelEnd; 860 861 mHandler.cancelUpdateSuggestionStrip(); 862 mHandler.cancelDoubleSpacePeriodTimer(); 863 864 mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable); 865 mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn, 866 currentSettingsValues.mKeyPreviewPopupDismissDelay); 867 mainKeyboardView.setSlidingKeyInputPreviewEnabled( 868 currentSettingsValues.mSlidingKeyInputPreviewEnabled); 869 mainKeyboardView.setGestureHandlingEnabledByUser( 870 currentSettingsValues.mGestureInputEnabled, 871 currentSettingsValues.mGestureTrailEnabled, 872 currentSettingsValues.mGestureFloatingPreviewTextEnabled); 873 874 // If we have a user dictionary addition in progress, we should check now if we should 875 // replace the previously committed string with the word that has actually been added 876 // to the user dictionary. 877 if (null != mPositionalInfoForUserDictPendingAddition 878 && mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( 879 mConnection, editorInfo, mLastSelectionEnd, currentLocale)) { 880 mPositionalInfoForUserDictPendingAddition = null; 881 } 882 // If tryReplaceWithActualWord returns false, we don't know what word was 883 // added to the user dictionary yet, so we keep the data and defer processing. The word will 884 // be replaced when the user dictionary reports back with the actual word, which ends 885 // up calling #onWordAddedToUserDictionary() in this class. 886 887 initPersonalizationDebugSettings(currentSettingsValues); 888 889 if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); 890 } 891 892 // Initialization of personalization debug settings. This must be called inside 893 // onStartInputView. 894 private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) { 895 if (mUseOnlyPersonalizationDictionaryForDebug 896 != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) { 897 // Only for debug 898 initSuggest(); 899 mUseOnlyPersonalizationDictionaryForDebug = 900 currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug; 901 } 902 903 if (mBoostPersonalizationDictionaryForDebug != 904 currentSettingsValues.mBoostPersonalizationDictionaryForDebug) { 905 // Only for debug 906 mBoostPersonalizationDictionaryForDebug = 907 currentSettingsValues.mBoostPersonalizationDictionaryForDebug; 908 if (mBoostPersonalizationDictionaryForDebug) { 909 UserHistoryForgettingCurveUtils.boostMaxFreqForDebug(); 910 } else { 911 UserHistoryForgettingCurveUtils.resetMaxFreqForDebug(); 912 } 913 } 914 } 915 916 // Callback for the TargetPackageInfoGetterTask 917 @Override 918 public void onTargetPackageInfoKnown(final PackageInfo info) { 919 mAppWorkAroundsUtils.setPackageInfo(info); 920 } 921 922 @Override 923 public void onWindowHidden() { 924 super.onWindowHidden(); 925 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 926 if (mainKeyboardView != null) { 927 mainKeyboardView.closing(); 928 } 929 } 930 931 private void onFinishInputInternal() { 932 super.onFinishInput(); 933 934 LatinImeLogger.commit(); 935 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 936 if (mainKeyboardView != null) { 937 mainKeyboardView.closing(); 938 } 939 } 940 941 private void onFinishInputViewInternal(final boolean finishingInput) { 942 super.onFinishInputView(finishingInput); 943 mKeyboardSwitcher.onFinishInputView(); 944 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 945 if (mainKeyboardView != null) { 946 mainKeyboardView.cancelAllOngoingEvents(); 947 mainKeyboardView.deallocateMemory(); 948 } 949 // Remove pending messages related to update suggestions 950 mHandler.cancelUpdateSuggestionStrip(); 951 // Should do the following in onFinishInputInternal but until JB MR2 it's not called :( 952 if (mWordComposer.isComposingWord()) mConnection.finishComposingText(); 953 resetComposingState(true /* alsoResetLastComposedWord */); 954 mRichImm.clearSubtypeCaches(); 955 // Notify ResearchLogger 956 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 957 ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart, 958 mLastSelectionEnd, getCurrentInputConnection()); 959 } 960 } 961 962 @Override 963 public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, 964 final int newSelStart, final int newSelEnd, 965 final int composingSpanStart, final int composingSpanEnd) { 966 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 967 composingSpanStart, composingSpanEnd); 968 if (DEBUG) { 969 Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart 970 + ", ose=" + oldSelEnd 971 + ", lss=" + mLastSelectionStart 972 + ", lse=" + mLastSelectionEnd 973 + ", nss=" + newSelStart 974 + ", nse=" + newSelEnd 975 + ", cs=" + composingSpanStart 976 + ", ce=" + composingSpanEnd); 977 } 978 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 979 final boolean expectingUpdateSelectionFromLogger = 980 ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection(); 981 ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd, 982 oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, 983 composingSpanEnd, mExpectingUpdateSelection, 984 expectingUpdateSelectionFromLogger, mConnection); 985 if (expectingUpdateSelectionFromLogger) { 986 // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work 987 return; 988 } 989 } 990 991 final boolean selectionChanged = mLastSelectionStart != newSelStart 992 || mLastSelectionEnd != newSelEnd; 993 994 // if composingSpanStart and composingSpanEnd are -1, it means there is no composing 995 // span in the view - we can use that to narrow down whether the cursor was moved 996 // by us or not. If we are composing a word but there is no composing span, then 997 // we know for sure the cursor moved while we were composing and we should reset 998 // the state. TODO: rescind this policy: the framework never removes the composing 999 // span on its own accord while editing. This test is useless. 1000 final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; 1001 1002 // If the keyboard is not visible, we don't need to do all the housekeeping work, as it 1003 // will be reset when the keyboard shows up anyway. 1004 // TODO: revisit this when LatinIME supports hardware keyboards. 1005 // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown(). 1006 // TODO: find a better way to simulate actual execution. 1007 if (isInputViewShown() && !mExpectingUpdateSelection 1008 && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) { 1009 // TAKE CARE: there is a race condition when we enter this test even when the user 1010 // did not explicitly move the cursor. This happens when typing fast, where two keys 1011 // turn this flag on in succession and both onUpdateSelection() calls arrive after 1012 // the second one - the first call successfully avoids this test, but the second one 1013 // enters. For the moment we rely on noComposingSpan to further reduce the impact. 1014 1015 // TODO: the following is probably better done in resetEntireInputState(). 1016 // it should only happen when the cursor moved, and the very purpose of the 1017 // test below is to narrow down whether this happened or not. Likewise with 1018 // the call to updateShiftState. 1019 // We set this to NONE because after a cursor move, we don't want the space 1020 // state-related special processing to kick in. 1021 mSpaceState = SPACE_STATE_NONE; 1022 1023 // TODO: is it still necessary to test for composingSpan related stuff? 1024 final boolean selectionChangedOrSafeToReset = selectionChanged 1025 || (!mWordComposer.isComposingWord()) || noComposingSpan; 1026 final boolean hasOrHadSelection = (oldSelStart != oldSelEnd 1027 || newSelStart != newSelEnd); 1028 final int moveAmount = newSelStart - oldSelStart; 1029 if (selectionChangedOrSafeToReset && (hasOrHadSelection 1030 || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { 1031 // If we are composing a word and moving the cursor, we would want to set a 1032 // suggestion span for recorrection to work correctly. Unfortunately, that 1033 // would involve the keyboard committing some new text, which would move the 1034 // cursor back to where it was. Latin IME could then fix the position of the cursor 1035 // again, but the asynchronous nature of the calls results in this wreaking havoc 1036 // with selection on double tap and the like. 1037 // Another option would be to send suggestions each time we set the composing 1038 // text, but that is probably too expensive to do, so we decided to leave things 1039 // as is. 1040 resetEntireInputState(newSelStart); 1041 } else { 1042 // resetEntireInputState calls resetCachesUponCursorMove, but with the second 1043 // argument as true. But in all cases where we don't reset the entire input state, 1044 // we still want to tell the rich input connection about the new cursor position so 1045 // that it can update its caches. 1046 mConnection.resetCachesUponCursorMove(newSelStart, 1047 false /* shouldFinishComposition */); 1048 } 1049 1050 // We moved the cursor. If we are touching a word, we need to resume suggestion, 1051 // unless suggestions are off. 1052 if (isSuggestionsStripVisible()) { 1053 mHandler.postResumeSuggestions(); 1054 } 1055 // Reset the last recapitalization. 1056 mRecapitalizeStatus.deactivate(); 1057 mKeyboardSwitcher.updateShiftState(); 1058 } 1059 mExpectingUpdateSelection = false; 1060 1061 // Make a note of the cursor position 1062 mLastSelectionStart = newSelStart; 1063 mLastSelectionEnd = newSelEnd; 1064 mSubtypeState.currentSubtypeUsed(); 1065 } 1066 1067 /** 1068 * This is called when the user has clicked on the extracted text view, 1069 * when running in fullscreen mode. The default implementation hides 1070 * the suggestions view when this happens, but only if the extracted text 1071 * editor has a vertical scroll bar because its text doesn't fit. 1072 * Here we override the behavior due to the possibility that a re-correction could 1073 * cause the suggestions strip to disappear and re-appear. 1074 */ 1075 @Override 1076 public void onExtractedTextClicked() { 1077 if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; 1078 1079 super.onExtractedTextClicked(); 1080 } 1081 1082 /** 1083 * This is called when the user has performed a cursor movement in the 1084 * extracted text view, when it is running in fullscreen mode. The default 1085 * implementation hides the suggestions view when a vertical movement 1086 * happens, but only if the extracted text editor has a vertical scroll bar 1087 * because its text doesn't fit. 1088 * Here we override the behavior due to the possibility that a re-correction could 1089 * cause the suggestions strip to disappear and re-appear. 1090 */ 1091 @Override 1092 public void onExtractedCursorMovement(final int dx, final int dy) { 1093 if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; 1094 1095 super.onExtractedCursorMovement(dx, dy); 1096 } 1097 1098 @Override 1099 public void hideWindow() { 1100 LatinImeLogger.commit(); 1101 mKeyboardSwitcher.onHideWindow(); 1102 1103 if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { 1104 AccessibleKeyboardViewProxy.getInstance().onHideWindow(); 1105 } 1106 1107 if (TRACE) Debug.stopMethodTracing(); 1108 if (mOptionsDialog != null && mOptionsDialog.isShowing()) { 1109 mOptionsDialog.dismiss(); 1110 mOptionsDialog = null; 1111 } 1112 super.hideWindow(); 1113 } 1114 1115 @Override 1116 public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) { 1117 if (DEBUG) { 1118 Log.i(TAG, "Received completions:"); 1119 if (applicationSpecifiedCompletions != null) { 1120 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { 1121 Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); 1122 } 1123 } 1124 } 1125 if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return; 1126 if (applicationSpecifiedCompletions == null) { 1127 clearSuggestionStrip(); 1128 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1129 ResearchLogger.latinIME_onDisplayCompletions(null); 1130 } 1131 return; 1132 } 1133 mApplicationSpecifiedCompletions = 1134 CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions); 1135 1136 final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = 1137 SuggestedWords.getFromApplicationSpecifiedCompletions( 1138 applicationSpecifiedCompletions); 1139 final SuggestedWords suggestedWords = new SuggestedWords( 1140 applicationSuggestedWords, 1141 false /* typedWordValid */, 1142 false /* hasAutoCorrectionCandidate */, 1143 false /* isPunctuationSuggestions */, 1144 false /* isObsoleteSuggestions */, 1145 false /* isPrediction */); 1146 // When in fullscreen mode, show completions generated by the application 1147 final boolean isAutoCorrection = false; 1148 setSuggestedWords(suggestedWords, isAutoCorrection); 1149 setAutoCorrectionIndicator(isAutoCorrection); 1150 setSuggestionStripShown(true); 1151 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1152 ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); 1153 } 1154 } 1155 1156 private void setSuggestionStripShownInternal(final boolean shown, 1157 final boolean needsInputViewShown) { 1158 // TODO: Modify this if we support suggestions with hard keyboard 1159 if (onEvaluateInputViewShown() && mSuggestionStripView != null) { 1160 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1161 final boolean inputViewShown = (mainKeyboardView != null) 1162 ? mainKeyboardView.isShown() : false; 1163 final boolean shouldShowSuggestions = shown 1164 && (needsInputViewShown ? inputViewShown : true); 1165 if (isFullscreenMode()) { 1166 mSuggestionStripView.setVisibility( 1167 shouldShowSuggestions ? View.VISIBLE : View.GONE); 1168 } else { 1169 mSuggestionStripView.setVisibility( 1170 shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); 1171 } 1172 if (shouldShowSuggestions && mainKeyboardView != null) { 1173 final int remainingHeight = getWindow().getWindow().getDecorView().getHeight() 1174 - mainKeyboardView.getHeight() - mSuggestionStripView.getHeight(); 1175 mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); 1176 } 1177 } 1178 } 1179 1180 private void setSuggestionStripShown(final boolean shown) { 1181 setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); 1182 } 1183 1184 @Override 1185 public void onComputeInsets(final InputMethodService.Insets outInsets) { 1186 super.onComputeInsets(outInsets); 1187 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1188 if (mainKeyboardView == null || mSuggestionStripView == null) { 1189 return; 1190 } 1191 // This method is never called when in fullscreen mode. 1192 // The contentTop is the top coordinate of the keyboard. The application behind will be 1193 // resized/panned above this coordibnate to be able to show an input field. 1194 final int contentTop = mInputView.getHeight() - mainKeyboardView.getHeight(); 1195 final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.VISIBLE) 1196 ? mSuggestionStripView.getHeight() : 0; 1197 // The visibleTop is the top coordinates of the visible part of this IME. The application 1198 // behind will never be resized, but may be panned or scrolled. 1199 final int visibleTop = mainKeyboardView.isShowingMoreKeysPanel() ? 0 1200 : contentTop - suggestionsHeight; 1201 outInsets.contentTopInsets = contentTop; 1202 outInsets.visibleTopInsets = visibleTop; 1203 // Need to set touchable region only if input view is being shown 1204 if (mainKeyboardView.isShown()) { 1205 final int touchLeft = 0; 1206 final int touchTop = visibleTop; 1207 final int touchRight = touchLeft + mainKeyboardView.getWidth(); 1208 final int touchBottom = contentTop + mainKeyboardView.getHeight() 1209 // Extend touchable region below the keyboard. 1210 + EXTENDED_TOUCHABLE_REGION_HEIGHT; 1211 // The touch event on touchableRegion will be delivered to this IME. 1212 outInsets.touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom); 1213 outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; 1214 } 1215 } 1216 1217 @Override 1218 public boolean onEvaluateFullscreenMode() { 1219 // Reread resource value here, because this method is called by framework anytime as needed. 1220 final boolean isFullscreenModeAllowed = 1221 Settings.readUseFullscreenMode(getResources()); 1222 if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) { 1223 // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI 1224 // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI 1225 // without NO_FULLSCREEN doesn't work as expected. Because of this we need this 1226 // hack for now. Let's get rid of this once the framework gets fixed. 1227 final EditorInfo ei = getCurrentInputEditorInfo(); 1228 return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0)); 1229 } else { 1230 return false; 1231 } 1232 } 1233 1234 @Override 1235 public void updateFullscreenMode() { 1236 super.updateFullscreenMode(); 1237 if (!isFullscreenMode()) { 1238 // Expand the input view to cover entire display to be able to show key previews and 1239 // more suggestions view that may be displayed above the keyboard. 1240 setInputViewMinHeight(getResources().getDisplayMetrics().heightPixels); 1241 } 1242 } 1243 1244 // This will reset the whole input state to the starting state. It will clear 1245 // the composing word, reset the last composed word, tell the inputconnection about it. 1246 private void resetEntireInputState(final int newCursorPosition) { 1247 final boolean shouldFinishComposition = mWordComposer.isComposingWord(); 1248 resetComposingState(true /* alsoResetLastComposedWord */); 1249 final SettingsValues settingsValues = mSettings.getCurrent(); 1250 if (settingsValues.mBigramPredictionEnabled) { 1251 clearSuggestionStrip(); 1252 } else { 1253 setSuggestedWords(settingsValues.mSuggestPuncList, false); 1254 } 1255 mConnection.resetCachesUponCursorMove(newCursorPosition, shouldFinishComposition); 1256 } 1257 1258 private void resetComposingState(final boolean alsoResetLastComposedWord) { 1259 mWordComposer.reset(); 1260 if (alsoResetLastComposedWord) 1261 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 1262 } 1263 1264 private void commitTyped(final String separatorString) { 1265 if (!mWordComposer.isComposingWord()) return; 1266 final String typedWord = mWordComposer.getTypedWord(); 1267 if (typedWord.length() > 0) { 1268 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1269 ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); 1270 } 1271 commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, 1272 separatorString); 1273 } 1274 } 1275 1276 // Called from the KeyboardSwitcher which needs to know auto caps state to display 1277 // the right layout. 1278 public int getCurrentAutoCapsState() { 1279 if (!mSettings.getCurrent().mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; 1280 1281 final EditorInfo ei = getCurrentInputEditorInfo(); 1282 if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; 1283 final int inputType = ei.inputType; 1284 // Warning: this depends on mSpaceState, which may not be the most current value. If 1285 // mSpaceState gets updated later, whoever called this may need to be told about it. 1286 return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(), 1287 SPACE_STATE_PHANTOM == mSpaceState); 1288 } 1289 1290 public int getCurrentRecapitalizeState() { 1291 if (!mRecapitalizeStatus.isActive() 1292 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { 1293 // Not recapitalizing at the moment 1294 return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; 1295 } 1296 return mRecapitalizeStatus.getCurrentMode(); 1297 } 1298 1299 // Factor in auto-caps and manual caps and compute the current caps mode. 1300 private int getActualCapsMode() { 1301 final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode(); 1302 if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode; 1303 final int auto = getCurrentAutoCapsState(); 1304 if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { 1305 return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; 1306 } 1307 if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED; 1308 return WordComposer.CAPS_MODE_OFF; 1309 } 1310 1311 private void swapSwapperAndSpace() { 1312 final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); 1313 // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. 1314 if (lastTwo != null && lastTwo.length() == 2 1315 && lastTwo.charAt(0) == Constants.CODE_SPACE) { 1316 mConnection.deleteSurroundingText(2, 0); 1317 final String text = lastTwo.charAt(1) + " "; 1318 mConnection.commitText(text, 1); 1319 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1320 ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text); 1321 } 1322 mKeyboardSwitcher.updateShiftState(); 1323 } 1324 } 1325 1326 private boolean maybeDoubleSpacePeriod() { 1327 final SettingsValues settingsValues = mSettings.getCurrent(); 1328 if (!settingsValues.mCorrectionEnabled) return false; 1329 if (!settingsValues.mUseDoubleSpacePeriod) return false; 1330 if (!mHandler.isAcceptingDoubleSpacePeriod()) return false; 1331 final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0); 1332 if (lastThree != null && lastThree.length() == 3 1333 && canBeFollowedByDoubleSpacePeriod(lastThree.charAt(0)) 1334 && lastThree.charAt(1) == Constants.CODE_SPACE 1335 && lastThree.charAt(2) == Constants.CODE_SPACE) { 1336 mHandler.cancelDoubleSpacePeriodTimer(); 1337 mConnection.deleteSurroundingText(2, 0); 1338 final String textToInsert = ". "; 1339 mConnection.commitText(textToInsert, 1); 1340 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1341 ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert, 1342 false /* isBatchMode */); 1343 } 1344 mKeyboardSwitcher.updateShiftState(); 1345 return true; 1346 } 1347 return false; 1348 } 1349 1350 private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { 1351 // TODO: Check again whether there really ain't a better way to check this. 1352 // TODO: This should probably be language-dependant... 1353 return Character.isLetterOrDigit(codePoint) 1354 || codePoint == Constants.CODE_SINGLE_QUOTE 1355 || codePoint == Constants.CODE_DOUBLE_QUOTE 1356 || codePoint == Constants.CODE_CLOSING_PARENTHESIS 1357 || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET 1358 || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET 1359 || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET; 1360 } 1361 1362 // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is 1363 // pressed. 1364 @Override 1365 public void addWordToUserDictionary(final String word) { 1366 if (TextUtils.isEmpty(word)) { 1367 // Probably never supposed to happen, but just in case. 1368 mPositionalInfoForUserDictPendingAddition = null; 1369 return; 1370 } 1371 final String wordToEdit; 1372 if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) { 1373 wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); 1374 } else { 1375 wordToEdit = word; 1376 } 1377 mUserDictionary.addWordToUserDictionary(wordToEdit); 1378 } 1379 1380 public void onWordAddedToUserDictionary(final String newSpelling) { 1381 // If word was added but not by us, bail out 1382 if (null == mPositionalInfoForUserDictPendingAddition) return; 1383 if (mWordComposer.isComposingWord()) { 1384 // We are late... give up and return 1385 mPositionalInfoForUserDictPendingAddition = null; 1386 return; 1387 } 1388 mPositionalInfoForUserDictPendingAddition.setActualWordBeingAdded(newSpelling); 1389 if (mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( 1390 mConnection, getCurrentInputEditorInfo(), mLastSelectionEnd, 1391 mSubtypeSwitcher.getCurrentSubtypeLocale())) { 1392 mPositionalInfoForUserDictPendingAddition = null; 1393 } 1394 } 1395 1396 private void onSettingsKeyPressed() { 1397 if (isShowingOptionDialog()) return; 1398 showSubtypeSelectorAndSettings(); 1399 } 1400 1401 @Override 1402 public boolean onCustomRequest(final int requestCode) { 1403 if (isShowingOptionDialog()) return false; 1404 switch (requestCode) { 1405 case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER: 1406 if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { 1407 mRichImm.getInputMethodManager().showInputMethodPicker(); 1408 return true; 1409 } 1410 return false; 1411 } 1412 return false; 1413 } 1414 1415 private boolean isShowingOptionDialog() { 1416 return mOptionsDialog != null && mOptionsDialog.isShowing(); 1417 } 1418 1419 private void performEditorAction(final int actionId) { 1420 mConnection.performEditorAction(actionId); 1421 } 1422 1423 // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. 1424 private void handleLanguageSwitchKey() { 1425 final IBinder token = getWindow().getWindow().getAttributes().token; 1426 if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) { 1427 mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); 1428 return; 1429 } 1430 mSubtypeState.switchSubtype(token, mRichImm); 1431 } 1432 1433 private void sendDownUpKeyEventForBackwardCompatibility(final int code) { 1434 final long eventTime = SystemClock.uptimeMillis(); 1435 mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, 1436 KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1437 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1438 mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, 1439 KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1440 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1441 } 1442 1443 private void sendKeyCodePoint(final int code) { 1444 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1445 ResearchLogger.latinIME_sendKeyCodePoint(code); 1446 } 1447 // TODO: Remove this special handling of digit letters. 1448 // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. 1449 if (code >= '0' && code <= '9') { 1450 sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0); 1451 return; 1452 } 1453 1454 if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) { 1455 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate 1456 // a hardware keyboard event on pressing enter or delete. This is bad for many 1457 // reasons (there are race conditions with commits) but some applications are 1458 // relying on this behavior so we continue to support it for older apps. 1459 sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER); 1460 } else { 1461 final String text = new String(new int[] { code }, 0, 1); 1462 mConnection.commitText(text, text.length()); 1463 } 1464 } 1465 1466 // Implementation of {@link KeyboardActionListener}. 1467 @Override 1468 public void onCodeInput(final int primaryCode, final int x, final int y) { 1469 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1470 ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); 1471 } 1472 final long when = SystemClock.uptimeMillis(); 1473 if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { 1474 mDeleteCount = 0; 1475 } 1476 mLastKeyTime = when; 1477 mConnection.beginBatchEdit(); 1478 final KeyboardSwitcher switcher = mKeyboardSwitcher; 1479 // The space state depends only on the last character pressed and its own previous 1480 // state. Here, we revert the space state to neutral if the key is actually modifying 1481 // the input contents (any non-shift key), which is what we should do for 1482 // all inputs that do not result in a special state. Each character handling is then 1483 // free to override the state as they see fit. 1484 final int spaceState = mSpaceState; 1485 if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false; 1486 1487 // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. 1488 if (primaryCode != Constants.CODE_SPACE) { 1489 mHandler.cancelDoubleSpacePeriodTimer(); 1490 } 1491 1492 boolean didAutoCorrect = false; 1493 switch (primaryCode) { 1494 case Constants.CODE_DELETE: 1495 mSpaceState = SPACE_STATE_NONE; 1496 handleBackspace(spaceState); 1497 mDeleteCount++; 1498 mExpectingUpdateSelection = true; 1499 LatinImeLogger.logOnDelete(x, y); 1500 break; 1501 case Constants.CODE_SHIFT: 1502 // Note: Calling back to the keyboard on Shift key is handled in 1503 // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. 1504 final Keyboard currentKeyboard = switcher.getKeyboard(); 1505 if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { 1506 // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for 1507 // alphabetic shift and shift while in symbol layout. 1508 handleRecapitalize(); 1509 } 1510 break; 1511 case Constants.CODE_CAPSLOCK: 1512 // Note: Changing keyboard to shift lock state is handled in 1513 // {@link KeyboardSwitcher#onCodeInput(int)}. 1514 break; 1515 case Constants.CODE_SWITCH_ALPHA_SYMBOL: 1516 // Note: Calling back to the keyboard on symbol key is handled in 1517 // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. 1518 break; 1519 case Constants.CODE_SETTINGS: 1520 onSettingsKeyPressed(); 1521 break; 1522 case Constants.CODE_SHORTCUT: 1523 mSubtypeSwitcher.switchToShortcutIME(this); 1524 break; 1525 case Constants.CODE_ACTION_NEXT: 1526 performEditorAction(EditorInfo.IME_ACTION_NEXT); 1527 break; 1528 case Constants.CODE_ACTION_PREVIOUS: 1529 performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 1530 break; 1531 case Constants.CODE_LANGUAGE_SWITCH: 1532 handleLanguageSwitchKey(); 1533 break; 1534 case Constants.CODE_EMOJI: 1535 // TODO: Implement emoji keyboard switch. 1536 break; 1537 case Constants.CODE_ENTER: 1538 final EditorInfo editorInfo = getCurrentInputEditorInfo(); 1539 final int imeOptionsActionId = 1540 InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); 1541 if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { 1542 // Either we have an actionLabel and we should performEditorAction with actionId 1543 // regardless of its value. 1544 performEditorAction(editorInfo.actionId); 1545 } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { 1546 // We didn't have an actionLabel, but we had another action to execute. 1547 // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, 1548 // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it 1549 // means there should be an action and the app didn't bother to set a specific 1550 // code for it - presumably it only handles one. It does not have to be treated 1551 // in any specific way: anything that is not IME_ACTION_NONE should be sent to 1552 // performEditorAction. 1553 performEditorAction(imeOptionsActionId); 1554 } else { 1555 // No action label, and the action from imeOptions is NONE: this is a regular 1556 // enter key that should input a carriage return. 1557 didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); 1558 } 1559 break; 1560 case Constants.CODE_SHIFT_ENTER: 1561 didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); 1562 break; 1563 default: 1564 didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState); 1565 break; 1566 } 1567 switcher.onCodeInput(primaryCode); 1568 // Reset after any single keystroke, except shift, capslock, and symbol-shift 1569 if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT 1570 && primaryCode != Constants.CODE_CAPSLOCK 1571 && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) 1572 mLastComposedWord.deactivate(); 1573 if (Constants.CODE_DELETE != primaryCode) { 1574 mEnteredText = null; 1575 } 1576 mConnection.endBatchEdit(); 1577 } 1578 1579 private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y, 1580 final int spaceState) { 1581 mSpaceState = SPACE_STATE_NONE; 1582 final boolean didAutoCorrect; 1583 final SettingsValues settingsValues = mSettings.getCurrent(); 1584 if (settingsValues.isWordSeparator(primaryCode)) { 1585 didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); 1586 } else { 1587 didAutoCorrect = false; 1588 if (SPACE_STATE_PHANTOM == spaceState) { 1589 if (settingsValues.mIsInternal) { 1590 if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { 1591 LatinImeLoggerUtils.onAutoCorrection( 1592 "", mWordComposer.getTypedWord(), " ", mWordComposer); 1593 } 1594 } 1595 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 1596 // If we are in the middle of a recorrection, we need to commit the recorrection 1597 // first so that we can insert the character at the current cursor position. 1598 resetEntireInputState(mLastSelectionStart); 1599 } else { 1600 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 1601 } 1602 } 1603 final int keyX, keyY; 1604 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1605 if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { 1606 keyX = x; 1607 keyY = y; 1608 } else { 1609 keyX = Constants.NOT_A_COORDINATE; 1610 keyY = Constants.NOT_A_COORDINATE; 1611 } 1612 handleCharacter(primaryCode, keyX, keyY, spaceState); 1613 } 1614 mExpectingUpdateSelection = true; 1615 return didAutoCorrect; 1616 } 1617 1618 // Called from PointerTracker through the KeyboardActionListener interface 1619 @Override 1620 public void onTextInput(final String rawText) { 1621 mConnection.beginBatchEdit(); 1622 if (mWordComposer.isComposingWord()) { 1623 commitCurrentAutoCorrection(rawText); 1624 } else { 1625 resetComposingState(true /* alsoResetLastComposedWord */); 1626 } 1627 mHandler.postUpdateSuggestionStrip(); 1628 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS 1629 && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) { 1630 ResearchLogger.getInstance().onResearchKeySelected(this); 1631 return; 1632 } 1633 final String text = specificTldProcessingOnTextInput(rawText); 1634 if (SPACE_STATE_PHANTOM == mSpaceState) { 1635 promotePhantomSpace(); 1636 } 1637 mConnection.commitText(text, 1); 1638 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1639 ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */); 1640 } 1641 mConnection.endBatchEdit(); 1642 // Space state must be updated before calling updateShiftState 1643 mSpaceState = SPACE_STATE_NONE; 1644 mKeyboardSwitcher.updateShiftState(); 1645 mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT); 1646 mEnteredText = text; 1647 } 1648 1649 @Override 1650 public void onStartBatchInput() { 1651 BatchInputUpdater.getInstance().onStartBatchInput(this); 1652 mHandler.cancelUpdateSuggestionStrip(); 1653 mConnection.beginBatchEdit(); 1654 final SettingsValues settingsValues = mSettings.getCurrent(); 1655 if (mWordComposer.isComposingWord()) { 1656 if (settingsValues.mIsInternal) { 1657 if (mWordComposer.isBatchMode()) { 1658 LatinImeLoggerUtils.onAutoCorrection( 1659 "", mWordComposer.getTypedWord(), " ", mWordComposer); 1660 } 1661 } 1662 final int wordComposerSize = mWordComposer.size(); 1663 // Since isComposingWord() is true, the size is at least 1. 1664 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 1665 // If we are in the middle of a recorrection, we need to commit the recorrection 1666 // first so that we can insert the batch input at the current cursor position. 1667 resetEntireInputState(mLastSelectionStart); 1668 } else if (wordComposerSize <= 1) { 1669 // We auto-correct the previous (typed, not gestured) string iff it's one character 1670 // long. The reason for this is, even in the middle of gesture typing, you'll still 1671 // tap one-letter words and you want them auto-corrected (typically, "i" in English 1672 // should become "I"). However for any longer word, we assume that the reason for 1673 // tapping probably is that the word you intend to type is not in the dictionary, 1674 // so we do not attempt to correct, on the assumption that if that was a dictionary 1675 // word, the user would probably have gestured instead. 1676 commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); 1677 } else { 1678 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 1679 } 1680 mExpectingUpdateSelection = true; 1681 } 1682 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 1683 if (Character.isLetterOrDigit(codePointBeforeCursor) 1684 || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) { 1685 mSpaceState = SPACE_STATE_PHANTOM; 1686 } 1687 mConnection.endBatchEdit(); 1688 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); 1689 } 1690 1691 private static final class BatchInputUpdater implements Handler.Callback { 1692 private final Handler mHandler; 1693 private LatinIME mLatinIme; 1694 private final Object mLock = new Object(); 1695 private boolean mInBatchInput; // synchronized using {@link #mLock}. 1696 1697 private BatchInputUpdater() { 1698 final HandlerThread handlerThread = new HandlerThread( 1699 BatchInputUpdater.class.getSimpleName()); 1700 handlerThread.start(); 1701 mHandler = new Handler(handlerThread.getLooper(), this); 1702 } 1703 1704 // Initialization-on-demand holder 1705 private static final class OnDemandInitializationHolder { 1706 public static final BatchInputUpdater sInstance = new BatchInputUpdater(); 1707 } 1708 1709 public static BatchInputUpdater getInstance() { 1710 return OnDemandInitializationHolder.sInstance; 1711 } 1712 1713 private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; 1714 1715 @Override 1716 public boolean handleMessage(final Message msg) { 1717 switch (msg.what) { 1718 case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: 1719 updateBatchInput((InputPointers)msg.obj); 1720 break; 1721 } 1722 return true; 1723 } 1724 1725 // Run in the UI thread. 1726 public void onStartBatchInput(final LatinIME latinIme) { 1727 synchronized (mLock) { 1728 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 1729 mInBatchInput = true; 1730 mLatinIme = latinIme; 1731 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1732 SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); 1733 } 1734 } 1735 1736 // Run in the Handler thread. 1737 private void updateBatchInput(final InputPointers batchPointers) { 1738 synchronized (mLock) { 1739 if (!mInBatchInput) { 1740 // Batch input has ended or canceled while the message was being delivered. 1741 return; 1742 } 1743 final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); 1744 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1745 suggestedWords, false /* dismissGestureFloatingPreviewText */); 1746 } 1747 } 1748 1749 // Run in the UI thread. 1750 public void onUpdateBatchInput(final InputPointers batchPointers) { 1751 if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) { 1752 return; 1753 } 1754 mHandler.obtainMessage( 1755 MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers) 1756 .sendToTarget(); 1757 } 1758 1759 public void onCancelBatchInput() { 1760 synchronized (mLock) { 1761 mInBatchInput = false; 1762 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1763 SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); 1764 } 1765 } 1766 1767 // Run in the UI thread. 1768 public SuggestedWords onEndBatchInput(final InputPointers batchPointers) { 1769 synchronized (mLock) { 1770 mInBatchInput = false; 1771 final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); 1772 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1773 suggestedWords, true /* dismissGestureFloatingPreviewText */); 1774 return suggestedWords; 1775 } 1776 } 1777 1778 // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to 1779 // be synchronized. 1780 private SuggestedWords getSuggestedWordsGestureLocked(final InputPointers batchPointers) { 1781 mLatinIme.mWordComposer.setBatchInputPointers(batchPointers); 1782 final SuggestedWords suggestedWords = 1783 mLatinIme.getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_GESTURE); 1784 final int suggestionCount = suggestedWords.size(); 1785 if (suggestionCount <= 1) { 1786 final String mostProbableSuggestion = (suggestionCount == 0) ? null 1787 : suggestedWords.getWord(0); 1788 return mLatinIme.getOlderSuggestions(mostProbableSuggestion); 1789 } 1790 return suggestedWords; 1791 } 1792 } 1793 1794 private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, 1795 final boolean dismissGestureFloatingPreviewText) { 1796 showSuggestionStrip(suggestedWords, null); 1797 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1798 mainKeyboardView.showGestureFloatingPreviewText(suggestedWords); 1799 if (dismissGestureFloatingPreviewText) { 1800 mainKeyboardView.dismissGestureFloatingPreviewText(); 1801 } 1802 } 1803 1804 @Override 1805 public void onUpdateBatchInput(final InputPointers batchPointers) { 1806 final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); 1807 if (null != candidate) { 1808 if (candidate.mSourceDict.shouldAutoCommit(candidate)) { 1809 // TODO: implement auto-commit 1810 } 1811 } 1812 BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers); 1813 } 1814 1815 @Override 1816 public void onEndBatchInput(final InputPointers batchPointers) { 1817 final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput( 1818 batchPointers); 1819 final String batchInputText = suggestedWords.isEmpty() 1820 ? null : suggestedWords.getWord(0); 1821 if (TextUtils.isEmpty(batchInputText)) { 1822 return; 1823 } 1824 mWordComposer.setBatchInputWord(batchInputText); 1825 mConnection.beginBatchEdit(); 1826 if (SPACE_STATE_PHANTOM == mSpaceState) { 1827 promotePhantomSpace(); 1828 } 1829 mConnection.setComposingText(batchInputText, 1); 1830 mExpectingUpdateSelection = true; 1831 mConnection.endBatchEdit(); 1832 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1833 ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); 1834 } 1835 // Space state must be updated before calling updateShiftState 1836 mSpaceState = SPACE_STATE_PHANTOM; 1837 mKeyboardSwitcher.updateShiftState(); 1838 } 1839 1840 private String specificTldProcessingOnTextInput(final String text) { 1841 if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD 1842 || !Character.isLetter(text.charAt(1))) { 1843 // Not a tld: do nothing. 1844 return text; 1845 } 1846 // We have a TLD (or something that looks like this): make sure we don't add 1847 // a space even if currently in phantom mode. 1848 mSpaceState = SPACE_STATE_NONE; 1849 // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code 1850 final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0); 1851 if (lastOne != null && lastOne.length() == 1 1852 && lastOne.charAt(0) == Constants.CODE_PERIOD) { 1853 return text.substring(1); 1854 } else { 1855 return text; 1856 } 1857 } 1858 1859 // Called from PointerTracker through the KeyboardActionListener interface 1860 @Override 1861 public void onFinishSlidingInput() { 1862 // User finished sliding input. 1863 mKeyboardSwitcher.onFinishSlidingInput(); 1864 } 1865 1866 // Called from PointerTracker through the KeyboardActionListener interface 1867 @Override 1868 public void onCancelInput() { 1869 // User released a finger outside any key 1870 // Nothing to do so far. 1871 } 1872 1873 @Override 1874 public void onCancelBatchInput() { 1875 BatchInputUpdater.getInstance().onCancelBatchInput(); 1876 } 1877 1878 private void handleBackspace(final int spaceState) { 1879 // In many cases, we may have to put the keyboard in auto-shift state again. However 1880 // we want to wait a few milliseconds before doing it to avoid the keyboard flashing 1881 // during key repeat. 1882 mHandler.postUpdateShiftState(); 1883 1884 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 1885 // If we are in the middle of a recorrection, we need to commit the recorrection 1886 // first so that we can remove the character at the current cursor position. 1887 resetEntireInputState(mLastSelectionStart); 1888 // When we exit this if-clause, mWordComposer.isComposingWord() will return false. 1889 } 1890 if (mWordComposer.isComposingWord()) { 1891 if (mWordComposer.isBatchMode()) { 1892 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1893 final String word = mWordComposer.getTypedWord(); 1894 ResearchLogger.latinIME_handleBackspace_batch(word, 1); 1895 } 1896 final String rejectedSuggestion = mWordComposer.getTypedWord(); 1897 mWordComposer.reset(); 1898 mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); 1899 } else { 1900 mWordComposer.deleteLast(); 1901 } 1902 mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1903 mHandler.postUpdateSuggestionStrip(); 1904 if (!mWordComposer.isComposingWord()) { 1905 // If we just removed the last character, auto-caps mode may have changed so we 1906 // need to re-evaluate. 1907 mKeyboardSwitcher.updateShiftState(); 1908 } 1909 } else { 1910 final SettingsValues currentSettings = mSettings.getCurrent(); 1911 if (mLastComposedWord.canRevertCommit()) { 1912 if (currentSettings.mIsInternal) { 1913 LatinImeLoggerUtils.onAutoCorrectionCancellation(); 1914 } 1915 revertCommit(); 1916 return; 1917 } 1918 if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { 1919 // Cancel multi-character input: remove the text we just entered. 1920 // This is triggered on backspace after a key that inputs multiple characters, 1921 // like the smiley key or the .com key. 1922 mConnection.deleteSurroundingText(mEnteredText.length(), 0); 1923 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1924 ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText); 1925 } 1926 mEnteredText = null; 1927 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. 1928 // In addition we know that spaceState is false, and that we should not be 1929 // reverting any autocorrect at this point. So we can safely return. 1930 return; 1931 } 1932 if (SPACE_STATE_DOUBLE == spaceState) { 1933 mHandler.cancelDoubleSpacePeriodTimer(); 1934 if (mConnection.revertDoubleSpacePeriod()) { 1935 // No need to reset mSpaceState, it has already be done (that's why we 1936 // receive it as a parameter) 1937 return; 1938 } 1939 } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1940 if (mConnection.revertSwapPunctuation()) { 1941 // Likewise 1942 return; 1943 } 1944 } 1945 1946 // No cancelling of commit/double space/swap: we have a regular backspace. 1947 // We should backspace one char and restart suggestion if at the end of a word. 1948 if (mLastSelectionStart != mLastSelectionEnd) { 1949 // If there is a selection, remove it. 1950 final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; 1951 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); 1952 // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to 1953 // happen, and if it's wrong, the next call to onUpdateSelection will correct it, 1954 // but we want to set it right away to avoid it being used with the wrong values 1955 // later (typically, in a subsequent press on backspace). 1956 mLastSelectionEnd = mLastSelectionStart; 1957 mConnection.deleteSurroundingText(numCharsDeleted, 0); 1958 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1959 ResearchLogger.latinIME_handleBackspace(numCharsDeleted, 1960 false /* shouldUncommitLogUnit */); 1961 } 1962 } else { 1963 // There is no selection, just delete one character. 1964 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { 1965 // This should never happen. 1966 Log.e(TAG, "Backspace when we don't know the selection position"); 1967 } 1968 final int lengthToDelete = Character.isSupplementaryCodePoint( 1969 mConnection.getCodePointBeforeCursor()) ? 2 : 1; 1970 if (mAppWorkAroundsUtils.isBeforeJellyBean()) { 1971 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate 1972 // a hardware keyboard event on pressing enter or delete. This is bad for many 1973 // reasons (there are race conditions with commits) but some applications are 1974 // relying on this behavior so we continue to support it for older apps. 1975 sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL); 1976 } else { 1977 mConnection.deleteSurroundingText(lengthToDelete, 0); 1978 } 1979 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1980 ResearchLogger.latinIME_handleBackspace(lengthToDelete, 1981 true /* shouldUncommitLogUnit */); 1982 } 1983 if (mDeleteCount > DELETE_ACCELERATE_AT) { 1984 final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( 1985 mConnection.getCodePointBeforeCursor()) ? 2 : 1; 1986 mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); 1987 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1988 ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain, 1989 true /* shouldUncommitLogUnit */); 1990 } 1991 } 1992 } 1993 if (currentSettings.isSuggestionsRequested(mDisplayOrientation) 1994 && currentSettings.mCurrentLanguageHasSpaces) { 1995 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(); 1996 } 1997 // We just removed a character. We need to update the auto-caps state. 1998 mKeyboardSwitcher.updateShiftState(); 1999 } 2000 } 2001 2002 /* 2003 * Strip a trailing space if necessary and returns whether it's a swap weak space situation. 2004 */ 2005 private boolean maybeStripSpace(final int code, 2006 final int spaceState, final boolean isFromSuggestionStrip) { 2007 if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 2008 mConnection.removeTrailingSpace(); 2009 return false; 2010 } 2011 if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState) 2012 && isFromSuggestionStrip) { 2013 final SettingsValues currentSettings = mSettings.getCurrent(); 2014 if (currentSettings.isUsuallyPrecededBySpace(code)) return false; 2015 if (currentSettings.isUsuallyFollowedBySpace(code)) return true; 2016 mConnection.removeTrailingSpace(); 2017 } 2018 return false; 2019 } 2020 2021 private void handleCharacter(final int primaryCode, final int x, 2022 final int y, final int spaceState) { 2023 // TODO: refactor this method to stop flipping isComposingWord around all the time, and 2024 // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter 2025 // which has the same name as other handle* methods but is not the same. 2026 boolean isComposingWord = mWordComposer.isComposingWord(); 2027 2028 // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. 2029 // See onStartBatchInput() to see how to do it. 2030 final SettingsValues currentSettings = mSettings.getCurrent(); 2031 if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) { 2032 if (isComposingWord) { 2033 // Sanity check 2034 throw new RuntimeException("Should not be composing here"); 2035 } 2036 promotePhantomSpace(); 2037 } 2038 2039 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 2040 // If we are in the middle of a recorrection, we need to commit the recorrection 2041 // first so that we can insert the character at the current cursor position. 2042 resetEntireInputState(mLastSelectionStart); 2043 isComposingWord = false; 2044 } 2045 // We want to find out whether to start composing a new word with this character. If so, 2046 // we need to reset the composing state and switch isComposingWord. The order of the 2047 // tests is important for good performance. 2048 // We only start composing if we're not already composing. 2049 if (!isComposingWord 2050 // We only start composing if this is a word code point. Essentially that means it's a 2051 // a letter or a word connector. 2052 && currentSettings.isWordCodePoint(primaryCode) 2053 // We never go into composing state if suggestions are not requested. 2054 && currentSettings.isSuggestionsRequested(mDisplayOrientation) && 2055 // In languages with spaces, we only start composing a word when we are not already 2056 // touching a word. In languages without spaces, the above conditions are sufficient. 2057 (!mConnection.isCursorTouchingWord(currentSettings) 2058 || !currentSettings.mCurrentLanguageHasSpaces)) { 2059 // Reset entirely the composing state anyway, then start composing a new word unless 2060 // the character is a single quote or a dash. The idea here is, single quote and dash 2061 // are not separators and they should be treated as normal characters, except in the 2062 // first position where they should not start composing a word. 2063 isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode 2064 && Constants.CODE_DASH != primaryCode); 2065 // Here we don't need to reset the last composed word. It will be reset 2066 // when we commit this one, if we ever do; if on the other hand we backspace 2067 // it entirely and resume suggestions on the previous word, we'd like to still 2068 // have touch coordinates for it. 2069 resetComposingState(false /* alsoResetLastComposedWord */); 2070 } 2071 if (isComposingWord) { 2072 final int keyX, keyY; 2073 if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) { 2074 final KeyDetector keyDetector = 2075 mKeyboardSwitcher.getMainKeyboardView().getKeyDetector(); 2076 keyX = keyDetector.getTouchX(x); 2077 keyY = keyDetector.getTouchY(y); 2078 } else { 2079 keyX = x; 2080 keyY = y; 2081 } 2082 mWordComposer.add(primaryCode, keyX, keyY); 2083 // If it's the first letter, make note of auto-caps state 2084 if (mWordComposer.size() == 1) { 2085 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); 2086 } 2087 mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 2088 } else { 2089 final boolean swapWeakSpace = maybeStripSpace(primaryCode, 2090 spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x); 2091 2092 sendKeyCodePoint(primaryCode); 2093 2094 if (swapWeakSpace) { 2095 swapSwapperAndSpace(); 2096 mSpaceState = SPACE_STATE_WEAK; 2097 } 2098 // In case the "add to dictionary" hint was still displayed. 2099 if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint(); 2100 } 2101 mHandler.postUpdateSuggestionStrip(); 2102 if (currentSettings.mIsInternal) { 2103 LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y); 2104 } 2105 } 2106 2107 private void handleRecapitalize() { 2108 if (mLastSelectionStart == mLastSelectionEnd) return; // No selection 2109 // If we have a recapitalize in progress, use it; otherwise, create a new one. 2110 if (!mRecapitalizeStatus.isActive() 2111 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { 2112 final CharSequence selectedText = 2113 mConnection.getSelectedText(0 /* flags, 0 for no styles */); 2114 if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection 2115 final SettingsValues currentSettings = mSettings.getCurrent(); 2116 mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd, 2117 selectedText.toString(), currentSettings.mLocale, 2118 currentSettings.mWordSeparators); 2119 // We trim leading and trailing whitespace. 2120 mRecapitalizeStatus.trim(); 2121 // Trimming the object may have changed the length of the string, and we need to 2122 // reposition the selection handles accordingly. As this result in an IPC call, 2123 // only do it if it's actually necessary, in other words if the recapitalize status 2124 // is not set at the same place as before. 2125 if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { 2126 mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); 2127 mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); 2128 mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); 2129 } 2130 } 2131 mRecapitalizeStatus.rotate(); 2132 final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; 2133 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); 2134 mConnection.deleteSurroundingText(numCharsDeleted, 0); 2135 mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); 2136 mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); 2137 mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); 2138 mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); 2139 // Match the keyboard to the new state. 2140 mKeyboardSwitcher.updateShiftState(); 2141 } 2142 2143 // Returns true if we did an autocorrection, false otherwise. 2144 private boolean handleSeparator(final int primaryCode, final int x, final int y, 2145 final int spaceState) { 2146 boolean didAutoCorrect = false; 2147 final SettingsValues currentSettings = mSettings.getCurrent(); 2148 // We avoid sending spaces in languages without spaces if we were composing. 2149 final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode 2150 && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord(); 2151 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 2152 // If we are in the middle of a recorrection, we need to commit the recorrection 2153 // first so that we can insert the separator at the current cursor position. 2154 resetEntireInputState(mLastSelectionStart); 2155 } 2156 if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing 2157 if (currentSettings.mCorrectionEnabled) { 2158 final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR 2159 : new String(new int[] { primaryCode }, 0, 1); 2160 commitCurrentAutoCorrection(separator); 2161 didAutoCorrect = true; 2162 } else { 2163 commitTyped(new String(new int[]{primaryCode}, 0, 1)); 2164 } 2165 } 2166 2167 final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, 2168 Constants.SUGGESTION_STRIP_COORDINATE == x); 2169 2170 if (SPACE_STATE_PHANTOM == spaceState && 2171 currentSettings.isUsuallyPrecededBySpace(primaryCode)) { 2172 promotePhantomSpace(); 2173 } 2174 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2175 ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); 2176 } 2177 2178 if (!shouldAvoidSendingCode) { 2179 sendKeyCodePoint(primaryCode); 2180 } 2181 2182 if (Constants.CODE_SPACE == primaryCode) { 2183 if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) { 2184 if (maybeDoubleSpacePeriod()) { 2185 mSpaceState = SPACE_STATE_DOUBLE; 2186 } else if (!isShowingPunctuationList()) { 2187 mSpaceState = SPACE_STATE_WEAK; 2188 } 2189 } 2190 2191 mHandler.startDoubleSpacePeriodTimer(); 2192 mHandler.postUpdateSuggestionStrip(); 2193 } else { 2194 if (swapWeakSpace) { 2195 swapSwapperAndSpace(); 2196 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; 2197 } else if (SPACE_STATE_PHANTOM == spaceState 2198 && currentSettings.isUsuallyFollowedBySpace(primaryCode)) { 2199 // If we are in phantom space state, and the user presses a separator, we want to 2200 // stay in phantom space state so that the next keypress has a chance to add the 2201 // space. For example, if I type "Good dat", pick "day" from the suggestion strip 2202 // then insert a comma and go on to typing the next word, I want the space to be 2203 // inserted automatically before the next word, the same way it is when I don't 2204 // input the comma. 2205 // The case is a little different if the separator is a space stripper. Such a 2206 // separator does not normally need a space on the right (that's the difference 2207 // between swappers and strippers), so we should not stay in phantom space state if 2208 // the separator is a stripper. Hence the additional test above. 2209 mSpaceState = SPACE_STATE_PHANTOM; 2210 } 2211 2212 // Set punctuation right away. onUpdateSelection will fire but tests whether it is 2213 // already displayed or not, so it's okay. 2214 setPunctuationSuggestions(); 2215 } 2216 if (currentSettings.mIsInternal) { 2217 LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y); 2218 } 2219 2220 mKeyboardSwitcher.updateShiftState(); 2221 return didAutoCorrect; 2222 } 2223 2224 private CharSequence getTextWithUnderline(final String text) { 2225 return mIsAutoCorrectionIndicatorOn 2226 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) 2227 : text; 2228 } 2229 2230 private void handleClose() { 2231 // TODO: Verify that words are logged properly when IME is closed. 2232 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 2233 requestHideSelf(0); 2234 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 2235 if (mainKeyboardView != null) { 2236 mainKeyboardView.closing(); 2237 } 2238 } 2239 2240 // TODO: make this private 2241 // Outside LatinIME, only used by the test suite. 2242 @UsedForTesting 2243 boolean isShowingPunctuationList() { 2244 if (mSuggestedWords == null) return false; 2245 return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords; 2246 } 2247 2248 private boolean isSuggestionsStripVisible() { 2249 final SettingsValues currentSettings = mSettings.getCurrent(); 2250 if (mSuggestionStripView == null) 2251 return false; 2252 if (mSuggestionStripView.isShowingAddToDictionaryHint()) 2253 return true; 2254 if (null == currentSettings) 2255 return false; 2256 if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation)) 2257 return false; 2258 if (currentSettings.isApplicationSpecifiedCompletionsOn()) 2259 return true; 2260 return currentSettings.isSuggestionsRequested(mDisplayOrientation); 2261 } 2262 2263 private void clearSuggestionStrip() { 2264 setSuggestedWords(SuggestedWords.EMPTY, false); 2265 setAutoCorrectionIndicator(false); 2266 } 2267 2268 private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { 2269 mSuggestedWords = words; 2270 if (mSuggestionStripView != null) { 2271 mSuggestionStripView.setSuggestions(words); 2272 mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); 2273 } 2274 } 2275 2276 private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { 2277 // Put a blue underline to a word in TextView which will be auto-corrected. 2278 if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator 2279 && mWordComposer.isComposingWord()) { 2280 mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; 2281 final CharSequence textWithUnderline = 2282 getTextWithUnderline(mWordComposer.getTypedWord()); 2283 // TODO: when called from an updateSuggestionStrip() call that results from a posted 2284 // message, this is called outside any batch edit. Potentially, this may result in some 2285 // janky flickering of the screen, although the display speed makes it unlikely in 2286 // the practice. 2287 mConnection.setComposingText(textWithUnderline, 1); 2288 } 2289 } 2290 2291 private void updateSuggestionStrip() { 2292 mHandler.cancelUpdateSuggestionStrip(); 2293 final SettingsValues currentSettings = mSettings.getCurrent(); 2294 2295 // Check if we have a suggestion engine attached. 2296 if (mSuggest == null 2297 || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) { 2298 if (mWordComposer.isComposingWord()) { 2299 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " 2300 + "requested!"); 2301 } 2302 return; 2303 } 2304 2305 if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) { 2306 setPunctuationSuggestions(); 2307 return; 2308 } 2309 2310 final SuggestedWords suggestedWords = 2311 getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_TYPING); 2312 final String typedWord = mWordComposer.getTypedWord(); 2313 showSuggestionStrip(suggestedWords, typedWord); 2314 } 2315 2316 private SuggestedWords getSuggestedWords(final int sessionId) { 2317 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 2318 final Suggest suggest = mSuggest; 2319 if (keyboard == null || suggest == null) { 2320 return SuggestedWords.EMPTY; 2321 } 2322 // Get the word on which we should search the bigrams. If we are composing a word, it's 2323 // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we 2324 // should just skip whitespace if any, so 1. 2325 final SettingsValues currentSettings = mSettings.getCurrent(); 2326 final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues; 2327 final String prevWord; 2328 if (currentSettings.mCurrentLanguageHasSpaces) { 2329 // If we are typing in a language with spaces we can just look up the previous 2330 // word from textview. 2331 prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2332 mWordComposer.isComposingWord() ? 2 : 1); 2333 } else { 2334 prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null 2335 : mLastComposedWord.mCommittedWord; 2336 } 2337 return suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(), 2338 currentSettings.mBlockPotentiallyOffensive, 2339 currentSettings.mCorrectionEnabled, additionalFeaturesOptions, sessionId); 2340 } 2341 2342 private SuggestedWords getSuggestedWordsOrOlderSuggestions(final int sessionId) { 2343 return maybeRetrieveOlderSuggestions(mWordComposer.getTypedWord(), 2344 getSuggestedWords(sessionId)); 2345 } 2346 2347 private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord, 2348 final SuggestedWords suggestedWords) { 2349 // TODO: consolidate this into getSuggestedWords 2350 // We update the suggestion strip only when we have some suggestions to show, i.e. when 2351 // the suggestion count is > 1; else, we leave the old suggestions, with the typed word 2352 // replaced with the new one. However, when the word is a dictionary word, or when the 2353 // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the 2354 // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to 2355 // revert to suggestions - although it is unclear how we can come here if it's displayed. 2356 if (suggestedWords.size() > 1 || typedWord.length() <= 1 2357 || suggestedWords.mTypedWordValid || null == mSuggestionStripView 2358 || mSuggestionStripView.isShowingAddToDictionaryHint()) { 2359 return suggestedWords; 2360 } else { 2361 return getOlderSuggestions(typedWord); 2362 } 2363 } 2364 2365 private SuggestedWords getOlderSuggestions(final String typedWord) { 2366 SuggestedWords previousSuggestedWords = mSuggestedWords; 2367 if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) { 2368 previousSuggestedWords = SuggestedWords.EMPTY; 2369 } 2370 if (typedWord == null) { 2371 return previousSuggestedWords; 2372 } 2373 final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = 2374 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, 2375 previousSuggestedWords); 2376 return new SuggestedWords(typedWordAndPreviousSuggestions, 2377 false /* typedWordValid */, 2378 false /* hasAutoCorrectionCandidate */, 2379 false /* isPunctuationSuggestions */, 2380 true /* isObsoleteSuggestions */, 2381 false /* isPrediction */); 2382 } 2383 2384 private void showSuggestionStrip(final SuggestedWords suggestedWords, final String typedWord) { 2385 if (suggestedWords.isEmpty()) { 2386 clearSuggestionStrip(); 2387 return; 2388 } 2389 final String autoCorrection; 2390 if (suggestedWords.mWillAutoCorrect) { 2391 autoCorrection = suggestedWords.getWord(1); 2392 } else { 2393 autoCorrection = typedWord; 2394 } 2395 mWordComposer.setAutoCorrection(autoCorrection); 2396 final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); 2397 setSuggestedWords(suggestedWords, isAutoCorrection); 2398 setAutoCorrectionIndicator(isAutoCorrection); 2399 setSuggestionStripShown(isSuggestionsStripVisible()); 2400 } 2401 2402 private void commitCurrentAutoCorrection(final String separatorString) { 2403 // Complete any pending suggestions query first 2404 if (mHandler.hasPendingUpdateSuggestions()) { 2405 updateSuggestionStrip(); 2406 } 2407 final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); 2408 final String typedWord = mWordComposer.getTypedWord(); 2409 final String autoCorrection = (typedAutoCorrection != null) 2410 ? typedAutoCorrection : typedWord; 2411 if (autoCorrection != null) { 2412 if (TextUtils.isEmpty(typedWord)) { 2413 throw new RuntimeException("We have an auto-correction but the typed word " 2414 + "is empty? Impossible! I must commit suicide."); 2415 } 2416 if (mSettings.isInternal()) { 2417 LatinImeLoggerUtils.onAutoCorrection( 2418 typedWord, autoCorrection, separatorString, mWordComposer); 2419 } 2420 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2421 final SuggestedWords suggestedWords = mSuggestedWords; 2422 ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, 2423 separatorString, mWordComposer.isBatchMode(), suggestedWords); 2424 } 2425 mExpectingUpdateSelection = true; 2426 commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, 2427 separatorString); 2428 if (!typedWord.equals(autoCorrection)) { 2429 // This will make the correction flash for a short while as a visual clue 2430 // to the user that auto-correction happened. It has no other effect; in particular 2431 // note that this won't affect the text inside the text field AT ALL: it only makes 2432 // the segment of text starting at the supplied index and running for the length 2433 // of the auto-correction flash. At this moment, the "typedWord" argument is 2434 // ignored by TextView. 2435 mConnection.commitCorrection( 2436 new CorrectionInfo(mLastSelectionEnd - typedWord.length(), 2437 typedWord, autoCorrection)); 2438 } 2439 } 2440 } 2441 2442 // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} 2443 // interface 2444 @Override 2445 public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { 2446 final SuggestedWords suggestedWords = mSuggestedWords; 2447 final String suggestion = suggestionInfo.mWord; 2448 // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput 2449 if (suggestion.length() == 1 && isShowingPunctuationList()) { 2450 // Word separators are suggested before the user inputs something. 2451 // So, LatinImeLogger logs "" as a user's input. 2452 LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); 2453 // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. 2454 final int primaryCode = suggestion.charAt(0); 2455 onCodeInput(primaryCode, 2456 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); 2457 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2458 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, 2459 false /* isBatchMode */, suggestedWords.mIsPrediction); 2460 } 2461 return; 2462 } 2463 2464 mConnection.beginBatchEdit(); 2465 final SettingsValues currentSettings = mSettings.getCurrent(); 2466 if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0 2467 // In the batch input mode, a manually picked suggested word should just replace 2468 // the current batch input text and there is no need for a phantom space. 2469 && !mWordComposer.isBatchMode()) { 2470 final int firstChar = Character.codePointAt(suggestion, 0); 2471 if (!currentSettings.isWordSeparator(firstChar) 2472 || currentSettings.isUsuallyPrecededBySpace(firstChar)) { 2473 promotePhantomSpace(); 2474 } 2475 } 2476 2477 if (currentSettings.isApplicationSpecifiedCompletionsOn() 2478 && mApplicationSpecifiedCompletions != null 2479 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 2480 mSuggestedWords = SuggestedWords.EMPTY; 2481 if (mSuggestionStripView != null) { 2482 mSuggestionStripView.clear(); 2483 } 2484 mKeyboardSwitcher.updateShiftState(); 2485 resetComposingState(true /* alsoResetLastComposedWord */); 2486 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; 2487 mConnection.commitCompletion(completionInfo); 2488 mConnection.endBatchEdit(); 2489 return; 2490 } 2491 2492 // We need to log before we commit, because the word composer will store away the user 2493 // typed word. 2494 final String replacedWord = mWordComposer.getTypedWord(); 2495 LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); 2496 mExpectingUpdateSelection = true; 2497 commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, 2498 LastComposedWord.NOT_A_SEPARATOR); 2499 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2500 ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, 2501 mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind, 2502 suggestionInfo.mSourceDict.mDictType); 2503 } 2504 mConnection.endBatchEdit(); 2505 // Don't allow cancellation of manual pick 2506 mLastComposedWord.deactivate(); 2507 // Space state must be updated before calling updateShiftState 2508 mSpaceState = SPACE_STATE_PHANTOM; 2509 mKeyboardSwitcher.updateShiftState(); 2510 2511 // We should show the "Touch again to save" hint if the user pressed the first entry 2512 // AND it's in none of our current dictionaries (main, user or otherwise). 2513 // Please note that if mSuggest is null, it means that everything is off: suggestion 2514 // and correction, so we shouldn't try to show the hint 2515 final Suggest suggest = mSuggest; 2516 final boolean showingAddToDictionaryHint = 2517 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind 2518 || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) 2519 && suggest != null 2520 // If the suggestion is not in the dictionary, the hint should be shown. 2521 && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true); 2522 2523 if (currentSettings.mIsInternal) { 2524 LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE, 2525 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 2526 } 2527 if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) { 2528 mSuggestionStripView.showAddToDictionaryHint( 2529 suggestion, currentSettings.mHintToSaveText); 2530 } else { 2531 // If we're not showing the "Touch again to save", then update the suggestion strip. 2532 mHandler.postUpdateSuggestionStrip(); 2533 } 2534 } 2535 2536 /** 2537 * Commits the chosen word to the text field and saves it for later retrieval. 2538 */ 2539 private void commitChosenWord(final String chosenWord, final int commitType, 2540 final String separatorString) { 2541 final SuggestedWords suggestedWords = mSuggestedWords; 2542 mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( 2543 this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); 2544 // Add the word to the user history dictionary 2545 final String prevWord = addToUserHistoryDictionary(chosenWord); 2546 // TODO: figure out here if this is an auto-correct or if the best word is actually 2547 // what user typed. Note: currently this is done much later in 2548 // LastComposedWord#didCommitTypedWord by string equality of the remembered 2549 // strings. 2550 mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString, 2551 prevWord); 2552 } 2553 2554 private void setPunctuationSuggestions() { 2555 final SettingsValues currentSettings = mSettings.getCurrent(); 2556 if (currentSettings.mBigramPredictionEnabled) { 2557 clearSuggestionStrip(); 2558 } else { 2559 setSuggestedWords(currentSettings.mSuggestPuncList, false); 2560 } 2561 setAutoCorrectionIndicator(false); 2562 setSuggestionStripShown(isSuggestionsStripVisible()); 2563 } 2564 2565 private String addToUserHistoryDictionary(final String suggestion) { 2566 if (TextUtils.isEmpty(suggestion)) return null; 2567 final Suggest suggest = mSuggest; 2568 if (suggest == null) return null; 2569 2570 // If correction is not enabled, we don't add words to the user history dictionary. 2571 // That's to avoid unintended additions in some sensitive fields, or fields that 2572 // expect to receive non-words. 2573 final SettingsValues currentSettings = mSettings.getCurrent(); 2574 if (!currentSettings.mCorrectionEnabled) return null; 2575 2576 final UserHistoryPredictionDictionary userHistoryPredictionDictionary = 2577 mUserHistoryPredictionDictionary; 2578 if (userHistoryPredictionDictionary == null) return null; 2579 2580 final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2); 2581 final String secondWord; 2582 if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { 2583 secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); 2584 } else { 2585 secondWord = suggestion; 2586 } 2587 // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". 2588 // We don't add words with 0-frequency (assuming they would be profanity etc.). 2589 final int maxFreq = AutoCorrectionUtils.getMaxFrequency( 2590 suggest.getUnigramDictionaries(), suggestion); 2591 if (maxFreq == 0) return null; 2592 userHistoryPredictionDictionary 2593 .addToPersonalizationPredictionDictionary(prevWord, secondWord, maxFreq > 0); 2594 return prevWord; 2595 } 2596 2597 /** 2598 * Check if the cursor is touching a word. If so, restart suggestions on this word, else 2599 * do nothing. 2600 */ 2601 private void restartSuggestionsOnWordTouchedByCursor() { 2602 // HACK: We may want to special-case some apps that exhibit bad behavior in case of 2603 // recorrection. This is a temporary, stopgap measure that will be removed later. 2604 // TODO: remove this. 2605 if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return; 2606 // Recorrection is not supported in languages without spaces because we don't know 2607 // how to segment them yet. 2608 if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return; 2609 // If the cursor is not touching a word, or if there is a selection, return right away. 2610 if (mLastSelectionStart != mLastSelectionEnd) return; 2611 // If we don't know the cursor location, return. 2612 if (mLastSelectionStart < 0) return; 2613 final SettingsValues currentSettings = mSettings.getCurrent(); 2614 if (!mConnection.isCursorTouchingWord(currentSettings)) return; 2615 final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators, 2616 0 /* additionalPrecedingWordsCount */); 2617 if (null == range) return; // Happens if we don't have an input connection at all 2618 // If for some strange reason (editor bug or so) we measure the text before the cursor as 2619 // longer than what the entire text is supposed to be, the safe thing to do is bail out. 2620 final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); 2621 if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return; 2622 final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); 2623 final String typedWord = range.mWord.toString(); 2624 int i = 0; 2625 for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { 2626 for (final String s : span.getSuggestions()) { 2627 ++i; 2628 if (!TextUtils.equals(s, typedWord)) { 2629 suggestions.add(new SuggestedWordInfo(s, 2630 SuggestionStripView.MAX_SUGGESTIONS - i, 2631 SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, 2632 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */)); 2633 } 2634 } 2635 } 2636 mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard()); 2637 mWordComposer.setCursorPositionWithinWord( 2638 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); 2639 mConnection.setComposingRegion( 2640 mLastSelectionStart - numberOfCharsInWordBeforeCursor, 2641 mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor()); 2642 final SuggestedWords suggestedWords; 2643 if (suggestions.isEmpty()) { 2644 // We come here if there weren't any suggestion spans on this word. We will try to 2645 // compute suggestions for it instead. 2646 final SuggestedWords suggestedWordsIncludingTypedWord = 2647 getSuggestedWords(Suggest.SESSION_TYPING); 2648 if (suggestedWordsIncludingTypedWord.size() > 1) { 2649 // We were able to compute new suggestions for this word. 2650 // Remove the typed word, since we don't want to display it in this case. 2651 // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false. 2652 suggestedWords = 2653 suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord(); 2654 } else { 2655 // No saved suggestions, and we were unable to compute any good one either. 2656 // Rather than displaying an empty suggestion strip, we'll display the original 2657 // word alone in the middle. 2658 // Since there is only one word, willAutoCorrect is false. 2659 suggestedWords = suggestedWordsIncludingTypedWord; 2660 } 2661 } else { 2662 // We found suggestion spans in the word. We'll create the SuggestedWords out of 2663 // them, and make willAutoCorrect false. 2664 suggestedWords = new SuggestedWords(suggestions, 2665 true /* typedWordValid */, false /* willAutoCorrect */, 2666 false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, 2667 false /* isPrediction */); 2668 } 2669 2670 // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. 2671 // We never want to auto-correct on a resumed suggestion. Please refer to the three 2672 // places above where suggestedWords is affected. We also need to reset 2673 // mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching the text to adapt it. 2674 // TODO: remove mIsAutoCorrectionIndicator on (see comment on definition) 2675 mIsAutoCorrectionIndicatorOn = false; 2676 showSuggestionStrip(suggestedWords, typedWord); 2677 } 2678 2679 /** 2680 * Check if the cursor is actually at the end of a word. If so, restart suggestions on this 2681 * word, else do nothing. 2682 */ 2683 private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() { 2684 final CharSequence word = 2685 mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent()); 2686 if (null != word) { 2687 final String wordString = word.toString(); 2688 restartSuggestionsOnWordBeforeCursor(wordString); 2689 // TODO: Handle the case where the user manually moves the cursor and then backs up over 2690 // a separator. In that case, the current log unit should not be uncommitted. 2691 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2692 ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString, 2693 true /* dumpCurrentLogUnit */); 2694 } 2695 } 2696 } 2697 2698 private void restartSuggestionsOnWordBeforeCursor(final String word) { 2699 mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); 2700 final int length = word.length(); 2701 mConnection.deleteSurroundingText(length, 0); 2702 mConnection.setComposingText(word, 1); 2703 mHandler.postUpdateSuggestionStrip(); 2704 } 2705 2706 private void revertCommit() { 2707 final String previousWord = mLastComposedWord.mPrevWord; 2708 final String originallyTypedWord = mLastComposedWord.mTypedWord; 2709 final String committedWord = mLastComposedWord.mCommittedWord; 2710 final int cancelLength = committedWord.length(); 2711 final int separatorLength = LastComposedWord.getSeparatorLength( 2712 mLastComposedWord.mSeparatorString); 2713 // TODO: should we check our saved separator against the actual contents of the text view? 2714 final int deleteLength = cancelLength + separatorLength; 2715 if (DEBUG) { 2716 if (mWordComposer.isComposingWord()) { 2717 throw new RuntimeException("revertCommit, but we are composing a word"); 2718 } 2719 final CharSequence wordBeforeCursor = 2720 mConnection.getTextBeforeCursor(deleteLength, 0) 2721 .subSequence(0, cancelLength); 2722 if (!TextUtils.equals(committedWord, wordBeforeCursor)) { 2723 throw new RuntimeException("revertCommit check failed: we thought we were " 2724 + "reverting \"" + committedWord 2725 + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); 2726 } 2727 } 2728 mConnection.deleteSurroundingText(deleteLength, 0); 2729 if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { 2730 mUserHistoryPredictionDictionary.cancelAddingUserHistory(previousWord, committedWord); 2731 } 2732 final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; 2733 if (mSettings.getCurrent().mCurrentLanguageHasSpaces) { 2734 // For languages with spaces, we revert to the typed string, but the cursor is still 2735 // after the separator so we don't resume suggestions. If the user wants to correct 2736 // the word, they have to press backspace again. 2737 mConnection.commitText(stringToCommit, 1); 2738 } else { 2739 // For languages without spaces, we revert the typed string but the cursor is flush 2740 // with the typed word, so we need to resume suggestions right away. 2741 mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard()); 2742 mConnection.setComposingText(stringToCommit, 1); 2743 } 2744 if (mSettings.isInternal()) { 2745 LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString, 2746 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 2747 } 2748 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2749 ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord, 2750 mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString); 2751 } 2752 // Don't restart suggestion yet. We'll restart if the user deletes the 2753 // separator. 2754 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 2755 // We have a separator between the word and the cursor: we should show predictions. 2756 mHandler.postUpdateSuggestionStrip(); 2757 } 2758 2759 // This essentially inserts a space, and that's it. 2760 public void promotePhantomSpace() { 2761 final SettingsValues currentSettings = mSettings.getCurrent(); 2762 if (currentSettings.shouldInsertSpacesAutomatically() 2763 && currentSettings.mCurrentLanguageHasSpaces 2764 && !mConnection.textBeforeCursorLooksLikeURL()) { 2765 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 2766 ResearchLogger.latinIME_promotePhantomSpace(); 2767 } 2768 sendKeyCodePoint(Constants.CODE_SPACE); 2769 } 2770 } 2771 2772 // TODO: Make this private 2773 // Outside LatinIME, only used by the {@link InputTestsBase} test suite. 2774 @UsedForTesting 2775 void loadKeyboard() { 2776 // Since we are switching languages, the most urgent thing is to let the keyboard graphics 2777 // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on 2778 // the screen. Anything we do right now will delay this, so wait until the next frame 2779 // before we do the rest, like reopening dictionaries and updating suggestions. So we 2780 // post a message. 2781 mHandler.postReopenDictionaries(); 2782 loadSettings(); 2783 if (mKeyboardSwitcher.getMainKeyboardView() != null) { 2784 // Reload keyboard because the current language has been changed. 2785 mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); 2786 } 2787 } 2788 2789 private void hapticAndAudioFeedback(final int code, final int repeatCount) { 2790 final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView(); 2791 if (keyboardView != null && keyboardView.isInSlidingKeyInput()) { 2792 // No need to feedback while sliding input. 2793 return; 2794 } 2795 if (repeatCount > 0) { 2796 if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) { 2797 // No need to feedback when repeat delete key will have no effect. 2798 return; 2799 } 2800 // TODO: Use event time that the last feedback has been generated instead of relying on 2801 // a repeat count to thin out feedback. 2802 if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) { 2803 return; 2804 } 2805 } 2806 final AudioAndHapticFeedbackManager feedbackManager = 2807 AudioAndHapticFeedbackManager.getInstance(); 2808 if (repeatCount == 0) { 2809 // TODO: Reconsider how to perform haptic feedback when repeating key. 2810 feedbackManager.performHapticFeedback(keyboardView); 2811 } 2812 feedbackManager.performAudioFeedback(code); 2813 } 2814 2815 // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed; 2816 // release matching call is {@link #onReleaseKey(int,boolean)} below. 2817 @Override 2818 public void onPressKey(final int primaryCode, final int repeatCount, 2819 final boolean isSinglePointer) { 2820 mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer); 2821 hapticAndAudioFeedback(primaryCode, repeatCount); 2822 } 2823 2824 // Callback of the {@link KeyboardActionListener}. This is called when a key is released; 2825 // press matching call is {@link #onPressKey(int,int,boolean)} above. 2826 @Override 2827 public void onReleaseKey(final int primaryCode, final boolean withSliding) { 2828 mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); 2829 2830 // If accessibility is on, ensure the user receives keyboard state updates. 2831 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 2832 switch (primaryCode) { 2833 case Constants.CODE_SHIFT: 2834 AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); 2835 break; 2836 case Constants.CODE_SWITCH_ALPHA_SYMBOL: 2837 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); 2838 break; 2839 } 2840 } 2841 } 2842 2843 // Hooks for hardware keyboard 2844 @Override 2845 public boolean onKeyDown(final int keyCode, final KeyEvent event) { 2846 if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event); 2847 // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if 2848 // it doesn't know what to do with it and leave it to the application. For example, 2849 // hardware key events for adjusting the screen's brightness are passed as is. 2850 if (mEventInterpreter.onHardwareKeyEvent(event)) { 2851 final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); 2852 mCurrentlyPressedHardwareKeys.add(keyIdentifier); 2853 return true; 2854 } 2855 return super.onKeyDown(keyCode, event); 2856 } 2857 2858 @Override 2859 public boolean onKeyUp(final int keyCode, final KeyEvent event) { 2860 final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); 2861 if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { 2862 return true; 2863 } 2864 return super.onKeyUp(keyCode, event); 2865 } 2866 2867 // onKeyDown and onKeyUp are the main events we are interested in. There are two more events 2868 // related to handling of hardware key events that we may want to implement in the future: 2869 // boolean onKeyLongPress(final int keyCode, final KeyEvent event); 2870 // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event); 2871 2872 // receive ringer mode change and network state change. 2873 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 2874 @Override 2875 public void onReceive(final Context context, final Intent intent) { 2876 final String action = intent.getAction(); 2877 if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 2878 mSubtypeSwitcher.onNetworkStateChanged(intent); 2879 } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { 2880 AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged(); 2881 } 2882 } 2883 }; 2884 2885 private void launchSettings() { 2886 handleClose(); 2887 launchSubActivity(SettingsActivity.class); 2888 } 2889 2890 public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) { 2891 // Put the text in the attached EditText into a safe, saved state before switching to a 2892 // new activity that will also use the soft keyboard. 2893 commitTyped(LastComposedWord.NOT_A_SEPARATOR); 2894 launchSubActivity(activityClass); 2895 } 2896 2897 private void launchSubActivity(final Class<? extends Activity> activityClass) { 2898 Intent intent = new Intent(); 2899 intent.setClass(LatinIME.this, activityClass); 2900 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 2901 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2902 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2903 startActivity(intent); 2904 } 2905 2906 private void showSubtypeSelectorAndSettings() { 2907 final CharSequence title = getString(R.string.english_ime_input_options); 2908 final CharSequence[] items = new CharSequence[] { 2909 // TODO: Should use new string "Select active input modes". 2910 getString(R.string.language_selection_title), 2911 getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)), 2912 }; 2913 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 2914 @Override 2915 public void onClick(DialogInterface di, int position) { 2916 di.dismiss(); 2917 switch (position) { 2918 case 0: 2919 final Intent intent = IntentUtils.getInputLanguageSelectionIntent( 2920 mRichImm.getInputMethodIdOfThisIme(), 2921 Intent.FLAG_ACTIVITY_NEW_TASK 2922 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2923 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2924 startActivity(intent); 2925 break; 2926 case 1: 2927 launchSettings(); 2928 break; 2929 } 2930 } 2931 }; 2932 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 2933 .setItems(items, listener) 2934 .setTitle(title); 2935 showOptionDialog(builder.create()); 2936 } 2937 2938 public void showOptionDialog(final AlertDialog dialog) { 2939 final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); 2940 if (windowToken == null) { 2941 return; 2942 } 2943 2944 dialog.setCancelable(true); 2945 dialog.setCanceledOnTouchOutside(true); 2946 2947 final Window window = dialog.getWindow(); 2948 final WindowManager.LayoutParams lp = window.getAttributes(); 2949 lp.token = windowToken; 2950 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 2951 window.setAttributes(lp); 2952 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 2953 2954 mOptionsDialog = dialog; 2955 dialog.show(); 2956 } 2957 2958 // TODO: can this be removed somehow without breaking the tests? 2959 @UsedForTesting 2960 /* package for test */ String getFirstSuggestedWord() { 2961 return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null; 2962 } 2963 2964 // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. 2965 @UsedForTesting 2966 /* package for test */ boolean isCurrentlyWaitingForMainDictionary() { 2967 return mSuggest.isCurrentlyWaitingForMainDictionary(); 2968 } 2969 2970 // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. 2971 @UsedForTesting 2972 /* package for test */ boolean hasMainDictionary() { 2973 return mSuggest.hasMainDictionary(); 2974 } 2975 2976 // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. 2977 @UsedForTesting 2978 /* package for test */ void replaceMainDictionaryForTest(final Locale locale) { 2979 mSuggest.resetMainDict(this, locale, null); 2980 } 2981 2982 public void debugDumpStateAndCrashWithException(final String context) { 2983 final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString()); 2984 s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes) 2985 .append("\nContext : ").append(context); 2986 throw new RuntimeException(s.toString()); 2987 } 2988 2989 @Override 2990 protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) { 2991 super.dump(fd, fout, args); 2992 2993 final Printer p = new PrintWriterPrinter(fout); 2994 p.println("LatinIME state :"); 2995 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 2996 final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; 2997 p.println(" Keyboard mode = " + keyboardMode); 2998 final SettingsValues settingsValues = mSettings.getCurrent(); 2999 p.println(" mIsSuggestionsSuggestionsRequested = " 3000 + settingsValues.isSuggestionsRequested(mDisplayOrientation)); 3001 p.println(" mCorrectionEnabled=" + settingsValues.mCorrectionEnabled); 3002 p.println(" isComposingWord=" + mWordComposer.isComposingWord()); 3003 p.println(" mSoundOn=" + settingsValues.mSoundOn); 3004 p.println(" mVibrateOn=" + settingsValues.mVibrateOn); 3005 p.println(" mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn); 3006 p.println(" inputAttributes=" + settingsValues.mInputAttributes); 3007 } 3008} 3009