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