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