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