LatinIME.java revision 9bc29d78a6ce83f77869aa63748176241e29d43c
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 (ProductionFlag.IS_EXPERIMENTAL) { 669 ResearchLogger.latinIME_onStartInputViewInternal(editorInfo); 670 } 671 if (StringUtils.inPrivateImeOptions(null, IME_OPTION_NO_MICROPHONE_COMPAT, editorInfo)) { 672 Log.w(TAG, "Deprecated private IME option specified: " 673 + editorInfo.privateImeOptions); 674 Log.w(TAG, "Use " + getPackageName() + "." + IME_OPTION_NO_MICROPHONE + " instead"); 675 } 676 if (StringUtils.inPrivateImeOptions(getPackageName(), IME_OPTION_FORCE_ASCII, editorInfo)) { 677 Log.w(TAG, "Deprecated private IME option specified: " 678 + editorInfo.privateImeOptions); 679 Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead"); 680 } 681 682 LatinImeLogger.onStartInputView(editorInfo); 683 // In landscape mode, this method gets called without the input view being created. 684 if (inputView == null) { 685 return; 686 } 687 688 // Forward this event to the accessibility utilities, if enabled. 689 final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); 690 if (accessUtils.isTouchExplorationEnabled()) { 691 accessUtils.onStartInputViewInternal(editorInfo, restarting); 692 } 693 694 mSubtypeSwitcher.updateParametersOnStartInputView(); 695 696 // The EditorInfo might have a flag that affects fullscreen mode. 697 // Note: This call should be done by InputMethodService? 698 updateFullscreenMode(); 699 mLastSelectionStart = editorInfo.initialSelStart; 700 mLastSelectionEnd = editorInfo.initialSelEnd; 701 mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode()); 702 mApplicationSpecifiedCompletions = null; 703 704 inputView.closing(); 705 mEnteredText = null; 706 resetComposingState(true /* alsoResetLastComposedWord */); 707 mDeleteCount = 0; 708 mSpaceState = SPACE_STATE_NONE; 709 710 loadSettings(); 711 updateCorrectionMode(); 712 updateSuggestionVisibility(mResources); 713 714 if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) { 715 mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold); 716 } 717 718 switcher.loadKeyboard(editorInfo, mSettingsValues); 719 720 if (mSuggestionsView != null) 721 mSuggestionsView.clear(); 722 setSuggestionStripShownInternal( 723 isSuggestionsStripVisible(), /* needsInputViewShown */ false); 724 // Delay updating suggestions because keyboard input view may not be shown at this point. 725 mHandler.postUpdateSuggestions(); 726 mHandler.cancelDoubleSpacesTimer(); 727 728 inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn, 729 mSettingsValues.mKeyPreviewPopupDismissDelay); 730 inputView.setProximityCorrectionEnabled(true); 731 732 if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); 733 } 734 735 @Override 736 public void onWindowHidden() { 737 super.onWindowHidden(); 738 KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 739 if (inputView != null) inputView.closing(); 740 } 741 742 private void onFinishInputInternal() { 743 super.onFinishInput(); 744 745 LatinImeLogger.commit(); 746 747 KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 748 if (inputView != null) inputView.closing(); 749 if (mUserHistoryDictionary != null) mUserHistoryDictionary.flushPendingWrites(); 750 } 751 752 private void onFinishInputViewInternal(boolean finishingInput) { 753 super.onFinishInputView(finishingInput); 754 mKeyboardSwitcher.onFinishInputView(); 755 KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 756 if (inputView != null) inputView.cancelAllMessages(); 757 // Remove pending messages related to update suggestions 758 mHandler.cancelUpdateSuggestions(); 759 } 760 761 @Override 762 public void onUpdateSelection(int oldSelStart, int oldSelEnd, 763 int newSelStart, int newSelEnd, 764 int composingSpanStart, int composingSpanEnd) { 765 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 766 composingSpanStart, composingSpanEnd); 767 768 if (DEBUG) { 769 Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart 770 + ", ose=" + oldSelEnd 771 + ", lss=" + mLastSelectionStart 772 + ", lse=" + mLastSelectionEnd 773 + ", nss=" + newSelStart 774 + ", nse=" + newSelEnd 775 + ", cs=" + composingSpanStart 776 + ", ce=" + composingSpanEnd); 777 } 778 if (ProductionFlag.IS_EXPERIMENTAL) { 779 ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd, 780 oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, 781 composingSpanEnd); 782 } 783 784 // TODO: refactor the following code to be less contrived. 785 // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means 786 // that the cursor is not at the end of the composing span, or there is a selection. 787 // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place 788 // as last time we were called (if there is a selection, it means the start hasn't 789 // changed, so it's the end that did). 790 final boolean selectionChanged = (newSelStart != composingSpanEnd 791 || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart; 792 // if composingSpanStart and composingSpanEnd are -1, it means there is no composing 793 // span in the view - we can use that to narrow down whether the cursor was moved 794 // by us or not. If we are composing a word but there is no composing span, then 795 // we know for sure the cursor moved while we were composing and we should reset 796 // the state. 797 final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; 798 if (!mExpectingUpdateSelection) { 799 // TAKE CARE: there is a race condition when we enter this test even when the user 800 // did not explicitly move the cursor. This happens when typing fast, where two keys 801 // turn this flag on in succession and both onUpdateSelection() calls arrive after 802 // the second one - the first call successfully avoids this test, but the second one 803 // enters. For the moment we rely on noComposingSpan to further reduce the impact. 804 805 // TODO: the following is probably better done in resetEntireInputState(). 806 // it should only happen when the cursor moved, and the very purpose of the 807 // test below is to narrow down whether this happened or not. Likewise with 808 // the call to postUpdateShiftState. 809 // We set this to NONE because after a cursor move, we don't want the space 810 // state-related special processing to kick in. 811 mSpaceState = SPACE_STATE_NONE; 812 813 if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) { 814 resetEntireInputState(); 815 } 816 817 mHandler.postUpdateShiftState(); 818 } 819 mExpectingUpdateSelection = false; 820 // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not 821 // here. It would probably be too expensive to call directly here but we may want to post a 822 // message to delay it. The point would be to unify behavior between backspace to the 823 // end of a word and manually put the pointer at the end of the word. 824 825 // Make a note of the cursor position 826 mLastSelectionStart = newSelStart; 827 mLastSelectionEnd = newSelEnd; 828 } 829 830 /** 831 * This is called when the user has clicked on the extracted text view, 832 * when running in fullscreen mode. The default implementation hides 833 * the suggestions view when this happens, but only if the extracted text 834 * editor has a vertical scroll bar because its text doesn't fit. 835 * Here we override the behavior due to the possibility that a re-correction could 836 * cause the suggestions strip to disappear and re-appear. 837 */ 838 @Override 839 public void onExtractedTextClicked() { 840 if (isSuggestionsRequested()) return; 841 842 super.onExtractedTextClicked(); 843 } 844 845 /** 846 * This is called when the user has performed a cursor movement in the 847 * extracted text view, when it is running in fullscreen mode. The default 848 * implementation hides the suggestions view when a vertical movement 849 * happens, but only if the extracted text editor has a vertical scroll bar 850 * because its text doesn't fit. 851 * Here we override the behavior due to the possibility that a re-correction could 852 * cause the suggestions strip to disappear and re-appear. 853 */ 854 @Override 855 public void onExtractedCursorMovement(int dx, int dy) { 856 if (isSuggestionsRequested()) return; 857 858 super.onExtractedCursorMovement(dx, dy); 859 } 860 861 @Override 862 public void hideWindow() { 863 LatinImeLogger.commit(); 864 mKeyboardSwitcher.onHideWindow(); 865 866 if (TRACE) Debug.stopMethodTracing(); 867 if (mOptionsDialog != null && mOptionsDialog.isShowing()) { 868 mOptionsDialog.dismiss(); 869 mOptionsDialog = null; 870 } 871 super.hideWindow(); 872 } 873 874 @Override 875 public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) { 876 if (DEBUG) { 877 Log.i(TAG, "Received completions:"); 878 if (applicationSpecifiedCompletions != null) { 879 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { 880 Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); 881 } 882 } 883 } 884 if (ProductionFlag.IS_EXPERIMENTAL) { 885 ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); 886 } 887 if (mInputAttributes.mApplicationSpecifiedCompletionOn) { 888 mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; 889 if (applicationSpecifiedCompletions == null) { 890 clearSuggestions(); 891 return; 892 } 893 894 final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = 895 SuggestedWords.getFromApplicationSpecifiedCompletions( 896 applicationSpecifiedCompletions); 897 final SuggestedWords suggestedWords = new SuggestedWords( 898 applicationSuggestedWords, 899 false /* typedWordValid */, 900 false /* hasAutoCorrectionCandidate */, 901 false /* allowsToBeAutoCorrected */, 902 false /* isPunctuationSuggestions */, 903 false /* isObsoleteSuggestions */); 904 // When in fullscreen mode, show completions generated by the application 905 final boolean isAutoCorrection = false; 906 setSuggestions(suggestedWords, isAutoCorrection); 907 setAutoCorrectionIndicator(isAutoCorrection); 908 // TODO: is this the right thing to do? What should we auto-correct to in 909 // this case? This says to keep whatever the user typed. 910 mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); 911 setSuggestionStripShown(true); 912 } 913 } 914 915 private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) { 916 // TODO: Modify this if we support suggestions with hard keyboard 917 if (onEvaluateInputViewShown() && mSuggestionsContainer != null) { 918 final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); 919 final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false; 920 final boolean shouldShowSuggestions = shown 921 && (needsInputViewShown ? inputViewShown : true); 922 if (isFullscreenMode()) { 923 mSuggestionsContainer.setVisibility( 924 shouldShowSuggestions ? View.VISIBLE : View.GONE); 925 } else { 926 mSuggestionsContainer.setVisibility( 927 shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); 928 } 929 } 930 } 931 932 private void setSuggestionStripShown(boolean shown) { 933 setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); 934 } 935 936 private void adjustInputViewHeight() { 937 if (mKeyPreviewBackingView.getHeight() > 0) { 938 return; 939 } 940 941 final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); 942 if (keyboardView == null) return; 943 final int keyboardHeight = keyboardView.getHeight(); 944 final int suggestionsHeight = mSuggestionsContainer.getHeight(); 945 final int displayHeight = mResources.getDisplayMetrics().heightPixels; 946 final Rect rect = new Rect(); 947 mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect); 948 final int notificationBarHeight = rect.top; 949 final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight 950 - keyboardHeight; 951 952 final LayoutParams params = mKeyPreviewBackingView.getLayoutParams(); 953 params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight); 954 mKeyPreviewBackingView.setLayoutParams(params); 955 } 956 957 @Override 958 public void onComputeInsets(InputMethodService.Insets outInsets) { 959 super.onComputeInsets(outInsets); 960 final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 961 if (inputView == null || mSuggestionsContainer == null) 962 return; 963 adjustInputViewHeight(); 964 // In fullscreen mode, the height of the extract area managed by InputMethodService should 965 // be considered. 966 // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. 967 final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; 968 final int backingHeight = (mKeyPreviewBackingView.getVisibility() == View.GONE) ? 0 969 : mKeyPreviewBackingView.getHeight(); 970 final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0 971 : mSuggestionsContainer.getHeight(); 972 final int extraHeight = extractHeight + backingHeight + suggestionsHeight; 973 int touchY = extraHeight; 974 // Need to set touchable region only if input view is being shown 975 final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); 976 if (keyboardView != null && keyboardView.isShown()) { 977 if (mSuggestionsContainer.getVisibility() == View.VISIBLE) { 978 touchY -= suggestionsHeight; 979 } 980 final int touchWidth = inputView.getWidth(); 981 final int touchHeight = inputView.getHeight() + extraHeight 982 // Extend touchable region below the keyboard. 983 + EXTENDED_TOUCHABLE_REGION_HEIGHT; 984 outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; 985 outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight); 986 } 987 outInsets.contentTopInsets = touchY; 988 outInsets.visibleTopInsets = touchY; 989 } 990 991 @Override 992 public boolean onEvaluateFullscreenMode() { 993 // Reread resource value here, because this method is called by framework anytime as needed. 994 final boolean isFullscreenModeAllowed = 995 mSettingsValues.isFullscreenModeAllowed(getResources()); 996 return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed; 997 } 998 999 @Override 1000 public void updateFullscreenMode() { 1001 super.updateFullscreenMode(); 1002 1003 if (mKeyPreviewBackingView == null) return; 1004 // In fullscreen mode, no need to have extra space to show the key preview. 1005 // If not, we should have extra space above the keyboard to show the key preview. 1006 mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); 1007 } 1008 1009 @Override 1010 public boolean onKeyDown(int keyCode, KeyEvent event) { 1011 switch (keyCode) { 1012 case KeyEvent.KEYCODE_BACK: 1013 if (event.getRepeatCount() == 0) { 1014 if (mSuggestionsView != null && mSuggestionsView.handleBack()) { 1015 return true; 1016 } 1017 final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); 1018 if (keyboardView != null && keyboardView.handleBack()) { 1019 return true; 1020 } 1021 } 1022 break; 1023 } 1024 return super.onKeyDown(keyCode, event); 1025 } 1026 1027 @Override 1028 public boolean onKeyUp(int keyCode, KeyEvent event) { 1029 switch (keyCode) { 1030 case KeyEvent.KEYCODE_DPAD_DOWN: 1031 case KeyEvent.KEYCODE_DPAD_UP: 1032 case KeyEvent.KEYCODE_DPAD_LEFT: 1033 case KeyEvent.KEYCODE_DPAD_RIGHT: 1034 final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); 1035 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1036 // Enable shift key and DPAD to do selections 1037 if ((keyboardView != null && keyboardView.isShown()) 1038 && (keyboard != null && keyboard.isShiftedOrShiftLocked())) { 1039 KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(), 1040 event.getAction(), event.getKeyCode(), event.getRepeatCount(), 1041 event.getDeviceId(), event.getScanCode(), 1042 KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); 1043 final InputConnection ic = getCurrentInputConnection(); 1044 if (ic != null) 1045 ic.sendKeyEvent(newEvent); 1046 return true; 1047 } 1048 break; 1049 } 1050 return super.onKeyUp(keyCode, event); 1051 } 1052 1053 // This will reset the whole input state to the starting state. It will clear 1054 // the composing word, reset the last composed word, tell the inputconnection 1055 // and the composingStateManager about it. 1056 private void resetEntireInputState() { 1057 resetComposingState(true /* alsoResetLastComposedWord */); 1058 updateSuggestions(); 1059 final InputConnection ic = getCurrentInputConnection(); 1060 if (ic != null) { 1061 ic.finishComposingText(); 1062 } 1063 } 1064 1065 private void resetComposingState(final boolean alsoResetLastComposedWord) { 1066 mWordComposer.reset(); 1067 if (alsoResetLastComposedWord) 1068 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 1069 } 1070 1071 public void commitTyped(final InputConnection ic, final int separatorCode) { 1072 if (!mWordComposer.isComposingWord()) return; 1073 final CharSequence typedWord = mWordComposer.getTypedWord(); 1074 if (typedWord.length() > 0) { 1075 mLastComposedWord = mWordComposer.commitWord( 1076 LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(), 1077 separatorCode); 1078 if (ic != null) { 1079 ic.commitText(typedWord, 1); 1080 } 1081 addToUserHistoryDictionary(typedWord); 1082 } 1083 updateSuggestions(); 1084 } 1085 1086 public boolean getCurrentAutoCapsState() { 1087 final InputConnection ic = getCurrentInputConnection(); 1088 EditorInfo ei = getCurrentInputEditorInfo(); 1089 if (mSettingsValues.mAutoCap && ic != null && ei != null 1090 && ei.inputType != InputType.TYPE_NULL) { 1091 return ic.getCursorCapsMode(ei.inputType) != 0; 1092 } 1093 return false; 1094 } 1095 1096 // "ic" may be null 1097 private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) { 1098 if (null == ic) return; 1099 CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); 1100 // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. 1101 if (lastTwo != null && lastTwo.length() == 2 1102 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) { 1103 ic.deleteSurroundingText(2, 0); 1104 ic.commitText(lastTwo.charAt(1) + " ", 1); 1105 mKeyboardSwitcher.updateShiftState(); 1106 } 1107 } 1108 1109 private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) { 1110 if (mCorrectionMode == Suggest.CORRECTION_NONE) return false; 1111 if (ic == null) return false; 1112 final CharSequence lastThree = ic.getTextBeforeCursor(3, 0); 1113 if (lastThree != null && lastThree.length() == 3 1114 && StringUtils.canBeFollowedByPeriod(lastThree.charAt(0)) 1115 && lastThree.charAt(1) == Keyboard.CODE_SPACE 1116 && lastThree.charAt(2) == Keyboard.CODE_SPACE 1117 && mHandler.isAcceptingDoubleSpaces()) { 1118 mHandler.cancelDoubleSpacesTimer(); 1119 ic.deleteSurroundingText(2, 0); 1120 ic.commitText(". ", 1); 1121 mKeyboardSwitcher.updateShiftState(); 1122 return true; 1123 } 1124 return false; 1125 } 1126 1127 // "ic" may be null 1128 private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) { 1129 if (ic == null) return; 1130 final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); 1131 if (lastOne != null && lastOne.length() == 1 1132 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { 1133 ic.deleteSurroundingText(1, 0); 1134 } 1135 } 1136 1137 @Override 1138 public boolean addWordToDictionary(String word) { 1139 mUserDictionary.addWord(word, 128); 1140 // Suggestion strip should be updated after the operation of adding word to the 1141 // user dictionary 1142 mHandler.postUpdateSuggestions(); 1143 return true; 1144 } 1145 1146 private static boolean isAlphabet(int code) { 1147 return Character.isLetter(code); 1148 } 1149 1150 private void onSettingsKeyPressed() { 1151 if (isShowingOptionDialog()) return; 1152 showSubtypeSelectorAndSettings(); 1153 } 1154 1155 // Virtual codes representing custom requests. These are used in onCustomRequest() below. 1156 public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1; 1157 public static final int CODE_HAPTIC_AND_AUDIO_FEEDBACK = 2; 1158 1159 @Override 1160 public boolean onCustomRequest(int requestCode) { 1161 if (isShowingOptionDialog()) return false; 1162 switch (requestCode) { 1163 case CODE_SHOW_INPUT_METHOD_PICKER: 1164 if (SubtypeUtils.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { 1165 mImm.showInputMethodPicker(); 1166 return true; 1167 } 1168 return false; 1169 case CODE_HAPTIC_AND_AUDIO_FEEDBACK: 1170 hapticAndAudioFeedback(Keyboard.CODE_UNSPECIFIED); 1171 return true; 1172 } 1173 return false; 1174 } 1175 1176 private boolean isShowingOptionDialog() { 1177 return mOptionsDialog != null && mOptionsDialog.isShowing(); 1178 } 1179 1180 private static int getActionId(Keyboard keyboard) { 1181 return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE; 1182 } 1183 1184 private void performEditorAction(int actionId) { 1185 final InputConnection ic = getCurrentInputConnection(); 1186 if (ic != null) { 1187 ic.performEditorAction(actionId); 1188 } 1189 } 1190 1191 private void handleLanguageSwitchKey() { 1192 final boolean includesOtherImes = mSettingsValues.mIncludesOtherImesInLanguageSwitchList; 1193 final IBinder token = getWindow().getWindow().getAttributes().token; 1194 if (mShouldSwitchToLastSubtype) { 1195 final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype(); 1196 final boolean lastSubtypeBelongsToThisIme = SubtypeUtils.checkIfSubtypeBelongsToThisIme( 1197 this, lastSubtype); 1198 if ((includesOtherImes || lastSubtypeBelongsToThisIme) 1199 && mImm.switchToLastInputMethod(token)) { 1200 mShouldSwitchToLastSubtype = false; 1201 } else { 1202 mImm.switchToNextInputMethod(token, !includesOtherImes); 1203 mShouldSwitchToLastSubtype = true; 1204 } 1205 } else { 1206 mImm.switchToNextInputMethod(token, !includesOtherImes); 1207 } 1208 } 1209 1210 private void sendKeyCodePoint(int code) { 1211 // TODO: Remove this special handling of digit letters. 1212 // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. 1213 if (code >= '0' && code <= '9') { 1214 super.sendKeyChar((char)code); 1215 return; 1216 } 1217 1218 final InputConnection ic = getCurrentInputConnection(); 1219 if (ic != null) { 1220 final String text = new String(new int[] { code }, 0, 1); 1221 ic.commitText(text, text.length()); 1222 } 1223 } 1224 1225 // Implementation of {@link KeyboardActionListener}. 1226 @Override 1227 public void onCodeInput(int primaryCode, int x, int y) { 1228 final long when = SystemClock.uptimeMillis(); 1229 if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { 1230 mDeleteCount = 0; 1231 } 1232 mLastKeyTime = when; 1233 1234 if (ProductionFlag.IS_EXPERIMENTAL) { 1235 if (ResearchLogger.sIsLogging) { 1236 ResearchLogger.getInstance().logKeyEvent(primaryCode, x, y); 1237 } 1238 } 1239 1240 final KeyboardSwitcher switcher = mKeyboardSwitcher; 1241 // The space state depends only on the last character pressed and its own previous 1242 // state. Here, we revert the space state to neutral if the key is actually modifying 1243 // the input contents (any non-shift key), which is what we should do for 1244 // all inputs that do not result in a special state. Each character handling is then 1245 // free to override the state as they see fit. 1246 final int spaceState = mSpaceState; 1247 if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false; 1248 1249 // TODO: Consolidate the double space timer, mLastKeyTime, and the space state. 1250 if (primaryCode != Keyboard.CODE_SPACE) { 1251 mHandler.cancelDoubleSpacesTimer(); 1252 } 1253 1254 boolean didAutoCorrect = false; 1255 switch (primaryCode) { 1256 case Keyboard.CODE_DELETE: 1257 mSpaceState = SPACE_STATE_NONE; 1258 handleBackspace(spaceState); 1259 mDeleteCount++; 1260 mExpectingUpdateSelection = true; 1261 mShouldSwitchToLastSubtype = true; 1262 LatinImeLogger.logOnDelete(x, y); 1263 break; 1264 case Keyboard.CODE_SHIFT: 1265 case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: 1266 // Shift and symbol key is handled in onPressKey() and onReleaseKey(). 1267 break; 1268 case Keyboard.CODE_SETTINGS: 1269 onSettingsKeyPressed(); 1270 break; 1271 case Keyboard.CODE_SHORTCUT: 1272 mSubtypeSwitcher.switchToShortcutIME(); 1273 break; 1274 case Keyboard.CODE_ACTION_ENTER: 1275 performEditorAction(getActionId(switcher.getKeyboard())); 1276 break; 1277 case Keyboard.CODE_ACTION_NEXT: 1278 performEditorAction(EditorInfo.IME_ACTION_NEXT); 1279 break; 1280 case Keyboard.CODE_ACTION_PREVIOUS: 1281 performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 1282 break; 1283 case Keyboard.CODE_LANGUAGE_SWITCH: 1284 handleLanguageSwitchKey(); 1285 break; 1286 default: 1287 mSpaceState = SPACE_STATE_NONE; 1288 if (mSettingsValues.isWordSeparator(primaryCode)) { 1289 didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); 1290 } else { 1291 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1292 if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { 1293 handleCharacter(primaryCode, x, y, spaceState); 1294 } else { 1295 handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE, 1296 spaceState); 1297 } 1298 } 1299 mExpectingUpdateSelection = true; 1300 mShouldSwitchToLastSubtype = true; 1301 break; 1302 } 1303 switcher.onCodeInput(primaryCode); 1304 // Reset after any single keystroke 1305 if (!didAutoCorrect) 1306 mLastComposedWord.deactivate(); 1307 mEnteredText = null; 1308 } 1309 1310 @Override 1311 public void onTextInput(CharSequence text) { 1312 final InputConnection ic = getCurrentInputConnection(); 1313 if (ic == null) return; 1314 ic.beginBatchEdit(); 1315 commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR); 1316 text = specificTldProcessingOnTextInput(ic, text); 1317 if (SPACE_STATE_PHANTOM == mSpaceState) { 1318 sendKeyCodePoint(Keyboard.CODE_SPACE); 1319 } 1320 ic.commitText(text, 1); 1321 ic.endBatchEdit(); 1322 mKeyboardSwitcher.updateShiftState(); 1323 mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT); 1324 mSpaceState = SPACE_STATE_NONE; 1325 mEnteredText = text; 1326 resetComposingState(true /* alsoResetLastComposedWord */); 1327 } 1328 1329 // ic may not be null 1330 private CharSequence specificTldProcessingOnTextInput(final InputConnection ic, 1331 final CharSequence text) { 1332 if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD 1333 || !Character.isLetter(text.charAt(1))) { 1334 // Not a tld: do nothing. 1335 return text; 1336 } 1337 // We have a TLD (or something that looks like this): make sure we don't add 1338 // a space even if currently in phantom mode. 1339 mSpaceState = SPACE_STATE_NONE; 1340 final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); 1341 if (lastOne != null && lastOne.length() == 1 1342 && lastOne.charAt(0) == Keyboard.CODE_PERIOD) { 1343 return text.subSequence(1, text.length()); 1344 } else { 1345 return text; 1346 } 1347 } 1348 1349 @Override 1350 public void onCancelInput() { 1351 // User released a finger outside any key 1352 mKeyboardSwitcher.onCancelInput(); 1353 } 1354 1355 private void handleBackspace(final int spaceState) { 1356 final InputConnection ic = getCurrentInputConnection(); 1357 if (ic == null) return; 1358 ic.beginBatchEdit(); 1359 handleBackspaceWhileInBatchEdit(spaceState, ic); 1360 ic.endBatchEdit(); 1361 } 1362 1363 // "ic" may not be null. 1364 private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) { 1365 // In many cases, we may have to put the keyboard in auto-shift state again. 1366 mHandler.postUpdateShiftState(); 1367 1368 if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { 1369 // Cancel multi-character input: remove the text we just entered. 1370 // This is triggered on backspace after a key that inputs multiple characters, 1371 // like the smiley key or the .com key. 1372 ic.deleteSurroundingText(mEnteredText.length(), 0); 1373 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. 1374 // In addition we know that spaceState is false, and that we should not be 1375 // reverting any autocorrect at this point. So we can safely return. 1376 return; 1377 } 1378 1379 if (mWordComposer.isComposingWord()) { 1380 final int length = mWordComposer.size(); 1381 if (length > 0) { 1382 mWordComposer.deleteLast(); 1383 ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1384 // If we have deleted the last remaining character of a word, then we are not 1385 // isComposingWord() any more. 1386 if (!mWordComposer.isComposingWord()) { 1387 // Not composing word any more, so we can show bigrams. 1388 mHandler.postUpdateBigramPredictions(); 1389 } else { 1390 // Still composing a word, so we still have letters to deduce a suggestion from. 1391 mHandler.postUpdateSuggestions(); 1392 } 1393 } else { 1394 ic.deleteSurroundingText(1, 0); 1395 } 1396 } else { 1397 if (mLastComposedWord.canRevertCommit()) { 1398 Utils.Stats.onAutoCorrectionCancellation(); 1399 revertCommit(ic); 1400 return; 1401 } 1402 if (SPACE_STATE_DOUBLE == spaceState) { 1403 if (revertDoubleSpaceWhileInBatchEdit(ic)) { 1404 // No need to reset mSpaceState, it has already be done (that's why we 1405 // receive it as a parameter) 1406 return; 1407 } 1408 } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1409 if (revertSwapPunctuation(ic)) { 1410 // Likewise 1411 return; 1412 } 1413 } 1414 1415 // No cancelling of commit/double space/swap: we have a regular backspace. 1416 // We should backspace one char and restart suggestion if at the end of a word. 1417 if (mLastSelectionStart != mLastSelectionEnd) { 1418 // If there is a selection, remove it. 1419 final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart; 1420 ic.setSelection(mLastSelectionEnd, mLastSelectionEnd); 1421 ic.deleteSurroundingText(lengthToDelete, 0); 1422 } else { 1423 // There is no selection, just delete one character. 1424 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { 1425 // This should never happen. 1426 Log.e(TAG, "Backspace when we don't know the selection position"); 1427 } 1428 ic.deleteSurroundingText(1, 0); 1429 if (mDeleteCount > DELETE_ACCELERATE_AT) { 1430 ic.deleteSurroundingText(1, 0); 1431 } 1432 } 1433 if (isSuggestionsRequested()) { 1434 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic); 1435 } 1436 } 1437 } 1438 1439 // ic may be null 1440 private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code, 1441 final int spaceState, final boolean isFromSuggestionStrip) { 1442 if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { 1443 removeTrailingSpaceWhileInBatchEdit(ic); 1444 return false; 1445 } else if ((SPACE_STATE_WEAK == spaceState 1446 || SPACE_STATE_SWAP_PUNCTUATION == spaceState) 1447 && isFromSuggestionStrip) { 1448 if (mSettingsValues.isWeakSpaceSwapper(code)) { 1449 return true; 1450 } else { 1451 if (mSettingsValues.isWeakSpaceStripper(code)) { 1452 removeTrailingSpaceWhileInBatchEdit(ic); 1453 } 1454 return false; 1455 } 1456 } else { 1457 return false; 1458 } 1459 } 1460 1461 private void handleCharacter(final int primaryCode, final int x, 1462 final int y, final int spaceState) { 1463 final InputConnection ic = getCurrentInputConnection(); 1464 if (null != ic) ic.beginBatchEdit(); 1465 // TODO: if ic is null, does it make any sense to call this? 1466 handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic); 1467 if (null != ic) ic.endBatchEdit(); 1468 } 1469 1470 // "ic" may be null without this crashing, but the behavior will be really strange 1471 private void handleCharacterWhileInBatchEdit(final int primaryCode, 1472 final int x, final int y, final int spaceState, final InputConnection ic) { 1473 boolean isComposingWord = mWordComposer.isComposingWord(); 1474 1475 if (SPACE_STATE_PHANTOM == spaceState && 1476 !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) { 1477 if (isComposingWord) { 1478 // Sanity check 1479 throw new RuntimeException("Should not be composing here"); 1480 } 1481 sendKeyCodePoint(Keyboard.CODE_SPACE); 1482 } 1483 1484 if ((isAlphabet(primaryCode) 1485 || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) 1486 && isSuggestionsRequested() && !isCursorTouchingWord()) { 1487 if (!isComposingWord) { 1488 // Reset entirely the composing state anyway, then start composing a new word unless 1489 // the character is a single quote. The idea here is, single quote is not a 1490 // separator and it should be treated as a normal character, except in the first 1491 // position where it should not start composing a word. 1492 isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode); 1493 // Here we don't need to reset the last composed word. It will be reset 1494 // when we commit this one, if we ever do; if on the other hand we backspace 1495 // it entirely and resume suggestions on the previous word, we'd like to still 1496 // have touch coordinates for it. 1497 resetComposingState(false /* alsoResetLastComposedWord */); 1498 clearSuggestions(); 1499 } 1500 } 1501 if (isComposingWord) { 1502 mWordComposer.add( 1503 primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector()); 1504 if (ic != null) { 1505 // If it's the first letter, make note of auto-caps state 1506 if (mWordComposer.size() == 1) { 1507 mWordComposer.setAutoCapitalized(getCurrentAutoCapsState()); 1508 } 1509 ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1510 } 1511 mHandler.postUpdateSuggestions(); 1512 } else { 1513 final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, 1514 spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x); 1515 1516 sendKeyCodePoint(primaryCode); 1517 1518 if (swapWeakSpace) { 1519 swapSwapperAndSpaceWhileInBatchEdit(ic); 1520 mSpaceState = SPACE_STATE_WEAK; 1521 } 1522 // Some characters are not word separators, yet they don't start a new 1523 // composing span. For these, we haven't changed the suggestion strip, and 1524 // if the "add to dictionary" hint is shown, we should do so now. Examples of 1525 // such characters include single quote, dollar, and others; the exact list is 1526 // the list of characters for which we enter handleCharacterWhileInBatchEdit 1527 // that don't match the test if ((isAlphabet...)) at the top of this method. 1528 if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) { 1529 mHandler.postUpdateBigramPredictions(); 1530 } 1531 } 1532 Utils.Stats.onNonSeparator((char)primaryCode, x, y); 1533 } 1534 1535 // Returns true if we did an autocorrection, false otherwise. 1536 private boolean handleSeparator(final int primaryCode, final int x, final int y, 1537 final int spaceState) { 1538 // Should dismiss the "Touch again to save" message when handling separator 1539 if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) { 1540 mHandler.cancelUpdateBigramPredictions(); 1541 mHandler.postUpdateSuggestions(); 1542 } 1543 1544 boolean didAutoCorrect = false; 1545 // Handle separator 1546 final InputConnection ic = getCurrentInputConnection(); 1547 if (ic != null) { 1548 ic.beginBatchEdit(); 1549 } 1550 if (mWordComposer.isComposingWord()) { 1551 // In certain languages where single quote is a separator, it's better 1552 // not to auto correct, but accept the typed word. For instance, 1553 // in Italian dov' should not be expanded to dove' because the elision 1554 // requires the last vowel to be removed. 1555 final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled 1556 && !mInputAttributes.mInputTypeNoAutoCorrect; 1557 if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) { 1558 commitCurrentAutoCorrection(primaryCode, ic); 1559 didAutoCorrect = true; 1560 } else { 1561 commitTyped(ic, primaryCode); 1562 } 1563 } 1564 1565 final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState, 1566 KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x); 1567 1568 if (SPACE_STATE_PHANTOM == spaceState && 1569 mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) { 1570 sendKeyCodePoint(Keyboard.CODE_SPACE); 1571 } 1572 sendKeyCodePoint(primaryCode); 1573 1574 if (Keyboard.CODE_SPACE == primaryCode) { 1575 if (isSuggestionsRequested()) { 1576 if (maybeDoubleSpaceWhileInBatchEdit(ic)) { 1577 mSpaceState = SPACE_STATE_DOUBLE; 1578 } else if (!isShowingPunctuationList()) { 1579 mSpaceState = SPACE_STATE_WEAK; 1580 } 1581 } 1582 1583 mHandler.startDoubleSpacesTimer(); 1584 if (!isCursorTouchingWord()) { 1585 mHandler.cancelUpdateSuggestions(); 1586 mHandler.postUpdateBigramPredictions(); 1587 } 1588 } else { 1589 if (swapWeakSpace) { 1590 swapSwapperAndSpaceWhileInBatchEdit(ic); 1591 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; 1592 } else if (SPACE_STATE_PHANTOM == spaceState) { 1593 // If we are in phantom space state, and the user presses a separator, we want to 1594 // stay in phantom space state so that the next keypress has a chance to add the 1595 // space. For example, if I type "Good dat", pick "day" from the suggestion strip 1596 // then insert a comma and go on to typing the next word, I want the space to be 1597 // inserted automatically before the next word, the same way it is when I don't 1598 // input the comma. 1599 mSpaceState = SPACE_STATE_PHANTOM; 1600 } 1601 1602 // Set punctuation right away. onUpdateSelection will fire but tests whether it is 1603 // already displayed or not, so it's okay. 1604 setPunctuationSuggestions(); 1605 } 1606 1607 Utils.Stats.onSeparator((char)primaryCode, x, y); 1608 1609 if (ic != null) { 1610 ic.endBatchEdit(); 1611 } 1612 return didAutoCorrect; 1613 } 1614 1615 private CharSequence getTextWithUnderline(final CharSequence text) { 1616 return mIsAutoCorrectionIndicatorOn 1617 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) 1618 : text; 1619 } 1620 1621 private void handleClose() { 1622 commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR); 1623 requestHideSelf(0); 1624 LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); 1625 if (inputView != null) 1626 inputView.closing(); 1627 } 1628 1629 public boolean isSuggestionsRequested() { 1630 return mInputAttributes.mIsSettingsSuggestionStripOn 1631 && (mCorrectionMode > 0 || isShowingSuggestionsStrip()); 1632 } 1633 1634 public boolean isShowingPunctuationList() { 1635 if (mSuggestionsView == null) return false; 1636 return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions(); 1637 } 1638 1639 public boolean isShowingSuggestionsStrip() { 1640 return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE) 1641 || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE 1642 && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT); 1643 } 1644 1645 public boolean isSuggestionsStripVisible() { 1646 if (mSuggestionsView == null) 1647 return false; 1648 if (mSuggestionsView.isShowingAddToDictionaryHint()) 1649 return true; 1650 if (!isShowingSuggestionsStrip()) 1651 return false; 1652 if (mInputAttributes.mApplicationSpecifiedCompletionOn) 1653 return true; 1654 return isSuggestionsRequested(); 1655 } 1656 1657 public void switchToKeyboardView() { 1658 if (DEBUG) { 1659 Log.d(TAG, "Switch to keyboard view."); 1660 } 1661 if (ProductionFlag.IS_EXPERIMENTAL) { 1662 ResearchLogger.latinIME_switchToKeyboardView(); 1663 } 1664 View v = mKeyboardSwitcher.getKeyboardView(); 1665 if (v != null) { 1666 // Confirms that the keyboard view doesn't have parent view. 1667 ViewParent p = v.getParent(); 1668 if (p != null && p instanceof ViewGroup) { 1669 ((ViewGroup) p).removeView(v); 1670 } 1671 setInputView(v); 1672 } 1673 setSuggestionStripShown(isSuggestionsStripVisible()); 1674 updateInputViewShown(); 1675 mHandler.postUpdateSuggestions(); 1676 } 1677 1678 public void clearSuggestions() { 1679 setSuggestions(SuggestedWords.EMPTY, false); 1680 setAutoCorrectionIndicator(false); 1681 } 1682 1683 private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) { 1684 if (mSuggestionsView != null) { 1685 mSuggestionsView.setSuggestions(words); 1686 mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); 1687 } 1688 } 1689 1690 private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { 1691 // Put a blue underline to a word in TextView which will be auto-corrected. 1692 final InputConnection ic = getCurrentInputConnection(); 1693 if (ic == null) return; 1694 if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator 1695 && mWordComposer.isComposingWord()) { 1696 mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; 1697 final CharSequence textWithUnderline = 1698 getTextWithUnderline(mWordComposer.getTypedWord()); 1699 ic.setComposingText(textWithUnderline, 1); 1700 } 1701 } 1702 1703 public void updateSuggestions() { 1704 // Check if we have a suggestion engine attached. 1705 if ((mSuggest == null || !isSuggestionsRequested())) { 1706 if (mWordComposer.isComposingWord()) { 1707 Log.w(TAG, "Called updateSuggestions but suggestions were not requested!"); 1708 mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); 1709 } 1710 return; 1711 } 1712 1713 mHandler.cancelUpdateSuggestions(); 1714 mHandler.cancelUpdateBigramPredictions(); 1715 1716 if (!mWordComposer.isComposingWord()) { 1717 setPunctuationSuggestions(); 1718 return; 1719 } 1720 1721 // TODO: May need a better way of retrieving previous word 1722 final InputConnection ic = getCurrentInputConnection(); 1723 final CharSequence prevWord; 1724 if (null == ic) { 1725 prevWord = null; 1726 } else { 1727 prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators); 1728 } 1729 1730 final CharSequence typedWord = mWordComposer.getTypedWord(); 1731 // getSuggestedWords handles gracefully a null value of prevWord 1732 final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer, 1733 prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode); 1734 1735 // Basically, we update the suggestion strip only when suggestion count > 1. However, 1736 // there is an exception: We update the suggestion strip whenever typed word's length 1737 // is 1 or typed word is found in dictionary, regardless of suggestion count. Actually, 1738 // in most cases, suggestion count is 1 when typed word's length is 1, but we do always 1739 // need to clear the previous state when the user starts typing a word (i.e. typed word's 1740 // length == 1). 1741 if (suggestedWords.size() > 1 || typedWord.length() == 1 1742 || !suggestedWords.mAllowsToBeAutoCorrected 1743 || mSuggestionsView.isShowingAddToDictionaryHint()) { 1744 showSuggestions(suggestedWords, typedWord); 1745 } else { 1746 SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions(); 1747 if (previousSuggestions == mSettingsValues.mSuggestPuncList) { 1748 previousSuggestions = SuggestedWords.EMPTY; 1749 } 1750 final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = 1751 SuggestedWords.getTypedWordAndPreviousSuggestions( 1752 typedWord, previousSuggestions); 1753 final SuggestedWords obsoleteSuggestedWords = 1754 new SuggestedWords(typedWordAndPreviousSuggestions, 1755 false /* typedWordValid */, 1756 false /* hasAutoCorrectionCandidate */, 1757 false /* allowsToBeAutoCorrected */, 1758 false /* isPunctuationSuggestions */, 1759 true /* isObsoleteSuggestions */); 1760 showSuggestions(obsoleteSuggestedWords, typedWord); 1761 } 1762 } 1763 1764 public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) { 1765 final CharSequence autoCorrection; 1766 if (suggestedWords.size() > 0) { 1767 if (suggestedWords.hasAutoCorrectionWord()) { 1768 autoCorrection = suggestedWords.getWord(1); 1769 } else { 1770 autoCorrection = typedWord; 1771 } 1772 } else { 1773 autoCorrection = null; 1774 } 1775 mWordComposer.setAutoCorrection(autoCorrection); 1776 final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); 1777 setSuggestions(suggestedWords, isAutoCorrection); 1778 setAutoCorrectionIndicator(isAutoCorrection); 1779 setSuggestionStripShown(isSuggestionsStripVisible()); 1780 } 1781 1782 private void commitCurrentAutoCorrection(final int separatorCodePoint, 1783 final InputConnection ic) { 1784 // Complete any pending suggestions query first 1785 if (mHandler.hasPendingUpdateSuggestions()) { 1786 mHandler.cancelUpdateSuggestions(); 1787 updateSuggestions(); 1788 } 1789 final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull(); 1790 if (autoCorrection != null) { 1791 final String typedWord = mWordComposer.getTypedWord(); 1792 if (TextUtils.isEmpty(typedWord)) { 1793 throw new RuntimeException("We have an auto-correction but the typed word " 1794 + "is empty? Impossible! I must commit suicide."); 1795 } 1796 Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint); 1797 mExpectingUpdateSelection = true; 1798 commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, 1799 separatorCodePoint); 1800 // Add the word to the user history dictionary 1801 addToUserHistoryDictionary(autoCorrection); 1802 if (!typedWord.equals(autoCorrection) && null != ic) { 1803 // This will make the correction flash for a short while as a visual clue 1804 // to the user that auto-correction happened. 1805 ic.commitCorrection(new CorrectionInfo(mLastSelectionEnd - typedWord.length(), 1806 typedWord, autoCorrection)); 1807 } 1808 } 1809 } 1810 1811 @Override 1812 public void pickSuggestionManually(final int index, final CharSequence suggestion) { 1813 final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions(); 1814 1815 if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) { 1816 int firstChar = Character.codePointAt(suggestion, 0); 1817 if ((!mSettingsValues.isWeakSpaceStripper(firstChar)) 1818 && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) { 1819 sendKeyCodePoint(Keyboard.CODE_SPACE); 1820 } 1821 } 1822 1823 if (mInputAttributes.mApplicationSpecifiedCompletionOn 1824 && mApplicationSpecifiedCompletions != null 1825 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 1826 if (mSuggestionsView != null) { 1827 mSuggestionsView.clear(); 1828 } 1829 mKeyboardSwitcher.updateShiftState(); 1830 resetComposingState(true /* alsoResetLastComposedWord */); 1831 final InputConnection ic = getCurrentInputConnection(); 1832 if (ic != null) { 1833 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; 1834 ic.commitCompletion(completionInfo); 1835 } 1836 return; 1837 } 1838 1839 // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput 1840 if (suggestion.length() == 1 && isShowingPunctuationList()) { 1841 // Word separators are suggested before the user inputs something. 1842 // So, LatinImeLogger logs "" as a user's input. 1843 LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords); 1844 // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. 1845 final int primaryCode = suggestion.charAt(0); 1846 onCodeInput(primaryCode, 1847 KeyboardActionListener.SUGGESTION_STRIP_COORDINATE, 1848 KeyboardActionListener.SUGGESTION_STRIP_COORDINATE); 1849 return; 1850 } 1851 // We need to log before we commit, because the word composer will store away the user 1852 // typed word. 1853 LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(), 1854 suggestion.toString(), index, suggestedWords); 1855 mExpectingUpdateSelection = true; 1856 commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, 1857 LastComposedWord.NOT_A_SEPARATOR); 1858 // Add the word to the user history dictionary 1859 addToUserHistoryDictionary(suggestion); 1860 mSpaceState = SPACE_STATE_PHANTOM; 1861 // TODO: is this necessary? 1862 mKeyboardSwitcher.updateShiftState(); 1863 1864 // We should show the "Touch again to save" hint if the user pressed the first entry 1865 // AND either: 1866 // - There is no dictionary (we know that because we tried to load it => null != mSuggest 1867 // AND mSuggest.hasMainDictionary() is false) 1868 // - There is a dictionary and the word is not in it 1869 // Please note that if mSuggest is null, it means that everything is off: suggestion 1870 // and correction, so we shouldn't try to show the hint 1871 // We used to look at mCorrectionMode here, but showing the hint should have nothing 1872 // to do with the autocorrection setting. 1873 final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null 1874 // If there is no dictionary the hint should be shown. 1875 && (!mSuggest.hasMainDictionary() 1876 // If "suggestion" is not in the dictionary, the hint should be shown. 1877 || !AutoCorrection.isValidWord( 1878 mSuggest.getUnigramDictionaries(), suggestion, true)); 1879 1880 Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE, 1881 WordComposer.NOT_A_COORDINATE); 1882 if (!showingAddToDictionaryHint) { 1883 // If we're not showing the "Touch again to save", then show corrections again. 1884 // In case the cursor position doesn't change, make sure we show the suggestions again. 1885 updateBigramPredictions(); 1886 // Updating the predictions right away may be slow and feel unresponsive on slower 1887 // terminals. On the other hand if we just postUpdateBigramPredictions() it will 1888 // take a noticeable delay to update them which may feel uneasy. 1889 } else { 1890 if (mIsUserDictionaryAvailable) { 1891 mSuggestionsView.showAddToDictionaryHint( 1892 suggestion, mSettingsValues.mHintToSaveText); 1893 } else { 1894 mHandler.postUpdateSuggestions(); 1895 } 1896 } 1897 } 1898 1899 /** 1900 * Commits the chosen word to the text field and saves it for later retrieval. 1901 */ 1902 private void commitChosenWord(final CharSequence bestWord, final int commitType, 1903 final int separatorCode) { 1904 final InputConnection ic = getCurrentInputConnection(); 1905 if (ic != null) { 1906 if (mSettingsValues.mEnableSuggestionSpanInsertion) { 1907 final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions(); 1908 ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( 1909 this, bestWord, suggestedWords), 1); 1910 } else { 1911 ic.commitText(bestWord, 1); 1912 } 1913 } 1914 // TODO: figure out here if this is an auto-correct or if the best word is actually 1915 // what user typed. Note: currently this is done much later in 1916 // LastComposedWord#didCommitTypedWord by string equality of the remembered 1917 // strings. 1918 mLastComposedWord = mWordComposer.commitWord(commitType, bestWord.toString(), 1919 separatorCode); 1920 } 1921 1922 public void updateBigramPredictions() { 1923 if (mSuggest == null || !isSuggestionsRequested()) 1924 return; 1925 1926 if (!mSettingsValues.mBigramPredictionEnabled) { 1927 setPunctuationSuggestions(); 1928 return; 1929 } 1930 1931 final SuggestedWords suggestedWords; 1932 if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { 1933 final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(), 1934 mSettingsValues.mWordSeparators); 1935 if (!TextUtils.isEmpty(prevWord)) { 1936 suggestedWords = mSuggest.getBigramPredictions(prevWord); 1937 } else { 1938 suggestedWords = null; 1939 } 1940 } else { 1941 suggestedWords = null; 1942 } 1943 1944 if (null != suggestedWords && suggestedWords.size() > 0) { 1945 // Explicitly supply an empty typed word (the no-second-arg version of 1946 // showSuggestions will retrieve the word near the cursor, we don't want that here) 1947 showSuggestions(suggestedWords, ""); 1948 } else { 1949 if (!isShowingPunctuationList()) setPunctuationSuggestions(); 1950 } 1951 } 1952 1953 public void setPunctuationSuggestions() { 1954 setSuggestions(mSettingsValues.mSuggestPuncList, false); 1955 setAutoCorrectionIndicator(false); 1956 setSuggestionStripShown(isSuggestionsStripVisible()); 1957 } 1958 1959 private void addToUserHistoryDictionary(final CharSequence suggestion) { 1960 if (TextUtils.isEmpty(suggestion)) return; 1961 1962 // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be 1963 // adding words in situations where the user or application really didn't 1964 // want corrections enabled or learned. 1965 if (!(mCorrectionMode == Suggest.CORRECTION_FULL 1966 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { 1967 return; 1968 } 1969 1970 if (mUserHistoryDictionary != null) { 1971 final InputConnection ic = getCurrentInputConnection(); 1972 final CharSequence prevWord; 1973 if (null != ic) { 1974 prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators); 1975 } else { 1976 prevWord = null; 1977 } 1978 final String secondWord; 1979 if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) { 1980 secondWord = suggestion.toString().toLowerCase(mSubtypeSwitcher.getInputLocale()); 1981 } else { 1982 secondWord = suggestion.toString(); 1983 } 1984 mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(), 1985 secondWord); 1986 } 1987 } 1988 1989 public boolean isCursorTouchingWord() { 1990 final InputConnection ic = getCurrentInputConnection(); 1991 if (ic == null) return false; 1992 CharSequence before = ic.getTextBeforeCursor(1, 0); 1993 CharSequence after = ic.getTextAfterCursor(1, 0); 1994 if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0)) 1995 && !mSettingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) { 1996 return true; 1997 } 1998 if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0)) 1999 && !mSettingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) { 2000 return true; 2001 } 2002 return false; 2003 } 2004 2005 // "ic" must not be null 2006 private static boolean sameAsTextBeforeCursor(final InputConnection ic, 2007 final CharSequence text) { 2008 final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0); 2009 return TextUtils.equals(text, beforeText); 2010 } 2011 2012 // "ic" must not be null 2013 /** 2014 * Check if the cursor is actually at the end of a word. If so, restart suggestions on this 2015 * word, else do nothing. 2016 */ 2017 private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord( 2018 final InputConnection ic) { 2019 // Bail out if the cursor is not at the end of a word (cursor must be preceded by 2020 // non-whitespace, non-separator, non-start-of-text) 2021 // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. 2022 final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0); 2023 if (TextUtils.isEmpty(textBeforeCursor) 2024 || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return; 2025 2026 // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, 2027 // separator or end of line/text) 2028 // Example: "test|"<EOL> "te|st" get rejected here 2029 final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0); 2030 if (!TextUtils.isEmpty(textAfterCursor) 2031 && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return; 2032 2033 // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) 2034 // Example: " -|" gets rejected here but "e-|" and "e|" are okay 2035 CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators); 2036 // We don't suggest on leading single quotes, so we have to remove them from the word if 2037 // it starts with single quotes. 2038 while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) { 2039 word = word.subSequence(1, word.length()); 2040 } 2041 if (TextUtils.isEmpty(word)) return; 2042 final char firstChar = word.charAt(0); // we just tested that word is not empty 2043 if (word.length() == 1 && !Character.isLetter(firstChar)) return; 2044 2045 // We only suggest on words that start with a letter or a symbol that is excluded from 2046 // word separators (see #handleCharacterWhileInBatchEdit). 2047 if (!(isAlphabet(firstChar) 2048 || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) { 2049 return; 2050 } 2051 2052 // Okay, we are at the end of a word. Restart suggestions. 2053 restartSuggestionsOnWordBeforeCursor(ic, word); 2054 } 2055 2056 // "ic" must not be null 2057 private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic, 2058 final CharSequence word) { 2059 mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); 2060 ic.deleteSurroundingText(word.length(), 0); 2061 ic.setComposingText(word, 1); 2062 mHandler.postUpdateSuggestions(); 2063 } 2064 2065 // "ic" must not be null 2066 private void revertCommit(final InputConnection ic) { 2067 final String originallyTypedWord = mLastComposedWord.mTypedWord; 2068 final CharSequence committedWord = mLastComposedWord.mCommittedWord; 2069 final int cancelLength = committedWord.length(); 2070 final int separatorLength = LastComposedWord.getSeparatorLength( 2071 mLastComposedWord.mSeparatorCode); 2072 // TODO: should we check our saved separator against the actual contents of the text view? 2073 if (DEBUG) { 2074 if (mWordComposer.isComposingWord()) { 2075 throw new RuntimeException("revertCommit, but we are composing a word"); 2076 } 2077 final String wordBeforeCursor = 2078 ic.getTextBeforeCursor(cancelLength + separatorLength, 0) 2079 .subSequence(0, cancelLength).toString(); 2080 if (!TextUtils.equals(committedWord, wordBeforeCursor)) { 2081 throw new RuntimeException("revertCommit check failed: we thought we were " 2082 + "reverting \"" + committedWord 2083 + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); 2084 } 2085 } 2086 ic.deleteSurroundingText(cancelLength + separatorLength, 0); 2087 if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) { 2088 // This is the case when we cancel a manual pick. 2089 // We should restart suggestion on the word right away. 2090 mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord); 2091 ic.setComposingText(originallyTypedWord, 1); 2092 } else { 2093 ic.commitText(originallyTypedWord, 1); 2094 // Re-insert the separator 2095 sendKeyCodePoint(mLastComposedWord.mSeparatorCode); 2096 Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE, 2097 WordComposer.NOT_A_COORDINATE); 2098 // Don't restart suggestion yet. We'll restart if the user deletes the 2099 // separator. 2100 } 2101 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 2102 mHandler.cancelUpdateBigramPredictions(); 2103 mHandler.postUpdateSuggestions(); 2104 } 2105 2106 // "ic" must not be null 2107 private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) { 2108 mHandler.cancelDoubleSpacesTimer(); 2109 // Here we test whether we indeed have a period and a space before us. This should not 2110 // be needed, but it's there just in case something went wrong. 2111 final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); 2112 if (!". ".equals(textBeforeCursor)) { 2113 // Theoretically we should not be coming here if there isn't ". " before the 2114 // cursor, but the application may be changing the text while we are typing, so 2115 // anything goes. We should not crash. 2116 Log.d(TAG, "Tried to revert double-space combo but we didn't find " 2117 + "\". \" just before the cursor."); 2118 return false; 2119 } 2120 ic.deleteSurroundingText(2, 0); 2121 ic.commitText(" ", 1); 2122 return true; 2123 } 2124 2125 private static boolean revertSwapPunctuation(final InputConnection ic) { 2126 // Here we test whether we indeed have a space and something else before us. This should not 2127 // be needed, but it's there just in case something went wrong. 2128 final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); 2129 // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to 2130 // enter surrogate pairs this code will have been removed. 2131 if (TextUtils.isEmpty(textBeforeCursor) 2132 || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) { 2133 // We may only come here if the application is changing the text while we are typing. 2134 // This is quite a broken case, but not logically impossible, so we shouldn't crash, 2135 // but some debugging log may be in order. 2136 Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " 2137 + "find a space just before the cursor."); 2138 return false; 2139 } 2140 ic.beginBatchEdit(); 2141 ic.deleteSurroundingText(2, 0); 2142 ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1); 2143 ic.endBatchEdit(); 2144 return true; 2145 } 2146 2147 public boolean isWordSeparator(int code) { 2148 return mSettingsValues.isWordSeparator(code); 2149 } 2150 2151 public boolean preferCapitalization() { 2152 return mWordComposer.isFirstCharCapitalized(); 2153 } 2154 2155 // Notify that language or mode have been changed and toggleLanguage will update KeyboardID 2156 // according to new language or mode. 2157 public void onRefreshKeyboard() { 2158 // When the device locale is changed in SetupWizard etc., this method may get called via 2159 // onConfigurationChanged before SoftInputWindow is shown. 2160 if (mKeyboardSwitcher.getKeyboardView() != null) { 2161 // Reload keyboard because the current language has been changed. 2162 mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues); 2163 } 2164 initSuggest(); 2165 updateCorrectionMode(); 2166 loadSettings(); 2167 // Since we just changed languages, we should re-evaluate suggestions with whatever word 2168 // we are currently composing. If we are not composing anything, we may want to display 2169 // predictions or punctuation signs (which is done by updateBigramPredictions anyway). 2170 if (isCursorTouchingWord()) { 2171 mHandler.postUpdateSuggestions(); 2172 } else { 2173 mHandler.postUpdateBigramPredictions(); 2174 } 2175 } 2176 2177 public void hapticAndAudioFeedback(final int primaryCode) { 2178 mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView()); 2179 } 2180 2181 @Override 2182 public void onPressKey(int primaryCode) { 2183 mKeyboardSwitcher.onPressKey(primaryCode); 2184 } 2185 2186 @Override 2187 public void onReleaseKey(int primaryCode, boolean withSliding) { 2188 mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); 2189 2190 // If accessibility is on, ensure the user receives keyboard state updates. 2191 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 2192 switch (primaryCode) { 2193 case Keyboard.CODE_SHIFT: 2194 AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); 2195 break; 2196 case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: 2197 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); 2198 break; 2199 } 2200 } 2201 } 2202 2203 // receive ringer mode change and network state change. 2204 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 2205 @Override 2206 public void onReceive(Context context, Intent intent) { 2207 final String action = intent.getAction(); 2208 if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 2209 mSubtypeSwitcher.onNetworkStateChanged(intent); 2210 } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { 2211 mFeedbackManager.onRingerModeChanged(); 2212 } 2213 } 2214 }; 2215 2216 private void updateCorrectionMode() { 2217 // TODO: cleanup messy flags 2218 final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled 2219 && !mInputAttributes.mInputTypeNoAutoCorrect; 2220 mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE; 2221 mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect) 2222 ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; 2223 } 2224 2225 private void updateSuggestionVisibility(final Resources res) { 2226 final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting; 2227 for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) { 2228 if (suggestionVisiblityStr.equals(res.getString(visibility))) { 2229 mSuggestionVisibility = visibility; 2230 break; 2231 } 2232 } 2233 } 2234 2235 protected void launchSettings() { 2236 launchSettingsClass(Settings.class); 2237 } 2238 2239 public void launchDebugSettings() { 2240 launchSettingsClass(DebugSettings.class); 2241 } 2242 2243 protected void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) { 2244 handleClose(); 2245 Intent intent = new Intent(); 2246 intent.setClass(LatinIME.this, settingsClass); 2247 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 2248 startActivity(intent); 2249 } 2250 2251 private void showSubtypeSelectorAndSettings() { 2252 final CharSequence title = getString(R.string.english_ime_input_options); 2253 final CharSequence[] items = new CharSequence[] { 2254 // TODO: Should use new string "Select active input modes". 2255 getString(R.string.language_selection_title), 2256 getString(R.string.english_ime_settings), 2257 }; 2258 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 2259 @Override 2260 public void onClick(DialogInterface di, int position) { 2261 di.dismiss(); 2262 switch (position) { 2263 case 0: 2264 Intent intent = CompatUtils.getInputLanguageSelectionIntent( 2265 SubtypeUtils.getInputMethodId(getPackageName()), 2266 Intent.FLAG_ACTIVITY_NEW_TASK 2267 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 2268 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 2269 startActivity(intent); 2270 break; 2271 case 1: 2272 launchSettings(); 2273 break; 2274 } 2275 } 2276 }; 2277 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 2278 .setItems(items, listener) 2279 .setTitle(title); 2280 showOptionDialogInternal(builder.create()); 2281 } 2282 2283 private void showOptionDialogInternal(AlertDialog dialog) { 2284 final IBinder windowToken = KeyboardSwitcher.getInstance().getKeyboardView() 2285 .getWindowToken(); 2286 if (windowToken == null) return; 2287 2288 dialog.setCancelable(true); 2289 dialog.setCanceledOnTouchOutside(true); 2290 2291 final Window window = dialog.getWindow(); 2292 final WindowManager.LayoutParams lp = window.getAttributes(); 2293 lp.token = windowToken; 2294 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 2295 window.setAttributes(lp); 2296 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 2297 2298 mOptionsDialog = dialog; 2299 dialog.show(); 2300 } 2301 2302 @Override 2303 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { 2304 super.dump(fd, fout, args); 2305 2306 final Printer p = new PrintWriterPrinter(fout); 2307 p.println("LatinIME state :"); 2308 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 2309 final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; 2310 p.println(" Keyboard mode = " + keyboardMode); 2311 p.println(" mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn); 2312 p.println(" mCorrectionMode=" + mCorrectionMode); 2313 p.println(" isComposingWord=" + mWordComposer.isComposingWord()); 2314 p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled); 2315 p.println(" mSoundOn=" + mSettingsValues.mSoundOn); 2316 p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn); 2317 p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn); 2318 p.println(" mInputAttributes=" + mInputAttributes.toString()); 2319 } 2320} 2321