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