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