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