LatinIME.java revision a905fcec00f78e828c1fe9109f27cc9f149941b5
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.Pair; 50import android.util.PrintWriterPrinter; 51import android.util.Printer; 52import android.view.KeyCharacterMap; 53import android.view.KeyEvent; 54import android.view.View; 55import android.view.ViewGroup.LayoutParams; 56import android.view.Window; 57import android.view.WindowManager; 58import android.view.inputmethod.CompletionInfo; 59import android.view.inputmethod.CorrectionInfo; 60import android.view.inputmethod.EditorInfo; 61import android.view.inputmethod.InputMethodSubtype; 62 63import com.android.inputmethod.accessibility.AccessibilityUtils; 64import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; 65import com.android.inputmethod.annotations.UsedForTesting; 66import com.android.inputmethod.compat.AppWorkaroundsUtils; 67import com.android.inputmethod.compat.InputMethodServiceCompatUtils; 68import com.android.inputmethod.compat.SuggestionSpanUtils; 69import com.android.inputmethod.dictionarypack.DictionaryPackConstants; 70import com.android.inputmethod.event.EventInterpreter; 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.inputlogic.InputLogic; 80import com.android.inputmethod.latin.inputlogic.SpaceState; 81import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever; 82import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister; 83import com.android.inputmethod.latin.personalization.UserHistoryDictionary; 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.LeakGuardHandlerWrapper; 99import com.android.inputmethod.latin.utils.RecapitalizeStatus; 100import com.android.inputmethod.latin.utils.StringUtils; 101import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; 102import com.android.inputmethod.latin.utils.TextRange; 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; 110import java.util.concurrent.TimeUnit; 111 112/** 113 * Input method implementation for Qwerty'ish keyboard. 114 */ 115public class LatinIME extends InputMethodService implements KeyboardActionListener, 116 SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener, 117 Suggest.SuggestInitializationListener { 118 private static final String TAG = LatinIME.class.getSimpleName(); 119 private static final boolean TRACE = false; 120 // TODO[IL]: Make this private 121 public static boolean DEBUG; 122 123 private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; 124 125 private static final int PENDING_IMS_CALLBACK_DURATION = 800; 126 127 private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; 128 129 // TODO: Set this value appropriately. 130 private static final int GET_SUGGESTED_WORDS_TIMEOUT = 200; 131 132 /** 133 * The name of the scheme used by the Package Manager to warn of a new package installation, 134 * replacement or removal. 135 */ 136 private static final String SCHEME_PACKAGE = "package"; 137 138 private final Settings mSettings; 139 private final InputLogic mInputLogic = new InputLogic(this); 140 141 private View mExtractArea; 142 private View mKeyPreviewBackingView; 143 private SuggestionStripView mSuggestionStripView; 144 145 private CompletionInfo[] mApplicationSpecifiedCompletions; 146 // TODO[IL]: Make this an AsyncResultHolder or a Future in SettingsValues 147 public AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils(); 148 149 private RichInputMethodManager mRichImm; 150 @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; 151 private final SubtypeSwitcher mSubtypeSwitcher; 152 private final SubtypeState mSubtypeState = new SubtypeState(); 153 154 private boolean mIsMainDictionaryAvailable; 155 private UserBinaryDictionary mUserDictionary; 156 private boolean mIsUserDictionaryAvailable; 157 158 // Personalization debugging params 159 private boolean mUseOnlyPersonalizationDictionaryForDebug = false; 160 private boolean mBoostPersonalizationDictionaryForDebug = false; 161 162 // Member variable for remembering the current device orientation. 163 // TODO[IL]: Move this to SettingsValues. 164 public int mDisplayOrientation; 165 166 // Object for reacting to adding/removing a dictionary pack. 167 private BroadcastReceiver mDictionaryPackInstallReceiver = 168 new DictionaryPackInstallBroadcastReceiver(this); 169 170 private AlertDialog mOptionsDialog; 171 172 private final boolean mIsHardwareAcceleratedDrawingEnabled; 173 174 public final UIHandler mHandler = new UIHandler(this); 175 private InputUpdater mInputUpdater; 176 177 public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> { 178 private static final int MSG_UPDATE_SHIFT_STATE = 0; 179 private static final int MSG_PENDING_IMS_CALLBACK = 1; 180 private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; 181 private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; 182 private static final int MSG_RESUME_SUGGESTIONS = 4; 183 private static final int MSG_REOPEN_DICTIONARIES = 5; 184 private static final int MSG_ON_END_BATCH_INPUT = 6; 185 private static final int MSG_RESET_CACHES = 7; 186 // Update this when adding new messages 187 private static final int MSG_LAST = MSG_RESET_CACHES; 188 189 private static final int ARG1_NOT_GESTURE_INPUT = 0; 190 private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; 191 private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2; 192 private static final int ARG2_WITHOUT_TYPED_WORD = 0; 193 private static final int ARG2_WITH_TYPED_WORD = 1; 194 195 private int mDelayUpdateSuggestions; 196 private int mDelayUpdateShiftState; 197 private long mDoubleSpacePeriodTimeout; 198 private long mDoubleSpacePeriodTimerStart; 199 200 public UIHandler(final LatinIME ownerInstance) { 201 super(ownerInstance); 202 } 203 204 public void onCreate() { 205 final Resources res = getOwnerInstance().getResources(); 206 mDelayUpdateSuggestions = 207 res.getInteger(R.integer.config_delay_update_suggestions); 208 mDelayUpdateShiftState = 209 res.getInteger(R.integer.config_delay_update_shift_state); 210 mDoubleSpacePeriodTimeout = 211 res.getInteger(R.integer.config_double_space_period_timeout); 212 } 213 214 @Override 215 public void handleMessage(final Message msg) { 216 final LatinIME latinIme = getOwnerInstance(); 217 final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; 218 switch (msg.what) { 219 case MSG_UPDATE_SUGGESTION_STRIP: 220 latinIme.updateSuggestionStrip(); 221 break; 222 case MSG_UPDATE_SHIFT_STATE: 223 switcher.updateShiftState(); 224 break; 225 case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: 226 if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) { 227 if (msg.arg2 == ARG2_WITH_TYPED_WORD) { 228 final Pair<SuggestedWords, String> p = 229 (Pair<SuggestedWords, String>) msg.obj; 230 latinIme.showSuggestionStripWithTypedWord(p.first, p.second); 231 } else { 232 latinIme.showSuggestionStrip((SuggestedWords) msg.obj); 233 } 234 } else { 235 latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj, 236 msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); 237 } 238 break; 239 case MSG_RESUME_SUGGESTIONS: 240 latinIme.restartSuggestionsOnWordTouchedByCursor(); 241 break; 242 case MSG_REOPEN_DICTIONARIES: 243 latinIme.initSuggest(); 244 // In theory we could call latinIme.updateSuggestionStrip() right away, but 245 // in the practice, the dictionary is not finished opening yet so we wouldn't 246 // get any suggestions. Wait one frame. 247 postUpdateSuggestionStrip(); 248 break; 249 case MSG_ON_END_BATCH_INPUT: 250 latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj); 251 break; 252 case MSG_RESET_CACHES: 253 latinIme.retryResetCaches(msg.arg1 == 1 /* tryResumeSuggestions */, 254 msg.arg2 /* remainingTries */); 255 break; 256 } 257 } 258 259 public void postUpdateSuggestionStrip() { 260 sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions); 261 } 262 263 public void postReopenDictionaries() { 264 sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)); 265 } 266 267 public void postResumeSuggestions() { 268 removeMessages(MSG_RESUME_SUGGESTIONS); 269 sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); 270 } 271 272 public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { 273 removeMessages(MSG_RESET_CACHES); 274 sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0, 275 remainingTries, null)); 276 } 277 278 public void cancelUpdateSuggestionStrip() { 279 removeMessages(MSG_UPDATE_SUGGESTION_STRIP); 280 } 281 282 public boolean hasPendingUpdateSuggestions() { 283 return hasMessages(MSG_UPDATE_SUGGESTION_STRIP); 284 } 285 286 public boolean hasPendingReopenDictionaries() { 287 return hasMessages(MSG_REOPEN_DICTIONARIES); 288 } 289 290 public void postUpdateShiftState() { 291 removeMessages(MSG_UPDATE_SHIFT_STATE); 292 sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState); 293 } 294 295 public void cancelUpdateShiftState() { 296 removeMessages(MSG_UPDATE_SHIFT_STATE); 297 } 298 299 @UsedForTesting 300 public void removeAllMessages() { 301 for (int i = 0; i <= MSG_LAST; ++i) { 302 removeMessages(i); 303 } 304 } 305 306 public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, 307 final boolean dismissGestureFloatingPreviewText) { 308 removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 309 final int arg1 = dismissGestureFloatingPreviewText 310 ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT 311 : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT; 312 obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 313 ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); 314 } 315 316 public void showSuggestionStrip(final SuggestedWords suggestedWords) { 317 removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 318 obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 319 ARG1_NOT_GESTURE_INPUT, ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); 320 } 321 322 // TODO: Remove this method. 323 public void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, 324 final String typedWord) { 325 removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 326 obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, ARG1_NOT_GESTURE_INPUT, 327 ARG2_WITH_TYPED_WORD, 328 new Pair<SuggestedWords, String>(suggestedWords, typedWord)).sendToTarget(); 329 } 330 331 public void onEndBatchInput(final SuggestedWords suggestedWords) { 332 obtainMessage(MSG_ON_END_BATCH_INPUT, suggestedWords).sendToTarget(); 333 } 334 335 public void startDoubleSpacePeriodTimer() { 336 mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis(); 337 } 338 339 public void cancelDoubleSpacePeriodTimer() { 340 mDoubleSpacePeriodTimerStart = 0; 341 } 342 343 public boolean isAcceptingDoubleSpacePeriod() { 344 return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart 345 < mDoubleSpacePeriodTimeout; 346 } 347 348 // Working variables for the following methods. 349 private boolean mIsOrientationChanging; 350 private boolean mPendingSuccessiveImsCallback; 351 private boolean mHasPendingStartInput; 352 private boolean mHasPendingFinishInputView; 353 private boolean mHasPendingFinishInput; 354 private EditorInfo mAppliedEditorInfo; 355 356 public void startOrientationChanging() { 357 removeMessages(MSG_PENDING_IMS_CALLBACK); 358 resetPendingImsCallback(); 359 mIsOrientationChanging = true; 360 final LatinIME latinIme = getOwnerInstance(); 361 if (latinIme.isInputViewShown()) { 362 latinIme.mKeyboardSwitcher.saveKeyboardState(); 363 } 364 } 365 366 private void resetPendingImsCallback() { 367 mHasPendingFinishInputView = false; 368 mHasPendingFinishInput = false; 369 mHasPendingStartInput = false; 370 } 371 372 private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, 373 boolean restarting) { 374 if (mHasPendingFinishInputView) 375 latinIme.onFinishInputViewInternal(mHasPendingFinishInput); 376 if (mHasPendingFinishInput) 377 latinIme.onFinishInputInternal(); 378 if (mHasPendingStartInput) 379 latinIme.onStartInputInternal(editorInfo, restarting); 380 resetPendingImsCallback(); 381 } 382 383 public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { 384 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 385 // Typically this is the second onStartInput after orientation changed. 386 mHasPendingStartInput = true; 387 } else { 388 if (mIsOrientationChanging && restarting) { 389 // This is the first onStartInput after orientation changed. 390 mIsOrientationChanging = false; 391 mPendingSuccessiveImsCallback = true; 392 } 393 final LatinIME latinIme = getOwnerInstance(); 394 executePendingImsCallback(latinIme, editorInfo, restarting); 395 latinIme.onStartInputInternal(editorInfo, restarting); 396 } 397 } 398 399 public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { 400 if (hasMessages(MSG_PENDING_IMS_CALLBACK) 401 && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { 402 // Typically this is the second onStartInputView after orientation changed. 403 resetPendingImsCallback(); 404 } else { 405 if (mPendingSuccessiveImsCallback) { 406 // This is the first onStartInputView after orientation changed. 407 mPendingSuccessiveImsCallback = false; 408 resetPendingImsCallback(); 409 sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), 410 PENDING_IMS_CALLBACK_DURATION); 411 } 412 final LatinIME latinIme = getOwnerInstance(); 413 executePendingImsCallback(latinIme, editorInfo, restarting); 414 latinIme.onStartInputViewInternal(editorInfo, restarting); 415 mAppliedEditorInfo = editorInfo; 416 } 417 } 418 419 public void onFinishInputView(final boolean finishingInput) { 420 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 421 // Typically this is the first onFinishInputView after orientation changed. 422 mHasPendingFinishInputView = true; 423 } else { 424 final LatinIME latinIme = getOwnerInstance(); 425 latinIme.onFinishInputViewInternal(finishingInput); 426 mAppliedEditorInfo = null; 427 } 428 } 429 430 public void onFinishInput() { 431 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 432 // Typically this is the first onFinishInput after orientation changed. 433 mHasPendingFinishInput = true; 434 } else { 435 final LatinIME latinIme = getOwnerInstance(); 436 executePendingImsCallback(latinIme, null, false); 437 latinIme.onFinishInputInternal(); 438 } 439 } 440 } 441 442 static final class SubtypeState { 443 private InputMethodSubtype mLastActiveSubtype; 444 private boolean mCurrentSubtypeUsed; 445 446 public void currentSubtypeUsed() { 447 mCurrentSubtypeUsed = true; 448 } 449 450 public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) { 451 final InputMethodSubtype currentSubtype = richImm.getInputMethodManager() 452 .getCurrentInputMethodSubtype(); 453 final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; 454 final boolean currentSubtypeUsed = mCurrentSubtypeUsed; 455 if (currentSubtypeUsed) { 456 mLastActiveSubtype = currentSubtype; 457 mCurrentSubtypeUsed = false; 458 } 459 if (currentSubtypeUsed 460 && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype) 461 && !currentSubtype.equals(lastActiveSubtype)) { 462 richImm.setInputMethodAndSubtype(token, lastActiveSubtype); 463 return; 464 } 465 richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */); 466 } 467 } 468 469 // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial 470 // JNI call as much as possible. 471 static { 472 JniUtils.loadNativeLibrary(); 473 } 474 475 public LatinIME() { 476 super(); 477 mSettings = Settings.getInstance(); 478 mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 479 mKeyboardSwitcher = KeyboardSwitcher.getInstance(); 480 mIsHardwareAcceleratedDrawingEnabled = 481 InputMethodServiceCompatUtils.enableHardwareAcceleration(this); 482 Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled); 483 } 484 485 @Override 486 public void onCreate() { 487 Settings.init(this); 488 LatinImeLogger.init(this); 489 RichInputMethodManager.init(this); 490 mRichImm = RichInputMethodManager.getInstance(); 491 SubtypeSwitcher.init(this); 492 KeyboardSwitcher.init(this); 493 AudioAndHapticFeedbackManager.init(this); 494 AccessibilityUtils.init(this); 495 PersonalizationDictionarySessionRegister.init(this); 496 497 super.onCreate(); 498 499 mHandler.onCreate(); 500 DEBUG = LatinImeLogger.sDBG; 501 502 // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}. 503 loadSettings(); 504 initSuggest(); 505 506 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 507 ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mInputLogic.mSuggest); 508 } 509 mDisplayOrientation = getResources().getConfiguration().orientation; 510 511 // Register to receive ringer mode change and network state change. 512 // Also receive installation and removal of a dictionary pack. 513 final IntentFilter filter = new IntentFilter(); 514 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 515 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 516 registerReceiver(mReceiver, filter); 517 518 final IntentFilter packageFilter = new IntentFilter(); 519 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 520 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 521 packageFilter.addDataScheme(SCHEME_PACKAGE); 522 registerReceiver(mDictionaryPackInstallReceiver, packageFilter); 523 524 final IntentFilter newDictFilter = new IntentFilter(); 525 newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); 526 registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); 527 528 DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this); 529 530 mInputUpdater = new InputUpdater(this); 531 } 532 533 // Has to be package-visible for unit tests 534 @UsedForTesting 535 void loadSettings() { 536 final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 537 final InputAttributes inputAttributes = 538 new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode()); 539 mSettings.loadSettings(locale, inputAttributes); 540 AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent()); 541 // To load the keyboard we need to load all the settings once, but resetting the 542 // contacts dictionary should be deferred until after the new layout has been displayed 543 // to improve responsivity. In the language switching process, we post a reopenDictionaries 544 // message, then come here to read the settings for the new language before we change 545 // the layout; at this time, we need to skip resetting the contacts dictionary. It will 546 // be done later inside {@see #initSuggest()} when the reopenDictionaries message is 547 // processed. 548 if (!mHandler.hasPendingReopenDictionaries() && mInputLogic.mSuggest != null) { 549 // May need to reset dictionaries depending on the user settings. 550 mInputLogic.mSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */, 551 mSettings.getCurrent()); 552 } 553 } 554 555 // Note that this method is called from a non-UI thread. 556 @Override 557 public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) { 558 mIsMainDictionaryAvailable = isMainDictionaryAvailable; 559 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 560 if (mainKeyboardView != null) { 561 mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable); 562 } 563 } 564 565 private void initSuggest() { 566 final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 567 final String switcherLocaleStr = switcherSubtypeLocale.toString(); 568 final Locale subtypeLocale; 569 if (TextUtils.isEmpty(switcherLocaleStr)) { 570 // This happens in very rare corner cases - for example, immediately after a switch 571 // to LatinIME has been requested, about a frame later another switch happens. In this 572 // case, we are about to go down but we still don't know it, however the system tells 573 // us there is no current subtype so the locale is the empty string. Take the best 574 // possible guess instead -- it's bound to have no consequences, and we have no way 575 // of knowing anyway. 576 Log.e(TAG, "System is reporting no current subtype."); 577 subtypeLocale = getResources().getConfiguration().locale; 578 } else { 579 subtypeLocale = switcherSubtypeLocale; 580 } 581 582 final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale, 583 this /* SuggestInitializationListener */); 584 final SettingsValues settingsValues = mSettings.getCurrent(); 585 if (settingsValues.mCorrectionEnabled) { 586 newSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold); 587 } 588 589 mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); 590 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 591 ResearchLogger.getInstance().initSuggest(newSuggest); 592 } 593 594 mUserDictionary = new UserBinaryDictionary(this, subtypeLocale); 595 mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); 596 newSuggest.setUserDictionary(mUserDictionary); 597 newSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */, 598 mSettings.getCurrent()); 599 final Suggest oldSuggest = mInputLogic.mSuggest; 600 mInputLogic.mSuggest = newSuggest; 601 if (oldSuggest != null) oldSuggest.close(); 602 } 603 604 /* package private */ void resetSuggestMainDict() { 605 final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 606 mInputLogic.mSuggest.resetMainDict(this, subtypeLocale, 607 this /* SuggestInitializationListener */); 608 mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); 609 } 610 611 @Override 612 public void onDestroy() { 613 final Suggest suggest = mInputLogic.mSuggest; 614 if (suggest != null) { 615 suggest.close(); 616 mInputLogic.mSuggest = null; 617 } 618 mSettings.onDestroy(); 619 unregisterReceiver(mReceiver); 620 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 621 ResearchLogger.getInstance().onDestroy(); 622 } 623 unregisterReceiver(mDictionaryPackInstallReceiver); 624 PersonalizationDictionarySessionRegister.onDestroy(this); 625 LatinImeLogger.commit(); 626 LatinImeLogger.onDestroy(); 627 if (mInputUpdater != null) { 628 mInputUpdater.quitLooper(); 629 } 630 super.onDestroy(); 631 } 632 633 @Override 634 public void onConfigurationChanged(final Configuration conf) { 635 // If orientation changed while predicting, commit the change 636 if (mDisplayOrientation != conf.orientation) { 637 mDisplayOrientation = conf.orientation; 638 mHandler.startOrientationChanging(); 639 mInputLogic.mConnection.beginBatchEdit(); 640 mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR); 641 mInputLogic.mConnection.finishComposingText(); 642 mInputLogic.mConnection.endBatchEdit(); 643 if (isShowingOptionDialog()) { 644 mOptionsDialog.dismiss(); 645 } 646 } 647 PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf); 648 super.onConfigurationChanged(conf); 649 } 650 651 @Override 652 public View onCreateInputView() { 653 return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); 654 } 655 656 @Override 657 public void setInputView(final View view) { 658 super.setInputView(view); 659 mExtractArea = getWindow().getWindow().getDecorView() 660 .findViewById(android.R.id.extractArea); 661 mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); 662 mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); 663 if (mSuggestionStripView != null) { 664 mSuggestionStripView.setListener(this, view); 665 } 666 if (LatinImeLogger.sVISUALDEBUG) { 667 mKeyPreviewBackingView.setBackgroundColor(0x10FF0000); 668 } 669 } 670 671 @Override 672 public void setCandidatesView(final View view) { 673 // To ensure that CandidatesView will never be set. 674 return; 675 } 676 677 @Override 678 public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { 679 mHandler.onStartInput(editorInfo, restarting); 680 } 681 682 @Override 683 public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { 684 mHandler.onStartInputView(editorInfo, restarting); 685 } 686 687 @Override 688 public void onFinishInputView(final boolean finishingInput) { 689 mHandler.onFinishInputView(finishingInput); 690 } 691 692 @Override 693 public void onFinishInput() { 694 mHandler.onFinishInput(); 695 } 696 697 @Override 698 public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { 699 // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() 700 // is not guaranteed. It may even be called at the same time on a different thread. 701 mSubtypeSwitcher.onSubtypeChanged(subtype); 702 loadKeyboard(); 703 } 704 705 private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { 706 super.onStartInput(editorInfo, restarting); 707 } 708 709 @SuppressWarnings("deprecation") 710 private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) { 711 super.onStartInputView(editorInfo, restarting); 712 mRichImm.clearSubtypeCaches(); 713 final KeyboardSwitcher switcher = mKeyboardSwitcher; 714 switcher.updateKeyboardTheme(); 715 final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView(); 716 // If we are starting input in a different text field from before, we'll have to reload 717 // settings, so currentSettingsValues can't be final. 718 SettingsValues currentSettingsValues = mSettings.getCurrent(); 719 720 if (editorInfo == null) { 721 Log.e(TAG, "Null EditorInfo in onStartInputView()"); 722 if (LatinImeLogger.sDBG) { 723 throw new NullPointerException("Null EditorInfo in onStartInputView()"); 724 } 725 return; 726 } 727 if (DEBUG) { 728 Log.d(TAG, "onStartInputView: editorInfo:" 729 + String.format("inputType=0x%08x imeOptions=0x%08x", 730 editorInfo.inputType, editorInfo.imeOptions)); 731 Log.d(TAG, "All caps = " 732 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) 733 + ", sentence caps = " 734 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) 735 + ", word caps = " 736 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); 737 } 738 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 739 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 740 ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs); 741 } 742 if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) { 743 Log.w(TAG, "Deprecated private IME option specified: " 744 + editorInfo.privateImeOptions); 745 Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead"); 746 } 747 if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) { 748 Log.w(TAG, "Deprecated private IME option specified: " 749 + editorInfo.privateImeOptions); 750 Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead"); 751 } 752 753 final PackageInfo packageInfo = 754 TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName); 755 mAppWorkAroundsUtils.setPackageInfo(packageInfo); 756 if (null == packageInfo) { 757 new TargetPackageInfoGetterTask(this /* context */, this /* listener */) 758 .execute(editorInfo.packageName); 759 } 760 761 LatinImeLogger.onStartInputView(editorInfo); 762 // In landscape mode, this method gets called without the input view being created. 763 if (mainKeyboardView == null) { 764 return; 765 } 766 767 // Forward this event to the accessibility utilities, if enabled. 768 final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); 769 if (accessUtils.isTouchExplorationEnabled()) { 770 accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting); 771 } 772 773 final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); 774 final boolean isDifferentTextField = !restarting || inputTypeChanged; 775 if (isDifferentTextField) { 776 mSubtypeSwitcher.updateParametersOnStartInputView(); 777 } 778 779 // The EditorInfo might have a flag that affects fullscreen mode. 780 // Note: This call should be done by InputMethodService? 781 updateFullscreenMode(); 782 mApplicationSpecifiedCompletions = null; 783 784 // The app calling setText() has the effect of clearing the composing 785 // span, so we should reset our state unconditionally, even if restarting is true. 786 mInputLogic.mEnteredText = null; 787 mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); 788 mInputLogic.mDeleteCount = 0; 789 mInputLogic.mSpaceState = SpaceState.NONE; 790 mInputLogic.mRecapitalizeStatus.deactivate(); 791 mInputLogic.mCurrentlyPressedHardwareKeys.clear(); 792 793 // Note: the following does a round-trip IPC on the main thread: be careful 794 final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 795 final Suggest suggest = mInputLogic.mSuggest; 796 if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) { 797 initSuggest(); 798 } 799 if (mSuggestionStripView != null) { 800 // This will set the punctuation suggestions if next word suggestion is off; 801 // otherwise it will clear the suggestion strip. 802 setPunctuationSuggestions(); 803 } 804 mInputLogic.mSuggestedWords = SuggestedWords.EMPTY; 805 806 // Sometimes, while rotating, for some reason the framework tells the app we are not 807 // connected to it and that means we can't refresh the cache. In this case, schedule a 808 // refresh later. 809 final boolean canReachInputConnection; 810 if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( 811 editorInfo.initialSelStart, editorInfo.initialSelEnd, 812 false /* shouldFinishComposition */)) { 813 // We try resetting the caches up to 5 times before giving up. 814 mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); 815 // mLastSelection{Start,End} are reset later in this method, don't need to do it here 816 canReachInputConnection = false; 817 } else { 818 if (isDifferentTextField) { 819 mHandler.postResumeSuggestions(); 820 } 821 canReachInputConnection = true; 822 } 823 824 if (isDifferentTextField) { 825 mainKeyboardView.closing(); 826 loadSettings(); 827 currentSettingsValues = mSettings.getCurrent(); 828 829 if (suggest != null && currentSettingsValues.mCorrectionEnabled) { 830 suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold); 831 } 832 833 switcher.loadKeyboard(editorInfo, currentSettingsValues); 834 if (!canReachInputConnection) { 835 // If we can't reach the input connection, we will call loadKeyboard again later, 836 // so we need to save its state now. The call will be done in #retryResetCaches. 837 switcher.saveKeyboardState(); 838 } 839 } else if (restarting) { 840 // TODO: Come up with a more comprehensive way to reset the keyboard layout when 841 // a keyboard layout set doesn't get reloaded in this method. 842 switcher.resetKeyboardStateToAlphabet(); 843 // In apps like Talk, we come here when the text is sent and the field gets emptied and 844 // we need to re-evaluate the shift state, but not the whole layout which would be 845 // disruptive. 846 // Space state must be updated before calling updateShiftState 847 switcher.updateShiftState(); 848 } 849 setSuggestionStripShownInternal( 850 isSuggestionsStripVisible(), /* needsInputViewShown */ false); 851 852 mInputLogic.mLastSelectionStart = editorInfo.initialSelStart; 853 mInputLogic.mLastSelectionEnd = editorInfo.initialSelEnd; 854 // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying 855 // so we try using some heuristics to find out about these and fix them. 856 tryFixLyingCursorPosition(); 857 858 mHandler.cancelUpdateSuggestionStrip(); 859 mHandler.cancelDoubleSpacePeriodTimer(); 860 861 mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable); 862 mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn, 863 currentSettingsValues.mKeyPreviewPopupDismissDelay); 864 mainKeyboardView.setSlidingKeyInputPreviewEnabled( 865 currentSettingsValues.mSlidingKeyInputPreviewEnabled); 866 mainKeyboardView.setGestureHandlingEnabledByUser( 867 currentSettingsValues.mGestureInputEnabled, 868 currentSettingsValues.mGestureTrailEnabled, 869 currentSettingsValues.mGestureFloatingPreviewTextEnabled); 870 871 initPersonalizationDebugSettings(currentSettingsValues); 872 873 if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); 874 } 875 876 /** 877 * Try to get the text from the editor to expose lies the framework may have been 878 * telling us. Concretely, when the device rotates, the frameworks tells us about where the 879 * cursor used to be initially in the editor at the time it first received the focus; this 880 * may be completely different from the place it is upon rotation. Since we don't have any 881 * means to get the real value, try at least to ask the text view for some characters and 882 * detect the most damaging cases: when the cursor position is declared to be much smaller 883 * than it really is. 884 */ 885 private void tryFixLyingCursorPosition() { 886 final CharSequence textBeforeCursor = mInputLogic.mConnection.getTextBeforeCursor( 887 Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); 888 if (null == textBeforeCursor) { 889 mInputLogic.mLastSelectionStart = mInputLogic.mLastSelectionEnd = 890 Constants.NOT_A_CURSOR_POSITION; 891 } else { 892 final int textLength = textBeforeCursor.length(); 893 if (textLength > mInputLogic.mLastSelectionStart 894 || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE 895 && mInputLogic.mLastSelectionStart < 896 Constants.EDITOR_CONTENTS_CACHE_SIZE)) { 897 // It should not be possible to have only one of those variables be 898 // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized 899 // (simple cursor, no selection) or there is no cursor/we don't know its pos 900 final boolean wasEqual = 901 mInputLogic.mLastSelectionStart == mInputLogic.mLastSelectionEnd; 902 mInputLogic.mLastSelectionStart = textLength; 903 // We can't figure out the value of mLastSelectionEnd :( 904 // But at least if it's smaller than mLastSelectionStart something is wrong, 905 // and if they used to be equal we also don't want to make it look like there is a 906 // selection. 907 if (wasEqual || mInputLogic.mLastSelectionStart > mInputLogic.mLastSelectionEnd) { 908 mInputLogic.mLastSelectionEnd = mInputLogic.mLastSelectionStart; 909 } 910 } 911 } 912 } 913 914 // Initialization of personalization debug settings. This must be called inside 915 // onStartInputView. 916 private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) { 917 if (mUseOnlyPersonalizationDictionaryForDebug 918 != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) { 919 // Only for debug 920 initSuggest(); 921 mUseOnlyPersonalizationDictionaryForDebug = 922 currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug; 923 } 924 925 if (mBoostPersonalizationDictionaryForDebug != 926 currentSettingsValues.mBoostPersonalizationDictionaryForDebug) { 927 // Only for debug 928 mBoostPersonalizationDictionaryForDebug = 929 currentSettingsValues.mBoostPersonalizationDictionaryForDebug; 930 } 931 } 932 933 // Callback for the TargetPackageInfoGetterTask 934 @Override 935 public void onTargetPackageInfoKnown(final PackageInfo info) { 936 mAppWorkAroundsUtils.setPackageInfo(info); 937 } 938 939 @Override 940 public void onWindowHidden() { 941 super.onWindowHidden(); 942 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 943 if (mainKeyboardView != null) { 944 mainKeyboardView.closing(); 945 } 946 } 947 948 private void onFinishInputInternal() { 949 super.onFinishInput(); 950 951 LatinImeLogger.commit(); 952 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 953 if (mainKeyboardView != null) { 954 mainKeyboardView.closing(); 955 } 956 } 957 958 private void onFinishInputViewInternal(final boolean finishingInput) { 959 super.onFinishInputView(finishingInput); 960 mKeyboardSwitcher.onFinishInputView(); 961 mKeyboardSwitcher.deallocateMemory(); 962 // Remove pending messages related to update suggestions 963 mHandler.cancelUpdateSuggestionStrip(); 964 // Should do the following in onFinishInputInternal but until JB MR2 it's not called :( 965 if (mInputLogic.mWordComposer.isComposingWord()) { 966 mInputLogic.mConnection.finishComposingText(); 967 } 968 mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); 969 // Notify ResearchLogger 970 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 971 ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, 972 mInputLogic.mLastSelectionStart, 973 mInputLogic.mLastSelectionEnd, getCurrentInputConnection()); 974 } 975 } 976 977 @Override 978 public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, 979 final int newSelStart, final int newSelEnd, 980 final int composingSpanStart, final int composingSpanEnd) { 981 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 982 composingSpanStart, composingSpanEnd); 983 if (DEBUG) { 984 Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart 985 + ", ose=" + oldSelEnd 986 + ", lss=" + mInputLogic.mLastSelectionStart 987 + ", lse=" + mInputLogic.mLastSelectionEnd 988 + ", nss=" + newSelStart 989 + ", nse=" + newSelEnd 990 + ", cs=" + composingSpanStart 991 + ", ce=" + composingSpanEnd); 992 } 993 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 994 ResearchLogger.latinIME_onUpdateSelection(mInputLogic.mLastSelectionStart, 995 mInputLogic.mLastSelectionEnd, 996 oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, 997 composingSpanEnd, mInputLogic.mConnection); 998 } 999 1000 final boolean selectionChanged = mInputLogic.mLastSelectionStart != newSelStart 1001 || mInputLogic.mLastSelectionEnd != newSelEnd; 1002 1003 // if composingSpanStart and composingSpanEnd are -1, it means there is no composing 1004 // span in the view - we can use that to narrow down whether the cursor was moved 1005 // by us or not. If we are composing a word but there is no composing span, then 1006 // we know for sure the cursor moved while we were composing and we should reset 1007 // the state. TODO: rescind this policy: the framework never removes the composing 1008 // span on its own accord while editing. This test is useless. 1009 final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; 1010 1011 // If the keyboard is not visible, we don't need to do all the housekeeping work, as it 1012 // will be reset when the keyboard shows up anyway. 1013 // TODO: revisit this when LatinIME supports hardware keyboards. 1014 // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown(). 1015 // TODO: find a better way to simulate actual execution. 1016 if (isInputViewShown() && !mInputLogic.mConnection.isBelatedExpectedUpdate(oldSelStart, 1017 newSelStart, oldSelEnd, newSelEnd)) { 1018 // TODO: the following is probably better done in resetEntireInputState(). 1019 // it should only happen when the cursor moved, and the very purpose of the 1020 // test below is to narrow down whether this happened or not. Likewise with 1021 // the call to updateShiftState. 1022 // We set this to NONE because after a cursor move, we don't want the space 1023 // state-related special processing to kick in. 1024 mInputLogic.mSpaceState = SpaceState.NONE; 1025 1026 // TODO: is it still necessary to test for composingSpan related stuff? 1027 final boolean selectionChangedOrSafeToReset = selectionChanged 1028 || (!mInputLogic.mWordComposer.isComposingWord()) || noComposingSpan; 1029 final boolean hasOrHadSelection = (oldSelStart != oldSelEnd 1030 || newSelStart != newSelEnd); 1031 final int moveAmount = newSelStart - oldSelStart; 1032 if (selectionChangedOrSafeToReset && (hasOrHadSelection 1033 || !mInputLogic.mWordComposer.moveCursorByAndReturnIfInsideComposingWord( 1034 moveAmount))) { 1035 // If we are composing a word and moving the cursor, we would want to set a 1036 // suggestion span for recorrection to work correctly. Unfortunately, that 1037 // would involve the keyboard committing some new text, which would move the 1038 // cursor back to where it was. Latin IME could then fix the position of the cursor 1039 // again, but the asynchronous nature of the calls results in this wreaking havoc 1040 // with selection on double tap and the like. 1041 // Another option would be to send suggestions each time we set the composing 1042 // text, but that is probably too expensive to do, so we decided to leave things 1043 // as is. 1044 mInputLogic.resetEntireInputState(mSettings.getCurrent(), newSelStart, newSelEnd); 1045 } else { 1046 // resetEntireInputState calls resetCachesUponCursorMove, but forcing the 1047 // composition to end. But in all cases where we don't reset the entire input 1048 // state, we still want to tell the rich input connection about the new cursor 1049 // position so that it can update its caches. 1050 mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( 1051 newSelStart, newSelEnd, false /* shouldFinishComposition */); 1052 } 1053 1054 // We moved the cursor. If we are touching a word, we need to resume suggestion, 1055 // unless suggestions are off. 1056 if (isSuggestionsStripVisible()) { 1057 mHandler.postResumeSuggestions(); 1058 } 1059 // Reset the last recapitalization. 1060 mInputLogic.mRecapitalizeStatus.deactivate(); 1061 mKeyboardSwitcher.updateShiftState(); 1062 } 1063 1064 // Make a note of the cursor position 1065 mInputLogic.mLastSelectionStart = newSelStart; 1066 mInputLogic.mLastSelectionEnd = newSelEnd; 1067 mSubtypeState.currentSubtypeUsed(); 1068 } 1069 1070 /** 1071 * This is called when the user has clicked on the extracted text view, 1072 * when running in fullscreen mode. The default implementation hides 1073 * the suggestions view when this happens, but only if the extracted text 1074 * editor has a vertical scroll bar because its text doesn't fit. 1075 * Here we override the behavior due to the possibility that a re-correction could 1076 * cause the suggestions strip to disappear and re-appear. 1077 */ 1078 @Override 1079 public void onExtractedTextClicked() { 1080 if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; 1081 1082 super.onExtractedTextClicked(); 1083 } 1084 1085 /** 1086 * This is called when the user has performed a cursor movement in the 1087 * extracted text view, when it is running in fullscreen mode. The default 1088 * implementation hides the suggestions view when a vertical movement 1089 * happens, but only if the extracted text editor has a vertical scroll bar 1090 * because its text doesn't fit. 1091 * Here we override the behavior due to the possibility that a re-correction could 1092 * cause the suggestions strip to disappear and re-appear. 1093 */ 1094 @Override 1095 public void onExtractedCursorMovement(final int dx, final int dy) { 1096 if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; 1097 1098 super.onExtractedCursorMovement(dx, dy); 1099 } 1100 1101 @Override 1102 public void hideWindow() { 1103 LatinImeLogger.commit(); 1104 mKeyboardSwitcher.onHideWindow(); 1105 1106 if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { 1107 AccessibleKeyboardViewProxy.getInstance().onHideWindow(); 1108 } 1109 1110 if (TRACE) Debug.stopMethodTracing(); 1111 if (mOptionsDialog != null && mOptionsDialog.isShowing()) { 1112 mOptionsDialog.dismiss(); 1113 mOptionsDialog = null; 1114 } 1115 super.hideWindow(); 1116 } 1117 1118 @Override 1119 public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) { 1120 if (DEBUG) { 1121 Log.i(TAG, "Received completions:"); 1122 if (applicationSpecifiedCompletions != null) { 1123 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { 1124 Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); 1125 } 1126 } 1127 } 1128 if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return; 1129 if (applicationSpecifiedCompletions == null) { 1130 clearSuggestionStrip(); 1131 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1132 ResearchLogger.latinIME_onDisplayCompletions(null); 1133 } 1134 return; 1135 } 1136 mApplicationSpecifiedCompletions = 1137 CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions); 1138 1139 final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = 1140 SuggestedWords.getFromApplicationSpecifiedCompletions( 1141 applicationSpecifiedCompletions); 1142 final SuggestedWords suggestedWords = new SuggestedWords( 1143 applicationSuggestedWords, 1144 false /* typedWordValid */, 1145 false /* hasAutoCorrectionCandidate */, 1146 false /* isPunctuationSuggestions */, 1147 false /* isObsoleteSuggestions */, 1148 false /* isPrediction */); 1149 // When in fullscreen mode, show completions generated by the application 1150 final boolean isAutoCorrection = false; 1151 setSuggestedWords(suggestedWords, isAutoCorrection); 1152 setAutoCorrectionIndicator(isAutoCorrection); 1153 setSuggestionStripShown(true); 1154 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1155 ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); 1156 } 1157 } 1158 1159 private void setSuggestionStripShownInternal(final boolean shown, 1160 final boolean needsInputViewShown) { 1161 // TODO: Modify this if we support suggestions with hard keyboard 1162 if (onEvaluateInputViewShown() && mSuggestionStripView != null) { 1163 final boolean inputViewShown = mKeyboardSwitcher.isShowingMainKeyboardOrEmojiPalettes(); 1164 final boolean shouldShowSuggestions = shown 1165 && (needsInputViewShown ? inputViewShown : true); 1166 if (isFullscreenMode()) { 1167 mSuggestionStripView.setVisibility( 1168 shouldShowSuggestions ? View.VISIBLE : View.GONE); 1169 } else { 1170 mSuggestionStripView.setVisibility( 1171 shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); 1172 } 1173 } 1174 } 1175 1176 private void setSuggestionStripShown(final boolean shown) { 1177 setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); 1178 } 1179 1180 private int getAdjustedBackingViewHeight() { 1181 final int currentHeight = mKeyPreviewBackingView.getHeight(); 1182 if (currentHeight > 0) { 1183 return currentHeight; 1184 } 1185 1186 final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); 1187 if (visibleKeyboardView == null) { 1188 return 0; 1189 } 1190 // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main !!! 1191 // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 1192 final int keyboardHeight = visibleKeyboardView.getHeight(); 1193 final int suggestionsHeight = mSuggestionStripView.getHeight(); 1194 final int displayHeight = getResources().getDisplayMetrics().heightPixels; 1195 final Rect rect = new Rect(); 1196 mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect); 1197 final int notificationBarHeight = rect.top; 1198 final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight 1199 - keyboardHeight; 1200 1201 final LayoutParams params = mKeyPreviewBackingView.getLayoutParams(); 1202 params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); 1203 mKeyPreviewBackingView.setLayoutParams(params); 1204 return params.height; 1205 } 1206 1207 @Override 1208 public void onComputeInsets(final InputMethodService.Insets outInsets) { 1209 super.onComputeInsets(outInsets); 1210 final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); 1211 if (visibleKeyboardView == null || mSuggestionStripView == null) { 1212 return; 1213 } 1214 final int adjustedBackingHeight = getAdjustedBackingViewHeight(); 1215 final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE); 1216 final int backingHeight = backingGone ? 0 : adjustedBackingHeight; 1217 // In fullscreen mode, the height of the extract area managed by InputMethodService should 1218 // be considered. 1219 // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. 1220 final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; 1221 final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0 1222 : mSuggestionStripView.getHeight(); 1223 final int extraHeight = extractHeight + backingHeight + suggestionsHeight; 1224 int visibleTopY = extraHeight; 1225 // Need to set touchable region only if input view is being shown 1226 if (visibleKeyboardView.isShown()) { 1227 // Note that the height of Emoji layout is the same as the height of the main keyboard 1228 // and the suggestion strip 1229 if (mKeyboardSwitcher.isShowingEmojiPalettes() 1230 || mSuggestionStripView.getVisibility() == View.VISIBLE) { 1231 visibleTopY -= suggestionsHeight; 1232 } 1233 final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; 1234 final int touchWidth = visibleKeyboardView.getWidth(); 1235 final int touchHeight = visibleKeyboardView.getHeight() + extraHeight 1236 // Extend touchable region below the keyboard. 1237 + EXTENDED_TOUCHABLE_REGION_HEIGHT; 1238 outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; 1239 outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight); 1240 } 1241 outInsets.contentTopInsets = visibleTopY; 1242 outInsets.visibleTopInsets = visibleTopY; 1243 } 1244 1245 @Override 1246 public boolean onEvaluateFullscreenMode() { 1247 // Reread resource value here, because this method is called by framework anytime as needed. 1248 final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources()); 1249 if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) { 1250 // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI 1251 // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI 1252 // without NO_FULLSCREEN doesn't work as expected. Because of this we need this 1253 // hack for now. Let's get rid of this once the framework gets fixed. 1254 final EditorInfo ei = getCurrentInputEditorInfo(); 1255 return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0)); 1256 } else { 1257 return false; 1258 } 1259 } 1260 1261 @Override 1262 public void updateFullscreenMode() { 1263 super.updateFullscreenMode(); 1264 1265 if (mKeyPreviewBackingView == null) return; 1266 // In fullscreen mode, no need to have extra space to show the key preview. 1267 // If not, we should have extra space above the keyboard to show the key preview. 1268 mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); 1269 } 1270 1271 // Called from the KeyboardSwitcher which needs to know auto caps state to display 1272 // the right layout. 1273 // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead? 1274 public int getCurrentAutoCapsState() { 1275 return mInputLogic.getCurrentAutoCapsState(null /* optionalSettingsValues */); 1276 } 1277 1278 // Called from the KeyboardSwitcher which needs to know recaps state to display 1279 // the right layout. 1280 // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead? 1281 public int getCurrentRecapitalizeState() { 1282 return mInputLogic.getCurrentRecapitalizeState(); 1283 } 1284 1285 // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is 1286 // pressed. 1287 @Override 1288 public void addWordToUserDictionary(final String word) { 1289 if (TextUtils.isEmpty(word)) { 1290 // Probably never supposed to happen, but just in case. 1291 return; 1292 } 1293 final String wordToEdit; 1294 if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) { 1295 wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); 1296 } else { 1297 wordToEdit = word; 1298 } 1299 mUserDictionary.addWordToUserDictionary(wordToEdit); 1300 } 1301 1302 public void displaySettingsDialog() { 1303 if (isShowingOptionDialog()) return; 1304 showSubtypeSelectorAndSettings(); 1305 } 1306 1307 @Override 1308 public boolean onCustomRequest(final int requestCode) { 1309 if (isShowingOptionDialog()) return false; 1310 switch (requestCode) { 1311 case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER: 1312 if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { 1313 mRichImm.getInputMethodManager().showInputMethodPicker(); 1314 return true; 1315 } 1316 return false; 1317 } 1318 return false; 1319 } 1320 1321 private boolean isShowingOptionDialog() { 1322 return mOptionsDialog != null && mOptionsDialog.isShowing(); 1323 } 1324 1325 // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. 1326 public void switchToNextSubtype() { 1327 final IBinder token = getWindow().getWindow().getAttributes().token; 1328 if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) { 1329 mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); 1330 return; 1331 } 1332 mSubtypeState.switchSubtype(token, mRichImm); 1333 } 1334 1335 // Implementation of {@link KeyboardActionListener}. 1336 @Override 1337 public void onCodeInput(final int primaryCode, final int x, final int y) { 1338 mInputLogic.onCodeInput(primaryCode, x, y, mHandler, mKeyboardSwitcher, mSubtypeSwitcher); 1339 } 1340 1341 // Called from PointerTracker through the KeyboardActionListener interface 1342 // TODO[IL]: Move this to InputLogic 1343 @Override 1344 public void onTextInput(final String rawText) { 1345 mInputLogic.mConnection.beginBatchEdit(); 1346 if (mInputLogic.mWordComposer.isComposingWord()) { 1347 mInputLogic.commitCurrentAutoCorrection(mSettings.getCurrent(), rawText, mHandler); 1348 } else { 1349 mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); 1350 } 1351 mHandler.postUpdateSuggestionStrip(); 1352 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS 1353 && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) { 1354 ResearchLogger.getInstance().onResearchKeySelected(this); 1355 return; 1356 } 1357 final String text = specificTldProcessingOnTextInput(rawText); 1358 if (SpaceState.PHANTOM == mInputLogic.mSpaceState) { 1359 mInputLogic.promotePhantomSpace(mSettings.getCurrent()); 1360 } 1361 mInputLogic.mConnection.commitText(text, 1); 1362 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1363 ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */); 1364 } 1365 mInputLogic.mConnection.endBatchEdit(); 1366 // Space state must be updated before calling updateShiftState 1367 mInputLogic.mSpaceState = SpaceState.NONE; 1368 mKeyboardSwitcher.updateShiftState(); 1369 mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT); 1370 mInputLogic.mEnteredText = text; 1371 } 1372 1373 @Override 1374 public void onStartBatchInput() { 1375 mInputUpdater.onStartBatchInput(); 1376 mHandler.cancelUpdateSuggestionStrip(); 1377 mInputLogic.mConnection.beginBatchEdit(); 1378 final SettingsValues currentSettingsValues = mSettings.getCurrent(); 1379 if (mInputLogic.mWordComposer.isComposingWord()) { 1380 if (currentSettingsValues.mIsInternal) { 1381 if (mInputLogic.mWordComposer.isBatchMode()) { 1382 LatinImeLoggerUtils.onAutoCorrection("", 1383 mInputLogic.mWordComposer.getTypedWord(), " ", 1384 mInputLogic.mWordComposer); 1385 } 1386 } 1387 final int wordComposerSize = mInputLogic.mWordComposer.size(); 1388 // Since isComposingWord() is true, the size is at least 1. 1389 if (mInputLogic.mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 1390 // If we are in the middle of a recorrection, we need to commit the recorrection 1391 // first so that we can insert the batch input at the current cursor position. 1392 mInputLogic.resetEntireInputState(currentSettingsValues, 1393 mInputLogic.mLastSelectionStart, mInputLogic.mLastSelectionEnd); 1394 } else if (wordComposerSize <= 1) { 1395 // We auto-correct the previous (typed, not gestured) string iff it's one character 1396 // long. The reason for this is, even in the middle of gesture typing, you'll still 1397 // tap one-letter words and you want them auto-corrected (typically, "i" in English 1398 // should become "I"). However for any longer word, we assume that the reason for 1399 // tapping probably is that the word you intend to type is not in the dictionary, 1400 // so we do not attempt to correct, on the assumption that if that was a dictionary 1401 // word, the user would probably have gestured instead. 1402 mInputLogic.commitCurrentAutoCorrection(currentSettingsValues, 1403 LastComposedWord.NOT_A_SEPARATOR, mHandler); 1404 } else { 1405 mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR); 1406 } 1407 } 1408 final int codePointBeforeCursor = mInputLogic.mConnection.getCodePointBeforeCursor(); 1409 if (Character.isLetterOrDigit(codePointBeforeCursor) 1410 || currentSettingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) { 1411 final boolean autoShiftHasBeenOverriden = mKeyboardSwitcher.getKeyboardShiftMode() != 1412 getCurrentAutoCapsState(); 1413 mInputLogic.mSpaceState = SpaceState.PHANTOM; 1414 if (!autoShiftHasBeenOverriden) { 1415 // When we change the space state, we need to update the shift state of the 1416 // keyboard unless it has been overridden manually. This is happening for example 1417 // after typing some letters and a period, then gesturing; the keyboard is not in 1418 // caps mode yet, but since a gesture is starting, it should go in caps mode, 1419 // unless the user explictly said it should not. 1420 mKeyboardSwitcher.updateShiftState(); 1421 } 1422 } 1423 mInputLogic.mConnection.endBatchEdit(); 1424 mInputLogic.mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( 1425 mInputLogic.getActualCapsMode(currentSettingsValues, mKeyboardSwitcher), 1426 // Prev word is 1st word before cursor 1427 mInputLogic.getNthPreviousWordForSuggestion(currentSettingsValues, 1428 1 /* nthPreviousWord */)); 1429 } 1430 1431 static final class InputUpdater implements Handler.Callback { 1432 private final Handler mHandler; 1433 private final LatinIME mLatinIme; 1434 private final Object mLock = new Object(); 1435 private boolean mInBatchInput; // synchronized using {@link #mLock}. 1436 1437 InputUpdater(final LatinIME latinIme) { 1438 final HandlerThread handlerThread = new HandlerThread( 1439 InputUpdater.class.getSimpleName()); 1440 handlerThread.start(); 1441 mHandler = new Handler(handlerThread.getLooper(), this); 1442 mLatinIme = latinIme; 1443 } 1444 1445 private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; 1446 private static final int MSG_GET_SUGGESTED_WORDS = 2; 1447 1448 @Override 1449 public boolean handleMessage(final Message msg) { 1450 // TODO: straighten message passing - we don't need two kinds of messages calling 1451 // each other. 1452 switch (msg.what) { 1453 case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: 1454 updateBatchInput((InputPointers)msg.obj, msg.arg2 /* sequenceNumber */); 1455 break; 1456 case MSG_GET_SUGGESTED_WORDS: 1457 mLatinIme.getSuggestedWords(msg.arg1 /* sessionId */, 1458 msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj); 1459 break; 1460 } 1461 return true; 1462 } 1463 1464 // Run in the UI thread. 1465 public void onStartBatchInput() { 1466 synchronized (mLock) { 1467 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 1468 mInBatchInput = true; 1469 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1470 SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); 1471 } 1472 } 1473 1474 // Run in the Handler thread. 1475 private void updateBatchInput(final InputPointers batchPointers, final int sequenceNumber) { 1476 synchronized (mLock) { 1477 if (!mInBatchInput) { 1478 // Batch input has ended or canceled while the message was being delivered. 1479 return; 1480 } 1481 1482 getSuggestedWordsGestureLocked(batchPointers, sequenceNumber, 1483 new OnGetSuggestedWordsCallback() { 1484 @Override 1485 public void onGetSuggestedWords(final SuggestedWords suggestedWords) { 1486 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1487 suggestedWords, false /* dismissGestureFloatingPreviewText */); 1488 } 1489 }); 1490 } 1491 } 1492 1493 // Run in the UI thread. 1494 public void onUpdateBatchInput(final InputPointers batchPointers, 1495 final int sequenceNumber) { 1496 if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) { 1497 return; 1498 } 1499 mHandler.obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 0 /* arg1 */, 1500 sequenceNumber /* arg2 */, batchPointers /* obj */).sendToTarget(); 1501 } 1502 1503 public void onCancelBatchInput() { 1504 synchronized (mLock) { 1505 mInBatchInput = false; 1506 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1507 SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); 1508 } 1509 } 1510 1511 // Run in the UI thread. 1512 public void onEndBatchInput(final InputPointers batchPointers) { 1513 synchronized(mLock) { 1514 getSuggestedWordsGestureLocked(batchPointers, SuggestedWords.NOT_A_SEQUENCE_NUMBER, 1515 new OnGetSuggestedWordsCallback() { 1516 @Override 1517 public void onGetSuggestedWords(final SuggestedWords suggestedWords) { 1518 mInBatchInput = false; 1519 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords, 1520 true /* dismissGestureFloatingPreviewText */); 1521 mLatinIme.mHandler.onEndBatchInput(suggestedWords); 1522 } 1523 }); 1524 } 1525 } 1526 1527 // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to 1528 // be synchronized. 1529 private void getSuggestedWordsGestureLocked(final InputPointers batchPointers, 1530 final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { 1531 mLatinIme.mInputLogic.mWordComposer.setBatchInputPointers(batchPointers); 1532 mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE, 1533 sequenceNumber, new OnGetSuggestedWordsCallback() { 1534 @Override 1535 public void onGetSuggestedWords(SuggestedWords suggestedWords) { 1536 final int suggestionCount = suggestedWords.size(); 1537 if (suggestionCount <= 1) { 1538 final String mostProbableSuggestion = (suggestionCount == 0) ? null 1539 : suggestedWords.getWord(0); 1540 callback.onGetSuggestedWords( 1541 mLatinIme.getOlderSuggestions(mostProbableSuggestion)); 1542 } 1543 callback.onGetSuggestedWords(suggestedWords); 1544 } 1545 }); 1546 } 1547 1548 public void getSuggestedWords(final int sessionId, final int sequenceNumber, 1549 final OnGetSuggestedWordsCallback callback) { 1550 mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback) 1551 .sendToTarget(); 1552 } 1553 1554 void quitLooper() { 1555 mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS); 1556 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 1557 mHandler.getLooper().quit(); 1558 } 1559 } 1560 1561 // This method must run in UI Thread. 1562 private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, 1563 final boolean dismissGestureFloatingPreviewText) { 1564 showSuggestionStrip(suggestedWords); 1565 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1566 mainKeyboardView.showGestureFloatingPreviewText(suggestedWords); 1567 if (dismissGestureFloatingPreviewText) { 1568 mainKeyboardView.dismissGestureFloatingPreviewText(); 1569 } 1570 } 1571 1572 /* The sequence number member is only used in onUpdateBatchInput. It is increased each time 1573 * auto-commit happens. The reason we need this is, when auto-commit happens we trim the 1574 * input pointers that are held in a singleton, and to know how much to trim we rely on the 1575 * results of the suggestion process that is held in mSuggestedWords. 1576 * However, the suggestion process is asynchronous, and sometimes we may enter the 1577 * onUpdateBatchInput method twice without having recomputed suggestions yet, or having 1578 * received new suggestions generated from not-yet-trimmed input pointers. In this case, the 1579 * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we 1580 * remove an unrelated number of pointers (possibly even more than are left in the input 1581 * pointers, leading to a crash). 1582 * To avoid that, we increase the sequence number each time we auto-commit and trim the 1583 * input pointers, and we do not use any suggested words that have been generated with an 1584 * earlier sequence number. 1585 */ 1586 private int mAutoCommitSequenceNumber = 1; 1587 @Override 1588 public void onUpdateBatchInput(final InputPointers batchPointers) { 1589 final SettingsValues settingsValues = mSettings.getCurrent(); 1590 if (settingsValues.mPhraseGestureEnabled) { 1591 final SuggestedWordInfo candidate = 1592 mInputLogic.mSuggestedWords.getAutoCommitCandidate(); 1593 // If these suggested words have been generated with out of date input pointers, then 1594 // we skip auto-commit (see comments above on the mSequenceNumber member). 1595 if (null != candidate 1596 && mInputLogic.mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { 1597 if (candidate.mSourceDict.shouldAutoCommit(candidate)) { 1598 final String[] commitParts = candidate.mWord.split(" ", 2); 1599 batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); 1600 mInputLogic.promotePhantomSpace(mSettings.getCurrent()); 1601 mInputLogic.mConnection.commitText(commitParts[0], 0); 1602 mInputLogic.mSpaceState = SpaceState.PHANTOM; 1603 mKeyboardSwitcher.updateShiftState(); 1604 mInputLogic.mWordComposer. 1605 setCapitalizedModeAndPreviousWordAtStartComposingTime( 1606 mInputLogic.getActualCapsMode(settingsValues, mKeyboardSwitcher), 1607 commitParts[0]); 1608 ++mAutoCommitSequenceNumber; 1609 } 1610 } 1611 } 1612 mInputUpdater.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber); 1613 } 1614 1615 // This method must run in UI Thread. 1616 public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) { 1617 final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); 1618 if (TextUtils.isEmpty(batchInputText)) { 1619 return; 1620 } 1621 mInputLogic.mConnection.beginBatchEdit(); 1622 if (SpaceState.PHANTOM == mInputLogic.mSpaceState) { 1623 mInputLogic.promotePhantomSpace(mSettings.getCurrent()); 1624 } 1625 if (mSettings.getCurrent().mPhraseGestureEnabled) { 1626 // Find the last space 1627 final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; 1628 if (0 != indexOfLastSpace) { 1629 mInputLogic.mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1630 1); 1631 showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture()); 1632 } 1633 final String lastWord = batchInputText.substring(indexOfLastSpace); 1634 mInputLogic.mWordComposer.setBatchInputWord(lastWord); 1635 mInputLogic.mConnection.setComposingText(lastWord, 1); 1636 } else { 1637 mInputLogic.mWordComposer.setBatchInputWord(batchInputText); 1638 mInputLogic.mConnection.setComposingText(batchInputText, 1); 1639 } 1640 mInputLogic.mConnection.endBatchEdit(); 1641 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1642 ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); 1643 } 1644 // Space state must be updated before calling updateShiftState 1645 mInputLogic.mSpaceState = SpaceState.PHANTOM; 1646 mKeyboardSwitcher.updateShiftState(); 1647 } 1648 1649 @Override 1650 public void onEndBatchInput(final InputPointers batchPointers) { 1651 mInputUpdater.onEndBatchInput(batchPointers); 1652 } 1653 1654 private String specificTldProcessingOnTextInput(final String text) { 1655 if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD 1656 || !Character.isLetter(text.charAt(1))) { 1657 // Not a tld: do nothing. 1658 return text; 1659 } 1660 // We have a TLD (or something that looks like this): make sure we don't add 1661 // a space even if currently in phantom mode. 1662 mInputLogic.mSpaceState = SpaceState.NONE; 1663 // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code 1664 final CharSequence lastOne = mInputLogic.mConnection.getTextBeforeCursor(1, 0); 1665 if (lastOne != null && lastOne.length() == 1 1666 && lastOne.charAt(0) == Constants.CODE_PERIOD) { 1667 return text.substring(1); 1668 } else { 1669 return text; 1670 } 1671 } 1672 1673 // Called from PointerTracker through the KeyboardActionListener interface 1674 @Override 1675 public void onFinishSlidingInput() { 1676 // User finished sliding input. 1677 mKeyboardSwitcher.onFinishSlidingInput(); 1678 } 1679 1680 // Called from PointerTracker through the KeyboardActionListener interface 1681 @Override 1682 public void onCancelInput() { 1683 // User released a finger outside any key 1684 // Nothing to do so far. 1685 } 1686 1687 @Override 1688 public void onCancelBatchInput() { 1689 mInputUpdater.onCancelBatchInput(); 1690 } 1691 1692 // TODO[IL]: Rename this to avoid using handle* 1693 private void handleClose() { 1694 // TODO: Verify that words are logged properly when IME is closed. 1695 mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR); 1696 requestHideSelf(0); 1697 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1698 if (mainKeyboardView != null) { 1699 mainKeyboardView.closing(); 1700 } 1701 } 1702 1703 // TODO[IL]: Move this to InputLogic and make it private 1704 // Outside LatinIME, only used by the test suite. 1705 @UsedForTesting 1706 public boolean isShowingPunctuationList() { 1707 if (mInputLogic.mSuggestedWords == null) return false; 1708 return mSettings.getCurrent().mSuggestPuncList == mInputLogic.mSuggestedWords; 1709 } 1710 1711 private boolean isSuggestionsStripVisible() { 1712 final SettingsValues currentSettings = mSettings.getCurrent(); 1713 if (mSuggestionStripView == null) 1714 return false; 1715 if (mSuggestionStripView.isShowingAddToDictionaryHint()) 1716 return true; 1717 if (null == currentSettings) 1718 return false; 1719 if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation)) 1720 return false; 1721 if (currentSettings.isApplicationSpecifiedCompletionsOn()) 1722 return true; 1723 return currentSettings.isSuggestionsRequested(mDisplayOrientation); 1724 } 1725 1726 public void dismissAddToDictionaryHint() { 1727 if (null != mSuggestionStripView) { 1728 mSuggestionStripView.dismissAddToDictionaryHint(); 1729 } 1730 } 1731 1732 // TODO[IL]: Define a clear interface for this 1733 public void clearSuggestionStrip() { 1734 setSuggestedWords(SuggestedWords.EMPTY, false); 1735 setAutoCorrectionIndicator(false); 1736 } 1737 1738 // TODO[IL]: Define a clear interface for this 1739 public void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { 1740 mInputLogic.mSuggestedWords = words; 1741 if (mSuggestionStripView != null) { 1742 mSuggestionStripView.setSuggestions(words); 1743 mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); 1744 } 1745 } 1746 1747 private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { 1748 // Put a blue underline to a word in TextView which will be auto-corrected. 1749 if (mInputLogic.mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator 1750 && mInputLogic.mWordComposer.isComposingWord()) { 1751 mInputLogic.mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; 1752 final CharSequence textWithUnderline = 1753 mInputLogic.getTextWithUnderline(mInputLogic.mWordComposer.getTypedWord()); 1754 // TODO: when called from an updateSuggestionStrip() call that results from a posted 1755 // message, this is called outside any batch edit. Potentially, this may result in some 1756 // janky flickering of the screen, although the display speed makes it unlikely in 1757 // the practice. 1758 mInputLogic.mConnection.setComposingText(textWithUnderline, 1); 1759 } 1760 } 1761 1762 // TODO[IL]: Move this to InputLogic and make private again 1763 public void updateSuggestionStrip() { 1764 mHandler.cancelUpdateSuggestionStrip(); 1765 final SettingsValues currentSettings = mSettings.getCurrent(); 1766 1767 // Check if we have a suggestion engine attached. 1768 if (mInputLogic.mSuggest == null 1769 || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) { 1770 if (mInputLogic.mWordComposer.isComposingWord()) { 1771 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " 1772 + "requested!"); 1773 } 1774 return; 1775 } 1776 1777 if (!mInputLogic.mWordComposer.isComposingWord() 1778 && !currentSettings.mBigramPredictionEnabled) { 1779 setPunctuationSuggestions(); 1780 return; 1781 } 1782 1783 final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>(); 1784 getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING, 1785 SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { 1786 @Override 1787 public void onGetSuggestedWords(final SuggestedWords suggestedWords) { 1788 holder.set(suggestedWords); 1789 } 1790 } 1791 ); 1792 1793 // This line may cause the current thread to wait. 1794 final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT); 1795 if (suggestedWords != null) { 1796 showSuggestionStrip(suggestedWords); 1797 } 1798 } 1799 1800 private void getSuggestedWords(final int sessionId, final int sequenceNumber, 1801 final OnGetSuggestedWordsCallback callback) { 1802 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1803 final Suggest suggest = mInputLogic.mSuggest; 1804 if (keyboard == null || suggest == null) { 1805 callback.onGetSuggestedWords(SuggestedWords.EMPTY); 1806 return; 1807 } 1808 // Get the word on which we should search the bigrams. If we are composing a word, it's 1809 // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we 1810 // should just skip whitespace if any, so 1. 1811 final SettingsValues currentSettings = mSettings.getCurrent(); 1812 final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues; 1813 1814 if (DEBUG) { 1815 if (mInputLogic.mWordComposer.isComposingWord() 1816 || mInputLogic.mWordComposer.isBatchMode()) { 1817 final String previousWord 1818 = mInputLogic.mWordComposer.getPreviousWordForSuggestion(); 1819 // TODO: this is for checking consistency with older versions. Remove this when 1820 // we are confident this is stable. 1821 // We're checking the previous word in the text field against the memorized previous 1822 // word. If we are composing a word we should have the second word before the cursor 1823 // memorized, otherwise we should have the first. 1824 final String rereadPrevWord = mInputLogic.getNthPreviousWordForSuggestion( 1825 currentSettings, mInputLogic.mWordComposer.isComposingWord() ? 2 : 1); 1826 if (!TextUtils.equals(previousWord, rereadPrevWord)) { 1827 throw new RuntimeException("Unexpected previous word: " 1828 + previousWord + " <> " + rereadPrevWord); 1829 } 1830 } 1831 } 1832 suggest.getSuggestedWords(mInputLogic.mWordComposer, 1833 mInputLogic.mWordComposer.getPreviousWordForSuggestion(), 1834 keyboard.getProximityInfo(), 1835 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled, 1836 additionalFeaturesOptions, sessionId, sequenceNumber, callback); 1837 } 1838 1839 private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId, 1840 final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { 1841 mInputUpdater.getSuggestedWords(sessionId, sequenceNumber, 1842 new OnGetSuggestedWordsCallback() { 1843 @Override 1844 public void onGetSuggestedWords(SuggestedWords suggestedWords) { 1845 callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions( 1846 mInputLogic.mWordComposer.getTypedWord(), suggestedWords)); 1847 } 1848 }); 1849 } 1850 1851 private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord, 1852 final SuggestedWords suggestedWords) { 1853 // TODO: consolidate this into getSuggestedWords 1854 // We update the suggestion strip only when we have some suggestions to show, i.e. when 1855 // the suggestion count is > 1; else, we leave the old suggestions, with the typed word 1856 // replaced with the new one. However, when the word is a dictionary word, or when the 1857 // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the 1858 // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to 1859 // revert to suggestions - although it is unclear how we can come here if it's displayed. 1860 if (suggestedWords.size() > 1 || typedWord.length() <= 1 1861 || suggestedWords.mTypedWordValid || null == mSuggestionStripView 1862 || mSuggestionStripView.isShowingAddToDictionaryHint()) { 1863 return suggestedWords; 1864 } else { 1865 return getOlderSuggestions(typedWord); 1866 } 1867 } 1868 1869 private SuggestedWords getOlderSuggestions(final String typedWord) { 1870 SuggestedWords previousSuggestedWords = mInputLogic.mSuggestedWords; 1871 if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) { 1872 previousSuggestedWords = SuggestedWords.EMPTY; 1873 } 1874 if (typedWord == null) { 1875 return previousSuggestedWords; 1876 } 1877 final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = 1878 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, 1879 previousSuggestedWords); 1880 return new SuggestedWords(typedWordAndPreviousSuggestions, 1881 false /* typedWordValid */, 1882 false /* hasAutoCorrectionCandidate */, 1883 false /* isPunctuationSuggestions */, 1884 true /* isObsoleteSuggestions */, 1885 false /* isPrediction */); 1886 } 1887 1888 private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { 1889 if (suggestedWords.isEmpty()) return; 1890 final String autoCorrection; 1891 if (suggestedWords.mWillAutoCorrect) { 1892 autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); 1893 } else { 1894 // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD) 1895 // because it may differ from mWordComposer.mTypedWord. 1896 autoCorrection = typedWord; 1897 } 1898 mInputLogic.mWordComposer.setAutoCorrection(autoCorrection); 1899 } 1900 1901 private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, 1902 final String typedWord) { 1903 if (suggestedWords.isEmpty()) { 1904 // No auto-correction is available, clear the cached values. 1905 AccessibilityUtils.getInstance().setAutoCorrection(null, null); 1906 clearSuggestionStrip(); 1907 return; 1908 } 1909 setAutoCorrection(suggestedWords, typedWord); 1910 final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); 1911 setSuggestedWords(suggestedWords, isAutoCorrection); 1912 setAutoCorrectionIndicator(isAutoCorrection); 1913 setSuggestionStripShown(isSuggestionsStripVisible()); 1914 // An auto-correction is available, cache it in accessibility code so 1915 // we can be speak it if the user touches a key that will insert it. 1916 AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord); 1917 } 1918 1919 private void showSuggestionStrip(final SuggestedWords suggestedWords) { 1920 if (suggestedWords.isEmpty()) { 1921 clearSuggestionStrip(); 1922 return; 1923 } 1924 showSuggestionStripWithTypedWord(suggestedWords, 1925 suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)); 1926 } 1927 1928 // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} 1929 // interface 1930 @Override 1931 public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { 1932 final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords; 1933 final String suggestion = suggestionInfo.mWord; 1934 // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput 1935 if (suggestion.length() == 1 && isShowingPunctuationList()) { 1936 // Word separators are suggested before the user inputs something. 1937 // So, LatinImeLogger logs "" as a user's input. 1938 LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); 1939 // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. 1940 final int primaryCode = suggestion.charAt(0); 1941 onCodeInput(primaryCode, 1942 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); 1943 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1944 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, 1945 false /* isBatchMode */, suggestedWords.mIsPrediction); 1946 } 1947 return; 1948 } 1949 1950 mInputLogic.mConnection.beginBatchEdit(); 1951 final SettingsValues currentSettings = mSettings.getCurrent(); 1952 if (SpaceState.PHANTOM == mInputLogic.mSpaceState && suggestion.length() > 0 1953 // In the batch input mode, a manually picked suggested word should just replace 1954 // the current batch input text and there is no need for a phantom space. 1955 && !mInputLogic.mWordComposer.isBatchMode()) { 1956 final int firstChar = Character.codePointAt(suggestion, 0); 1957 if (!currentSettings.isWordSeparator(firstChar) 1958 || currentSettings.isUsuallyPrecededBySpace(firstChar)) { 1959 mInputLogic.promotePhantomSpace(currentSettings); 1960 } 1961 } 1962 1963 if (currentSettings.isApplicationSpecifiedCompletionsOn() 1964 && mApplicationSpecifiedCompletions != null 1965 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 1966 mInputLogic.mSuggestedWords = SuggestedWords.EMPTY; 1967 if (mSuggestionStripView != null) { 1968 mSuggestionStripView.clear(); 1969 } 1970 mKeyboardSwitcher.updateShiftState(); 1971 mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); 1972 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; 1973 mInputLogic.mConnection.commitCompletion(completionInfo); 1974 mInputLogic.mConnection.endBatchEdit(); 1975 return; 1976 } 1977 1978 // We need to log before we commit, because the word composer will store away the user 1979 // typed word. 1980 final String replacedWord = mInputLogic.mWordComposer.getTypedWord(); 1981 LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); 1982 commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, 1983 LastComposedWord.NOT_A_SEPARATOR); 1984 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1985 ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, 1986 mInputLogic.mWordComposer.isBatchMode(), suggestionInfo.mScore, 1987 suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType); 1988 } 1989 mInputLogic.mConnection.endBatchEdit(); 1990 // Don't allow cancellation of manual pick 1991 mInputLogic.mLastComposedWord.deactivate(); 1992 // Space state must be updated before calling updateShiftState 1993 mInputLogic.mSpaceState = SpaceState.PHANTOM; 1994 mKeyboardSwitcher.updateShiftState(); 1995 1996 // We should show the "Touch again to save" hint if the user pressed the first entry 1997 // AND it's in none of our current dictionaries (main, user or otherwise). 1998 // Please note that if mSuggest is null, it means that everything is off: suggestion 1999 // and correction, so we shouldn't try to show the hint 2000 final Suggest suggest = mInputLogic.mSuggest; 2001 final boolean showingAddToDictionaryHint = 2002 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind 2003 || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) 2004 && suggest != null 2005 // If the suggestion is not in the dictionary, the hint should be shown. 2006 && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true); 2007 2008 if (currentSettings.mIsInternal) { 2009 LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE, 2010 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 2011 } 2012 if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) { 2013 mSuggestionStripView.showAddToDictionaryHint( 2014 suggestion, currentSettings.mHintToSaveText); 2015 } else { 2016 // If we're not showing the "Touch again to save", then update the suggestion strip. 2017 mHandler.postUpdateSuggestionStrip(); 2018 } 2019 } 2020 2021 /** 2022 * Commits the chosen word to the text field and saves it for later retrieval. 2023 */ 2024 // TODO[IL]: Move to InputLogic and make public again 2025 public void commitChosenWord(final String chosenWord, final int commitType, 2026 final String separatorString) { 2027 final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords; 2028 mInputLogic.mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( 2029 this, chosenWord, suggestedWords), 1); 2030 // Add the word to the user history dictionary 2031 final String prevWord = addToUserHistoryDictionary(chosenWord); 2032 // TODO: figure out here if this is an auto-correct or if the best word is actually 2033 // what user typed. Note: currently this is done much later in 2034 // LastComposedWord#didCommitTypedWord by string equality of the remembered 2035 // strings. 2036 mInputLogic.mLastComposedWord = mInputLogic.mWordComposer.commitWord(commitType, 2037 chosenWord, separatorString, prevWord); 2038 final boolean shouldDiscardPreviousWordForSuggestion; 2039 if (0 == StringUtils.codePointCount(separatorString)) { 2040 // Separator is 0-length. Discard the word only if the current language has spaces. 2041 shouldDiscardPreviousWordForSuggestion = 2042 mSettings.getCurrent().mCurrentLanguageHasSpaces; 2043 } else { 2044 // Otherwise, we discard if the separator contains any non-whitespace. 2045 shouldDiscardPreviousWordForSuggestion = 2046 !StringUtils.containsOnlyWhitespace(separatorString); 2047 } 2048 if (shouldDiscardPreviousWordForSuggestion) { 2049 mInputLogic.mWordComposer.discardPreviousWordForSuggestion(); 2050 } 2051 } 2052 2053 // TODO[IL]: Define a clean interface for this 2054 public void setPunctuationSuggestions() { 2055 final SettingsValues currentSettings = mSettings.getCurrent(); 2056 if (currentSettings.mBigramPredictionEnabled) { 2057 clearSuggestionStrip(); 2058 } else { 2059 setSuggestedWords(currentSettings.mSuggestPuncList, false); 2060 } 2061 setAutoCorrectionIndicator(false); 2062 setSuggestionStripShown(isSuggestionsStripVisible()); 2063 } 2064 2065 private String addToUserHistoryDictionary(final String suggestion) { 2066 if (TextUtils.isEmpty(suggestion)) return null; 2067 final Suggest suggest = mInputLogic.mSuggest; 2068 if (suggest == null) return null; 2069 2070 // If correction is not enabled, we don't add words to the user history dictionary. 2071 // That's to avoid unintended additions in some sensitive fields, or fields that 2072 // expect to receive non-words. 2073 final SettingsValues currentSettings = mSettings.getCurrent(); 2074 if (!currentSettings.mCorrectionEnabled) return null; 2075 2076 final UserHistoryDictionary userHistoryDictionary = suggest.getUserHistoryDictionary(); 2077 if (userHistoryDictionary == null) return null; 2078 2079 final String prevWord = mInputLogic.mConnection.getNthPreviousWord(currentSettings, 2); 2080 final String secondWord; 2081 if (mInputLogic.mWordComposer.wasAutoCapitalized() 2082 && !mInputLogic.mWordComposer.isMostlyCaps()) { 2083 secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); 2084 } else { 2085 secondWord = suggestion; 2086 } 2087 // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". 2088 // We don't add words with 0-frequency (assuming they would be profanity etc.). 2089 final int maxFreq = AutoCorrectionUtils.getMaxFrequency( 2090 suggest.getUnigramDictionaries(), suggestion); 2091 if (maxFreq == 0) return null; 2092 userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0, 2093 (int)TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis()))); 2094 return prevWord; 2095 } 2096 2097 private boolean isResumableWord(final String word, final SettingsValues settings) { 2098 final int firstCodePoint = word.codePointAt(0); 2099 return settings.isWordCodePoint(firstCodePoint) 2100 && Constants.CODE_SINGLE_QUOTE != firstCodePoint 2101 && Constants.CODE_DASH != firstCodePoint; 2102 } 2103 2104 /** 2105 * Check if the cursor is touching a word. If so, restart suggestions on this word, else 2106 * do nothing. 2107 */ 2108 private void restartSuggestionsOnWordTouchedByCursor() { 2109 // HACK: We may want to special-case some apps that exhibit bad behavior in case of 2110 // recorrection. This is a temporary, stopgap measure that will be removed later. 2111 // TODO: remove this. 2112 if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return; 2113 // A simple way to test for support from the TextView. 2114 if (!isSuggestionsStripVisible()) return; 2115 // Recorrection is not supported in languages without spaces because we don't know 2116 // how to segment them yet. 2117 if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return; 2118 // If the cursor is not touching a word, or if there is a selection, return right away. 2119 if (mInputLogic.mLastSelectionStart != mInputLogic.mLastSelectionEnd) return; 2120 // If we don't know the cursor location, return. 2121 if (mInputLogic.mLastSelectionStart < 0) return; 2122 final SettingsValues currentSettings = mSettings.getCurrent(); 2123 if (!mInputLogic.mConnection.isCursorTouchingWord(currentSettings)) return; 2124 final TextRange range = mInputLogic.mConnection.getWordRangeAtCursor( 2125 currentSettings.mWordSeparators, 0 /* additionalPrecedingWordsCount */); 2126 if (null == range) return; // Happens if we don't have an input connection at all 2127 if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out. 2128 // If for some strange reason (editor bug or so) we measure the text before the cursor as 2129 // longer than what the entire text is supposed to be, the safe thing to do is bail out. 2130 final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); 2131 if (numberOfCharsInWordBeforeCursor > mInputLogic.mLastSelectionStart) return; 2132 final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); 2133 final String typedWord = range.mWord.toString(); 2134 if (!isResumableWord(typedWord, currentSettings)) return; 2135 int i = 0; 2136 for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { 2137 for (final String s : span.getSuggestions()) { 2138 ++i; 2139 if (!TextUtils.equals(s, typedWord)) { 2140 suggestions.add(new SuggestedWordInfo(s, 2141 SuggestionStripView.MAX_SUGGESTIONS - i, 2142 SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, 2143 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 2144 SuggestedWordInfo.NOT_A_CONFIDENCE 2145 /* autoCommitFirstWordConfidence */)); 2146 } 2147 } 2148 } 2149 mInputLogic.mWordComposer.setComposingWord(typedWord, 2150 mInputLogic.getNthPreviousWordForSuggestion(currentSettings, 2151 // We want the previous word for suggestion. If we have chars in the word 2152 // before the cursor, then we want the word before that, hence 2; otherwise, 2153 // we want the word immediately before the cursor, hence 1. 2154 0 == numberOfCharsInWordBeforeCursor ? 1 : 2), 2155 mKeyboardSwitcher.getKeyboard()); 2156 mInputLogic.mWordComposer.setCursorPositionWithinWord( 2157 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); 2158 mInputLogic.mConnection.setComposingRegion( 2159 mInputLogic.mLastSelectionStart - numberOfCharsInWordBeforeCursor, 2160 mInputLogic.mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor()); 2161 if (suggestions.isEmpty()) { 2162 // We come here if there weren't any suggestion spans on this word. We will try to 2163 // compute suggestions for it instead. 2164 mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING, 2165 SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { 2166 @Override 2167 public void onGetSuggestedWords( 2168 final SuggestedWords suggestedWordsIncludingTypedWord) { 2169 final SuggestedWords suggestedWords; 2170 if (suggestedWordsIncludingTypedWord.size() > 1) { 2171 // We were able to compute new suggestions for this word. 2172 // Remove the typed word, since we don't want to display it in this 2173 // case. The #getSuggestedWordsExcludingTypedWord() method sets 2174 // willAutoCorrect to false. 2175 suggestedWords = suggestedWordsIncludingTypedWord 2176 .getSuggestedWordsExcludingTypedWord(); 2177 } else { 2178 // No saved suggestions, and we were unable to compute any good one 2179 // either. Rather than displaying an empty suggestion strip, we'll 2180 // display the original word alone in the middle. 2181 // Since there is only one word, willAutoCorrect is false. 2182 suggestedWords = suggestedWordsIncludingTypedWord; 2183 } 2184 // We need to pass typedWord because mWordComposer.mTypedWord may 2185 // differ from typedWord. 2186 unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip( 2187 suggestedWords, typedWord); 2188 }}); 2189 } else { 2190 // We found suggestion spans in the word. We'll create the SuggestedWords out of 2191 // them, and make willAutoCorrect false. 2192 final SuggestedWords suggestedWords = new SuggestedWords(suggestions, 2193 true /* typedWordValid */, false /* willAutoCorrect */, 2194 false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, 2195 false /* isPrediction */); 2196 // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord. 2197 unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, typedWord); 2198 } 2199 } 2200 2201 public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip( 2202 final SuggestedWords suggestedWords, final String typedWord) { 2203 // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. 2204 // We never want to auto-correct on a resumed suggestion. Please refer to the three places 2205 // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected. 2206 // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching 2207 // the text to adapt it. 2208 // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition) 2209 mInputLogic.mIsAutoCorrectionIndicatorOn = false; 2210 mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord); 2211 } 2212 2213 /** 2214 * Retry resetting caches in the rich input connection. 2215 * 2216 * When the editor can't be accessed we can't reset the caches, so we schedule a retry. 2217 * This method handles the retry, and re-schedules a new retry if we still can't access. 2218 * We only retry up to 5 times before giving up. 2219 * 2220 * @param tryResumeSuggestions Whether we should resume suggestions or not. 2221 * @param remainingTries How many times we may try again before giving up. 2222 */ 2223 private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { 2224 if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( 2225 mInputLogic.mLastSelectionStart, mInputLogic.mLastSelectionEnd, false)) { 2226 if (0 < remainingTries) { 2227 mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1); 2228 return; 2229 } 2230 // If remainingTries is 0, we should stop waiting for new tries, but it's still 2231 // better to load the keyboard (less things will be broken). 2232 } 2233 tryFixLyingCursorPosition(); 2234 mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); 2235 if (tryResumeSuggestions) mHandler.postResumeSuggestions(); 2236 } 2237 2238 // TODO: Make this private 2239 // Outside LatinIME, only used by the {@link InputTestsBase} test suite. 2240 @UsedForTesting 2241 void loadKeyboard() { 2242 // Since we are switching languages, the most urgent thing is to let the keyboard graphics 2243 // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on 2244 // the screen. Anything we do right now will delay this, so wait until the next frame 2245 // before we do the rest, like reopening dictionaries and updating suggestions. So we 2246 // post a message. 2247 mHandler.postReopenDictionaries(); 2248 loadSettings(); 2249 if (mKeyboardSwitcher.getMainKeyboardView() != null) { 2250 // Reload keyboard because the current language has been changed. 2251 mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); 2252 } 2253 } 2254 2255 private void hapticAndAudioFeedback(final int code, final int repeatCount) { 2256 final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView(); 2257 if (keyboardView != null && keyboardView.isInDraggingFinger()) { 2258 // No need to feedback while finger is dragging. 2259 return; 2260 } 2261 if (repeatCount > 0) { 2262 if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) { 2263 // No need to feedback when repeat delete key will have no effect. 2264 return; 2265 } 2266 // TODO: Use event time that the last feedback has been generated instead of relying on 2267 // a repeat count to thin out feedback. 2268 if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) { 2269 return; 2270 } 2271 } 2272 final AudioAndHapticFeedbackManager feedbackManager = 2273 AudioAndHapticFeedbackManager.getInstance(); 2274 if (repeatCount == 0) { 2275 // TODO: Reconsider how to perform haptic feedback when repeating key. 2276 feedbackManager.performHapticFeedback(keyboardView); 2277 } 2278 feedbackManager.performAudioFeedback(code); 2279 } 2280 2281 // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed; 2282 // release matching call is {@link #onReleaseKey(int,boolean)} below. 2283 @Override 2284 public void onPressKey(final int primaryCode, final int repeatCount, 2285 final boolean isSinglePointer) { 2286 mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer); 2287 hapticAndAudioFeedback(primaryCode, repeatCount); 2288 } 2289 2290 // Callback of the {@link KeyboardActionListener}. This is called when a key is released; 2291 // press matching call is {@link #onPressKey(int,int,boolean)} above. 2292 @Override 2293 public void onReleaseKey(final int primaryCode, final boolean withSliding) { 2294 mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); 2295 2296 // If accessibility is on, ensure the user receives keyboard state updates. 2297 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 2298 switch (primaryCode) { 2299 case Constants.CODE_SHIFT: 2300 AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); 2301 break; 2302 case Constants.CODE_SWITCH_ALPHA_SYMBOL: 2303 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); 2304 break; 2305 } 2306 } 2307 } 2308 2309 // Hooks for hardware keyboard 2310 @Override 2311 public boolean onKeyDown(final int keyCode, final KeyEvent event) { 2312 if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event); 2313 // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if 2314 // it doesn't know what to do with it and leave it to the application. For example, 2315 // hardware key events for adjusting the screen's brightness are passed as is. 2316 if (mInputLogic.mEventInterpreter.onHardwareKeyEvent(event)) { 2317 final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); 2318 mInputLogic.mCurrentlyPressedHardwareKeys.add(keyIdentifier); 2319 return true; 2320 } 2321 return super.onKeyDown(keyCode, event); 2322 } 2323 2324 @Override 2325 public boolean onKeyUp(final int keyCode, final KeyEvent event) { 2326 final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); 2327 if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { 2328 return true; 2329 } 2330 return super.onKeyUp(keyCode, event); 2331 } 2332 2333 // onKeyDown and onKeyUp are the main events we are interested in. There are two more events 2334 // related to handling of hardware key events that we may want to implement in the future: 2335 // boolean onKeyLongPress(final int keyCode, final KeyEvent event); 2336 // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event); 2337 2338 // receive ringer mode change and network state change. 2339 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 2340 @Override 2341 public void onReceive(final Context context, final Intent intent) { 2342 final String action = intent.getAction(); 2343 if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 2344 mSubtypeSwitcher.onNetworkStateChanged(intent); 2345 } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { 2346 AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged(); 2347 } 2348 } 2349 }; 2350 2351 private void launchSettings() { 2352 handleClose(); 2353 launchSubActivity(SettingsActivity.class); 2354 } 2355 2356 public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) { 2357 // Put the text in the attached EditText into a safe, saved state before switching to a 2358 // new activity that will also use the soft keyboard. 2359 mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR); 2360 launchSubActivity(activityClass); 2361 } 2362 2363 private void launchSubActivity(final Class<? extends Activity> activityClass) { 2364 Intent intent = new Intent(); 2365 intent.setClass(LatinIME.this, activityClass); 2366 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 2367 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2368 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2369 startActivity(intent); 2370 } 2371 2372 private void showSubtypeSelectorAndSettings() { 2373 final CharSequence title = getString(R.string.english_ime_input_options); 2374 final CharSequence[] items = new CharSequence[] { 2375 // TODO: Should use new string "Select active input modes". 2376 getString(R.string.language_selection_title), 2377 getString(ApplicationUtils.getActivityTitleResId(this, SettingsActivity.class)), 2378 }; 2379 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 2380 @Override 2381 public void onClick(DialogInterface di, int position) { 2382 di.dismiss(); 2383 switch (position) { 2384 case 0: 2385 final Intent intent = IntentUtils.getInputLanguageSelectionIntent( 2386 mRichImm.getInputMethodIdOfThisIme(), 2387 Intent.FLAG_ACTIVITY_NEW_TASK 2388 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2389 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2390 startActivity(intent); 2391 break; 2392 case 1: 2393 launchSettings(); 2394 break; 2395 } 2396 } 2397 }; 2398 final AlertDialog.Builder builder = 2399 new AlertDialog.Builder(this).setItems(items, listener).setTitle(title); 2400 showOptionDialog(builder.create()); 2401 } 2402 2403 public void showOptionDialog(final AlertDialog dialog) { 2404 final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); 2405 if (windowToken == null) { 2406 return; 2407 } 2408 2409 dialog.setCancelable(true); 2410 dialog.setCanceledOnTouchOutside(true); 2411 2412 final Window window = dialog.getWindow(); 2413 final WindowManager.LayoutParams lp = window.getAttributes(); 2414 lp.token = windowToken; 2415 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 2416 window.setAttributes(lp); 2417 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 2418 2419 mOptionsDialog = dialog; 2420 dialog.show(); 2421 } 2422 2423 // TODO: can this be removed somehow without breaking the tests? 2424 @UsedForTesting 2425 /* package for test */ SuggestedWords getSuggestedWords() { 2426 // You may not use this method for anything else than debug 2427 return DEBUG ? mInputLogic.mSuggestedWords : null; 2428 } 2429 2430 // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. 2431 @UsedForTesting 2432 /* package for test */ boolean isCurrentlyWaitingForMainDictionary() { 2433 return mInputLogic.mSuggest.isCurrentlyWaitingForMainDictionary(); 2434 } 2435 2436 // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. 2437 @UsedForTesting 2438 /* package for test */ void replaceMainDictionaryForTest(final Locale locale) { 2439 mInputLogic.mSuggest.resetMainDict(this, locale, null); 2440 } 2441 2442 public void debugDumpStateAndCrashWithException(final String context) { 2443 final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString()); 2444 s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes) 2445 .append("\nContext : ").append(context); 2446 throw new RuntimeException(s.toString()); 2447 } 2448 2449 @Override 2450 protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) { 2451 super.dump(fd, fout, args); 2452 2453 final Printer p = new PrintWriterPrinter(fout); 2454 p.println("LatinIME state :"); 2455 p.println(" VersionCode = " + ApplicationUtils.getVersionCode(this)); 2456 p.println(" VersionName = " + ApplicationUtils.getVersionName(this)); 2457 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 2458 final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; 2459 p.println(" Keyboard mode = " + keyboardMode); 2460 final SettingsValues settingsValues = mSettings.getCurrent(); 2461 p.println(" mIsSuggestionsRequested = " 2462 + settingsValues.isSuggestionsRequested(mDisplayOrientation)); 2463 p.println(" mCorrectionEnabled=" + settingsValues.mCorrectionEnabled); 2464 p.println(" isComposingWord=" + mInputLogic.mWordComposer.isComposingWord()); 2465 p.println(" mSoundOn=" + settingsValues.mSoundOn); 2466 p.println(" mVibrateOn=" + settingsValues.mVibrateOn); 2467 p.println(" mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn); 2468 p.println(" inputAttributes=" + settingsValues.mInputAttributes); 2469 } 2470} 2471