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