LatinIME.java revision 5f922caff80d5067c5af2bbbae2731ef25c9572a
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 com.android.inputmethod.keyboard.Keyboard; 20import com.android.inputmethod.keyboard.KeyboardActionListener; 21import com.android.inputmethod.keyboard.KeyboardId; 22import com.android.inputmethod.keyboard.KeyboardSwitcher; 23import com.android.inputmethod.keyboard.KeyboardView; 24import com.android.inputmethod.keyboard.LatinKeyboardView; 25import com.android.inputmethod.latin.Utils.RingCharBuffer; 26import com.android.inputmethod.voice.VoiceIMEConnector; 27 28import org.xmlpull.v1.XmlPullParser; 29import org.xmlpull.v1.XmlPullParserException; 30 31import android.app.AlertDialog; 32import android.content.BroadcastReceiver; 33import android.content.Context; 34import android.content.DialogInterface; 35import android.content.Intent; 36import android.content.IntentFilter; 37import android.content.SharedPreferences; 38import android.content.res.Configuration; 39import android.content.res.Resources; 40import android.content.res.XmlResourceParser; 41import android.inputmethodservice.InputMethodService; 42import android.media.AudioManager; 43import android.os.Debug; 44import android.os.Handler; 45import android.os.Message; 46import android.os.SystemClock; 47import android.os.Vibrator; 48import android.preference.PreferenceActivity; 49import android.preference.PreferenceManager; 50import android.text.InputType; 51import android.text.TextUtils; 52import android.util.DisplayMetrics; 53import android.util.Log; 54import android.util.PrintWriterPrinter; 55import android.util.Printer; 56import android.view.Gravity; 57import android.view.HapticFeedbackConstants; 58import android.view.KeyEvent; 59import android.view.LayoutInflater; 60import android.view.View; 61import android.view.ViewGroup; 62import android.view.ViewParent; 63import android.view.Window; 64import android.view.WindowManager; 65import android.view.inputmethod.CompletionInfo; 66import android.view.inputmethod.CorrectionInfo; 67import android.view.inputmethod.EditorInfo; 68import android.view.inputmethod.ExtractedText; 69import android.view.inputmethod.ExtractedTextRequest; 70import android.view.inputmethod.InputConnection; 71import android.view.inputmethod.InputMethodManager; 72import android.view.inputmethod.InputMethodSubtype; 73import android.widget.HorizontalScrollView; 74import android.widget.LinearLayout; 75 76import java.io.FileDescriptor; 77import java.io.IOException; 78import java.io.PrintWriter; 79import java.util.ArrayList; 80import java.util.Arrays; 81import java.util.Locale; 82 83/** 84 * Input method implementation for Qwerty'ish keyboard. 85 */ 86public class LatinIME extends InputMethodService implements KeyboardActionListener, 87 SharedPreferences.OnSharedPreferenceChangeListener { 88 private static final String TAG = "LatinIME"; 89 private static final boolean PERF_DEBUG = false; 90 private static final boolean DEBUG = false; 91 private static final boolean TRACE = false; 92 93 private static final int DELAY_UPDATE_SUGGESTIONS = 180; 94 private static final int DELAY_UPDATE_OLD_SUGGESTIONS = 300; 95 private static final int DELAY_UPDATE_SHIFT_STATE = 300; 96 97 // How many continuous deletes at which to start deleting at a higher speed. 98 private static final int DELETE_ACCELERATE_AT = 20; 99 // Key events coming any faster than this are long-presses. 100 private static final int QUICK_PRESS = 200; 101 102 // Contextual menu positions 103 private static final int POS_METHOD = 0; 104 private static final int POS_SETTINGS = 1; 105 106 private int mSuggestionVisibility; 107 private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE 108 = R.string.prefs_suggestion_visibility_show_value; 109 private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE 110 = R.string.prefs_suggestion_visibility_show_only_portrait_value; 111 private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE 112 = R.string.prefs_suggestion_visibility_hide_value; 113 114 private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] { 115 SUGGESTION_VISIBILILTY_SHOW_VALUE, 116 SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE, 117 SUGGESTION_VISIBILILTY_HIDE_VALUE 118 }; 119 120 private View mCandidateViewContainer; 121 private CandidateView mCandidateView; 122 private Suggest mSuggest; 123 private CompletionInfo[] mApplicationSpecifiedCompletions; 124 125 private AlertDialog mOptionsDialog; 126 127 private InputMethodManager mImm; 128 private KeyboardSwitcher mKeyboardSwitcher; 129 private SubtypeSwitcher mSubtypeSwitcher; 130 private VoiceIMEConnector mVoiceConnector; 131 132 private UserDictionary mUserDictionary; 133 private UserBigramDictionary mUserBigramDictionary; 134 private ContactsDictionary mContactsDictionary; 135 private AutoDictionary mAutoDictionary; 136 137 private Resources mResources; 138 private SharedPreferences mPrefs; 139 140 private final StringBuilder mComposing = new StringBuilder(); 141 private WordComposer mWord = new WordComposer(); 142 private CharSequence mBestWord; 143 private boolean mHasValidSuggestions; 144 private boolean mIsSettingsSuggestionStripOn; 145 private boolean mApplicationSpecifiedCompletionOn; 146 private boolean mHasDictionary; 147 private boolean mAutoSpace; 148 private boolean mJustAddedAutoSpace; 149 private boolean mAutoCorrectEnabled; 150 private boolean mReCorrectionEnabled; 151 private boolean mBigramSuggestionEnabled; 152 private boolean mAutoCorrectOn; 153 private boolean mVibrateOn; 154 private boolean mSoundOn; 155 private boolean mPopupOn; 156 private boolean mAutoCap; 157 private boolean mQuickFixes; 158 private boolean mConfigSwipeDownDismissKeyboardEnabled; 159 160 private int mCorrectionMode; 161 private int mCommittedLength; 162 private int mOrientation; 163 // Keep track of the last selection range to decide if we need to show word alternatives 164 private int mLastSelectionStart; 165 private int mLastSelectionEnd; 166 private SuggestedWords mSuggestPuncList; 167 168 // Input type is such that we should not auto-correct 169 private boolean mInputTypeNoAutoCorrect; 170 171 // Indicates whether the suggestion strip is to be on in landscape 172 private boolean mJustAccepted; 173 private boolean mJustReverted; 174 private int mDeleteCount; 175 private long mLastKeyTime; 176 177 private AudioManager mAudioManager; 178 // Align sound effect volume on music volume 179 private static final float FX_VOLUME = -1.0f; 180 private boolean mSilentMode; 181 182 /* package */ String mWordSeparators; 183 private String mSentenceSeparators; 184 private String mSuggestPuncs; 185 // TODO: Move this flag to VoiceIMEConnector 186 private boolean mConfigurationChanging; 187 188 // Keeps track of most recently inserted text (multi-character key) for reverting 189 private CharSequence mEnteredText; 190 private boolean mRefreshKeyboardRequired; 191 192 private final ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>(); 193 194 public abstract static class WordAlternatives { 195 protected CharSequence mChosenWord; 196 197 public WordAlternatives() { 198 // Nothing 199 } 200 201 public WordAlternatives(CharSequence chosenWord) { 202 mChosenWord = chosenWord; 203 } 204 205 @Override 206 public int hashCode() { 207 return mChosenWord.hashCode(); 208 } 209 210 public abstract CharSequence getOriginalWord(); 211 212 public CharSequence getChosenWord() { 213 return mChosenWord; 214 } 215 216 public abstract SuggestedWords.Builder getAlternatives(); 217 } 218 219 public class TypedWordAlternatives extends WordAlternatives { 220 private WordComposer word; 221 222 public TypedWordAlternatives() { 223 // Nothing 224 } 225 226 public TypedWordAlternatives(CharSequence chosenWord, WordComposer wordComposer) { 227 super(chosenWord); 228 word = wordComposer; 229 } 230 231 @Override 232 public CharSequence getOriginalWord() { 233 return word.getTypedWord(); 234 } 235 236 @Override 237 public SuggestedWords.Builder getAlternatives() { 238 return getTypedSuggestions(word); 239 } 240 } 241 242 public final UIHandler mHandler = new UIHandler(); 243 244 public class UIHandler extends Handler { 245 private static final int MSG_UPDATE_SUGGESTIONS = 0; 246 private static final int MSG_UPDATE_OLD_SUGGESTIONS = 1; 247 private static final int MSG_UPDATE_SHIFT_STATE = 2; 248 private static final int MSG_VOICE_RESULTS = 3; 249 250 @Override 251 public void handleMessage(Message msg) { 252 switch (msg.what) { 253 case MSG_UPDATE_SUGGESTIONS: 254 updateSuggestions(); 255 break; 256 case MSG_UPDATE_OLD_SUGGESTIONS: 257 setOldSuggestions(); 258 break; 259 case MSG_UPDATE_SHIFT_STATE: 260 mKeyboardSwitcher.updateShiftState(); 261 break; 262 case MSG_VOICE_RESULTS: 263 mVoiceConnector.handleVoiceResults(preferCapitalization() 264 || (mKeyboardSwitcher.isAlphabetMode() 265 && mKeyboardSwitcher.isShiftedOrShiftLocked())); 266 break; 267 } 268 } 269 270 public void postUpdateSuggestions() { 271 removeMessages(MSG_UPDATE_SUGGESTIONS); 272 sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), DELAY_UPDATE_SUGGESTIONS); 273 } 274 275 public void cancelUpdateSuggestions() { 276 removeMessages(MSG_UPDATE_SUGGESTIONS); 277 } 278 279 public boolean hasPendingUpdateSuggestions() { 280 return hasMessages(MSG_UPDATE_SUGGESTIONS); 281 } 282 283 public void postUpdateOldSuggestions() { 284 removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); 285 sendMessageDelayed(obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), 286 DELAY_UPDATE_OLD_SUGGESTIONS); 287 } 288 289 public void cancelUpdateOldSuggestions() { 290 removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); 291 } 292 293 public void postUpdateShiftKeyState() { 294 removeMessages(MSG_UPDATE_SHIFT_STATE); 295 sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), DELAY_UPDATE_SHIFT_STATE); 296 } 297 298 public void cancelUpdateShiftState() { 299 removeMessages(MSG_UPDATE_SHIFT_STATE); 300 } 301 302 public void updateVoiceResults() { 303 sendMessage(obtainMessage(MSG_VOICE_RESULTS)); 304 } 305 } 306 307 @Override 308 public void onCreate() { 309 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 310 mPrefs = prefs; 311 LatinImeLogger.init(this, prefs); 312 SubtypeSwitcher.init(this, prefs); 313 KeyboardSwitcher.init(this, prefs); 314 315 super.onCreate(); 316 317 mImm = ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)); 318 mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 319 mKeyboardSwitcher = KeyboardSwitcher.getInstance(); 320 321 final Resources res = getResources(); 322 mResources = res; 323 mReCorrectionEnabled = prefs.getBoolean(Settings.PREF_RECORRECTION_ENABLED, 324 res.getBoolean(R.bool.default_recorrection_enabled)); 325 mConfigSwipeDownDismissKeyboardEnabled = res.getBoolean( 326 R.bool.config_swipe_down_dismiss_keyboard_enabled); 327 328 Utils.GCUtils.getInstance().reset(); 329 boolean tryGC = true; 330 for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { 331 try { 332 initSuggest(); 333 tryGC = false; 334 } catch (OutOfMemoryError e) { 335 tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e); 336 } 337 } 338 339 mOrientation = res.getConfiguration().orientation; 340 initSuggestPuncList(); 341 342 // register to receive ringer mode changes for silent mode 343 IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); 344 registerReceiver(mReceiver, filter); 345 mVoiceConnector = VoiceIMEConnector.init(this, prefs, mHandler); 346 prefs.registerOnSharedPreferenceChangeListener(this); 347 } 348 349 /** 350 * Loads a dictionary or multiple separated dictionary 351 * @return returns array of dictionary resource ids 352 */ 353 public static int[] getDictionary(Resources res) { 354 String packageName = LatinIME.class.getPackage().getName(); 355 XmlResourceParser xrp = res.getXml(R.xml.dictionary); 356 ArrayList<Integer> dictionaries = new ArrayList<Integer>(); 357 358 try { 359 int current = xrp.getEventType(); 360 while (current != XmlPullParser.END_DOCUMENT) { 361 if (current == XmlPullParser.START_TAG) { 362 String tag = xrp.getName(); 363 if (tag != null) { 364 if (tag.equals("part")) { 365 String dictFileName = xrp.getAttributeValue(null, "name"); 366 dictionaries.add(res.getIdentifier(dictFileName, "raw", packageName)); 367 } 368 } 369 } 370 xrp.next(); 371 current = xrp.getEventType(); 372 } 373 } catch (XmlPullParserException e) { 374 Log.e(TAG, "Dictionary XML parsing failure"); 375 } catch (IOException e) { 376 Log.e(TAG, "Dictionary XML IOException"); 377 } 378 379 int count = dictionaries.size(); 380 int[] dict = new int[count]; 381 for (int i = 0; i < count; i++) { 382 dict[i] = dictionaries.get(i); 383 } 384 385 return dict; 386 } 387 388 private void initSuggest() { 389 updateAutoTextEnabled(); 390 String locale = mSubtypeSwitcher.getInputLocaleStr(); 391 392 Resources orig = getResources(); 393 Locale savedLocale = mSubtypeSwitcher.changeSystemLocale(new Locale(locale)); 394 if (mSuggest != null) { 395 mSuggest.close(); 396 } 397 final SharedPreferences prefs = mPrefs; 398 mQuickFixes = prefs.getBoolean(Settings.PREF_QUICK_FIXES, true); 399 400 int[] dictionaries = getDictionary(orig); 401 mSuggest = new Suggest(this, dictionaries); 402 loadAndSetAutoCorrectionThreshold(prefs); 403 if (mUserDictionary != null) mUserDictionary.close(); 404 mUserDictionary = new UserDictionary(this, locale); 405 if (mContactsDictionary == null) { 406 mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS); 407 } 408 if (mAutoDictionary != null) { 409 mAutoDictionary.close(); 410 } 411 mAutoDictionary = new AutoDictionary(this, this, locale, Suggest.DIC_AUTO); 412 if (mUserBigramDictionary != null) { 413 mUserBigramDictionary.close(); 414 } 415 mUserBigramDictionary = new UserBigramDictionary(this, this, locale, Suggest.DIC_USER); 416 mSuggest.setUserBigramDictionary(mUserBigramDictionary); 417 mSuggest.setUserDictionary(mUserDictionary); 418 mSuggest.setContactsDictionary(mContactsDictionary); 419 mSuggest.setAutoDictionary(mAutoDictionary); 420 updateCorrectionMode(); 421 mWordSeparators = mResources.getString(R.string.word_separators); 422 mSentenceSeparators = mResources.getString(R.string.sentence_separators); 423 424 mSubtypeSwitcher.changeSystemLocale(savedLocale); 425 } 426 427 @Override 428 public void onDestroy() { 429 if (mUserDictionary != null) { 430 mUserDictionary.close(); 431 } 432 if (mContactsDictionary != null) { 433 mContactsDictionary.close(); 434 } 435 unregisterReceiver(mReceiver); 436 mVoiceConnector.destroy(); 437 LatinImeLogger.commit(); 438 LatinImeLogger.onDestroy(); 439 super.onDestroy(); 440 } 441 442 @Override 443 public void onConfigurationChanged(Configuration conf) { 444 mSubtypeSwitcher.onConfigurationChanged(conf); 445 if (mSubtypeSwitcher.isKeyboardMode()) 446 onKeyboardLanguageChanged(); 447 updateAutoTextEnabled(); 448 449 // If orientation changed while predicting, commit the change 450 if (conf.orientation != mOrientation) { 451 InputConnection ic = getCurrentInputConnection(); 452 commitTyped(ic); 453 if (ic != null) ic.finishComposingText(); // For voice input 454 mOrientation = conf.orientation; 455 final int mode = mKeyboardSwitcher.getKeyboardMode(); 456 final EditorInfo attribute = getCurrentInputEditorInfo(); 457 final int imeOptions = (attribute != null) ? attribute.imeOptions : 0; 458 mKeyboardSwitcher.loadKeyboard(mode, imeOptions, 459 mVoiceConnector.isVoiceButtonEnabled(), 460 mVoiceConnector.isVoiceButtonOnPrimary()); 461 } 462 463 mConfigurationChanging = true; 464 super.onConfigurationChanged(conf); 465 mVoiceConnector.onConfigurationChanged(mConfigurationChanging); 466 mConfigurationChanging = false; 467 } 468 469 @Override 470 public View onCreateInputView() { 471 return mKeyboardSwitcher.onCreateInputView(); 472 } 473 474 @Override 475 public View onCreateCandidatesView() { 476 LayoutInflater inflater = getLayoutInflater(); 477 LinearLayout container = (LinearLayout)inflater.inflate(R.layout.candidates, null); 478 mCandidateViewContainer = container; 479 if (container.getPaddingRight() != 0) { 480 HorizontalScrollView scrollView = 481 (HorizontalScrollView) container.findViewById(R.id.candidates_scroll_view); 482 scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); 483 container.setGravity(Gravity.CENTER_HORIZONTAL); 484 } 485 mCandidateView = (CandidateView) container.findViewById(R.id.candidates); 486 mCandidateView.setService(this); 487 setCandidatesViewShown(true); 488 return container; 489 } 490 491 private static boolean isPasswordVariation(int variation) { 492 return variation == InputType.TYPE_TEXT_VARIATION_PASSWORD 493 || variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD 494 || variation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; 495 } 496 497 private static boolean isEmailVariation(int variation) { 498 return variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS 499 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; 500 } 501 502 @Override 503 public void onStartInputView(EditorInfo attribute, boolean restarting) { 504 final KeyboardSwitcher switcher = mKeyboardSwitcher; 505 LatinKeyboardView inputView = switcher.getInputView(); 506 507 // In landscape mode, this method gets called without the input view being created. 508 if (inputView == null) { 509 return; 510 } 511 512 mSubtypeSwitcher.updateParametersOnStartInputView(); 513 514 if (mRefreshKeyboardRequired) { 515 mRefreshKeyboardRequired = false; 516 onKeyboardLanguageChanged(); 517 } 518 519 TextEntryState.newSession(this); 520 521 // Most such things we decide below in the switch statement, but we need to know 522 // now whether this is a password text field, because we need to know now (before 523 // the switch statement) whether we want to enable the voice button. 524 int variation = attribute.inputType & InputType.TYPE_MASK_VARIATION; 525 mVoiceConnector.resetVoiceStates(isPasswordVariation(variation)); 526 mInputTypeNoAutoCorrect = false; 527 mIsSettingsSuggestionStripOn = false; 528 mApplicationSpecifiedCompletionOn = false; 529 mApplicationSpecifiedCompletions = null; 530 mEnteredText = null; 531 532 final int mode; 533 switch (attribute.inputType & InputType.TYPE_MASK_CLASS) { 534 case InputType.TYPE_CLASS_NUMBER: 535 case InputType.TYPE_CLASS_DATETIME: 536 mode = KeyboardId.MODE_NUMBER; 537 break; 538 case InputType.TYPE_CLASS_PHONE: 539 mode = KeyboardId.MODE_PHONE; 540 break; 541 case InputType.TYPE_CLASS_TEXT: 542 mIsSettingsSuggestionStripOn = true; 543 // Make sure that passwords are not displayed in candidate view 544 if (isPasswordVariation(variation)) { 545 mIsSettingsSuggestionStripOn = false; 546 } 547 if (isEmailVariation(variation) 548 || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { 549 mAutoSpace = false; 550 } else { 551 mAutoSpace = true; 552 } 553 if (isEmailVariation(variation)) { 554 mIsSettingsSuggestionStripOn = false; 555 mode = KeyboardId.MODE_EMAIL; 556 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 557 mIsSettingsSuggestionStripOn = false; 558 mode = KeyboardId.MODE_URL; 559 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { 560 mode = KeyboardId.MODE_IM; 561 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 562 mIsSettingsSuggestionStripOn = false; 563 mode = KeyboardId.MODE_TEXT; 564 } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { 565 mode = KeyboardId.MODE_WEB; 566 // If it's a browser edit field and auto correct is not ON explicitly, then 567 // disable auto correction, but keep suggestions on. 568 if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) { 569 mInputTypeNoAutoCorrect = true; 570 } 571 } else { 572 mode = KeyboardId.MODE_TEXT; 573 } 574 575 // If NO_SUGGESTIONS is set, don't do prediction. 576 if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) { 577 mIsSettingsSuggestionStripOn = false; 578 mInputTypeNoAutoCorrect = true; 579 } 580 // If it's not multiline and the autoCorrect flag is not set, then don't correct 581 if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 && 582 (attribute.inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) { 583 mInputTypeNoAutoCorrect = true; 584 } 585 if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { 586 mIsSettingsSuggestionStripOn = false; 587 mApplicationSpecifiedCompletionOn = isFullscreenMode(); 588 } 589 break; 590 default: 591 mode = KeyboardId.MODE_TEXT; 592 break; 593 } 594 inputView.closing(); 595 mComposing.setLength(0); 596 mHasValidSuggestions = false; 597 mDeleteCount = 0; 598 mJustAddedAutoSpace = false; 599 600 loadSettings(attribute); 601 if (mSubtypeSwitcher.isKeyboardMode()) { 602 switcher.loadKeyboard(mode, attribute.imeOptions, 603 mVoiceConnector.isVoiceButtonEnabled(), 604 mVoiceConnector.isVoiceButtonOnPrimary()); 605 switcher.updateShiftState(); 606 } 607 608 setCandidatesViewShownInternal(isCandidateStripVisible(), 609 false /* needsInputViewShown */ ); 610 // Delay updating suggestions because keyboard input view may not be shown at this point. 611 mHandler.postUpdateSuggestions(); 612 613 // If the dictionary is not big enough, don't auto correct 614 mHasDictionary = mSuggest.hasMainDictionary(); 615 616 updateCorrectionMode(); 617 618 inputView.setPreviewEnabled(mPopupOn); 619 inputView.setProximityCorrectionEnabled(true); 620 mIsSettingsSuggestionStripOn &= (mCorrectionMode > 0 || isShowingSuggestionsStrip()); 621 // If we just entered a text field, maybe it has some old text that requires correction 622 checkReCorrectionOnStart(); 623 inputView.setForeground(true); 624 625 mVoiceConnector.onStartInputView(mKeyboardSwitcher.getInputView().getWindowToken()); 626 627 if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); 628 } 629 630 private void checkReCorrectionOnStart() { 631 if (!mReCorrectionEnabled) return; 632 633 final InputConnection ic = getCurrentInputConnection(); 634 if (ic == null) return; 635 // There could be a pending composing span. Clean it up first. 636 ic.finishComposingText(); 637 638 if (isShowingSuggestionsStrip() && isSuggestionsRequested()) { 639 // First get the cursor position. This is required by setOldSuggestions(), so that 640 // it can pass the correct range to setComposingRegion(). At this point, we don't 641 // have valid values for mLastSelectionStart/End because onUpdateSelection() has 642 // not been called yet. 643 ExtractedTextRequest etr = new ExtractedTextRequest(); 644 etr.token = 0; // anything is fine here 645 ExtractedText et = ic.getExtractedText(etr, 0); 646 if (et == null) return; 647 648 mLastSelectionStart = et.startOffset + et.selectionStart; 649 mLastSelectionEnd = et.startOffset + et.selectionEnd; 650 651 // Then look for possible corrections in a delayed fashion 652 if (!TextUtils.isEmpty(et.text) && isCursorTouchingWord()) { 653 mHandler.postUpdateOldSuggestions(); 654 } 655 } 656 } 657 658 @Override 659 public void onFinishInput() { 660 super.onFinishInput(); 661 662 LatinImeLogger.commit(); 663 mKeyboardSwitcher.onAutoCorrectionStateChanged(false); 664 665 mVoiceConnector.flushVoiceInputLogs(mConfigurationChanging); 666 667 KeyboardView inputView = mKeyboardSwitcher.getInputView(); 668 if (inputView != null) inputView.closing(); 669 if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites(); 670 if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); 671 } 672 673 @Override 674 public void onFinishInputView(boolean finishingInput) { 675 super.onFinishInputView(finishingInput); 676 KeyboardView inputView = mKeyboardSwitcher.getInputView(); 677 if (inputView != null) inputView.setForeground(false); 678 // Remove pending messages related to update suggestions 679 mHandler.cancelUpdateSuggestions(); 680 mHandler.cancelUpdateOldSuggestions(); 681 } 682 683 @Override 684 public void onUpdateExtractedText(int token, ExtractedText text) { 685 super.onUpdateExtractedText(token, text); 686 mVoiceConnector.showPunctuationHintIfNecessary(); 687 } 688 689 @Override 690 public void onUpdateSelection(int oldSelStart, int oldSelEnd, 691 int newSelStart, int newSelEnd, 692 int candidatesStart, int candidatesEnd) { 693 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 694 candidatesStart, candidatesEnd); 695 696 if (DEBUG) { 697 Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart 698 + ", ose=" + oldSelEnd 699 + ", nss=" + newSelStart 700 + ", nse=" + newSelEnd 701 + ", cs=" + candidatesStart 702 + ", ce=" + candidatesEnd); 703 } 704 705 mVoiceConnector.setCursorAndSelection(newSelEnd, newSelStart); 706 707 // If the current selection in the text view changes, we should 708 // clear whatever candidate text we have. 709 if ((((mComposing.length() > 0 && mHasValidSuggestions) 710 || mVoiceConnector.isVoiceInputHighlighted()) && (newSelStart != candidatesEnd 711 || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart)) { 712 mComposing.setLength(0); 713 mHasValidSuggestions = false; 714 mHandler.postUpdateSuggestions(); 715 TextEntryState.reset(); 716 InputConnection ic = getCurrentInputConnection(); 717 if (ic != null) { 718 ic.finishComposingText(); 719 } 720 mVoiceConnector.setVoiceInputHighlighted(false); 721 } else if (!mHasValidSuggestions && !mJustAccepted) { 722 switch (TextEntryState.getState()) { 723 case ACCEPTED_DEFAULT: 724 TextEntryState.reset(); 725 // $FALL-THROUGH$ 726 case SPACE_AFTER_PICKED: 727 mJustAddedAutoSpace = false; // The user moved the cursor. 728 break; 729 default: 730 break; 731 } 732 } 733 mJustAccepted = false; 734 mHandler.postUpdateShiftKeyState(); 735 736 // Make a note of the cursor position 737 mLastSelectionStart = newSelStart; 738 mLastSelectionEnd = newSelEnd; 739 740 if (mReCorrectionEnabled && isShowingSuggestionsStrip()) { 741 // Don't look for corrections if the keyboard is not visible 742 if (mKeyboardSwitcher.isInputViewShown()) { 743 // Check if we should go in or out of correction mode. 744 if (isSuggestionsRequested() && !mJustReverted 745 && (candidatesStart == candidatesEnd || newSelStart != oldSelStart 746 || TextEntryState.isCorrecting()) 747 && (newSelStart < newSelEnd - 1 || !mHasValidSuggestions)) { 748 if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) { 749 mHandler.postUpdateOldSuggestions(); 750 } else { 751 abortCorrection(false); 752 // Show the punctuation suggestions list if the current one is not 753 // and if not showing "Touch again to save". 754 if (mCandidateView != null && !isShowingPunctuationList() 755 && !mCandidateView.isShowingAddToDictionaryHint()) { 756 setPunctuationSuggestions(); 757 } 758 } 759 } 760 } 761 } 762 } 763 764 /** 765 * This is called when the user has clicked on the extracted text view, 766 * when running in fullscreen mode. The default implementation hides 767 * the candidates view when this happens, but only if the extracted text 768 * editor has a vertical scroll bar because its text doesn't fit. 769 * Here we override the behavior due to the possibility that a re-correction could 770 * cause the candidate strip to disappear and re-appear. 771 */ 772 @Override 773 public void onExtractedTextClicked() { 774 if (mReCorrectionEnabled && isSuggestionsRequested()) return; 775 776 super.onExtractedTextClicked(); 777 } 778 779 /** 780 * This is called when the user has performed a cursor movement in the 781 * extracted text view, when it is running in fullscreen mode. The default 782 * implementation hides the candidates view when a vertical movement 783 * happens, but only if the extracted text editor has a vertical scroll bar 784 * because its text doesn't fit. 785 * Here we override the behavior due to the possibility that a re-correction could 786 * cause the candidate strip to disappear and re-appear. 787 */ 788 @Override 789 public void onExtractedCursorMovement(int dx, int dy) { 790 if (mReCorrectionEnabled && isSuggestionsRequested()) return; 791 792 super.onExtractedCursorMovement(dx, dy); 793 } 794 795 @Override 796 public void hideWindow() { 797 LatinImeLogger.commit(); 798 mKeyboardSwitcher.onAutoCorrectionStateChanged(false); 799 800 if (TRACE) Debug.stopMethodTracing(); 801 if (mOptionsDialog != null && mOptionsDialog.isShowing()) { 802 mOptionsDialog.dismiss(); 803 mOptionsDialog = null; 804 } 805 mVoiceConnector.hideVoiceWindow(mConfigurationChanging); 806 mWordHistory.clear(); 807 super.hideWindow(); 808 TextEntryState.endSession(); 809 } 810 811 @Override 812 public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) { 813 if (DEBUG) { 814 Log.i("foo", "Received completions:"); 815 final int count = (applicationSpecifiedCompletions != null) 816 ? applicationSpecifiedCompletions.length : 0; 817 for (int i = 0; i < count; i++) { 818 Log.i("foo", " #" + i + ": " + applicationSpecifiedCompletions[i]); 819 } 820 } 821 if (mApplicationSpecifiedCompletionOn) { 822 mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; 823 if (applicationSpecifiedCompletions == null) { 824 clearSuggestions(); 825 return; 826 } 827 828 SuggestedWords.Builder builder = new SuggestedWords.Builder() 829 .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions) 830 .setTypedWordValid(true) 831 .setHasMinimalSuggestion(true); 832 // When in fullscreen mode, show completions generated by the application 833 setSuggestions(builder.build()); 834 mBestWord = null; 835 setCandidatesViewShown(true); 836 } 837 } 838 839 private void setCandidatesViewShownInternal(boolean shown, boolean needsInputViewShown) { 840 // TODO: Remove this if we support candidates with hard keyboard 841 if (onEvaluateInputViewShown()) { 842 super.setCandidatesViewShown(shown 843 && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true)); 844 } 845 } 846 847 @Override 848 public void setCandidatesViewShown(boolean shown) { 849 setCandidatesViewShownInternal(shown, true /* needsInputViewShown */ ); 850 } 851 852 @Override 853 public void onComputeInsets(InputMethodService.Insets outInsets) { 854 super.onComputeInsets(outInsets); 855 if (!isFullscreenMode()) { 856 outInsets.contentTopInsets = outInsets.visibleTopInsets; 857 } 858 } 859 860 @Override 861 public boolean onEvaluateFullscreenMode() { 862 DisplayMetrics dm = getResources().getDisplayMetrics(); 863 float displayHeight = dm.heightPixels; 864 // If the display is more than X inches high, don't go to fullscreen mode 865 float dimen = getResources().getDimension(R.dimen.max_height_for_fullscreen); 866 if (displayHeight > dimen) { 867 return false; 868 } else { 869 return super.onEvaluateFullscreenMode(); 870 } 871 } 872 873 @Override 874 public boolean onKeyDown(int keyCode, KeyEvent event) { 875 switch (keyCode) { 876 case KeyEvent.KEYCODE_BACK: 877 if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) { 878 if (mKeyboardSwitcher.getInputView().handleBack()) { 879 return true; 880 } 881 } 882 break; 883 } 884 return super.onKeyDown(keyCode, event); 885 } 886 887 @Override 888 public boolean onKeyUp(int keyCode, KeyEvent event) { 889 switch (keyCode) { 890 case KeyEvent.KEYCODE_DPAD_DOWN: 891 case KeyEvent.KEYCODE_DPAD_UP: 892 case KeyEvent.KEYCODE_DPAD_LEFT: 893 case KeyEvent.KEYCODE_DPAD_RIGHT: 894 // Enable shift key and DPAD to do selections 895 if (mKeyboardSwitcher.isInputViewShown() 896 && mKeyboardSwitcher.isShiftedOrShiftLocked()) { 897 KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(), 898 event.getAction(), event.getKeyCode(), event.getRepeatCount(), 899 event.getDeviceId(), event.getScanCode(), 900 KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); 901 InputConnection ic = getCurrentInputConnection(); 902 if (ic != null) 903 ic.sendKeyEvent(newEvent); 904 return true; 905 } 906 break; 907 } 908 return super.onKeyUp(keyCode, event); 909 } 910 911 public void commitTyped(InputConnection inputConnection) { 912 if (mHasValidSuggestions) { 913 mHasValidSuggestions = false; 914 if (mComposing.length() > 0) { 915 if (inputConnection != null) { 916 inputConnection.commitText(mComposing, 1); 917 } 918 mCommittedLength = mComposing.length(); 919 TextEntryState.acceptedTyped(mComposing); 920 addToDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED); 921 } 922 updateSuggestions(); 923 } 924 } 925 926 public boolean getCurrentAutoCapsState() { 927 InputConnection ic = getCurrentInputConnection(); 928 EditorInfo ei = getCurrentInputEditorInfo(); 929 if (mAutoCap && ic != null && ei != null && ei.inputType != InputType.TYPE_NULL) { 930 return ic.getCursorCapsMode(ei.inputType) != 0; 931 } 932 return false; 933 } 934 935 private void swapPunctuationAndSpace() { 936 final InputConnection ic = getCurrentInputConnection(); 937 if (ic == null) return; 938 CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); 939 if (lastTwo != null && lastTwo.length() == 2 940 && lastTwo.charAt(0) == Keyboard.CODE_SPACE 941 && isSentenceSeparator(lastTwo.charAt(1))) { 942 ic.beginBatchEdit(); 943 ic.deleteSurroundingText(2, 0); 944 ic.commitText(lastTwo.charAt(1) + " ", 1); 945 ic.endBatchEdit(); 946 mKeyboardSwitcher.updateShiftState(); 947 mJustAddedAutoSpace = true; 948 } 949 } 950 951 private void reswapPeriodAndSpace() { 952 final InputConnection ic = getCurrentInputConnection(); 953 if (ic == null) return; 954 CharSequence lastThree = ic.getTextBeforeCursor(3, 0); 955 if (lastThree != null && lastThree.length() == 3 956 && lastThree.charAt(0) == Keyboard.CODE_PERIOD 957 && lastThree.charAt(1) == Keyboard.CODE_SPACE 958 && lastThree.charAt(2) == Keyboard.CODE_PERIOD) { 959 ic.beginBatchEdit(); 960 ic.deleteSurroundingText(3, 0); 961 ic.commitText(" ..", 1); 962 ic.endBatchEdit(); 963 mKeyboardSwitcher.updateShiftState(); 964 } 965 } 966 967 private void doubleSpace() { 968 //if (!mAutoPunctuate) return; 969 if (mCorrectionMode == Suggest.CORRECTION_NONE) return; 970 final InputConnection ic = getCurrentInputConnection(); 971 if (ic == null) return; 972 CharSequence lastThree = ic.getTextBeforeCursor(3, 0); 973 if (lastThree != null && lastThree.length() == 3 974 && Character.isLetterOrDigit(lastThree.charAt(0)) 975 && lastThree.charAt(1) == Keyboard.CODE_SPACE 976 && lastThree.charAt(2) == Keyboard.CODE_SPACE) { 977 ic.beginBatchEdit(); 978 ic.deleteSurroundingText(2, 0); 979 ic.commitText(". ", 1); 980 ic.endBatchEdit(); 981 mKeyboardSwitcher.updateShiftState(); 982 mJustAddedAutoSpace = true; 983 } 984 } 985 986 private void maybeRemovePreviousPeriod(CharSequence text) { 987 final InputConnection ic = getCurrentInputConnection(); 988 if (ic == null) return; 989 990 // When the text's first character is '.', remove the previous period 991 // if there is one. 992 CharSequence lastOne = ic.getTextBeforeCursor(1, 0); 993 if (lastOne != null && lastOne.length() == 1 994 && lastOne.charAt(0) == Keyboard.CODE_PERIOD 995 && text.charAt(0) == Keyboard.CODE_PERIOD) { 996 ic.deleteSurroundingText(1, 0); 997 } 998 } 999 1000 private void removeTrailingSpace() { 1001 final InputConnection ic = getCurrentInputConnection(); 1002 if (ic == null) return; 1003 1004 CharSequence lastOne = ic.getTextBeforeCursor(1, 0); 1005 if (lastOne != null && lastOne.length() == 1 1006 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { 1007 ic.deleteSurroundingText(1, 0); 1008 } 1009 } 1010 1011 public boolean addWordToDictionary(String word) { 1012 mUserDictionary.addWord(word, 128); 1013 // Suggestion strip should be updated after the operation of adding word to the 1014 // user dictionary 1015 mHandler.postUpdateSuggestions(); 1016 return true; 1017 } 1018 1019 private boolean isAlphabet(int code) { 1020 if (Character.isLetter(code)) { 1021 return true; 1022 } else { 1023 return false; 1024 } 1025 } 1026 1027 private void onSettingsKeyPressed() { 1028 if (!isShowingOptionDialog()) { 1029 if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) { 1030 showOptionsMenu(); 1031 } else { 1032 launchSettings(); 1033 } 1034 } 1035 } 1036 1037 private void onSettingsKeyLongPressed() { 1038 if (!isShowingOptionDialog()) { 1039 if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) { 1040 mImm.showInputMethodPicker(); 1041 } else { 1042 launchSettings(); 1043 } 1044 } 1045 } 1046 1047 private boolean isShowingOptionDialog() { 1048 return mOptionsDialog != null && mOptionsDialog.isShowing(); 1049 } 1050 1051 // Implementation of {@link KeyboardActionListener}. 1052 @Override 1053 public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { 1054 long when = SystemClock.uptimeMillis(); 1055 if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { 1056 mDeleteCount = 0; 1057 } 1058 mLastKeyTime = when; 1059 KeyboardSwitcher switcher = mKeyboardSwitcher; 1060 final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); 1061 switch (primaryCode) { 1062 case Keyboard.CODE_DELETE: 1063 handleBackspace(); 1064 mDeleteCount++; 1065 LatinImeLogger.logOnDelete(); 1066 break; 1067 case Keyboard.CODE_SHIFT: 1068 // Shift key is handled in onPress() when device has distinct multi-touch panel. 1069 if (!distinctMultiTouch) 1070 switcher.toggleShift(); 1071 break; 1072 case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: 1073 // Symbol key is handled in onPress() when device has distinct multi-touch panel. 1074 if (!distinctMultiTouch) 1075 switcher.changeKeyboardMode(); 1076 break; 1077 case Keyboard.CODE_CANCEL: 1078 if (!isShowingOptionDialog()) { 1079 handleClose(); 1080 } 1081 break; 1082 case Keyboard.CODE_SETTINGS: 1083 onSettingsKeyPressed(); 1084 break; 1085 case Keyboard.CODE_SETTINGS_LONGPRESS: 1086 onSettingsKeyLongPressed(); 1087 break; 1088 case Keyboard.CODE_NEXT_LANGUAGE: 1089 toggleLanguage(false, true); 1090 break; 1091 case Keyboard.CODE_PREV_LANGUAGE: 1092 toggleLanguage(false, false); 1093 break; 1094 case Keyboard.CODE_CAPSLOCK: 1095 switcher.toggleCapsLock(); 1096 break; 1097 case Keyboard.CODE_VOICE: 1098 mSubtypeSwitcher.switchToShortcutIME(); 1099 break; 1100 case Keyboard.CODE_TAB: 1101 handleTab(); 1102 break; 1103 default: 1104 if (primaryCode != Keyboard.CODE_ENTER) { 1105 mJustAddedAutoSpace = false; 1106 } 1107 RingCharBuffer.getInstance().push((char)primaryCode, x, y); 1108 LatinImeLogger.logOnInputChar(); 1109 if (isWordSeparator(primaryCode)) { 1110 handleSeparator(primaryCode); 1111 } else { 1112 handleCharacter(primaryCode, keyCodes); 1113 } 1114 // Cancel the just reverted state 1115 mJustReverted = false; 1116 } 1117 switcher.onKey(primaryCode); 1118 // Reset after any single keystroke 1119 mEnteredText = null; 1120 } 1121 1122 @Override 1123 public void onTextInput(CharSequence text) { 1124 mVoiceConnector.commitVoiceInput(); 1125 InputConnection ic = getCurrentInputConnection(); 1126 if (ic == null) return; 1127 abortCorrection(false); 1128 ic.beginBatchEdit(); 1129 commitTyped(ic); 1130 maybeRemovePreviousPeriod(text); 1131 ic.commitText(text, 1); 1132 ic.endBatchEdit(); 1133 mKeyboardSwitcher.updateShiftState(); 1134 mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY); 1135 mJustReverted = false; 1136 mJustAddedAutoSpace = false; 1137 mEnteredText = text; 1138 } 1139 1140 @Override 1141 public void onCancelInput() { 1142 // User released a finger outside any key 1143 mKeyboardSwitcher.onCancelInput(); 1144 } 1145 1146 private void handleBackspace() { 1147 if (mVoiceConnector.logAndRevertVoiceInput()) return; 1148 boolean deleteChar = false; 1149 InputConnection ic = getCurrentInputConnection(); 1150 if (ic == null) return; 1151 1152 ic.beginBatchEdit(); 1153 1154 mVoiceConnector.handleBackspace(); 1155 1156 if (mHasValidSuggestions) { 1157 final int length = mComposing.length(); 1158 if (length > 0) { 1159 mComposing.delete(length - 1, length); 1160 mWord.deleteLast(); 1161 ic.setComposingText(mComposing, 1); 1162 if (mComposing.length() == 0) { 1163 mHasValidSuggestions = false; 1164 } 1165 mHandler.postUpdateSuggestions(); 1166 } else { 1167 ic.deleteSurroundingText(1, 0); 1168 } 1169 } else { 1170 deleteChar = true; 1171 } 1172 mHandler.postUpdateShiftKeyState(); 1173 TextEntryState.backspace(); 1174 if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) { 1175 revertLastWord(deleteChar); 1176 ic.endBatchEdit(); 1177 return; 1178 } else if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { 1179 ic.deleteSurroundingText(mEnteredText.length(), 0); 1180 } else if (deleteChar) { 1181 if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { 1182 // Go back to the suggestion mode if the user canceled the 1183 // "Touch again to save". 1184 // NOTE: In gerenal, we don't revert the word when backspacing 1185 // from a manual suggestion pick. We deliberately chose a 1186 // different behavior only in the case of picking the first 1187 // suggestion (typed word). It's intentional to have made this 1188 // inconsistent with backspacing after selecting other suggestions. 1189 revertLastWord(deleteChar); 1190 } else { 1191 sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); 1192 if (mDeleteCount > DELETE_ACCELERATE_AT) { 1193 sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); 1194 } 1195 } 1196 } 1197 mJustReverted = false; 1198 ic.endBatchEdit(); 1199 } 1200 1201 private void handleTab() { 1202 final int imeOptions = getCurrentInputEditorInfo().imeOptions; 1203 final int navigationFlags = 1204 EditorInfo.IME_FLAG_NAVIGATE_NEXT | EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS; 1205 if ((imeOptions & navigationFlags) == 0) { 1206 sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB); 1207 return; 1208 } 1209 1210 final InputConnection ic = getCurrentInputConnection(); 1211 if (ic == null) 1212 return; 1213 1214 // True if keyboard is in either chording shift or manual temporary upper case mode. 1215 final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase(); 1216 if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0 1217 && !isManualTemporaryUpperCase) { 1218 ic.performEditorAction(EditorInfo.IME_ACTION_NEXT); 1219 } else if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0 1220 && isManualTemporaryUpperCase) { 1221 ic.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 1222 } 1223 } 1224 1225 private void abortCorrection(boolean force) { 1226 if (force || TextEntryState.isCorrecting()) { 1227 TextEntryState.onAbortCorrection(); 1228 setCandidatesViewShown(isCandidateStripVisible()); 1229 getCurrentInputConnection().finishComposingText(); 1230 clearSuggestions(); 1231 } 1232 } 1233 1234 private void handleCharacter(int primaryCode, int[] keyCodes) { 1235 mVoiceConnector.handleCharacter(); 1236 1237 if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) { 1238 abortCorrection(false); 1239 } 1240 1241 int code = primaryCode; 1242 if (isAlphabet(code) && isSuggestionsRequested() && !isCursorTouchingWord()) { 1243 if (!mHasValidSuggestions) { 1244 mHasValidSuggestions = true; 1245 mComposing.setLength(0); 1246 saveWordInHistory(mBestWord); 1247 mWord.reset(); 1248 } 1249 } 1250 KeyboardSwitcher switcher = mKeyboardSwitcher; 1251 if (switcher.isShiftedOrShiftLocked()) { 1252 if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT 1253 || keyCodes[0] > Character.MAX_CODE_POINT) { 1254 return; 1255 } 1256 code = keyCodes[0]; 1257 if (switcher.isAlphabetMode() && Character.isLowerCase(code)) { 1258 int upperCaseCode = Character.toUpperCase(code); 1259 if (upperCaseCode != code) { 1260 code = upperCaseCode; 1261 } else { 1262 // Some keys, such as [eszett], have upper case as multi-characters. 1263 String upperCase = new String(new int[] {code}, 0, 1).toUpperCase(); 1264 onTextInput(upperCase); 1265 return; 1266 } 1267 } 1268 } 1269 if (mHasValidSuggestions) { 1270 if (mComposing.length() == 0 && switcher.isAlphabetMode() 1271 && switcher.isShiftedOrShiftLocked()) { 1272 mWord.setFirstCharCapitalized(true); 1273 } 1274 mComposing.append((char) code); 1275 mWord.add(code, keyCodes); 1276 InputConnection ic = getCurrentInputConnection(); 1277 if (ic != null) { 1278 // If it's the first letter, make note of auto-caps state 1279 if (mWord.size() == 1) { 1280 mWord.setAutoCapitalized(getCurrentAutoCapsState()); 1281 } 1282 ic.setComposingText(mComposing, 1); 1283 } 1284 mHandler.postUpdateSuggestions(); 1285 } else { 1286 sendKeyChar((char)code); 1287 } 1288 switcher.updateShiftState(); 1289 if (LatinIME.PERF_DEBUG) measureCps(); 1290 TextEntryState.typedCharacter((char) code, isWordSeparator(code)); 1291 } 1292 1293 private void handleSeparator(int primaryCode) { 1294 mVoiceConnector.handleSeparator(); 1295 1296 // Should dismiss the "Touch again to save" message when handling separator 1297 if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { 1298 mHandler.postUpdateSuggestions(); 1299 } 1300 1301 boolean pickedDefault = false; 1302 // Handle separator 1303 final InputConnection ic = getCurrentInputConnection(); 1304 if (ic != null) { 1305 ic.beginBatchEdit(); 1306 abortCorrection(false); 1307 } 1308 if (mHasValidSuggestions) { 1309 // In certain languages where single quote is a separator, it's better 1310 // not to auto correct, but accept the typed word. For instance, 1311 // in Italian dov' should not be expanded to dove' because the elision 1312 // requires the last vowel to be removed. 1313 if (mAutoCorrectOn && primaryCode != '\'' && !mJustReverted) { 1314 pickedDefault = pickDefaultSuggestion(); 1315 // Picked the suggestion by the space key. We consider this 1316 // as "added an auto space". 1317 if (primaryCode == Keyboard.CODE_SPACE) { 1318 mJustAddedAutoSpace = true; 1319 } 1320 } else { 1321 commitTyped(ic); 1322 } 1323 } 1324 if (mJustAddedAutoSpace && primaryCode == Keyboard.CODE_ENTER) { 1325 removeTrailingSpace(); 1326 mJustAddedAutoSpace = false; 1327 } 1328 sendKeyChar((char)primaryCode); 1329 1330 // Handle the case of ". ." -> " .." with auto-space if necessary 1331 // before changing the TextEntryState. 1332 if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED 1333 && primaryCode == Keyboard.CODE_PERIOD) { 1334 reswapPeriodAndSpace(); 1335 } 1336 1337 TextEntryState.typedCharacter((char) primaryCode, true); 1338 if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED 1339 && primaryCode != Keyboard.CODE_ENTER) { 1340 swapPunctuationAndSpace(); 1341 } else if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) { 1342 doubleSpace(); 1343 } 1344 if (pickedDefault) { 1345 CharSequence typedWord = mWord.getTypedWord(); 1346 TextEntryState.backToAcceptedDefault(typedWord); 1347 if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) { 1348 if (ic != null) { 1349 CorrectionInfo correctionInfo = new CorrectionInfo( 1350 mLastSelectionEnd - typedWord.length(), typedWord, mBestWord); 1351 ic.commitCorrection(correctionInfo); 1352 } 1353 if (mCandidateView != null) 1354 mCandidateView.onAutoCorrectionInverted(mBestWord); 1355 } 1356 setPunctuationSuggestions(); 1357 } 1358 mKeyboardSwitcher.updateShiftState(); 1359 if (ic != null) { 1360 ic.endBatchEdit(); 1361 } 1362 } 1363 1364 private void handleClose() { 1365 commitTyped(getCurrentInputConnection()); 1366 mVoiceConnector.handleClose(); 1367 requestHideSelf(0); 1368 LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); 1369 if (inputView != null) 1370 inputView.closing(); 1371 TextEntryState.endSession(); 1372 } 1373 1374 private void saveWordInHistory(CharSequence result) { 1375 if (mWord.size() <= 1) { 1376 mWord.reset(); 1377 return; 1378 } 1379 // Skip if result is null. It happens in some edge case. 1380 if (TextUtils.isEmpty(result)) { 1381 return; 1382 } 1383 1384 // Make a copy of the CharSequence, since it is/could be a mutable CharSequence 1385 final String resultCopy = result.toString(); 1386 TypedWordAlternatives entry = new TypedWordAlternatives(resultCopy, 1387 new WordComposer(mWord)); 1388 mWordHistory.add(entry); 1389 } 1390 1391 private boolean isSuggestionsRequested() { 1392 return mIsSettingsSuggestionStripOn; 1393 } 1394 1395 private boolean isShowingPunctuationList() { 1396 return mSuggestPuncList == mCandidateView.getSuggestions(); 1397 } 1398 1399 private boolean isShowingSuggestionsStrip() { 1400 return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE) 1401 || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE 1402 && mOrientation == Configuration.ORIENTATION_PORTRAIT); 1403 } 1404 1405 private boolean isCandidateStripVisible() { 1406 if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isCorrecting()) 1407 return true; 1408 if (!isShowingSuggestionsStrip()) 1409 return false; 1410 if (mApplicationSpecifiedCompletionOn) 1411 return true; 1412 return isSuggestionsRequested(); 1413 } 1414 1415 public void switchToKeyboardView() { 1416 mHandler.post(new Runnable() { 1417 @Override 1418 public void run() { 1419 if (DEBUG) { 1420 Log.d(TAG, "Switch to keyboard view."); 1421 } 1422 View v = mKeyboardSwitcher.getInputView(); 1423 if (v != null) { 1424 // Confirms that the keyboard view doesn't have parent view. 1425 ViewParent p = v.getParent(); 1426 if (p != null && p instanceof ViewGroup) { 1427 ((ViewGroup) p).removeView(v); 1428 } 1429 setInputView(v); 1430 } 1431 setCandidatesViewShown(isCandidateStripVisible()); 1432 updateInputViewShown(); 1433 mHandler.postUpdateSuggestions(); 1434 } 1435 }); 1436 } 1437 1438 public void clearSuggestions() { 1439 setSuggestions(SuggestedWords.EMPTY); 1440 } 1441 1442 public void setSuggestions(SuggestedWords words) { 1443 if (mVoiceConnector.getAndResetIsShowingHint()) { 1444 setCandidatesView(mCandidateViewContainer); 1445 } 1446 1447 if (mCandidateView != null) { 1448 mCandidateView.setSuggestions(words); 1449 if (mCandidateView.isConfigCandidateHighlightFontColorEnabled()) { 1450 mKeyboardSwitcher.onAutoCorrectionStateChanged( 1451 words.hasWordAboveAutoCorrectionScoreThreshold()); 1452 } 1453 } 1454 } 1455 1456 public void updateSuggestions() { 1457 mKeyboardSwitcher.setPreferredLetters(null); 1458 1459 // Check if we have a suggestion engine attached. 1460 if ((mSuggest == null || !isSuggestionsRequested()) 1461 && !mVoiceConnector.isVoiceInputHighlighted()) { 1462 return; 1463 } 1464 1465 if (!mHasValidSuggestions) { 1466 setPunctuationSuggestions(); 1467 return; 1468 } 1469 showSuggestions(mWord); 1470 } 1471 1472 private SuggestedWords.Builder getTypedSuggestions(WordComposer word) { 1473 return mSuggest.getSuggestedWordBuilder(mKeyboardSwitcher.getInputView(), word, null); 1474 } 1475 1476 private void showCorrections(WordAlternatives alternatives) { 1477 mKeyboardSwitcher.setPreferredLetters(null); 1478 SuggestedWords.Builder builder = alternatives.getAlternatives(); 1479 builder.setTypedWordValid(false).setHasMinimalSuggestion(false); 1480 showSuggestions(builder.build(), alternatives.getOriginalWord()); 1481 } 1482 1483 private void showSuggestions(WordComposer word) { 1484 // long startTime = System.currentTimeMillis(); // TIME MEASUREMENT! 1485 // TODO Maybe need better way of retrieving previous word 1486 CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(), 1487 mWordSeparators); 1488 SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder( 1489 mKeyboardSwitcher.getInputView(), word, prevWord); 1490 1491 int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies(); 1492 mKeyboardSwitcher.setPreferredLetters(nextLettersFrequencies); 1493 1494 boolean correctionAvailable = !mInputTypeNoAutoCorrect && !mJustReverted 1495 && mSuggest.hasMinimalCorrection(); 1496 CharSequence typedWord = word.getTypedWord(); 1497 // If we're in basic correct 1498 boolean typedWordValid = mSuggest.isValidWord(typedWord) || 1499 (preferCapitalization() 1500 && mSuggest.isValidWord(typedWord.toString().toLowerCase())); 1501 if (mCorrectionMode == Suggest.CORRECTION_FULL 1502 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { 1503 correctionAvailable |= typedWordValid; 1504 } 1505 // Don't auto-correct words with multiple capital letter 1506 correctionAvailable &= !word.isMostlyCaps(); 1507 correctionAvailable &= !TextEntryState.isCorrecting(); 1508 1509 if (builder.size() > 1 || mCandidateView.isShowingAddToDictionaryHint()) { 1510 builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion(correctionAvailable); 1511 } else { 1512 final SuggestedWords previousSuggestions = mCandidateView.getSuggestions(); 1513 if (previousSuggestions == mSuggestPuncList) 1514 return; 1515 builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions); 1516 } 1517 showSuggestions(builder.build(), typedWord); 1518 } 1519 1520 private void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) { 1521 setSuggestions(suggestedWords); 1522 if (suggestedWords.size() > 0) { 1523 if (suggestedWords.hasAutoCorrectionWord()) { 1524 mBestWord = suggestedWords.getWord(1); 1525 } else { 1526 mBestWord = typedWord; 1527 } 1528 } else { 1529 mBestWord = null; 1530 } 1531 setCandidatesViewShown(isCandidateStripVisible()); 1532 } 1533 1534 private boolean pickDefaultSuggestion() { 1535 // Complete any pending candidate query first 1536 if (mHandler.hasPendingUpdateSuggestions()) { 1537 mHandler.cancelUpdateSuggestions(); 1538 updateSuggestions(); 1539 } 1540 if (mBestWord != null && mBestWord.length() > 0) { 1541 TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); 1542 mJustAccepted = true; 1543 pickSuggestion(mBestWord); 1544 // Add the word to the auto dictionary if it's not a known word 1545 addToDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); 1546 return true; 1547 1548 } 1549 return false; 1550 } 1551 1552 public void pickSuggestionManually(int index, CharSequence suggestion) { 1553 SuggestedWords suggestions = mCandidateView.getSuggestions(); 1554 mVoiceConnector.flushAndLogAllTextModificationCounters(index, suggestion, mWordSeparators); 1555 1556 final boolean correcting = TextEntryState.isCorrecting(); 1557 InputConnection ic = getCurrentInputConnection(); 1558 if (ic != null) { 1559 ic.beginBatchEdit(); 1560 } 1561 if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null 1562 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 1563 CompletionInfo ci = mApplicationSpecifiedCompletions[index]; 1564 if (ic != null) { 1565 ic.commitCompletion(ci); 1566 } 1567 mCommittedLength = suggestion.length(); 1568 if (mCandidateView != null) { 1569 mCandidateView.clear(); 1570 } 1571 mKeyboardSwitcher.updateShiftState(); 1572 if (ic != null) { 1573 ic.endBatchEdit(); 1574 } 1575 return; 1576 } 1577 1578 // If this is a punctuation, apply it through the normal key press 1579 if (suggestion.length() == 1 && (isWordSeparator(suggestion.charAt(0)) 1580 || isSuggestedPunctuation(suggestion.charAt(0)))) { 1581 // Word separators are suggested before the user inputs something. 1582 // So, LatinImeLogger logs "" as a user's input. 1583 LatinImeLogger.logOnManualSuggestion( 1584 "", suggestion.toString(), index, suggestions.mWords); 1585 final char primaryCode = suggestion.charAt(0); 1586 onCodeInput(primaryCode, new int[]{primaryCode}, KeyboardView.NOT_A_TOUCH_COORDINATE, 1587 KeyboardView.NOT_A_TOUCH_COORDINATE); 1588 if (ic != null) { 1589 ic.endBatchEdit(); 1590 } 1591 return; 1592 } 1593 mJustAccepted = true; 1594 pickSuggestion(suggestion); 1595 // Add the word to the auto dictionary if it's not a known word 1596 if (index == 0) { 1597 addToDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); 1598 } else { 1599 addToBigramDictionary(suggestion, 1); 1600 } 1601 LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(), 1602 index, suggestions.mWords); 1603 TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); 1604 // Follow it with a space 1605 if (mAutoSpace && !correcting) { 1606 sendSpace(); 1607 mJustAddedAutoSpace = true; 1608 } 1609 1610 final boolean showingAddToDictionaryHint = index == 0 && mCorrectionMode > 0 1611 && !mSuggest.isValidWord(suggestion) 1612 && !mSuggest.isValidWord(suggestion.toString().toLowerCase()); 1613 1614 if (!correcting) { 1615 // Fool the state watcher so that a subsequent backspace will not do a revert, unless 1616 // we just did a correction, in which case we need to stay in 1617 // TextEntryState.State.PICKED_SUGGESTION state. 1618 TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true); 1619 setPunctuationSuggestions(); 1620 } else if (!showingAddToDictionaryHint) { 1621 // If we're not showing the "Touch again to save", then show corrections again. 1622 // In case the cursor position doesn't change, make sure we show the suggestions again. 1623 clearSuggestions(); 1624 mHandler.postUpdateOldSuggestions(); 1625 } 1626 if (showingAddToDictionaryHint) { 1627 mCandidateView.showAddToDictionaryHint(suggestion); 1628 } 1629 if (ic != null) { 1630 ic.endBatchEdit(); 1631 } 1632 } 1633 1634 /** 1635 * Commits the chosen word to the text field and saves it for later 1636 * retrieval. 1637 * @param suggestion the suggestion picked by the user to be committed to 1638 * the text field 1639 */ 1640 private void pickSuggestion(CharSequence suggestion) { 1641 KeyboardSwitcher switcher = mKeyboardSwitcher; 1642 if (!switcher.isKeyboardAvailable()) 1643 return; 1644 InputConnection ic = getCurrentInputConnection(); 1645 if (ic != null) { 1646 mVoiceConnector.rememberReplacedWord(suggestion, mWordSeparators); 1647 ic.commitText(suggestion, 1); 1648 } 1649 saveWordInHistory(suggestion); 1650 mHasValidSuggestions = false; 1651 mCommittedLength = suggestion.length(); 1652 switcher.setPreferredLetters(null); 1653 } 1654 1655 /** 1656 * Tries to apply any typed alternatives for the word if we have any cached alternatives, 1657 * otherwise tries to find new corrections and completions for the word. 1658 * @param touching The word that the cursor is touching, with position information 1659 * @return true if an alternative was found, false otherwise. 1660 */ 1661 private boolean applyTypedAlternatives(EditingUtils.SelectedWord touching) { 1662 // If we didn't find a match, search for result in typed word history 1663 WordComposer foundWord = null; 1664 WordAlternatives alternatives = null; 1665 for (WordAlternatives entry : mWordHistory) { 1666 if (TextUtils.equals(entry.getChosenWord(), touching.mWord)) { 1667 if (entry instanceof TypedWordAlternatives) { 1668 foundWord = ((TypedWordAlternatives) entry).word; 1669 } 1670 alternatives = entry; 1671 break; 1672 } 1673 } 1674 // If we didn't find a match, at least suggest corrections. 1675 if (foundWord == null 1676 && (mSuggest.isValidWord(touching.mWord) 1677 || mSuggest.isValidWord(touching.mWord.toString().toLowerCase()))) { 1678 foundWord = new WordComposer(); 1679 for (int i = 0; i < touching.mWord.length(); i++) { 1680 foundWord.add(touching.mWord.charAt(i), new int[] { 1681 touching.mWord.charAt(i) 1682 }); 1683 } 1684 foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.mWord.charAt(0))); 1685 } 1686 // Found a match, show suggestions 1687 if (foundWord != null || alternatives != null) { 1688 if (alternatives == null) { 1689 alternatives = new TypedWordAlternatives(touching.mWord, foundWord); 1690 } 1691 showCorrections(alternatives); 1692 if (foundWord != null) { 1693 mWord = new WordComposer(foundWord); 1694 } else { 1695 mWord.reset(); 1696 } 1697 return true; 1698 } 1699 return false; 1700 } 1701 1702 private void setOldSuggestions() { 1703 mVoiceConnector.setShowingVoiceSuggestions(false); 1704 if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) { 1705 return; 1706 } 1707 InputConnection ic = getCurrentInputConnection(); 1708 if (ic == null) return; 1709 if (!mHasValidSuggestions) { 1710 // Extract the selected or touching text 1711 EditingUtils.SelectedWord touching = EditingUtils.getWordAtCursorOrSelection(ic, 1712 mLastSelectionStart, mLastSelectionEnd, mWordSeparators); 1713 1714 if (touching != null && touching.mWord.length() > 1) { 1715 ic.beginBatchEdit(); 1716 1717 if (!mVoiceConnector.applyVoiceAlternatives(touching) 1718 && !applyTypedAlternatives(touching)) { 1719 abortCorrection(true); 1720 } else { 1721 TextEntryState.selectedForCorrection(); 1722 EditingUtils.underlineWord(ic, touching); 1723 } 1724 1725 ic.endBatchEdit(); 1726 } else { 1727 abortCorrection(true); 1728 setPunctuationSuggestions(); // Show the punctuation suggestions list 1729 } 1730 } else { 1731 abortCorrection(true); 1732 } 1733 } 1734 1735 private void setPunctuationSuggestions() { 1736 setSuggestions(mSuggestPuncList); 1737 setCandidatesViewShown(isCandidateStripVisible()); 1738 } 1739 1740 private void addToDictionaries(CharSequence suggestion, int frequencyDelta) { 1741 checkAddToDictionary(suggestion, frequencyDelta, false); 1742 } 1743 1744 private void addToBigramDictionary(CharSequence suggestion, int frequencyDelta) { 1745 checkAddToDictionary(suggestion, frequencyDelta, true); 1746 } 1747 1748 /** 1749 * Adds to the UserBigramDictionary and/or AutoDictionary 1750 * @param addToBigramDictionary true if it should be added to bigram dictionary if possible 1751 */ 1752 private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta, 1753 boolean addToBigramDictionary) { 1754 if (suggestion == null || suggestion.length() < 1) return; 1755 // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be 1756 // adding words in situations where the user or application really didn't 1757 // want corrections enabled or learned. 1758 if (!(mCorrectionMode == Suggest.CORRECTION_FULL 1759 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { 1760 return; 1761 } 1762 if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion) 1763 || (!mSuggest.isValidWord(suggestion.toString()) 1764 && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) { 1765 mAutoDictionary.addWord(suggestion.toString(), frequencyDelta); 1766 } 1767 1768 if (mUserBigramDictionary != null) { 1769 CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(), 1770 mSentenceSeparators); 1771 if (!TextUtils.isEmpty(prevWord)) { 1772 mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString()); 1773 } 1774 } 1775 } 1776 1777 private boolean isCursorTouchingWord() { 1778 InputConnection ic = getCurrentInputConnection(); 1779 if (ic == null) return false; 1780 CharSequence toLeft = ic.getTextBeforeCursor(1, 0); 1781 CharSequence toRight = ic.getTextAfterCursor(1, 0); 1782 if (!TextUtils.isEmpty(toLeft) 1783 && !isWordSeparator(toLeft.charAt(0)) 1784 && !isSuggestedPunctuation(toLeft.charAt(0))) { 1785 return true; 1786 } 1787 if (!TextUtils.isEmpty(toRight) 1788 && !isWordSeparator(toRight.charAt(0)) 1789 && !isSuggestedPunctuation(toRight.charAt(0))) { 1790 return true; 1791 } 1792 return false; 1793 } 1794 1795 private boolean sameAsTextBeforeCursor(InputConnection ic, CharSequence text) { 1796 CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0); 1797 return TextUtils.equals(text, beforeText); 1798 } 1799 1800 public void revertLastWord(boolean deleteChar) { 1801 final int length = mComposing.length(); 1802 if (!mHasValidSuggestions && length > 0) { 1803 final InputConnection ic = getCurrentInputConnection(); 1804 mHasValidSuggestions = true; 1805 mJustReverted = true; 1806 if (deleteChar) ic.deleteSurroundingText(1, 0); 1807 int toDelete = mCommittedLength; 1808 CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); 1809 if (toTheLeft != null && toTheLeft.length() > 0 1810 && isWordSeparator(toTheLeft.charAt(0))) { 1811 toDelete--; 1812 } 1813 ic.deleteSurroundingText(toDelete, 0); 1814 ic.setComposingText(mComposing, 1); 1815 TextEntryState.backspace(); 1816 mHandler.postUpdateSuggestions(); 1817 } else { 1818 sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); 1819 mJustReverted = false; 1820 } 1821 } 1822 1823 protected String getWordSeparators() { 1824 return mWordSeparators; 1825 } 1826 1827 public boolean isWordSeparator(int code) { 1828 String separators = getWordSeparators(); 1829 return separators.contains(String.valueOf((char)code)); 1830 } 1831 1832 private boolean isSentenceSeparator(int code) { 1833 return mSentenceSeparators.contains(String.valueOf((char)code)); 1834 } 1835 1836 private void sendSpace() { 1837 sendKeyChar((char)Keyboard.CODE_SPACE); 1838 mKeyboardSwitcher.updateShiftState(); 1839 } 1840 1841 public boolean preferCapitalization() { 1842 return mWord.isFirstCharCapitalized(); 1843 } 1844 1845 // Notify that Language has been changed and toggleLanguage will update KeyboaredID according 1846 // to new Language. 1847 public void onKeyboardLanguageChanged() { 1848 toggleLanguage(true, true); 1849 } 1850 1851 // "reset" and "next" are used only for USE_SPACEBAR_LANGUAGE_SWITCHER. 1852 private void toggleLanguage(boolean reset, boolean next) { 1853 if (SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCHER) { 1854 mSubtypeSwitcher.toggleLanguage(reset, next); 1855 } 1856 // Reload keyboard because the current language has been changed. 1857 KeyboardSwitcher switcher = mKeyboardSwitcher; 1858 final int mode = switcher.getKeyboardMode(); 1859 final EditorInfo attribute = getCurrentInputEditorInfo(); 1860 final int imeOptions = (attribute != null) ? attribute.imeOptions : 0; 1861 switcher.loadKeyboard(mode, imeOptions, mVoiceConnector.isVoiceButtonEnabled(), 1862 mVoiceConnector.isVoiceButtonOnPrimary()); 1863 initSuggest(); 1864 switcher.updateShiftState(); 1865 } 1866 1867 @Override 1868 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, 1869 String key) { 1870 mSubtypeSwitcher.onSharedPreferenceChanged(sharedPreferences, key); 1871 if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) { 1872 mRefreshKeyboardRequired = true; 1873 } else if (Settings.PREF_RECORRECTION_ENABLED.equals(key)) { 1874 mReCorrectionEnabled = sharedPreferences.getBoolean( 1875 Settings.PREF_RECORRECTION_ENABLED, 1876 getResources().getBoolean(R.bool.default_recorrection_enabled)); 1877 } 1878 } 1879 1880 @Override 1881 public void onSwipeDown() { 1882 if (mConfigSwipeDownDismissKeyboardEnabled) 1883 handleClose(); 1884 } 1885 1886 @Override 1887 public void onPress(int primaryCode) { 1888 if (mKeyboardSwitcher.isVibrateAndSoundFeedbackRequired()) { 1889 vibrate(); 1890 playKeyClick(primaryCode); 1891 } 1892 KeyboardSwitcher switcher = mKeyboardSwitcher; 1893 final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); 1894 if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { 1895 switcher.onPressShift(); 1896 } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { 1897 switcher.onPressSymbol(); 1898 } else { 1899 switcher.onOtherKeyPressed(); 1900 } 1901 } 1902 1903 @Override 1904 public void onRelease(int primaryCode) { 1905 KeyboardSwitcher switcher = mKeyboardSwitcher; 1906 // Reset any drag flags in the keyboard 1907 switcher.keyReleased(); 1908 final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); 1909 if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { 1910 switcher.onReleaseShift(); 1911 } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { 1912 switcher.onReleaseSymbol(); 1913 } 1914 } 1915 1916 1917 // receive ringer mode changes to detect silent mode 1918 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 1919 @Override 1920 public void onReceive(Context context, Intent intent) { 1921 updateRingerMode(); 1922 } 1923 }; 1924 1925 // update flags for silent mode 1926 private void updateRingerMode() { 1927 if (mAudioManager == null) { 1928 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 1929 } 1930 if (mAudioManager != null) { 1931 mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); 1932 } 1933 } 1934 1935 private void playKeyClick(int primaryCode) { 1936 // if mAudioManager is null, we don't have the ringer state yet 1937 // mAudioManager will be set by updateRingerMode 1938 if (mAudioManager == null) { 1939 if (mKeyboardSwitcher.getInputView() != null) { 1940 updateRingerMode(); 1941 } 1942 } 1943 if (mSoundOn && !mSilentMode) { 1944 // FIXME: Volume and enable should come from UI settings 1945 // FIXME: These should be triggered after auto-repeat logic 1946 int sound = AudioManager.FX_KEYPRESS_STANDARD; 1947 switch (primaryCode) { 1948 case Keyboard.CODE_DELETE: 1949 sound = AudioManager.FX_KEYPRESS_DELETE; 1950 break; 1951 case Keyboard.CODE_ENTER: 1952 sound = AudioManager.FX_KEYPRESS_RETURN; 1953 break; 1954 case Keyboard.CODE_SPACE: 1955 sound = AudioManager.FX_KEYPRESS_SPACEBAR; 1956 break; 1957 } 1958 mAudioManager.playSoundEffect(sound, FX_VOLUME); 1959 } 1960 } 1961 1962 public void vibrate() { 1963 if (!mVibrateOn) { 1964 return; 1965 } 1966 LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); 1967 if (inputView != null) { 1968 inputView.performHapticFeedback( 1969 HapticFeedbackConstants.KEYBOARD_TAP, 1970 HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); 1971 } 1972 } 1973 1974 public void promoteToUserDictionary(String word, int frequency) { 1975 if (mUserDictionary.isValidWord(word)) return; 1976 mUserDictionary.addWord(word, frequency); 1977 } 1978 1979 public WordComposer getCurrentWord() { 1980 return mWord; 1981 } 1982 1983 public boolean getPopupOn() { 1984 return mPopupOn; 1985 } 1986 1987 private void updateCorrectionMode() { 1988 mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false; 1989 mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes) 1990 && !mInputTypeNoAutoCorrect && mHasDictionary; 1991 mCorrectionMode = (mAutoCorrectOn && mAutoCorrectEnabled) 1992 ? Suggest.CORRECTION_FULL 1993 : (mAutoCorrectOn ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); 1994 mCorrectionMode = (mBigramSuggestionEnabled && mAutoCorrectOn && mAutoCorrectEnabled) 1995 ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; 1996 if (mSuggest != null) { 1997 mSuggest.setCorrectionMode(mCorrectionMode); 1998 } 1999 } 2000 2001 private void updateAutoTextEnabled() { 2002 if (mSuggest == null) return; 2003 mSuggest.setAutoTextEnabled(mQuickFixes 2004 && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage()); 2005 } 2006 2007 private void updateSuggestionVisibility(SharedPreferences prefs) { 2008 final String suggestionVisiblityStr = prefs.getString( 2009 Settings.PREF_SHOW_SUGGESTIONS_SETTING, 2010 mResources.getString(R.string.prefs_suggestion_visibility_default_value)); 2011 for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) { 2012 if (suggestionVisiblityStr.equals(mResources.getString(visibility))) { 2013 mSuggestionVisibility = visibility; 2014 break; 2015 } 2016 } 2017 } 2018 2019 protected void launchSettings() { 2020 launchSettings(Settings.class); 2021 } 2022 2023 public void launchDebugSettings() { 2024 launchSettings(DebugSettings.class); 2025 } 2026 2027 protected void launchSettings(Class<? extends PreferenceActivity> settingsClass) { 2028 handleClose(); 2029 Intent intent = new Intent(); 2030 intent.setClass(LatinIME.this, settingsClass); 2031 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 2032 startActivity(intent); 2033 } 2034 2035 private void loadSettings(EditorInfo attribute) { 2036 // Get the settings preferences 2037 final SharedPreferences prefs = mPrefs; 2038 Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); 2039 mVibrateOn = vibrator != null && vibrator.hasVibrator() 2040 && prefs.getBoolean(Settings.PREF_VIBRATE_ON, false); 2041 mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, false); 2042 mPopupOn = prefs.getBoolean(Settings.PREF_POPUP_ON, 2043 mResources.getBoolean(R.bool.config_default_popup_preview)); 2044 mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true); 2045 mQuickFixes = prefs.getBoolean(Settings.PREF_QUICK_FIXES, true); 2046 2047 mAutoCorrectEnabled = isAutoCorrectEnabled(prefs); 2048 mBigramSuggestionEnabled = mAutoCorrectEnabled && isBigramSuggestionEnabled(prefs); 2049 loadAndSetAutoCorrectionThreshold(prefs); 2050 2051 mVoiceConnector.loadSettings(attribute, prefs); 2052 2053 updateCorrectionMode(); 2054 updateAutoTextEnabled(); 2055 updateSuggestionVisibility(prefs); 2056 SubtypeSwitcher.getInstance().loadSettings(); 2057 } 2058 2059 /** 2060 * Load Auto correction threshold from SharedPreferences, and modify mSuggest's threshold. 2061 */ 2062 private void loadAndSetAutoCorrectionThreshold(SharedPreferences sp) { 2063 // When mSuggest is not initialized, cannnot modify mSuggest's threshold. 2064 if (mSuggest == null) return; 2065 // When auto correction setting is turned off, the threshold is ignored. 2066 if (!isAutoCorrectEnabled(sp)) return; 2067 2068 final String currentAutoCorrectionSetting = sp.getString( 2069 Settings.PREF_AUTO_CORRECTION_THRESHOLD, 2070 mResources.getString(R.string.auto_correction_threshold_mode_index_modest)); 2071 final String[] autoCorrectionThresholdValues = mResources.getStringArray( 2072 R.array.auto_correction_threshold_values); 2073 // When autoCrrectionThreshold is greater than 1.0, auto correction is virtually turned off. 2074 double autoCorrectionThreshold = Double.MAX_VALUE; 2075 try { 2076 final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting); 2077 if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) { 2078 autoCorrectionThreshold = Double.parseDouble( 2079 autoCorrectionThresholdValues[arrayIndex]); 2080 } 2081 } catch (NumberFormatException e) { 2082 // Whenever the threshold settings are correct, never come here. 2083 autoCorrectionThreshold = Double.MAX_VALUE; 2084 Log.w(TAG, "Cannot load auto correction threshold setting." 2085 + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting 2086 + ", autoCorrectionThresholdValues: " 2087 + Arrays.toString(autoCorrectionThresholdValues)); 2088 } 2089 // TODO: This should be refactored : 2090 // setAutoCorrectionThreshold should be called outside of this method. 2091 mSuggest.setAutoCorrectionThreshold(autoCorrectionThreshold); 2092 } 2093 2094 private boolean isAutoCorrectEnabled(SharedPreferences sp) { 2095 final String currentAutoCorrectionSetting = sp.getString( 2096 Settings.PREF_AUTO_CORRECTION_THRESHOLD, 2097 mResources.getString(R.string.auto_correction_threshold_mode_index_modest)); 2098 final String autoCorrectionOff = mResources.getString( 2099 R.string.auto_correction_threshold_mode_index_off); 2100 return !currentAutoCorrectionSetting.equals(autoCorrectionOff); 2101 } 2102 2103 private boolean isBigramSuggestionEnabled(SharedPreferences sp) { 2104 // TODO: Define default value instead of 'true'. 2105 return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTIONS, true); 2106 } 2107 2108 private void initSuggestPuncList() { 2109 if (mSuggestPuncs != null || mSuggestPuncList != null) 2110 return; 2111 SuggestedWords.Builder builder = new SuggestedWords.Builder(); 2112 String puncs = mResources.getString(R.string.suggested_punctuations); 2113 if (puncs != null) { 2114 for (int i = 0; i < puncs.length(); i++) { 2115 builder.addWord(puncs.subSequence(i, i + 1)); 2116 } 2117 } 2118 mSuggestPuncList = builder.build(); 2119 mSuggestPuncs = puncs; 2120 } 2121 2122 private boolean isSuggestedPunctuation(int code) { 2123 return mSuggestPuncs.contains(String.valueOf((char)code)); 2124 } 2125 2126 private void showOptionsMenu() { 2127 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2128 builder.setCancelable(true); 2129 builder.setIcon(R.drawable.ic_dialog_keyboard); 2130 builder.setNegativeButton(android.R.string.cancel, null); 2131 CharSequence itemSettings = getString(R.string.english_ime_settings); 2132 CharSequence itemInputMethod = getString(R.string.selectInputMethod); 2133 builder.setItems(new CharSequence[] { 2134 itemInputMethod, itemSettings}, 2135 new DialogInterface.OnClickListener() { 2136 2137 @Override 2138 public void onClick(DialogInterface di, int position) { 2139 di.dismiss(); 2140 switch (position) { 2141 case POS_SETTINGS: 2142 launchSettings(); 2143 break; 2144 case POS_METHOD: 2145 mImm.showInputMethodPicker(); 2146 break; 2147 } 2148 } 2149 }); 2150 builder.setTitle(mResources.getString(R.string.english_ime_input_options)); 2151 mOptionsDialog = builder.create(); 2152 Window window = mOptionsDialog.getWindow(); 2153 WindowManager.LayoutParams lp = window.getAttributes(); 2154 lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); 2155 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 2156 window.setAttributes(lp); 2157 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 2158 mOptionsDialog.show(); 2159 } 2160 2161 @Override 2162 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { 2163 super.dump(fd, fout, args); 2164 2165 final Printer p = new PrintWriterPrinter(fout); 2166 p.println("LatinIME state :"); 2167 p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode()); 2168 p.println(" mComposing=" + mComposing.toString()); 2169 p.println(" mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn); 2170 p.println(" mCorrectionMode=" + mCorrectionMode); 2171 p.println(" mHasValidSuggestions=" + mHasValidSuggestions); 2172 p.println(" mAutoCorrectOn=" + mAutoCorrectOn); 2173 p.println(" mAutoSpace=" + mAutoSpace); 2174 p.println(" mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn); 2175 p.println(" TextEntryState.state=" + TextEntryState.getState()); 2176 p.println(" mSoundOn=" + mSoundOn); 2177 p.println(" mVibrateOn=" + mVibrateOn); 2178 p.println(" mPopupOn=" + mPopupOn); 2179 } 2180 2181 // Characters per second measurement 2182 2183 private long mLastCpsTime; 2184 private static final int CPS_BUFFER_SIZE = 16; 2185 private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE]; 2186 private int mCpsIndex; 2187 2188 private void measureCps() { 2189 long now = System.currentTimeMillis(); 2190 if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial 2191 mCpsIntervals[mCpsIndex] = now - mLastCpsTime; 2192 mLastCpsTime = now; 2193 mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE; 2194 long total = 0; 2195 for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i]; 2196 System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total)); 2197 } 2198 2199 @Override 2200 public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) { 2201 SubtypeSwitcher.getInstance().updateSubtype(subtype); 2202 } 2203} 2204