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