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