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