LatinIME.java revision d418580a7185754df3c9d3c65a5cd529b4bc5e25
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_VISIBILITY_SHOW_VALUE 115 = R.string.prefs_suggestion_visibility_show_value; 116 private static final int SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE 117 = R.string.prefs_suggestion_visibility_show_only_portrait_value; 118 private static final int SUGGESTION_VISIBILITY_HIDE_VALUE 119 = R.string.prefs_suggestion_visibility_hide_value; 120 121 private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] { 122 SUGGESTION_VISIBILITY_SHOW_VALUE, 123 SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE, 124 SUGGESTION_VISIBILITY_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 = UserHistoryDictionary.getInstance( 501 this, localeStr, Suggest.DIC_USER_HISTORY, mPrefs); 502 mSuggest.setUserHistoryDictionary(mUserHistoryDictionary); 503 } 504 505 /** 506 * Resets the contacts dictionary in mSuggest according to the user settings. 507 * 508 * This method takes an optional contacts dictionary to use 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 case Keyboard.CODE_RESEARCH: 1349 if (ProductionFlag.IS_EXPERIMENTAL) { 1350 ResearchLogger.getInstance().presentResearchDialog(this); 1351 } 1352 break; 1353 default: 1354 if (primaryCode == Keyboard.CODE_TAB 1355 && mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT) { 1356 performEditorAction(EditorInfo.IME_ACTION_NEXT); 1357 break; 1358 } 1359 mSpaceState = SPACE_STATE_NONE; 1360 if (mSettingsValues.isWordSeparator(primaryCode)) { 1361 didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); 1362 } else { 1363 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1364 if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { 1365 handleCharacter(primaryCode, x, y, spaceState); 1366 } else { 1367 handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE, 1368 spaceState); 1369 } 1370 } 1371 mExpectingUpdateSelection = true; 1372 mShouldSwitchToLastSubtype = true; 1373 break; 1374 } 1375 switcher.onCodeInput(primaryCode); 1376 // Reset after any single keystroke, except shift and symbol-shift 1377 if (!didAutoCorrect && primaryCode != Keyboard.CODE_SHIFT 1378 && primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL) 1379 mLastComposedWord.deactivate(); 1380 mEnteredText = null; 1381 } 1382 1383 @Override 1384 public void onTextInput(CharSequence text) { 1385 final InputConnection ic = getCurrentInputConnection(); 1386 if (ic == null) return; 1387 ic.beginBatchEdit(); 1388 commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR); 1389 text = specificTldProcessingOnTextInput(ic, text); 1390 if (SPACE_STATE_PHANTOM == mSpaceState) { 1391 sendKeyCodePoint(Keyboard.CODE_SPACE); 1392 } 1393 ic.commitText(text, 1); 1394 if (ProductionFlag.IS_EXPERIMENTAL) { 1395 ResearchLogger.latinIME_commitText(text); 1396 } 1397 ic.endBatchEdit(); 1398 mKeyboardSwitcher.updateShiftState(); 1399 mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT); 1400 mSpaceState = SPACE_STATE_NONE; 1401 mEnteredText = text; 1402 resetComposingState(true /* alsoResetLastComposedWord */); 1403 } 1404 1405 // ic may not be null 1406 private CharSequence specificTldProcessingOnTextInput(final InputConnection ic, 1407 final CharSequence text) { 1408 if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD 1409 || !Character.isLetter(text.charAt(1))) { 1410 // Not a tld: do nothing. 1411 return text; 1412 } 1413 // We have a TLD (or something that looks like this): make sure we don't add 1414 // a space even if currently in phantom mode. 1415 mSpaceState = SPACE_STATE_NONE; 1416 final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); 1417 if (lastOne != null && lastOne.length() == 1 1418 && lastOne.charAt(0) == Keyboard.CODE_PERIOD) { 1419 return text.subSequence(1, text.length()); 1420 } else { 1421 return text; 1422 } 1423 } 1424 1425 @Override 1426 public void onCancelInput() { 1427 // User released a finger outside any key 1428 mKeyboardSwitcher.onCancelInput(); 1429 } 1430 1431 private void handleBackspace(final int spaceState) { 1432 final InputConnection ic = getCurrentInputConnection(); 1433 if (ic == null) return; 1434 ic.beginBatchEdit(); 1435 handleBackspaceWhileInBatchEdit(spaceState, ic); 1436 ic.endBatchEdit(); 1437 } 1438 1439 // "ic" may not be null. 1440 private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) { 1441 // In many cases, we may have to put the keyboard in auto-shift state again. 1442 mHandler.postUpdateShiftState(); 1443 1444 if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { 1445 // Cancel multi-character input: remove the text we just entered. 1446 // This is triggered on backspace after a key that inputs multiple characters, 1447 // like the smiley key or the .com key. 1448 final int length = mEnteredText.length(); 1449 ic.deleteSurroundingText(length, 0); 1450 if (ProductionFlag.IS_EXPERIMENTAL) { 1451 ResearchLogger.latinIME_deleteSurroundingText(length); 1452 } 1453 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. 1454 // In addition we know that spaceState is false, and that we should not be 1455 // reverting any autocorrect at this point. So we can safely return. 1456 return; 1457 } 1458 1459 if (mWordComposer.isComposingWord()) { 1460 final int length = mWordComposer.size(); 1461 if (length > 0) { 1462 mWordComposer.deleteLast(); 1463 ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1464 // If we have deleted the last remaining character of a word, then we are not 1465 // isComposingWord() any more. 1466 if (!mWordComposer.isComposingWord()) { 1467 // Not composing word any more, so we can show bigrams. 1468 mHandler.postUpdateBigramPredictions(); 1469 } else { 1470 // Still composing a word, so we still have letters to deduce a suggestion from. 1471 mHandler.postUpdateSuggestions(); 1472 } 1473 } else { 1474 ic.deleteSurroundingText(1, 0); 1475 if (ProductionFlag.IS_EXPERIMENTAL) { 1476 ResearchLogger.latinIME_deleteSurroundingText(1); 1477 } 1478 } 1479 } else { 1480 if (mLastComposedWord.canRevertCommit()) { 1481 Utils.Stats.onAutoCorrectionCancellation(); 1482 revertCommit(ic); 1483 return; 1484 } 1485 if (SPACE_STATE_DOUBLE == spaceState) { 1486 if (revertDoubleSpaceWhileInBatchEdit(ic)) { 1487 // No need to reset mSpaceState, it has already be done (that's why we 1488 // receive it as a parameter) 1489 return; 1490 } 1491 } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1492 if (revertSwapPunctuation(ic)) { 1493 // Likewise 1494 return; 1495 } 1496 } 1497 1498 // No cancelling of commit/double space/swap: we have a regular backspace. 1499 // We should backspace one char and restart suggestion if at the end of a word. 1500 if (mLastSelectionStart != mLastSelectionEnd) { 1501 // If there is a selection, remove it. 1502 final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart; 1503 ic.setSelection(mLastSelectionEnd, mLastSelectionEnd); 1504 ic.deleteSurroundingText(lengthToDelete, 0); 1505 if (ProductionFlag.IS_EXPERIMENTAL) { 1506 ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete); 1507 } 1508 } else { 1509 // There is no selection, just delete one character. 1510 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { 1511 // This should never happen. 1512 Log.e(TAG, "Backspace when we don't know the selection position"); 1513 } 1514 // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because 1515 // we want to be able to compile against the Ice Cream Sandwich SDK. 1516 if (mTargetApplicationInfo != null 1517 && mTargetApplicationInfo.targetSdkVersion < 16) { 1518 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate 1519 // a hardware keyboard event on pressing enter or delete. This is bad for many 1520 // reasons (there are race conditions with commits) but some applications are 1521 // relying on this behavior so we continue to support it for older apps. 1522 sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL, ic); 1523 } else { 1524 ic.deleteSurroundingText(1, 0); 1525 } 1526 if (ProductionFlag.IS_EXPERIMENTAL) { 1527 ResearchLogger.latinIME_deleteSurroundingText(1); 1528 } 1529 if (mDeleteCount > DELETE_ACCELERATE_AT) { 1530 ic.deleteSurroundingText(1, 0); 1531 if (ProductionFlag.IS_EXPERIMENTAL) { 1532 ResearchLogger.latinIME_deleteSurroundingText(1); 1533 } 1534 } 1535 } 1536 if (isSuggestionsRequested()) { 1537 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic); 1538 } 1539 } 1540 } 1541 1542 // ic may be null 1543 private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code, 1544 final int spaceState, final boolean isFromSuggestionStrip) { 1545 if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1546 removeTrailingSpaceWhileInBatchEdit(ic); 1547 return false; 1548 } else if ((SPACE_STATE_WEAK == spaceState 1549 || SPACE_STATE_SWAP_PUNCTUATION == spaceState) 1550 && isFromSuggestionStrip) { 1551 if (mSettingsValues.isWeakSpaceSwapper(code)) { 1552 return true; 1553 } else { 1554 if (mSettingsValues.isWeakSpaceStripper(code)) { 1555 removeTrailingSpaceWhileInBatchEdit(ic); 1556 } 1557 return false; 1558 } 1559 } else { 1560 return false; 1561 } 1562 } 1563 1564 private void handleCharacter(final int primaryCode, final int x, 1565 final int y, final int spaceState) { 1566 final InputConnection ic = getCurrentInputConnection(); 1567 if (null != ic) ic.beginBatchEdit(); 1568 // TODO: if ic is null, does it make any sense to call this? 1569 handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic); 1570 if (null != ic) ic.endBatchEdit(); 1571 } 1572 1573 // "ic" may be null without this crashing, but the behavior will be really strange 1574 private void handleCharacterWhileInBatchEdit(final int primaryCode, 1575 final int x, final int y, final int spaceState, final InputConnection ic) { 1576 boolean isComposingWord = mWordComposer.isComposingWord(); 1577 1578 if (SPACE_STATE_PHANTOM == spaceState && 1579 !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) { 1580 if (isComposingWord) { 1581 // Sanity check 1582 throw new RuntimeException("Should not be composing here"); 1583 } 1584 sendKeyCodePoint(Keyboard.CODE_SPACE); 1585 } 1586 1587 // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several 1588 // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI 1589 // thread here. 1590 if (!isComposingWord && (isAlphabet(primaryCode) 1591 || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) 1592 && isSuggestionsRequested() && !isCursorTouchingWord()) { 1593 // Reset entirely the composing state anyway, then start composing a new word unless 1594 // the character is a single quote. The idea here is, single quote is not a 1595 // separator and it should be treated as a normal character, except in the first 1596 // position where it should not start composing a word. 1597 isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode); 1598 // Here we don't need to reset the last composed word. It will be reset 1599 // when we commit this one, if we ever do; if on the other hand we backspace 1600 // it entirely and resume suggestions on the previous word, we'd like to still 1601 // have touch coordinates for it. 1602 resetComposingState(false /* alsoResetLastComposedWord */); 1603 clearSuggestions(); 1604 } 1605 if (isComposingWord) { 1606 mWordComposer.add( 1607 primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector()); 1608 if (ic != null) { 1609 // If it's the first letter, make note of auto-caps state 1610 if (mWordComposer.size() == 1) { 1611 mWordComposer.setAutoCapitalized( 1612 getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF); 1613 } 1614 ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1615 } 1616 mHandler.postUpdateSuggestions(); 1617 } else { 1618 final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, 1619 spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x); 1620 1621 sendKeyCodePoint(primaryCode); 1622 1623 if (swapWeakSpace) { 1624 swapSwapperAndSpaceWhileInBatchEdit(ic); 1625 mSpaceState = SPACE_STATE_WEAK; 1626 } 1627 // Some characters are not word separators, yet they don't start a new 1628 // composing span. For these, we haven't changed the suggestion strip, and 1629 // if the "add to dictionary" hint is shown, we should do so now. Examples of 1630 // such characters include single quote, dollar, and others; the exact list is 1631 // the list of characters for which we enter handleCharacterWhileInBatchEdit 1632 // that don't match the test if ((isAlphabet...)) at the top of this method. 1633 if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) { 1634 mHandler.postUpdateBigramPredictions(); 1635 } 1636 } 1637 Utils.Stats.onNonSeparator((char)primaryCode, x, y); 1638 } 1639 1640 // Returns true if we did an autocorrection, false otherwise. 1641 private boolean handleSeparator(final int primaryCode, final int x, final int y, 1642 final int spaceState) { 1643 // Should dismiss the "Touch again to save" message when handling separator 1644 if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) { 1645 mHandler.cancelUpdateBigramPredictions(); 1646 mHandler.postUpdateSuggestions(); 1647 } 1648 1649 boolean didAutoCorrect = false; 1650 // Handle separator 1651 final InputConnection ic = getCurrentInputConnection(); 1652 if (ic != null) { 1653 ic.beginBatchEdit(); 1654 } 1655 if (mWordComposer.isComposingWord()) { 1656 // In certain languages where single quote is a separator, it's better 1657 // not to auto correct, but accept the typed word. For instance, 1658 // in Italian dov' should not be expanded to dove' because the elision 1659 // requires the last vowel to be removed. 1660 final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled 1661 && !mInputAttributes.mInputTypeNoAutoCorrect; 1662 if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) { 1663 commitCurrentAutoCorrection(primaryCode, ic); 1664 didAutoCorrect = true; 1665 } else { 1666 commitTyped(ic, primaryCode); 1667 } 1668 } 1669 1670 final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState, 1671 KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x); 1672 1673 if (SPACE_STATE_PHANTOM == spaceState && 1674 mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) { 1675 sendKeyCodePoint(Keyboard.CODE_SPACE); 1676 } 1677 sendKeyCodePoint(primaryCode); 1678 1679 if (Keyboard.CODE_SPACE == primaryCode) { 1680 if (isSuggestionsRequested()) { 1681 if (maybeDoubleSpaceWhileInBatchEdit(ic)) { 1682 mSpaceState = SPACE_STATE_DOUBLE; 1683 } else if (!isShowingPunctuationList()) { 1684 mSpaceState = SPACE_STATE_WEAK; 1685 } 1686 } 1687 1688 mHandler.startDoubleSpacesTimer(); 1689 if (!isCursorTouchingWord()) { 1690 mHandler.cancelUpdateSuggestions(); 1691 mHandler.postUpdateBigramPredictions(); 1692 } 1693 } else { 1694 if (swapWeakSpace) { 1695 swapSwapperAndSpaceWhileInBatchEdit(ic); 1696 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; 1697 } else if (SPACE_STATE_PHANTOM == spaceState) { 1698 // If we are in phantom space state, and the user presses a separator, we want to 1699 // stay in phantom space state so that the next keypress has a chance to add the 1700 // space. For example, if I type "Good dat", pick "day" from the suggestion strip 1701 // then insert a comma and go on to typing the next word, I want the space to be 1702 // inserted automatically before the next word, the same way it is when I don't 1703 // input the comma. 1704 mSpaceState = SPACE_STATE_PHANTOM; 1705 } 1706 1707 // Set punctuation right away. onUpdateSelection will fire but tests whether it is 1708 // already displayed or not, so it's okay. 1709 setPunctuationSuggestions(); 1710 } 1711 1712 Utils.Stats.onSeparator((char)primaryCode, x, y); 1713 1714 if (ic != null) { 1715 ic.endBatchEdit(); 1716 } 1717 return didAutoCorrect; 1718 } 1719 1720 private CharSequence getTextWithUnderline(final CharSequence text) { 1721 return mIsAutoCorrectionIndicatorOn 1722 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) 1723 : text; 1724 } 1725 1726 private void handleClose() { 1727 commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR); 1728 requestHideSelf(0); 1729 LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 1730 if (inputView != null) 1731 inputView.closing(); 1732 } 1733 1734 public boolean isSuggestionsRequested() { 1735 return mInputAttributes.mIsSettingsSuggestionStripOn 1736 && (mCorrectionMode > 0 || isShowingSuggestionsStrip()); 1737 } 1738 1739 public boolean isShowingPunctuationList() { 1740 if (mSuggestionsView == null) return false; 1741 return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions(); 1742 } 1743 1744 public boolean isShowingSuggestionsStrip() { 1745 return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE) 1746 || (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE 1747 && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT); 1748 } 1749 1750 public boolean isSuggestionsStripVisible() { 1751 if (mSuggestionsView == null) 1752 return false; 1753 if (mSuggestionsView.isShowingAddToDictionaryHint()) 1754 return true; 1755 if (!isShowingSuggestionsStrip()) 1756 return false; 1757 if (mInputAttributes.mApplicationSpecifiedCompletionOn) 1758 return true; 1759 return isSuggestionsRequested(); 1760 } 1761 1762 public void switchToKeyboardView() { 1763 if (DEBUG) { 1764 Log.d(TAG, "Switch to keyboard view."); 1765 } 1766 if (ProductionFlag.IS_EXPERIMENTAL) { 1767 ResearchLogger.latinIME_switchToKeyboardView(); 1768 } 1769 View v = mKeyboardSwitcher.getKeyboardView(); 1770 if (v != null) { 1771 // Confirms that the keyboard view doesn't have parent view. 1772 ViewParent p = v.getParent(); 1773 if (p != null && p instanceof ViewGroup) { 1774 ((ViewGroup) p).removeView(v); 1775 } 1776 setInputView(v); 1777 } 1778 setSuggestionStripShown(isSuggestionsStripVisible()); 1779 updateInputViewShown(); 1780 mHandler.postUpdateSuggestions(); 1781 } 1782 1783 public void clearSuggestions() { 1784 setSuggestions(SuggestedWords.EMPTY, false); 1785 setAutoCorrectionIndicator(false); 1786 } 1787 1788 private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) { 1789 if (mSuggestionsView != null) { 1790 mSuggestionsView.setSuggestions(words); 1791 mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); 1792 } 1793 } 1794 1795 private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { 1796 // Put a blue underline to a word in TextView which will be auto-corrected. 1797 final InputConnection ic = getCurrentInputConnection(); 1798 if (ic == null) return; 1799 if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator 1800 && mWordComposer.isComposingWord()) { 1801 mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; 1802 final CharSequence textWithUnderline = 1803 getTextWithUnderline(mWordComposer.getTypedWord()); 1804 ic.setComposingText(textWithUnderline, 1); 1805 } 1806 } 1807 1808 public void updateSuggestions() { 1809 // Check if we have a suggestion engine attached. 1810 if ((mSuggest == null || !isSuggestionsRequested())) { 1811 if (mWordComposer.isComposingWord()) { 1812 Log.w(TAG, "Called updateSuggestions but suggestions were not requested!"); 1813 mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); 1814 } 1815 return; 1816 } 1817 1818 mHandler.cancelUpdateSuggestions(); 1819 mHandler.cancelUpdateBigramPredictions(); 1820 1821 if (!mWordComposer.isComposingWord()) { 1822 setPunctuationSuggestions(); 1823 return; 1824 } 1825 1826 // TODO: May need a better way of retrieving previous word 1827 final InputConnection ic = getCurrentInputConnection(); 1828 final CharSequence prevWord; 1829 if (null == ic) { 1830 prevWord = null; 1831 } else { 1832 prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators); 1833 } 1834 1835 final CharSequence typedWord = mWordComposer.getTypedWord(); 1836 // getSuggestedWords handles gracefully a null value of prevWord 1837 final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer, 1838 prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode); 1839 1840 // Basically, we update the suggestion strip only when suggestion count > 1. However, 1841 // there is an exception: We update the suggestion strip whenever typed word's length 1842 // is 1 or typed word is found in dictionary, regardless of suggestion count. Actually, 1843 // in most cases, suggestion count is 1 when typed word's length is 1, but we do always 1844 // need to clear the previous state when the user starts typing a word (i.e. typed word's 1845 // length == 1). 1846 if (suggestedWords.size() > 1 || typedWord.length() == 1 1847 || !suggestedWords.mAllowsToBeAutoCorrected 1848 || mSuggestionsView.isShowingAddToDictionaryHint()) { 1849 showSuggestions(suggestedWords, typedWord); 1850 } else { 1851 SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions(); 1852 if (previousSuggestions == mSettingsValues.mSuggestPuncList) { 1853 previousSuggestions = SuggestedWords.EMPTY; 1854 } 1855 final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = 1856 SuggestedWords.getTypedWordAndPreviousSuggestions( 1857 typedWord, previousSuggestions); 1858 final SuggestedWords obsoleteSuggestedWords = 1859 new SuggestedWords(typedWordAndPreviousSuggestions, 1860 false /* typedWordValid */, 1861 false /* hasAutoCorrectionCandidate */, 1862 false /* allowsToBeAutoCorrected */, 1863 false /* isPunctuationSuggestions */, 1864 true /* isObsoleteSuggestions */, 1865 false /* isPrediction */); 1866 showSuggestions(obsoleteSuggestedWords, typedWord); 1867 } 1868 } 1869 1870 public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) { 1871 final CharSequence autoCorrection; 1872 if (suggestedWords.size() > 0) { 1873 if (suggestedWords.hasAutoCorrectionWord()) { 1874 autoCorrection = suggestedWords.getWord(1); 1875 } else { 1876 autoCorrection = typedWord; 1877 } 1878 } else { 1879 autoCorrection = null; 1880 } 1881 mWordComposer.setAutoCorrection(autoCorrection); 1882 final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); 1883 setSuggestions(suggestedWords, isAutoCorrection); 1884 setAutoCorrectionIndicator(isAutoCorrection); 1885 setSuggestionStripShown(isSuggestionsStripVisible()); 1886 } 1887 1888 private void commitCurrentAutoCorrection(final int separatorCodePoint, 1889 final InputConnection ic) { 1890 // Complete any pending suggestions query first 1891 if (mHandler.hasPendingUpdateSuggestions()) { 1892 mHandler.cancelUpdateSuggestions(); 1893 updateSuggestions(); 1894 } 1895 final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull(); 1896 if (autoCorrection != null) { 1897 final String typedWord = mWordComposer.getTypedWord(); 1898 if (TextUtils.isEmpty(typedWord)) { 1899 throw new RuntimeException("We have an auto-correction but the typed word " 1900 + "is empty? Impossible! I must commit suicide."); 1901 } 1902 Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint); 1903 if (ProductionFlag.IS_EXPERIMENTAL) { 1904 ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord, 1905 autoCorrection.toString()); 1906 } 1907 mExpectingUpdateSelection = true; 1908 commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, 1909 separatorCodePoint); 1910 if (!typedWord.equals(autoCorrection) && null != ic) { 1911 // This will make the correction flash for a short while as a visual clue 1912 // to the user that auto-correction happened. 1913 ic.commitCorrection(new CorrectionInfo(mLastSelectionEnd - typedWord.length(), 1914 typedWord, autoCorrection)); 1915 } 1916 } 1917 } 1918 1919 @Override 1920 public void pickSuggestionManually(final int index, final CharSequence suggestion, 1921 int x, int y) { 1922 final InputConnection ic = getCurrentInputConnection(); 1923 if (null != ic) ic.beginBatchEdit(); 1924 pickSuggestionManuallyWhileInBatchEdit(index, suggestion, x, y, ic); 1925 if (null != ic) ic.endBatchEdit(); 1926 } 1927 1928 public void pickSuggestionManuallyWhileInBatchEdit(final int index, 1929 final CharSequence suggestion, final int x, final int y, final InputConnection ic) { 1930 final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions(); 1931 // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput 1932 if (suggestion.length() == 1 && isShowingPunctuationList()) { 1933 // Word separators are suggested before the user inputs something. 1934 // So, LatinImeLogger logs "" as a user's input. 1935 LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords); 1936 // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. 1937 if (ProductionFlag.IS_EXPERIMENTAL) { 1938 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y); 1939 } 1940 final int primaryCode = suggestion.charAt(0); 1941 onCodeInput(primaryCode, 1942 KeyboardActionListener.SUGGESTION_STRIP_COORDINATE, 1943 KeyboardActionListener.SUGGESTION_STRIP_COORDINATE); 1944 return; 1945 } 1946 1947 if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) { 1948 int firstChar = Character.codePointAt(suggestion, 0); 1949 if ((!mSettingsValues.isWeakSpaceStripper(firstChar)) 1950 && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) { 1951 sendKeyCodePoint(Keyboard.CODE_SPACE); 1952 } 1953 } 1954 1955 if (mInputAttributes.mApplicationSpecifiedCompletionOn 1956 && mApplicationSpecifiedCompletions != null 1957 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 1958 if (mSuggestionsView != null) { 1959 mSuggestionsView.clear(); 1960 } 1961 mKeyboardSwitcher.updateShiftState(); 1962 resetComposingState(true /* alsoResetLastComposedWord */); 1963 if (ic != null) { 1964 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; 1965 ic.commitCompletion(completionInfo); 1966 if (ProductionFlag.IS_EXPERIMENTAL) { 1967 ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index, 1968 completionInfo.getText(), x, y); 1969 } 1970 } 1971 return; 1972 } 1973 1974 // We need to log before we commit, because the word composer will store away the user 1975 // typed word. 1976 final String replacedWord = mWordComposer.getTypedWord().toString(); 1977 LatinImeLogger.logOnManualSuggestion(replacedWord, 1978 suggestion.toString(), index, suggestedWords); 1979 if (ProductionFlag.IS_EXPERIMENTAL) { 1980 ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y); 1981 } 1982 mExpectingUpdateSelection = true; 1983 commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, 1984 LastComposedWord.NOT_A_SEPARATOR); 1985 // Don't allow cancellation of manual pick 1986 mLastComposedWord.deactivate(); 1987 mSpaceState = SPACE_STATE_PHANTOM; 1988 // TODO: is this necessary? 1989 mKeyboardSwitcher.updateShiftState(); 1990 1991 // We should show the "Touch again to save" hint if the user pressed the first entry 1992 // AND either: 1993 // - There is no dictionary (we know that because we tried to load it => null != mSuggest 1994 // AND mSuggest.hasMainDictionary() is false) 1995 // - There is a dictionary and the word is not in it 1996 // Please note that if mSuggest is null, it means that everything is off: suggestion 1997 // and correction, so we shouldn't try to show the hint 1998 // We used to look at mCorrectionMode here, but showing the hint should have nothing 1999 // to do with the autocorrection setting. 2000 final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null 2001 // If there is no dictionary the hint should be shown. 2002 && (!mSuggest.hasMainDictionary() 2003 // If "suggestion" is not in the dictionary, the hint should be shown. 2004 || !AutoCorrection.isValidWord( 2005 mSuggest.getUnigramDictionaries(), suggestion, true)); 2006 2007 Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE, 2008 WordComposer.NOT_A_COORDINATE); 2009 if (!showingAddToDictionaryHint) { 2010 // If we're not showing the "Touch again to save", then show corrections again. 2011 // In case the cursor position doesn't change, make sure we show the suggestions again. 2012 updateBigramPredictions(); 2013 // Updating the predictions right away may be slow and feel unresponsive on slower 2014 // terminals. On the other hand if we just postUpdateBigramPredictions() it will 2015 // take a noticeable delay to update them which may feel uneasy. 2016 } else { 2017 if (mIsUserDictionaryAvailable) { 2018 mSuggestionsView.showAddToDictionaryHint( 2019 suggestion, mSettingsValues.mHintToSaveText); 2020 } else { 2021 mHandler.postUpdateSuggestions(); 2022 } 2023 } 2024 } 2025 2026 /** 2027 * Commits the chosen word to the text field and saves it for later retrieval. 2028 */ 2029 private void commitChosenWord(final CharSequence chosenWord, final int commitType, 2030 final int separatorCode) { 2031 final InputConnection ic = getCurrentInputConnection(); 2032 if (ic != null) { 2033 if (mSettingsValues.mEnableSuggestionSpanInsertion) { 2034 final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions(); 2035 ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( 2036 this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 2037 1); 2038 if (ProductionFlag.IS_EXPERIMENTAL) { 2039 ResearchLogger.latinIME_commitText(chosenWord); 2040 } 2041 } else { 2042 ic.commitText(chosenWord, 1); 2043 if (ProductionFlag.IS_EXPERIMENTAL) { 2044 ResearchLogger.latinIME_commitText(chosenWord); 2045 } 2046 } 2047 } 2048 // Add the word to the user history dictionary 2049 final CharSequence prevWord = addToUserHistoryDictionary(chosenWord); 2050 // TODO: figure out here if this is an auto-correct or if the best word is actually 2051 // what user typed. Note: currently this is done much later in 2052 // LastComposedWord#didCommitTypedWord by string equality of the remembered 2053 // strings. 2054 mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord.toString(), 2055 separatorCode, prevWord); 2056 } 2057 2058 public void updateBigramPredictions() { 2059 if (mSuggest == null || !isSuggestionsRequested()) 2060 return; 2061 2062 if (!mSettingsValues.mBigramPredictionEnabled) { 2063 setPunctuationSuggestions(); 2064 return; 2065 } 2066 2067 final SuggestedWords suggestedWords; 2068 if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { 2069 final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(), 2070 mSettingsValues.mWordSeparators); 2071 if (!TextUtils.isEmpty(prevWord)) { 2072 suggestedWords = mSuggest.getBigramPredictions(prevWord); 2073 } else { 2074 suggestedWords = null; 2075 } 2076 } else { 2077 suggestedWords = null; 2078 } 2079 2080 if (null != suggestedWords && suggestedWords.size() > 0) { 2081 // Explicitly supply an empty typed word (the no-second-arg version of 2082 // showSuggestions will retrieve the word near the cursor, we don't want that here) 2083 showSuggestions(suggestedWords, ""); 2084 } else { 2085 if (!isShowingPunctuationList()) setPunctuationSuggestions(); 2086 } 2087 } 2088 2089 public void setPunctuationSuggestions() { 2090 setSuggestions(mSettingsValues.mSuggestPuncList, false); 2091 setAutoCorrectionIndicator(false); 2092 setSuggestionStripShown(isSuggestionsStripVisible()); 2093 } 2094 2095 private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) { 2096 if (TextUtils.isEmpty(suggestion)) return null; 2097 2098 // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be 2099 // adding words in situations where the user or application really didn't 2100 // want corrections enabled or learned. 2101 if (!(mCorrectionMode == Suggest.CORRECTION_FULL 2102 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { 2103 return null; 2104 } 2105 2106 if (mUserHistoryDictionary != null) { 2107 final InputConnection ic = getCurrentInputConnection(); 2108 final CharSequence prevWord; 2109 if (null != ic) { 2110 prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators); 2111 } else { 2112 prevWord = null; 2113 } 2114 final String secondWord; 2115 if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) { 2116 secondWord = suggestion.toString().toLowerCase( 2117 mSubtypeSwitcher.getCurrentSubtypeLocale()); 2118 } else { 2119 secondWord = suggestion.toString(); 2120 } 2121 // We demote unrecognized word and words with 0-frequency (assuming they would be 2122 // profanity etc.) by specifying them as "invalid". 2123 final int maxFreq = AutoCorrection.getMaxFrequency( 2124 mSuggest.getUnigramDictionaries(), suggestion); 2125 mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(), 2126 secondWord, maxFreq > 0); 2127 return prevWord; 2128 } 2129 return null; 2130 } 2131 2132 public boolean isCursorTouchingWord() { 2133 final InputConnection ic = getCurrentInputConnection(); 2134 if (ic == null) return false; 2135 CharSequence before = ic.getTextBeforeCursor(1, 0); 2136 CharSequence after = ic.getTextAfterCursor(1, 0); 2137 if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0)) 2138 && !mSettingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) { 2139 return true; 2140 } 2141 if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0)) 2142 && !mSettingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) { 2143 return true; 2144 } 2145 return false; 2146 } 2147 2148 // "ic" must not be null 2149 private static boolean sameAsTextBeforeCursor(final InputConnection ic, 2150 final CharSequence text) { 2151 final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0); 2152 return TextUtils.equals(text, beforeText); 2153 } 2154 2155 // "ic" must not be null 2156 /** 2157 * Check if the cursor is actually at the end of a word. If so, restart suggestions on this 2158 * word, else do nothing. 2159 */ 2160 private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord( 2161 final InputConnection ic) { 2162 // Bail out if the cursor is not at the end of a word (cursor must be preceded by 2163 // non-whitespace, non-separator, non-start-of-text) 2164 // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. 2165 final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0); 2166 if (TextUtils.isEmpty(textBeforeCursor) 2167 || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return; 2168 2169 // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, 2170 // separator or end of line/text) 2171 // Example: "test|"<EOL> "te|st" get rejected here 2172 final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0); 2173 if (!TextUtils.isEmpty(textAfterCursor) 2174 && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return; 2175 2176 // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) 2177 // Example: " -|" gets rejected here but "e-|" and "e|" are okay 2178 CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators); 2179 // We don't suggest on leading single quotes, so we have to remove them from the word if 2180 // it starts with single quotes. 2181 while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) { 2182 word = word.subSequence(1, word.length()); 2183 } 2184 if (TextUtils.isEmpty(word)) return; 2185 final char firstChar = word.charAt(0); // we just tested that word is not empty 2186 if (word.length() == 1 && !Character.isLetter(firstChar)) return; 2187 2188 // We only suggest on words that start with a letter or a symbol that is excluded from 2189 // word separators (see #handleCharacterWhileInBatchEdit). 2190 if (!(isAlphabet(firstChar) 2191 || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) { 2192 return; 2193 } 2194 2195 // Okay, we are at the end of a word. Restart suggestions. 2196 restartSuggestionsOnWordBeforeCursor(ic, word); 2197 } 2198 2199 // "ic" must not be null 2200 private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic, 2201 final CharSequence word) { 2202 mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); 2203 final int length = word.length(); 2204 ic.deleteSurroundingText(length, 0); 2205 if (ProductionFlag.IS_EXPERIMENTAL) { 2206 ResearchLogger.latinIME_deleteSurroundingText(length); 2207 } 2208 ic.setComposingText(word, 1); 2209 mHandler.postUpdateSuggestions(); 2210 } 2211 2212 // "ic" must not be null 2213 private void revertCommit(final InputConnection ic) { 2214 final CharSequence previousWord = mLastComposedWord.mPrevWord; 2215 final String originallyTypedWord = mLastComposedWord.mTypedWord; 2216 final CharSequence committedWord = mLastComposedWord.mCommittedWord; 2217 final int cancelLength = committedWord.length(); 2218 final int separatorLength = LastComposedWord.getSeparatorLength( 2219 mLastComposedWord.mSeparatorCode); 2220 // TODO: should we check our saved separator against the actual contents of the text view? 2221 final int deleteLength = cancelLength + separatorLength; 2222 if (DEBUG) { 2223 if (mWordComposer.isComposingWord()) { 2224 throw new RuntimeException("revertCommit, but we are composing a word"); 2225 } 2226 final String wordBeforeCursor = 2227 ic.getTextBeforeCursor(deleteLength, 0) 2228 .subSequence(0, cancelLength).toString(); 2229 if (!TextUtils.equals(committedWord, wordBeforeCursor)) { 2230 throw new RuntimeException("revertCommit check failed: we thought we were " 2231 + "reverting \"" + committedWord 2232 + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); 2233 } 2234 } 2235 ic.deleteSurroundingText(deleteLength, 0); 2236 if (ProductionFlag.IS_EXPERIMENTAL) { 2237 ResearchLogger.latinIME_deleteSurroundingText(deleteLength); 2238 } 2239 if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { 2240 mUserHistoryDictionary.cancelAddingUserHistory( 2241 previousWord.toString(), committedWord.toString()); 2242 } 2243 if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) { 2244 // This is the case when we cancel a manual pick. 2245 // We should restart suggestion on the word right away. 2246 mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord); 2247 ic.setComposingText(originallyTypedWord, 1); 2248 } else { 2249 ic.commitText(originallyTypedWord, 1); 2250 // Re-insert the separator 2251 sendKeyCodePoint(mLastComposedWord.mSeparatorCode); 2252 Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE, 2253 WordComposer.NOT_A_COORDINATE); 2254 if (ProductionFlag.IS_EXPERIMENTAL) { 2255 ResearchLogger.latinIME_revertCommit(originallyTypedWord); 2256 } 2257 // Don't restart suggestion yet. We'll restart if the user deletes the 2258 // separator. 2259 } 2260 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 2261 mHandler.cancelUpdateBigramPredictions(); 2262 mHandler.postUpdateSuggestions(); 2263 } 2264 2265 // "ic" must not be null 2266 private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) { 2267 mHandler.cancelDoubleSpacesTimer(); 2268 // Here we test whether we indeed have a period and a space before us. This should not 2269 // be needed, but it's there just in case something went wrong. 2270 final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); 2271 if (!". ".equals(textBeforeCursor)) { 2272 // Theoretically we should not be coming here if there isn't ". " before the 2273 // cursor, but the application may be changing the text while we are typing, so 2274 // anything goes. We should not crash. 2275 Log.d(TAG, "Tried to revert double-space combo but we didn't find " 2276 + "\". \" just before the cursor."); 2277 return false; 2278 } 2279 ic.deleteSurroundingText(2, 0); 2280 if (ProductionFlag.IS_EXPERIMENTAL) { 2281 ResearchLogger.latinIME_deleteSurroundingText(2); 2282 } 2283 ic.commitText(" ", 1); 2284 if (ProductionFlag.IS_EXPERIMENTAL) { 2285 ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit(); 2286 } 2287 return true; 2288 } 2289 2290 private static boolean revertSwapPunctuation(final InputConnection ic) { 2291 // Here we test whether we indeed have a space and something else before us. This should not 2292 // be needed, but it's there just in case something went wrong. 2293 final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); 2294 // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to 2295 // enter surrogate pairs this code will have been removed. 2296 if (TextUtils.isEmpty(textBeforeCursor) 2297 || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) { 2298 // We may only come here if the application is changing the text while we are typing. 2299 // This is quite a broken case, but not logically impossible, so we shouldn't crash, 2300 // but some debugging log may be in order. 2301 Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " 2302 + "find a space just before the cursor."); 2303 return false; 2304 } 2305 ic.beginBatchEdit(); 2306 ic.deleteSurroundingText(2, 0); 2307 if (ProductionFlag.IS_EXPERIMENTAL) { 2308 ResearchLogger.latinIME_deleteSurroundingText(2); 2309 } 2310 ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1); 2311 if (ProductionFlag.IS_EXPERIMENTAL) { 2312 ResearchLogger.latinIME_revertSwapPunctuation(); 2313 } 2314 ic.endBatchEdit(); 2315 return true; 2316 } 2317 2318 public boolean isWordSeparator(int code) { 2319 return mSettingsValues.isWordSeparator(code); 2320 } 2321 2322 public boolean preferCapitalization() { 2323 return mWordComposer.isFirstCharCapitalized(); 2324 } 2325 2326 // Notify that language or mode have been changed and toggleLanguage will update KeyboardID 2327 // according to new language or mode. 2328 public void onRefreshKeyboard() { 2329 // When the device locale is changed in SetupWizard etc., this method may get called via 2330 // onConfigurationChanged before SoftInputWindow is shown. 2331 if (mKeyboardSwitcher.getKeyboardView() != null) { 2332 // Reload keyboard because the current language has been changed. 2333 mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues); 2334 } 2335 initSuggest(); 2336 updateCorrectionMode(); 2337 loadSettings(); 2338 // Since we just changed languages, we should re-evaluate suggestions with whatever word 2339 // we are currently composing. If we are not composing anything, we may want to display 2340 // predictions or punctuation signs (which is done by updateBigramPredictions anyway). 2341 if (isCursorTouchingWord()) { 2342 mHandler.postUpdateSuggestions(); 2343 } else { 2344 mHandler.postUpdateBigramPredictions(); 2345 } 2346 } 2347 2348 // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to 2349 // {@link KeyboardSwitcher}. 2350 public void hapticAndAudioFeedback(final int primaryCode) { 2351 mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView()); 2352 } 2353 2354 @Override 2355 public void onPressKey(int primaryCode) { 2356 mKeyboardSwitcher.onPressKey(primaryCode); 2357 } 2358 2359 @Override 2360 public void onReleaseKey(int primaryCode, boolean withSliding) { 2361 mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); 2362 2363 // If accessibility is on, ensure the user receives keyboard state updates. 2364 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 2365 switch (primaryCode) { 2366 case Keyboard.CODE_SHIFT: 2367 AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); 2368 break; 2369 case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: 2370 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); 2371 break; 2372 } 2373 } 2374 2375 if (Keyboard.CODE_DELETE == primaryCode) { 2376 // This is a stopgap solution to avoid leaving a high surrogate alone in a text view. 2377 // In the future, we need to deprecate deteleSurroundingText() and have a surrogate 2378 // pair-friendly way of deleting characters in InputConnection. 2379 final InputConnection ic = getCurrentInputConnection(); 2380 if (null != ic) { 2381 final CharSequence lastChar = ic.getTextBeforeCursor(1, 0); 2382 if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) { 2383 ic.deleteSurroundingText(1, 0); 2384 } 2385 } 2386 } 2387 } 2388 2389 // receive ringer mode change and network state change. 2390 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 2391 @Override 2392 public void onReceive(Context context, Intent intent) { 2393 final String action = intent.getAction(); 2394 if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 2395 mSubtypeSwitcher.onNetworkStateChanged(intent); 2396 } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { 2397 mFeedbackManager.onRingerModeChanged(); 2398 } 2399 } 2400 }; 2401 2402 private void updateCorrectionMode() { 2403 // TODO: cleanup messy flags 2404 final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled 2405 && !mInputAttributes.mInputTypeNoAutoCorrect; 2406 mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE; 2407 mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect) 2408 ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; 2409 } 2410 2411 private void updateSuggestionVisibility(final Resources res) { 2412 final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting; 2413 for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) { 2414 if (suggestionVisiblityStr.equals(res.getString(visibility))) { 2415 mSuggestionVisibility = visibility; 2416 break; 2417 } 2418 } 2419 } 2420 2421 private void launchSettings() { 2422 launchSettingsClass(SettingsActivity.class); 2423 } 2424 2425 public void launchDebugSettings() { 2426 launchSettingsClass(DebugSettingsActivity.class); 2427 } 2428 2429 private void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) { 2430 handleClose(); 2431 Intent intent = new Intent(); 2432 intent.setClass(LatinIME.this, settingsClass); 2433 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 2434 startActivity(intent); 2435 } 2436 2437 private void showSubtypeSelectorAndSettings() { 2438 final CharSequence title = getString(R.string.english_ime_input_options); 2439 final CharSequence[] items = new CharSequence[] { 2440 // TODO: Should use new string "Select active input modes". 2441 getString(R.string.language_selection_title), 2442 getString(R.string.english_ime_settings), 2443 }; 2444 final Context context = this; 2445 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 2446 @Override 2447 public void onClick(DialogInterface di, int position) { 2448 di.dismiss(); 2449 switch (position) { 2450 case 0: 2451 Intent intent = CompatUtils.getInputLanguageSelectionIntent( 2452 ImfUtils.getInputMethodIdOfThisIme(context), 2453 Intent.FLAG_ACTIVITY_NEW_TASK 2454 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2455 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2456 startActivity(intent); 2457 break; 2458 case 1: 2459 launchSettings(); 2460 break; 2461 } 2462 } 2463 }; 2464 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 2465 .setItems(items, listener) 2466 .setTitle(title); 2467 showOptionDialog(builder.create()); 2468 } 2469 2470 /* package */ void showOptionDialog(AlertDialog dialog) { 2471 final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken(); 2472 if (windowToken == null) return; 2473 2474 dialog.setCancelable(true); 2475 dialog.setCanceledOnTouchOutside(true); 2476 2477 final Window window = dialog.getWindow(); 2478 final WindowManager.LayoutParams lp = window.getAttributes(); 2479 lp.token = windowToken; 2480 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 2481 window.setAttributes(lp); 2482 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 2483 2484 mOptionsDialog = dialog; 2485 dialog.show(); 2486 } 2487 2488 @Override 2489 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { 2490 super.dump(fd, fout, args); 2491 2492 final Printer p = new PrintWriterPrinter(fout); 2493 p.println("LatinIME state :"); 2494 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 2495 final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; 2496 p.println(" Keyboard mode = " + keyboardMode); 2497 p.println(" mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn); 2498 p.println(" mCorrectionMode=" + mCorrectionMode); 2499 p.println(" isComposingWord=" + mWordComposer.isComposingWord()); 2500 p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled); 2501 p.println(" mSoundOn=" + mSettingsValues.mSoundOn); 2502 p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn); 2503 p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn); 2504 p.println(" mInputAttributes=" + mInputAttributes.toString()); 2505 } 2506} 2507