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