LatinIME.java revision 1587be697752b12e18d5cadb90e56883cb93c242
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII; 20import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; 21import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; 22 23import android.app.Activity; 24import android.app.AlertDialog; 25import android.content.BroadcastReceiver; 26import android.content.Context; 27import android.content.DialogInterface; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.content.SharedPreferences; 31import android.content.res.Configuration; 32import android.content.res.Resources; 33import android.graphics.Rect; 34import android.inputmethodservice.InputMethodService; 35import android.media.AudioManager; 36import android.net.ConnectivityManager; 37import android.os.Debug; 38import android.os.Handler; 39import android.os.HandlerThread; 40import android.os.IBinder; 41import android.os.Message; 42import android.os.SystemClock; 43import android.preference.PreferenceManager; 44import android.text.InputType; 45import android.text.TextUtils; 46import android.util.Log; 47import android.util.Pair; 48import android.util.PrintWriterPrinter; 49import android.util.Printer; 50import android.view.KeyEvent; 51import android.view.View; 52import android.view.ViewGroup.LayoutParams; 53import android.view.Window; 54import android.view.WindowManager; 55import android.view.inputmethod.CompletionInfo; 56import android.view.inputmethod.EditorInfo; 57import android.view.inputmethod.InputMethodSubtype; 58 59import com.android.inputmethod.accessibility.AccessibilityUtils; 60import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; 61import com.android.inputmethod.annotations.UsedForTesting; 62import com.android.inputmethod.compat.InputMethodServiceCompatUtils; 63import com.android.inputmethod.dictionarypack.DictionaryPackConstants; 64import com.android.inputmethod.keyboard.Keyboard; 65import com.android.inputmethod.keyboard.KeyboardActionListener; 66import com.android.inputmethod.keyboard.KeyboardId; 67import com.android.inputmethod.keyboard.KeyboardSwitcher; 68import com.android.inputmethod.keyboard.MainKeyboardView; 69import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; 70import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 71import com.android.inputmethod.latin.define.ProductionFlag; 72import com.android.inputmethod.latin.inputlogic.InputLogic; 73import com.android.inputmethod.latin.inputlogic.SpaceState; 74import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever; 75import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister; 76import com.android.inputmethod.latin.settings.Settings; 77import com.android.inputmethod.latin.settings.SettingsActivity; 78import com.android.inputmethod.latin.settings.SettingsValues; 79import com.android.inputmethod.latin.suggestions.SuggestionStripView; 80import com.android.inputmethod.latin.utils.ApplicationUtils; 81import com.android.inputmethod.latin.utils.AutoCorrectionUtils; 82import com.android.inputmethod.latin.utils.CapsModeUtils; 83import com.android.inputmethod.latin.utils.CompletionInfoUtils; 84import com.android.inputmethod.latin.utils.IntentUtils; 85import com.android.inputmethod.latin.utils.JniUtils; 86import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; 87import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; 88import com.android.inputmethod.research.ResearchLogger; 89 90import java.io.FileDescriptor; 91import java.io.PrintWriter; 92import java.util.ArrayList; 93import java.util.Locale; 94 95/** 96 * Input method implementation for Qwerty'ish keyboard. 97 */ 98public class LatinIME extends InputMethodService implements KeyboardActionListener, 99 SuggestionStripView.Listener, Suggest.SuggestInitializationListener { 100 private static final String TAG = LatinIME.class.getSimpleName(); 101 private static final boolean TRACE = false; 102 private static boolean DEBUG = false; 103 104 private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; 105 106 private static final int PENDING_IMS_CALLBACK_DURATION = 800; 107 108 private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; 109 110 /** 111 * The name of the scheme used by the Package Manager to warn of a new package installation, 112 * replacement or removal. 113 */ 114 private static final String SCHEME_PACKAGE = "package"; 115 116 private final Settings mSettings; 117 private final InputLogic mInputLogic = new InputLogic(this); 118 119 private View mExtractArea; 120 private View mKeyPreviewBackingView; 121 private SuggestionStripView mSuggestionStripView; 122 123 private CompletionInfo[] mApplicationSpecifiedCompletions; 124 125 private RichInputMethodManager mRichImm; 126 @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; 127 private final SubtypeSwitcher mSubtypeSwitcher; 128 private final SubtypeState mSubtypeState = new SubtypeState(); 129 130 private UserBinaryDictionary mUserDictionary; 131 132 // Object for reacting to adding/removing a dictionary pack. 133 private BroadcastReceiver mDictionaryPackInstallReceiver = 134 new DictionaryPackInstallBroadcastReceiver(this); 135 136 private AlertDialog mOptionsDialog; 137 138 private final boolean mIsHardwareAcceleratedDrawingEnabled; 139 140 public final UIHandler mHandler = new UIHandler(this); 141 private InputUpdater mInputUpdater; 142 143 public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> { 144 private static final int MSG_UPDATE_SHIFT_STATE = 0; 145 private static final int MSG_PENDING_IMS_CALLBACK = 1; 146 private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; 147 private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; 148 private static final int MSG_RESUME_SUGGESTIONS = 4; 149 private static final int MSG_REOPEN_DICTIONARIES = 5; 150 private static final int MSG_ON_END_BATCH_INPUT = 6; 151 private static final int MSG_RESET_CACHES = 7; 152 // Update this when adding new messages 153 private static final int MSG_LAST = MSG_RESET_CACHES; 154 155 private static final int ARG1_NOT_GESTURE_INPUT = 0; 156 private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; 157 private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2; 158 private static final int ARG2_WITHOUT_TYPED_WORD = 0; 159 private static final int ARG2_WITH_TYPED_WORD = 1; 160 161 private int mDelayUpdateSuggestions; 162 private int mDelayUpdateShiftState; 163 private long mDoubleSpacePeriodTimeout; 164 private long mDoubleSpacePeriodTimerStart; 165 166 public UIHandler(final LatinIME ownerInstance) { 167 super(ownerInstance); 168 } 169 170 public void onCreate() { 171 final Resources res = getOwnerInstance().getResources(); 172 mDelayUpdateSuggestions = 173 res.getInteger(R.integer.config_delay_update_suggestions); 174 mDelayUpdateShiftState = 175 res.getInteger(R.integer.config_delay_update_shift_state); 176 mDoubleSpacePeriodTimeout = 177 res.getInteger(R.integer.config_double_space_period_timeout); 178 } 179 180 @Override 181 public void handleMessage(final Message msg) { 182 final LatinIME latinIme = getOwnerInstance(); 183 final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; 184 switch (msg.what) { 185 case MSG_UPDATE_SUGGESTION_STRIP: 186 latinIme.mInputLogic.performUpdateSuggestionStripSync( 187 latinIme.mSettings.getCurrent(), this); 188 break; 189 case MSG_UPDATE_SHIFT_STATE: 190 switcher.updateShiftState(); 191 break; 192 case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: 193 if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) { 194 if (msg.arg2 == ARG2_WITH_TYPED_WORD) { 195 final Pair<SuggestedWords, String> p = 196 (Pair<SuggestedWords, String>) msg.obj; 197 latinIme.showSuggestionStripWithTypedWord(p.first, p.second); 198 } else { 199 latinIme.showSuggestionStrip((SuggestedWords) msg.obj); 200 } 201 } else { 202 latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj, 203 msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); 204 } 205 break; 206 case MSG_RESUME_SUGGESTIONS: 207 latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor( 208 latinIme.mSettings.getCurrent(), latinIme.mKeyboardSwitcher, 209 latinIme.mInputUpdater); 210 break; 211 case MSG_REOPEN_DICTIONARIES: 212 latinIme.initSuggest(); 213 // In theory we could call latinIme.updateSuggestionStrip() right away, but 214 // in the practice, the dictionary is not finished opening yet so we wouldn't 215 // get any suggestions. Wait one frame. 216 postUpdateSuggestionStrip(); 217 break; 218 case MSG_ON_END_BATCH_INPUT: 219 latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj); 220 break; 221 case MSG_RESET_CACHES: 222 latinIme.mInputLogic.retryResetCaches(latinIme.mSettings.getCurrent(), 223 msg.arg1 == 1 /* tryResumeSuggestions */, 224 msg.arg2 /* remainingTries */, 225 latinIme.mKeyboardSwitcher, this); 226 break; 227 } 228 } 229 230 public void postUpdateSuggestionStrip() { 231 sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions); 232 } 233 234 public void postReopenDictionaries() { 235 sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)); 236 } 237 238 public void postResumeSuggestions() { 239 removeMessages(MSG_RESUME_SUGGESTIONS); 240 sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); 241 } 242 243 public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { 244 removeMessages(MSG_RESET_CACHES); 245 sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0, 246 remainingTries, null)); 247 } 248 249 public void cancelUpdateSuggestionStrip() { 250 removeMessages(MSG_UPDATE_SUGGESTION_STRIP); 251 } 252 253 public boolean hasPendingUpdateSuggestions() { 254 return hasMessages(MSG_UPDATE_SUGGESTION_STRIP); 255 } 256 257 public boolean hasPendingReopenDictionaries() { 258 return hasMessages(MSG_REOPEN_DICTIONARIES); 259 } 260 261 public void postUpdateShiftState() { 262 removeMessages(MSG_UPDATE_SHIFT_STATE); 263 sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState); 264 } 265 266 public void cancelUpdateShiftState() { 267 removeMessages(MSG_UPDATE_SHIFT_STATE); 268 } 269 270 @UsedForTesting 271 public void removeAllMessages() { 272 for (int i = 0; i <= MSG_LAST; ++i) { 273 removeMessages(i); 274 } 275 } 276 277 public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, 278 final boolean dismissGestureFloatingPreviewText) { 279 removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 280 final int arg1 = dismissGestureFloatingPreviewText 281 ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT 282 : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT; 283 obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 284 ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); 285 } 286 287 public void showSuggestionStrip(final SuggestedWords suggestedWords) { 288 removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 289 obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 290 ARG1_NOT_GESTURE_INPUT, ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); 291 } 292 293 // TODO: Remove this method. 294 public void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, 295 final String typedWord) { 296 removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 297 obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, ARG1_NOT_GESTURE_INPUT, 298 ARG2_WITH_TYPED_WORD, 299 new Pair<SuggestedWords, String>(suggestedWords, typedWord)).sendToTarget(); 300 } 301 302 public void onEndBatchInput(final SuggestedWords suggestedWords) { 303 obtainMessage(MSG_ON_END_BATCH_INPUT, suggestedWords).sendToTarget(); 304 } 305 306 public void startDoubleSpacePeriodTimer() { 307 mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis(); 308 } 309 310 public void cancelDoubleSpacePeriodTimer() { 311 mDoubleSpacePeriodTimerStart = 0; 312 } 313 314 public boolean isAcceptingDoubleSpacePeriod() { 315 return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart 316 < mDoubleSpacePeriodTimeout; 317 } 318 319 // Working variables for the following methods. 320 private boolean mIsOrientationChanging; 321 private boolean mPendingSuccessiveImsCallback; 322 private boolean mHasPendingStartInput; 323 private boolean mHasPendingFinishInputView; 324 private boolean mHasPendingFinishInput; 325 private EditorInfo mAppliedEditorInfo; 326 327 public void startOrientationChanging() { 328 removeMessages(MSG_PENDING_IMS_CALLBACK); 329 resetPendingImsCallback(); 330 mIsOrientationChanging = true; 331 final LatinIME latinIme = getOwnerInstance(); 332 if (latinIme.isInputViewShown()) { 333 latinIme.mKeyboardSwitcher.saveKeyboardState(); 334 } 335 } 336 337 private void resetPendingImsCallback() { 338 mHasPendingFinishInputView = false; 339 mHasPendingFinishInput = false; 340 mHasPendingStartInput = false; 341 } 342 343 private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, 344 boolean restarting) { 345 if (mHasPendingFinishInputView) 346 latinIme.onFinishInputViewInternal(mHasPendingFinishInput); 347 if (mHasPendingFinishInput) 348 latinIme.onFinishInputInternal(); 349 if (mHasPendingStartInput) 350 latinIme.onStartInputInternal(editorInfo, restarting); 351 resetPendingImsCallback(); 352 } 353 354 public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { 355 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 356 // Typically this is the second onStartInput after orientation changed. 357 mHasPendingStartInput = true; 358 } else { 359 if (mIsOrientationChanging && restarting) { 360 // This is the first onStartInput after orientation changed. 361 mIsOrientationChanging = false; 362 mPendingSuccessiveImsCallback = true; 363 } 364 final LatinIME latinIme = getOwnerInstance(); 365 executePendingImsCallback(latinIme, editorInfo, restarting); 366 latinIme.onStartInputInternal(editorInfo, restarting); 367 } 368 } 369 370 public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { 371 if (hasMessages(MSG_PENDING_IMS_CALLBACK) 372 && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { 373 // Typically this is the second onStartInputView after orientation changed. 374 resetPendingImsCallback(); 375 } else { 376 if (mPendingSuccessiveImsCallback) { 377 // This is the first onStartInputView after orientation changed. 378 mPendingSuccessiveImsCallback = false; 379 resetPendingImsCallback(); 380 sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), 381 PENDING_IMS_CALLBACK_DURATION); 382 } 383 final LatinIME latinIme = getOwnerInstance(); 384 executePendingImsCallback(latinIme, editorInfo, restarting); 385 latinIme.onStartInputViewInternal(editorInfo, restarting); 386 mAppliedEditorInfo = editorInfo; 387 } 388 } 389 390 public void onFinishInputView(final boolean finishingInput) { 391 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 392 // Typically this is the first onFinishInputView after orientation changed. 393 mHasPendingFinishInputView = true; 394 } else { 395 final LatinIME latinIme = getOwnerInstance(); 396 latinIme.onFinishInputViewInternal(finishingInput); 397 mAppliedEditorInfo = null; 398 } 399 } 400 401 public void onFinishInput() { 402 if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { 403 // Typically this is the first onFinishInput after orientation changed. 404 mHasPendingFinishInput = true; 405 } else { 406 final LatinIME latinIme = getOwnerInstance(); 407 executePendingImsCallback(latinIme, null, false); 408 latinIme.onFinishInputInternal(); 409 } 410 } 411 } 412 413 static final class SubtypeState { 414 private InputMethodSubtype mLastActiveSubtype; 415 private boolean mCurrentSubtypeUsed; 416 417 public void currentSubtypeUsed() { 418 mCurrentSubtypeUsed = true; 419 } 420 421 public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) { 422 final InputMethodSubtype currentSubtype = richImm.getInputMethodManager() 423 .getCurrentInputMethodSubtype(); 424 final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; 425 final boolean currentSubtypeUsed = mCurrentSubtypeUsed; 426 if (currentSubtypeUsed) { 427 mLastActiveSubtype = currentSubtype; 428 mCurrentSubtypeUsed = false; 429 } 430 if (currentSubtypeUsed 431 && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype) 432 && !currentSubtype.equals(lastActiveSubtype)) { 433 richImm.setInputMethodAndSubtype(token, lastActiveSubtype); 434 return; 435 } 436 richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */); 437 } 438 } 439 440 // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial 441 // JNI call as much as possible. 442 static { 443 JniUtils.loadNativeLibrary(); 444 } 445 446 public LatinIME() { 447 super(); 448 mSettings = Settings.getInstance(); 449 mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 450 mKeyboardSwitcher = KeyboardSwitcher.getInstance(); 451 mIsHardwareAcceleratedDrawingEnabled = 452 InputMethodServiceCompatUtils.enableHardwareAcceleration(this); 453 Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled); 454 } 455 456 @Override 457 public void onCreate() { 458 Settings.init(this); 459 LatinImeLogger.init(this); 460 RichInputMethodManager.init(this); 461 mRichImm = RichInputMethodManager.getInstance(); 462 SubtypeSwitcher.init(this); 463 KeyboardSwitcher.init(this); 464 AudioAndHapticFeedbackManager.init(this); 465 AccessibilityUtils.init(this); 466 PersonalizationDictionarySessionRegister.init(this); 467 468 super.onCreate(); 469 470 mHandler.onCreate(); 471 DEBUG = LatinImeLogger.sDBG; 472 473 // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}. 474 loadSettings(); 475 initSuggest(); 476 477 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 478 ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mInputLogic.mSuggest); 479 } 480 481 // Register to receive ringer mode change and network state change. 482 // Also receive installation and removal of a dictionary pack. 483 final IntentFilter filter = new IntentFilter(); 484 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 485 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 486 registerReceiver(mReceiver, filter); 487 488 final IntentFilter packageFilter = new IntentFilter(); 489 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 490 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 491 packageFilter.addDataScheme(SCHEME_PACKAGE); 492 registerReceiver(mDictionaryPackInstallReceiver, packageFilter); 493 494 final IntentFilter newDictFilter = new IntentFilter(); 495 newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); 496 registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); 497 498 DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this); 499 500 mInputUpdater = new InputUpdater(this); 501 } 502 503 // Has to be package-visible for unit tests 504 @UsedForTesting 505 void loadSettings() { 506 final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 507 final EditorInfo editorInfo = getCurrentInputEditorInfo(); 508 final InputAttributes inputAttributes = new InputAttributes(editorInfo, isFullscreenMode()); 509 mSettings.loadSettings(this, locale, inputAttributes); 510 AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent()); 511 // To load the keyboard we need to load all the settings once, but resetting the 512 // contacts dictionary should be deferred until after the new layout has been displayed 513 // to improve responsivity. In the language switching process, we post a reopenDictionaries 514 // message, then come here to read the settings for the new language before we change 515 // the layout; at this time, we need to skip resetting the contacts dictionary. It will 516 // be done later inside {@see #initSuggest()} when the reopenDictionaries message is 517 // processed. 518 if (!mHandler.hasPendingReopenDictionaries() && mInputLogic.mSuggest != null) { 519 // May need to reset dictionaries depending on the user settings. 520 mInputLogic.mSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */, 521 mSettings.getCurrent()); 522 } 523 } 524 525 // Note that this method is called from a non-UI thread. 526 @Override 527 public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) { 528 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 529 if (mainKeyboardView != null) { 530 mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable); 531 } 532 } 533 534 private void initSuggest() { 535 final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 536 final String switcherLocaleStr = switcherSubtypeLocale.toString(); 537 final Locale subtypeLocale; 538 if (TextUtils.isEmpty(switcherLocaleStr)) { 539 // This happens in very rare corner cases - for example, immediately after a switch 540 // to LatinIME has been requested, about a frame later another switch happens. In this 541 // case, we are about to go down but we still don't know it, however the system tells 542 // us there is no current subtype so the locale is the empty string. Take the best 543 // possible guess instead -- it's bound to have no consequences, and we have no way 544 // of knowing anyway. 545 Log.e(TAG, "System is reporting no current subtype."); 546 subtypeLocale = getResources().getConfiguration().locale; 547 } else { 548 subtypeLocale = switcherSubtypeLocale; 549 } 550 551 final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale, 552 this /* SuggestInitializationListener */); 553 final SettingsValues settingsValues = mSettings.getCurrent(); 554 if (settingsValues.mCorrectionEnabled) { 555 newSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold); 556 } 557 558 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 559 ResearchLogger.getInstance().initSuggest(newSuggest); 560 } 561 562 mUserDictionary = new UserBinaryDictionary(this, subtypeLocale); 563 newSuggest.setUserDictionary(mUserDictionary); 564 newSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */, 565 mSettings.getCurrent()); 566 final Suggest oldSuggest = mInputLogic.mSuggest; 567 mInputLogic.mSuggest = newSuggest; 568 if (oldSuggest != null) oldSuggest.close(); 569 } 570 571 /* package private */ void resetSuggestMainDict() { 572 final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 573 mInputLogic.mSuggest.resetMainDict(this, subtypeLocale, 574 this /* SuggestInitializationListener */); 575 } 576 577 @Override 578 public void onDestroy() { 579 final Suggest suggest = mInputLogic.mSuggest; 580 if (suggest != null) { 581 suggest.close(); 582 mInputLogic.mSuggest = null; 583 } 584 if (mInputUpdater != null) { 585 mInputUpdater.quitLooper(); 586 } 587 mSettings.onDestroy(); 588 unregisterReceiver(mReceiver); 589 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 590 ResearchLogger.getInstance().onDestroy(); 591 } 592 unregisterReceiver(mDictionaryPackInstallReceiver); 593 PersonalizationDictionarySessionRegister.onDestroy(this); 594 LatinImeLogger.commit(); 595 LatinImeLogger.onDestroy(); 596 super.onDestroy(); 597 } 598 599 @Override 600 public void onConfigurationChanged(final Configuration conf) { 601 // If orientation changed while predicting, commit the change 602 final SettingsValues settingsValues = mSettings.getCurrent(); 603 if (settingsValues.mDisplayOrientation != conf.orientation) { 604 mHandler.startOrientationChanging(); 605 mInputLogic.mConnection.beginBatchEdit(); 606 mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR); 607 mInputLogic.mConnection.finishComposingText(); 608 mInputLogic.mConnection.endBatchEdit(); 609 if (isShowingOptionDialog()) { 610 mOptionsDialog.dismiss(); 611 } 612 } 613 PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf); 614 super.onConfigurationChanged(conf); 615 } 616 617 @Override 618 public View onCreateInputView() { 619 return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); 620 } 621 622 @Override 623 public void setInputView(final View view) { 624 super.setInputView(view); 625 mExtractArea = getWindow().getWindow().getDecorView() 626 .findViewById(android.R.id.extractArea); 627 mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); 628 mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); 629 if (mSuggestionStripView != null) { 630 mSuggestionStripView.setListener(this, view); 631 } 632 if (LatinImeLogger.sVISUALDEBUG) { 633 mKeyPreviewBackingView.setBackgroundColor(0x10FF0000); 634 } 635 } 636 637 @Override 638 public void setCandidatesView(final View view) { 639 // To ensure that CandidatesView will never be set. 640 return; 641 } 642 643 @Override 644 public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { 645 mHandler.onStartInput(editorInfo, restarting); 646 } 647 648 @Override 649 public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { 650 mHandler.onStartInputView(editorInfo, restarting); 651 } 652 653 @Override 654 public void onFinishInputView(final boolean finishingInput) { 655 mHandler.onFinishInputView(finishingInput); 656 } 657 658 @Override 659 public void onFinishInput() { 660 mHandler.onFinishInput(); 661 } 662 663 @Override 664 public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { 665 // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() 666 // is not guaranteed. It may even be called at the same time on a different thread. 667 mSubtypeSwitcher.onSubtypeChanged(subtype); 668 loadKeyboard(); 669 } 670 671 private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { 672 super.onStartInput(editorInfo, restarting); 673 } 674 675 @SuppressWarnings("deprecation") 676 private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) { 677 super.onStartInputView(editorInfo, restarting); 678 mRichImm.clearSubtypeCaches(); 679 final KeyboardSwitcher switcher = mKeyboardSwitcher; 680 switcher.updateKeyboardTheme(); 681 final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView(); 682 // If we are starting input in a different text field from before, we'll have to reload 683 // settings, so currentSettingsValues can't be final. 684 SettingsValues currentSettingsValues = mSettings.getCurrent(); 685 686 if (editorInfo == null) { 687 Log.e(TAG, "Null EditorInfo in onStartInputView()"); 688 if (LatinImeLogger.sDBG) { 689 throw new NullPointerException("Null EditorInfo in onStartInputView()"); 690 } 691 return; 692 } 693 if (DEBUG) { 694 Log.d(TAG, "onStartInputView: editorInfo:" 695 + String.format("inputType=0x%08x imeOptions=0x%08x", 696 editorInfo.inputType, editorInfo.imeOptions)); 697 Log.d(TAG, "All caps = " 698 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) 699 + ", sentence caps = " 700 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) 701 + ", word caps = " 702 + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); 703 } 704 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 705 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 706 ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs); 707 } 708 if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) { 709 Log.w(TAG, "Deprecated private IME option specified: " 710 + editorInfo.privateImeOptions); 711 Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead"); 712 } 713 if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) { 714 Log.w(TAG, "Deprecated private IME option specified: " 715 + editorInfo.privateImeOptions); 716 Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead"); 717 } 718 719 LatinImeLogger.onStartInputView(editorInfo); 720 // In landscape mode, this method gets called without the input view being created. 721 if (mainKeyboardView == null) { 722 return; 723 } 724 725 // Forward this event to the accessibility utilities, if enabled. 726 final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); 727 if (accessUtils.isTouchExplorationEnabled()) { 728 accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting); 729 } 730 731 final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); 732 final boolean isDifferentTextField = !restarting || inputTypeChanged; 733 if (isDifferentTextField) { 734 mSubtypeSwitcher.updateParametersOnStartInputView(); 735 } 736 737 // The EditorInfo might have a flag that affects fullscreen mode. 738 // Note: This call should be done by InputMethodService? 739 updateFullscreenMode(); 740 mApplicationSpecifiedCompletions = null; 741 742 // The app calling setText() has the effect of clearing the composing 743 // span, so we should reset our state unconditionally, even if restarting is true. 744 mInputLogic.mEnteredText = null; 745 mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); 746 mInputLogic.mDeleteCount = 0; 747 mInputLogic.mSpaceState = SpaceState.NONE; 748 mInputLogic.mRecapitalizeStatus.deactivate(); 749 mInputLogic.mCurrentlyPressedHardwareKeys.clear(); 750 751 // Note: the following does a round-trip IPC on the main thread: be careful 752 final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); 753 final Suggest suggest = mInputLogic.mSuggest; 754 if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) { 755 initSuggest(); 756 } 757 if (mSuggestionStripView != null) { 758 // This will set the punctuation suggestions if next word suggestion is off; 759 // otherwise it will clear the suggestion strip. 760 setPunctuationSuggestions(); 761 } 762 mInputLogic.mSuggestedWords = SuggestedWords.EMPTY; 763 764 // Sometimes, while rotating, for some reason the framework tells the app we are not 765 // connected to it and that means we can't refresh the cache. In this case, schedule a 766 // refresh later. 767 final boolean canReachInputConnection; 768 if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( 769 editorInfo.initialSelStart, editorInfo.initialSelEnd, 770 false /* shouldFinishComposition */)) { 771 // We try resetting the caches up to 5 times before giving up. 772 mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); 773 // mLastSelection{Start,End} are reset later in this method, don't need to do it here 774 canReachInputConnection = false; 775 } else { 776 if (isDifferentTextField) { 777 mHandler.postResumeSuggestions(); 778 } 779 canReachInputConnection = true; 780 } 781 782 if (isDifferentTextField || 783 !currentSettingsValues.hasSameOrientation(getResources().getConfiguration())) { 784 loadSettings(); 785 } 786 if (isDifferentTextField) { 787 mainKeyboardView.closing(); 788 currentSettingsValues = mSettings.getCurrent(); 789 790 if (suggest != null && currentSettingsValues.mCorrectionEnabled) { 791 suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold); 792 } 793 794 switcher.loadKeyboard(editorInfo, currentSettingsValues); 795 if (!canReachInputConnection) { 796 // If we can't reach the input connection, we will call loadKeyboard again later, 797 // so we need to save its state now. The call will be done in #retryResetCaches. 798 switcher.saveKeyboardState(); 799 } 800 } else if (restarting) { 801 // TODO: Come up with a more comprehensive way to reset the keyboard layout when 802 // a keyboard layout set doesn't get reloaded in this method. 803 switcher.resetKeyboardStateToAlphabet(); 804 // In apps like Talk, we come here when the text is sent and the field gets emptied and 805 // we need to re-evaluate the shift state, but not the whole layout which would be 806 // disruptive. 807 // Space state must be updated before calling updateShiftState 808 switcher.updateShiftState(); 809 } 810 setSuggestionStripShownInternal( 811 isSuggestionsStripVisible(), /* needsInputViewShown */ false); 812 813 mInputLogic.mLastSelectionStart = editorInfo.initialSelStart; 814 mInputLogic.mLastSelectionEnd = editorInfo.initialSelEnd; 815 // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying 816 // so we try using some heuristics to find out about these and fix them. 817 mInputLogic.tryFixLyingCursorPosition(); 818 819 mHandler.cancelUpdateSuggestionStrip(); 820 mHandler.cancelDoubleSpacePeriodTimer(); 821 822 mainKeyboardView.setMainDictionaryAvailability(null != suggest 823 ? suggest.hasMainDictionary() : false); 824 mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn, 825 currentSettingsValues.mKeyPreviewPopupDismissDelay); 826 mainKeyboardView.setSlidingKeyInputPreviewEnabled( 827 currentSettingsValues.mSlidingKeyInputPreviewEnabled); 828 mainKeyboardView.setGestureHandlingEnabledByUser( 829 currentSettingsValues.mGestureInputEnabled, 830 currentSettingsValues.mGestureTrailEnabled, 831 currentSettingsValues.mGestureFloatingPreviewTextEnabled); 832 833 if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); 834 } 835 836 @Override 837 public void onWindowHidden() { 838 super.onWindowHidden(); 839 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 840 if (mainKeyboardView != null) { 841 mainKeyboardView.closing(); 842 } 843 } 844 845 private void onFinishInputInternal() { 846 super.onFinishInput(); 847 848 LatinImeLogger.commit(); 849 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 850 if (mainKeyboardView != null) { 851 mainKeyboardView.closing(); 852 } 853 } 854 855 private void onFinishInputViewInternal(final boolean finishingInput) { 856 super.onFinishInputView(finishingInput); 857 mKeyboardSwitcher.onFinishInputView(); 858 mKeyboardSwitcher.deallocateMemory(); 859 // Remove pending messages related to update suggestions 860 mHandler.cancelUpdateSuggestionStrip(); 861 // Should do the following in onFinishInputInternal but until JB MR2 it's not called :( 862 if (mInputLogic.mWordComposer.isComposingWord()) { 863 mInputLogic.mConnection.finishComposingText(); 864 } 865 mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); 866 // Notify ResearchLogger 867 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 868 ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, 869 mInputLogic.mLastSelectionStart, 870 mInputLogic.mLastSelectionEnd, getCurrentInputConnection()); 871 } 872 } 873 874 @Override 875 public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, 876 final int newSelStart, final int newSelEnd, 877 final int composingSpanStart, final int composingSpanEnd) { 878 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 879 composingSpanStart, composingSpanEnd); 880 if (DEBUG) { 881 Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart 882 + ", ose=" + oldSelEnd 883 + ", lss=" + mInputLogic.mLastSelectionStart 884 + ", lse=" + mInputLogic.mLastSelectionEnd 885 + ", nss=" + newSelStart 886 + ", nse=" + newSelEnd 887 + ", cs=" + composingSpanStart 888 + ", ce=" + composingSpanEnd); 889 } 890 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 891 ResearchLogger.latinIME_onUpdateSelection(mInputLogic.mLastSelectionStart, 892 mInputLogic.mLastSelectionEnd, 893 oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, 894 composingSpanEnd, mInputLogic.mConnection); 895 } 896 897 final boolean selectionChanged = mInputLogic.mLastSelectionStart != newSelStart 898 || mInputLogic.mLastSelectionEnd != newSelEnd; 899 900 // if composingSpanStart and composingSpanEnd are -1, it means there is no composing 901 // span in the view - we can use that to narrow down whether the cursor was moved 902 // by us or not. If we are composing a word but there is no composing span, then 903 // we know for sure the cursor moved while we were composing and we should reset 904 // the state. TODO: rescind this policy: the framework never removes the composing 905 // span on its own accord while editing. This test is useless. 906 final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; 907 908 // If the keyboard is not visible, we don't need to do all the housekeeping work, as it 909 // will be reset when the keyboard shows up anyway. 910 // TODO: revisit this when LatinIME supports hardware keyboards. 911 // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown(). 912 // TODO: find a better way to simulate actual execution. 913 if (isInputViewShown() && !mInputLogic.mConnection.isBelatedExpectedUpdate(oldSelStart, 914 newSelStart, oldSelEnd, newSelEnd)) { 915 // TODO: the following is probably better done in resetEntireInputState(). 916 // it should only happen when the cursor moved, and the very purpose of the 917 // test below is to narrow down whether this happened or not. Likewise with 918 // the call to updateShiftState. 919 // We set this to NONE because after a cursor move, we don't want the space 920 // state-related special processing to kick in. 921 mInputLogic.mSpaceState = SpaceState.NONE; 922 923 // TODO: is it still necessary to test for composingSpan related stuff? 924 final boolean selectionChangedOrSafeToReset = selectionChanged 925 || (!mInputLogic.mWordComposer.isComposingWord()) || noComposingSpan; 926 final boolean hasOrHadSelection = (oldSelStart != oldSelEnd 927 || newSelStart != newSelEnd); 928 final int moveAmount = newSelStart - oldSelStart; 929 if (selectionChangedOrSafeToReset && (hasOrHadSelection 930 || !mInputLogic.mWordComposer.moveCursorByAndReturnIfInsideComposingWord( 931 moveAmount))) { 932 // If we are composing a word and moving the cursor, we would want to set a 933 // suggestion span for recorrection to work correctly. Unfortunately, that 934 // would involve the keyboard committing some new text, which would move the 935 // cursor back to where it was. Latin IME could then fix the position of the cursor 936 // again, but the asynchronous nature of the calls results in this wreaking havoc 937 // with selection on double tap and the like. 938 // Another option would be to send suggestions each time we set the composing 939 // text, but that is probably too expensive to do, so we decided to leave things 940 // as is. 941 mInputLogic.resetEntireInputState(mSettings.getCurrent(), newSelStart, newSelEnd); 942 } else { 943 // resetEntireInputState calls resetCachesUponCursorMove, but forcing the 944 // composition to end. But in all cases where we don't reset the entire input 945 // state, we still want to tell the rich input connection about the new cursor 946 // position so that it can update its caches. 947 mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( 948 newSelStart, newSelEnd, false /* shouldFinishComposition */); 949 } 950 951 // We moved the cursor. If we are touching a word, we need to resume suggestion, 952 // unless suggestions are off. 953 if (isSuggestionsStripVisible()) { 954 mHandler.postResumeSuggestions(); 955 } 956 // Reset the last recapitalization. 957 mInputLogic.mRecapitalizeStatus.deactivate(); 958 mKeyboardSwitcher.updateShiftState(); 959 } 960 961 // Make a note of the cursor position 962 mInputLogic.mLastSelectionStart = newSelStart; 963 mInputLogic.mLastSelectionEnd = newSelEnd; 964 mSubtypeState.currentSubtypeUsed(); 965 } 966 967 /** 968 * This is called when the user has clicked on the extracted text view, 969 * when running in fullscreen mode. The default implementation hides 970 * the suggestions view when this happens, but only if the extracted text 971 * editor has a vertical scroll bar because its text doesn't fit. 972 * Here we override the behavior due to the possibility that a re-correction could 973 * cause the suggestions strip to disappear and re-appear. 974 */ 975 @Override 976 public void onExtractedTextClicked() { 977 if (mSettings.getCurrent().isSuggestionsRequested()) { 978 return; 979 } 980 981 super.onExtractedTextClicked(); 982 } 983 984 /** 985 * This is called when the user has performed a cursor movement in the 986 * extracted text view, when it is running in fullscreen mode. The default 987 * implementation hides the suggestions view when a vertical movement 988 * happens, but only if the extracted text editor has a vertical scroll bar 989 * because its text doesn't fit. 990 * Here we override the behavior due to the possibility that a re-correction could 991 * cause the suggestions strip to disappear and re-appear. 992 */ 993 @Override 994 public void onExtractedCursorMovement(final int dx, final int dy) { 995 if (mSettings.getCurrent().isSuggestionsRequested()) { 996 return; 997 } 998 999 super.onExtractedCursorMovement(dx, dy); 1000 } 1001 1002 @Override 1003 public void hideWindow() { 1004 LatinImeLogger.commit(); 1005 mKeyboardSwitcher.onHideWindow(); 1006 1007 if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { 1008 AccessibleKeyboardViewProxy.getInstance().onHideWindow(); 1009 } 1010 1011 if (TRACE) Debug.stopMethodTracing(); 1012 if (mOptionsDialog != null && mOptionsDialog.isShowing()) { 1013 mOptionsDialog.dismiss(); 1014 mOptionsDialog = null; 1015 } 1016 super.hideWindow(); 1017 } 1018 1019 @Override 1020 public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) { 1021 if (DEBUG) { 1022 Log.i(TAG, "Received completions:"); 1023 if (applicationSpecifiedCompletions != null) { 1024 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { 1025 Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); 1026 } 1027 } 1028 } 1029 if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return; 1030 if (applicationSpecifiedCompletions == null) { 1031 clearSuggestionStrip(); 1032 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1033 ResearchLogger.latinIME_onDisplayCompletions(null); 1034 } 1035 return; 1036 } 1037 mApplicationSpecifiedCompletions = 1038 CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions); 1039 1040 final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = 1041 SuggestedWords.getFromApplicationSpecifiedCompletions( 1042 applicationSpecifiedCompletions); 1043 final SuggestedWords suggestedWords = new SuggestedWords( 1044 applicationSuggestedWords, 1045 false /* typedWordValid */, 1046 false /* hasAutoCorrectionCandidate */, 1047 false /* isPunctuationSuggestions */, 1048 false /* isObsoleteSuggestions */, 1049 false /* isPrediction */); 1050 // When in fullscreen mode, show completions generated by the application 1051 final boolean isAutoCorrection = false; 1052 setSuggestedWords(suggestedWords, isAutoCorrection); 1053 setAutoCorrectionIndicator(isAutoCorrection); 1054 setSuggestionStripShown(true); 1055 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1056 ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); 1057 } 1058 } 1059 1060 private void setSuggestionStripShownInternal(final boolean shown, 1061 final boolean needsInputViewShown) { 1062 // TODO: Modify this if we support suggestions with hard keyboard 1063 if (onEvaluateInputViewShown() && mSuggestionStripView != null) { 1064 final boolean inputViewShown = mKeyboardSwitcher.isShowingMainKeyboardOrEmojiPalettes(); 1065 final boolean shouldShowSuggestions = shown 1066 && (needsInputViewShown ? inputViewShown : true); 1067 if (isFullscreenMode()) { 1068 mSuggestionStripView.setVisibility( 1069 shouldShowSuggestions ? View.VISIBLE : View.GONE); 1070 } else { 1071 mSuggestionStripView.setVisibility( 1072 shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); 1073 } 1074 } 1075 } 1076 1077 private void setSuggestionStripShown(final boolean shown) { 1078 setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); 1079 } 1080 1081 private int getAdjustedBackingViewHeight() { 1082 final int currentHeight = mKeyPreviewBackingView.getHeight(); 1083 if (currentHeight > 0) { 1084 return currentHeight; 1085 } 1086 1087 final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); 1088 if (visibleKeyboardView == null) { 1089 return 0; 1090 } 1091 // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main !!! 1092 // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 1093 final int keyboardHeight = visibleKeyboardView.getHeight(); 1094 final int suggestionsHeight = mSuggestionStripView.getHeight(); 1095 final int displayHeight = getResources().getDisplayMetrics().heightPixels; 1096 final Rect rect = new Rect(); 1097 mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect); 1098 final int notificationBarHeight = rect.top; 1099 final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight 1100 - keyboardHeight; 1101 1102 final LayoutParams params = mKeyPreviewBackingView.getLayoutParams(); 1103 params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); 1104 mKeyPreviewBackingView.setLayoutParams(params); 1105 return params.height; 1106 } 1107 1108 @Override 1109 public void onComputeInsets(final InputMethodService.Insets outInsets) { 1110 super.onComputeInsets(outInsets); 1111 final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); 1112 if (visibleKeyboardView == null || mSuggestionStripView == null) { 1113 return; 1114 } 1115 final int adjustedBackingHeight = getAdjustedBackingViewHeight(); 1116 final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE); 1117 final int backingHeight = backingGone ? 0 : adjustedBackingHeight; 1118 // In fullscreen mode, the height of the extract area managed by InputMethodService should 1119 // be considered. 1120 // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. 1121 final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; 1122 final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0 1123 : mSuggestionStripView.getHeight(); 1124 final int extraHeight = extractHeight + backingHeight + suggestionsHeight; 1125 int visibleTopY = extraHeight; 1126 // Need to set touchable region only if input view is being shown 1127 if (visibleKeyboardView.isShown()) { 1128 // Note that the height of Emoji layout is the same as the height of the main keyboard 1129 // and the suggestion strip 1130 if (mKeyboardSwitcher.isShowingEmojiPalettes() 1131 || mSuggestionStripView.getVisibility() == View.VISIBLE) { 1132 visibleTopY -= suggestionsHeight; 1133 } 1134 final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; 1135 final int touchWidth = visibleKeyboardView.getWidth(); 1136 final int touchHeight = visibleKeyboardView.getHeight() + extraHeight 1137 // Extend touchable region below the keyboard. 1138 + EXTENDED_TOUCHABLE_REGION_HEIGHT; 1139 outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; 1140 outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight); 1141 } 1142 outInsets.contentTopInsets = visibleTopY; 1143 outInsets.visibleTopInsets = visibleTopY; 1144 } 1145 1146 @Override 1147 public boolean onEvaluateFullscreenMode() { 1148 // Reread resource value here, because this method is called by framework anytime as needed. 1149 final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources()); 1150 if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) { 1151 // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI 1152 // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI 1153 // without NO_FULLSCREEN doesn't work as expected. Because of this we need this 1154 // hack for now. Let's get rid of this once the framework gets fixed. 1155 final EditorInfo ei = getCurrentInputEditorInfo(); 1156 return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0)); 1157 } else { 1158 return false; 1159 } 1160 } 1161 1162 @Override 1163 public void updateFullscreenMode() { 1164 super.updateFullscreenMode(); 1165 1166 if (mKeyPreviewBackingView == null) return; 1167 // In fullscreen mode, no need to have extra space to show the key preview. 1168 // If not, we should have extra space above the keyboard to show the key preview. 1169 mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); 1170 } 1171 1172 // Called from the KeyboardSwitcher which needs to know auto caps state to display 1173 // the right layout. 1174 // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead? 1175 public int getCurrentAutoCapsState() { 1176 return mInputLogic.getCurrentAutoCapsState(null /* optionalSettingsValues */); 1177 } 1178 1179 // Called from the KeyboardSwitcher which needs to know recaps state to display 1180 // the right layout. 1181 // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead? 1182 public int getCurrentRecapitalizeState() { 1183 return mInputLogic.getCurrentRecapitalizeState(); 1184 } 1185 1186 public Locale getCurrentSubtypeLocale() { 1187 return mSubtypeSwitcher.getCurrentSubtypeLocale(); 1188 } 1189 1190 // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is 1191 // pressed. 1192 @Override 1193 public void addWordToUserDictionary(final String word) { 1194 if (TextUtils.isEmpty(word)) { 1195 // Probably never supposed to happen, but just in case. 1196 return; 1197 } 1198 final String wordToEdit; 1199 if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) { 1200 wordToEdit = word.toLowerCase(getCurrentSubtypeLocale()); 1201 } else { 1202 wordToEdit = word; 1203 } 1204 mUserDictionary.addWordToUserDictionary(wordToEdit); 1205 } 1206 1207 public void displaySettingsDialog() { 1208 if (isShowingOptionDialog()) return; 1209 showSubtypeSelectorAndSettings(); 1210 } 1211 1212 @Override 1213 public boolean onCustomRequest(final int requestCode) { 1214 if (isShowingOptionDialog()) return false; 1215 switch (requestCode) { 1216 case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER: 1217 if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { 1218 mRichImm.getInputMethodManager().showInputMethodPicker(); 1219 return true; 1220 } 1221 return false; 1222 } 1223 return false; 1224 } 1225 1226 private boolean isShowingOptionDialog() { 1227 return mOptionsDialog != null && mOptionsDialog.isShowing(); 1228 } 1229 1230 // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. 1231 public void switchToNextSubtype() { 1232 final IBinder token = getWindow().getWindow().getAttributes().token; 1233 if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) { 1234 mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); 1235 return; 1236 } 1237 mSubtypeState.switchSubtype(token, mRichImm); 1238 } 1239 1240 // Implementation of {@link KeyboardActionListener}. 1241 @Override 1242 public void onCodeInput(final int primaryCode, final int x, final int y) { 1243 mInputLogic.onCodeInput(primaryCode, x, y, mHandler, mKeyboardSwitcher, mSubtypeSwitcher); 1244 } 1245 1246 // Called from PointerTracker through the KeyboardActionListener interface 1247 @Override 1248 public void onTextInput(final String rawText) { 1249 mInputLogic.onTextInput(mSettings.getCurrent(), rawText, mKeyboardSwitcher, mHandler); 1250 } 1251 1252 @Override 1253 public void onStartBatchInput() { 1254 mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler, 1255 mInputUpdater); 1256 } 1257 1258 @Override 1259 public void onUpdateBatchInput(final InputPointers batchPointers) { 1260 mInputLogic.onUpdateBatchInput(mSettings.getCurrent(), batchPointers, mKeyboardSwitcher, 1261 mInputUpdater); 1262 } 1263 1264 @Override 1265 public void onEndBatchInput(final InputPointers batchPointers) { 1266 mInputLogic.onEndBatchInput(mSettings.getCurrent(), batchPointers, mInputUpdater); 1267 } 1268 1269 @Override 1270 public void onCancelBatchInput() { 1271 mInputLogic.onCancelBatchInput(mInputUpdater); 1272 } 1273 1274 // TODO[IL]: Make this a package-private standalone class in inputlogic/ and remove all 1275 // references to it in LatinIME 1276 public static final class InputUpdater implements Handler.Callback { 1277 private final Handler mHandler; 1278 private final LatinIME mLatinIme; 1279 private final Object mLock = new Object(); 1280 private boolean mInBatchInput; // synchronized using {@link #mLock}. 1281 1282 InputUpdater(final LatinIME latinIme) { 1283 final HandlerThread handlerThread = new HandlerThread( 1284 InputUpdater.class.getSimpleName()); 1285 handlerThread.start(); 1286 mHandler = new Handler(handlerThread.getLooper(), this); 1287 mLatinIme = latinIme; 1288 } 1289 1290 private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; 1291 private static final int MSG_GET_SUGGESTED_WORDS = 2; 1292 1293 @Override 1294 public boolean handleMessage(final Message msg) { 1295 // TODO: straighten message passing - we don't need two kinds of messages calling 1296 // each other. 1297 switch (msg.what) { 1298 case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: 1299 updateBatchInput((InputPointers)msg.obj, msg.arg2 /* sequenceNumber */); 1300 break; 1301 case MSG_GET_SUGGESTED_WORDS: 1302 mLatinIme.getSuggestedWords(msg.arg1 /* sessionId */, 1303 msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj); 1304 break; 1305 } 1306 return true; 1307 } 1308 1309 // Run in the UI thread. 1310 public void onStartBatchInput() { 1311 synchronized (mLock) { 1312 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 1313 mInBatchInput = true; 1314 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1315 SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); 1316 } 1317 } 1318 1319 // Run in the Handler thread. 1320 private void updateBatchInput(final InputPointers batchPointers, final int sequenceNumber) { 1321 synchronized (mLock) { 1322 if (!mInBatchInput) { 1323 // Batch input has ended or canceled while the message was being delivered. 1324 return; 1325 } 1326 1327 getSuggestedWordsGestureLocked(batchPointers, sequenceNumber, 1328 new OnGetSuggestedWordsCallback() { 1329 @Override 1330 public void onGetSuggestedWords(final SuggestedWords suggestedWords) { 1331 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1332 suggestedWords, false /* dismissGestureFloatingPreviewText */); 1333 } 1334 }); 1335 } 1336 } 1337 1338 // Run in the UI thread. 1339 public void onUpdateBatchInput(final InputPointers batchPointers, 1340 final int sequenceNumber) { 1341 if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) { 1342 return; 1343 } 1344 mHandler.obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 0 /* arg1 */, 1345 sequenceNumber /* arg2 */, batchPointers /* obj */).sendToTarget(); 1346 } 1347 1348 public void onCancelBatchInput() { 1349 synchronized (mLock) { 1350 mInBatchInput = false; 1351 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( 1352 SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); 1353 } 1354 } 1355 1356 // Run in the UI thread. 1357 public void onEndBatchInput(final InputPointers batchPointers) { 1358 synchronized(mLock) { 1359 getSuggestedWordsGestureLocked(batchPointers, SuggestedWords.NOT_A_SEQUENCE_NUMBER, 1360 new OnGetSuggestedWordsCallback() { 1361 @Override 1362 public void onGetSuggestedWords(final SuggestedWords suggestedWords) { 1363 mInBatchInput = false; 1364 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords, 1365 true /* dismissGestureFloatingPreviewText */); 1366 mLatinIme.mHandler.onEndBatchInput(suggestedWords); 1367 } 1368 }); 1369 } 1370 } 1371 1372 // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to 1373 // be synchronized. 1374 private void getSuggestedWordsGestureLocked(final InputPointers batchPointers, 1375 final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { 1376 mLatinIme.mInputLogic.mWordComposer.setBatchInputPointers(batchPointers); 1377 mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE, 1378 sequenceNumber, new OnGetSuggestedWordsCallback() { 1379 @Override 1380 public void onGetSuggestedWords(SuggestedWords suggestedWords) { 1381 final int suggestionCount = suggestedWords.size(); 1382 if (suggestionCount <= 1) { 1383 final String mostProbableSuggestion = (suggestionCount == 0) ? null 1384 : suggestedWords.getWord(0); 1385 callback.onGetSuggestedWords( 1386 mLatinIme.getOlderSuggestions(mostProbableSuggestion)); 1387 } 1388 callback.onGetSuggestedWords(suggestedWords); 1389 } 1390 }); 1391 } 1392 1393 public void getSuggestedWords(final int sessionId, final int sequenceNumber, 1394 final OnGetSuggestedWordsCallback callback) { 1395 mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback) 1396 .sendToTarget(); 1397 } 1398 1399 void quitLooper() { 1400 mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS); 1401 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); 1402 mHandler.getLooper().quit(); 1403 } 1404 } 1405 1406 // This method must run in UI Thread. 1407 private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, 1408 final boolean dismissGestureFloatingPreviewText) { 1409 showSuggestionStrip(suggestedWords); 1410 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1411 mainKeyboardView.showGestureFloatingPreviewText(suggestedWords); 1412 if (dismissGestureFloatingPreviewText) { 1413 mainKeyboardView.dismissGestureFloatingPreviewText(); 1414 } 1415 } 1416 1417 // This method must run in UI Thread. 1418 public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) { 1419 final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); 1420 if (TextUtils.isEmpty(batchInputText)) { 1421 return; 1422 } 1423 mInputLogic.mConnection.beginBatchEdit(); 1424 if (SpaceState.PHANTOM == mInputLogic.mSpaceState) { 1425 mInputLogic.promotePhantomSpace(mSettings.getCurrent()); 1426 } 1427 if (mSettings.getCurrent().mPhraseGestureEnabled) { 1428 // Find the last space 1429 final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; 1430 if (0 != indexOfLastSpace) { 1431 mInputLogic.mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1432 1); 1433 showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture()); 1434 } 1435 final String lastWord = batchInputText.substring(indexOfLastSpace); 1436 mInputLogic.mWordComposer.setBatchInputWord(lastWord); 1437 mInputLogic.mConnection.setComposingText(lastWord, 1); 1438 } else { 1439 mInputLogic.mWordComposer.setBatchInputWord(batchInputText); 1440 mInputLogic.mConnection.setComposingText(batchInputText, 1); 1441 } 1442 mInputLogic.mConnection.endBatchEdit(); 1443 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1444 ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); 1445 } 1446 // Space state must be updated before calling updateShiftState 1447 mInputLogic.mSpaceState = SpaceState.PHANTOM; 1448 mKeyboardSwitcher.updateShiftState(); 1449 } 1450 1451 // Called from PointerTracker through the KeyboardActionListener interface 1452 @Override 1453 public void onFinishSlidingInput() { 1454 // User finished sliding input. 1455 mKeyboardSwitcher.onFinishSlidingInput(); 1456 } 1457 1458 // Called from PointerTracker through the KeyboardActionListener interface 1459 @Override 1460 public void onCancelInput() { 1461 // User released a finger outside any key 1462 // Nothing to do so far. 1463 } 1464 1465 // TODO[IL]: Move this to InputLogic and make it private 1466 // Outside LatinIME, only used by the test suite. 1467 @UsedForTesting 1468 public boolean isShowingPunctuationList() { 1469 if (mInputLogic.mSuggestedWords == null) return false; 1470 return mSettings.getCurrent().mSuggestPuncList == mInputLogic.mSuggestedWords; 1471 } 1472 1473 // TODO[IL]: Define a clear interface for this 1474 public boolean isSuggestionsStripVisible() { 1475 final SettingsValues currentSettings = mSettings.getCurrent(); 1476 if (mSuggestionStripView == null) 1477 return false; 1478 if (mSuggestionStripView.isShowingAddToDictionaryHint()) 1479 return true; 1480 if (null == currentSettings) 1481 return false; 1482 if (!currentSettings.isSuggestionStripVisible()) 1483 return false; 1484 if (currentSettings.isApplicationSpecifiedCompletionsOn()) 1485 return true; 1486 return currentSettings.isSuggestionsRequested(); 1487 } 1488 1489 public void dismissAddToDictionaryHint() { 1490 if (null != mSuggestionStripView) { 1491 mSuggestionStripView.dismissAddToDictionaryHint(); 1492 } 1493 } 1494 1495 // TODO[IL]: Define a clear interface for this 1496 public void clearSuggestionStrip() { 1497 setSuggestedWords(SuggestedWords.EMPTY, false); 1498 setAutoCorrectionIndicator(false); 1499 } 1500 1501 // TODO[IL]: Define a clear interface for this 1502 public void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { 1503 mInputLogic.mSuggestedWords = words; 1504 if (mSuggestionStripView != null) { 1505 mSuggestionStripView.setSuggestions(words); 1506 mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); 1507 } 1508 } 1509 1510 private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { 1511 // Put a blue underline to a word in TextView which will be auto-corrected. 1512 if (mInputLogic.mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator 1513 && mInputLogic.mWordComposer.isComposingWord()) { 1514 mInputLogic.mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; 1515 final CharSequence textWithUnderline = 1516 mInputLogic.getTextWithUnderline(mInputLogic.mWordComposer.getTypedWord()); 1517 // TODO: when called from an updateSuggestionStrip() call that results from a posted 1518 // message, this is called outside any batch edit. Potentially, this may result in some 1519 // janky flickering of the screen, although the display speed makes it unlikely in 1520 // the practice. 1521 mInputLogic.mConnection.setComposingText(textWithUnderline, 1); 1522 } 1523 } 1524 1525 private void getSuggestedWords(final int sessionId, final int sequenceNumber, 1526 final OnGetSuggestedWordsCallback callback) { 1527 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1528 final Suggest suggest = mInputLogic.mSuggest; 1529 if (keyboard == null || suggest == null) { 1530 callback.onGetSuggestedWords(SuggestedWords.EMPTY); 1531 return; 1532 } 1533 // Get the word on which we should search the bigrams. If we are composing a word, it's 1534 // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we 1535 // should just skip whitespace if any, so 1. 1536 final SettingsValues currentSettings = mSettings.getCurrent(); 1537 final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues; 1538 1539 if (DEBUG) { 1540 if (mInputLogic.mWordComposer.isComposingWord() 1541 || mInputLogic.mWordComposer.isBatchMode()) { 1542 final String previousWord 1543 = mInputLogic.mWordComposer.getPreviousWordForSuggestion(); 1544 // TODO: this is for checking consistency with older versions. Remove this when 1545 // we are confident this is stable. 1546 // We're checking the previous word in the text field against the memorized previous 1547 // word. If we are composing a word we should have the second word before the cursor 1548 // memorized, otherwise we should have the first. 1549 final String rereadPrevWord = mInputLogic.getNthPreviousWordForSuggestion( 1550 currentSettings, mInputLogic.mWordComposer.isComposingWord() ? 2 : 1); 1551 if (!TextUtils.equals(previousWord, rereadPrevWord)) { 1552 throw new RuntimeException("Unexpected previous word: " 1553 + previousWord + " <> " + rereadPrevWord); 1554 } 1555 } 1556 } 1557 suggest.getSuggestedWords(mInputLogic.mWordComposer, 1558 mInputLogic.mWordComposer.getPreviousWordForSuggestion(), 1559 keyboard.getProximityInfo(), 1560 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled, 1561 additionalFeaturesOptions, sessionId, sequenceNumber, callback); 1562 } 1563 1564 // TODO[IL]: Move this to InputLogic? 1565 public void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId, 1566 final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { 1567 mInputUpdater.getSuggestedWords(sessionId, sequenceNumber, 1568 new OnGetSuggestedWordsCallback() { 1569 @Override 1570 public void onGetSuggestedWords(SuggestedWords suggestedWords) { 1571 callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions( 1572 mInputLogic.mWordComposer.getTypedWord(), suggestedWords)); 1573 } 1574 }); 1575 } 1576 1577 private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord, 1578 final SuggestedWords suggestedWords) { 1579 // TODO: consolidate this into getSuggestedWords 1580 // We update the suggestion strip only when we have some suggestions to show, i.e. when 1581 // the suggestion count is > 1; else, we leave the old suggestions, with the typed word 1582 // replaced with the new one. However, when the word is a dictionary word, or when the 1583 // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the 1584 // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to 1585 // revert to suggestions - although it is unclear how we can come here if it's displayed. 1586 if (suggestedWords.size() > 1 || typedWord.length() <= 1 1587 || suggestedWords.mTypedWordValid || null == mSuggestionStripView 1588 || mSuggestionStripView.isShowingAddToDictionaryHint()) { 1589 return suggestedWords; 1590 } else { 1591 return getOlderSuggestions(typedWord); 1592 } 1593 } 1594 1595 private SuggestedWords getOlderSuggestions(final String typedWord) { 1596 SuggestedWords previousSuggestedWords = mInputLogic.mSuggestedWords; 1597 if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) { 1598 previousSuggestedWords = SuggestedWords.EMPTY; 1599 } 1600 if (typedWord == null) { 1601 return previousSuggestedWords; 1602 } 1603 final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = 1604 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, 1605 previousSuggestedWords); 1606 return new SuggestedWords(typedWordAndPreviousSuggestions, 1607 false /* typedWordValid */, 1608 false /* hasAutoCorrectionCandidate */, 1609 false /* isPunctuationSuggestions */, 1610 true /* isObsoleteSuggestions */, 1611 false /* isPrediction */); 1612 } 1613 1614 private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { 1615 if (suggestedWords.isEmpty()) return; 1616 final String autoCorrection; 1617 if (suggestedWords.mWillAutoCorrect) { 1618 autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); 1619 } else { 1620 // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD) 1621 // because it may differ from mWordComposer.mTypedWord. 1622 autoCorrection = typedWord; 1623 } 1624 mInputLogic.mWordComposer.setAutoCorrection(autoCorrection); 1625 } 1626 1627 private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, 1628 final String typedWord) { 1629 if (suggestedWords.isEmpty()) { 1630 // No auto-correction is available, clear the cached values. 1631 AccessibilityUtils.getInstance().setAutoCorrection(null, null); 1632 clearSuggestionStrip(); 1633 return; 1634 } 1635 setAutoCorrection(suggestedWords, typedWord); 1636 final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); 1637 setSuggestedWords(suggestedWords, isAutoCorrection); 1638 setAutoCorrectionIndicator(isAutoCorrection); 1639 setSuggestionStripShown(isSuggestionsStripVisible()); 1640 // An auto-correction is available, cache it in accessibility code so 1641 // we can be speak it if the user touches a key that will insert it. 1642 AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord); 1643 } 1644 1645 // TODO[IL]: Define a clean interface for this 1646 public void showSuggestionStrip(final SuggestedWords suggestedWords) { 1647 if (suggestedWords.isEmpty()) { 1648 clearSuggestionStrip(); 1649 return; 1650 } 1651 showSuggestionStripWithTypedWord(suggestedWords, 1652 suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)); 1653 } 1654 1655 // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} 1656 // interface 1657 @Override 1658 public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { 1659 final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords; 1660 final String suggestion = suggestionInfo.mWord; 1661 // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput 1662 if (suggestion.length() == 1 && isShowingPunctuationList()) { 1663 // Word separators are suggested before the user inputs something. 1664 // So, LatinImeLogger logs "" as a user's input. 1665 LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); 1666 // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. 1667 final int primaryCode = suggestion.charAt(0); 1668 onCodeInput(primaryCode, 1669 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); 1670 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1671 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, 1672 false /* isBatchMode */, suggestedWords.mIsPrediction); 1673 } 1674 return; 1675 } 1676 1677 mInputLogic.mConnection.beginBatchEdit(); 1678 final SettingsValues currentSettings = mSettings.getCurrent(); 1679 if (SpaceState.PHANTOM == mInputLogic.mSpaceState && suggestion.length() > 0 1680 // In the batch input mode, a manually picked suggested word should just replace 1681 // the current batch input text and there is no need for a phantom space. 1682 && !mInputLogic.mWordComposer.isBatchMode()) { 1683 final int firstChar = Character.codePointAt(suggestion, 0); 1684 if (!currentSettings.isWordSeparator(firstChar) 1685 || currentSettings.isUsuallyPrecededBySpace(firstChar)) { 1686 mInputLogic.promotePhantomSpace(currentSettings); 1687 } 1688 } 1689 1690 if (currentSettings.isApplicationSpecifiedCompletionsOn() 1691 && mApplicationSpecifiedCompletions != null 1692 && index >= 0 && index < mApplicationSpecifiedCompletions.length) { 1693 mInputLogic.mSuggestedWords = SuggestedWords.EMPTY; 1694 if (mSuggestionStripView != null) { 1695 mSuggestionStripView.clear(); 1696 } 1697 mKeyboardSwitcher.updateShiftState(); 1698 mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); 1699 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; 1700 mInputLogic.mConnection.commitCompletion(completionInfo); 1701 mInputLogic.mConnection.endBatchEdit(); 1702 return; 1703 } 1704 1705 // We need to log before we commit, because the word composer will store away the user 1706 // typed word. 1707 final String replacedWord = mInputLogic.mWordComposer.getTypedWord(); 1708 LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); 1709 mInputLogic.commitChosenWord(currentSettings, suggestion, 1710 LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); 1711 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1712 ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, 1713 mInputLogic.mWordComposer.isBatchMode(), suggestionInfo.mScore, 1714 suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType); 1715 } 1716 mInputLogic.mConnection.endBatchEdit(); 1717 // Don't allow cancellation of manual pick 1718 mInputLogic.mLastComposedWord.deactivate(); 1719 // Space state must be updated before calling updateShiftState 1720 mInputLogic.mSpaceState = SpaceState.PHANTOM; 1721 mKeyboardSwitcher.updateShiftState(); 1722 1723 // We should show the "Touch again to save" hint if the user pressed the first entry 1724 // AND it's in none of our current dictionaries (main, user or otherwise). 1725 // Please note that if mSuggest is null, it means that everything is off: suggestion 1726 // and correction, so we shouldn't try to show the hint 1727 final Suggest suggest = mInputLogic.mSuggest; 1728 final boolean showingAddToDictionaryHint = 1729 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind 1730 || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) 1731 && suggest != null 1732 // If the suggestion is not in the dictionary, the hint should be shown. 1733 && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true); 1734 1735 if (currentSettings.mIsInternal) { 1736 LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE, 1737 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 1738 } 1739 if (showingAddToDictionaryHint && mUserDictionary.mEnabled) { 1740 mSuggestionStripView.showAddToDictionaryHint( 1741 suggestion, currentSettings.mHintToSaveText); 1742 } else { 1743 // If we're not showing the "Touch again to save", then update the suggestion strip. 1744 mHandler.postUpdateSuggestionStrip(); 1745 } 1746 } 1747 1748 // TODO[IL]: Define a clean interface for this 1749 public void setPunctuationSuggestions() { 1750 final SettingsValues currentSettings = mSettings.getCurrent(); 1751 if (currentSettings.mBigramPredictionEnabled) { 1752 clearSuggestionStrip(); 1753 } else { 1754 setSuggestedWords(currentSettings.mSuggestPuncList, false); 1755 } 1756 setAutoCorrectionIndicator(false); 1757 setSuggestionStripShown(isSuggestionsStripVisible()); 1758 } 1759 1760 public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip( 1761 final SuggestedWords suggestedWords, final String typedWord) { 1762 // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. 1763 // We never want to auto-correct on a resumed suggestion. Please refer to the three places 1764 // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected. 1765 // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching 1766 // the text to adapt it. 1767 // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition) 1768 mInputLogic.mIsAutoCorrectionIndicatorOn = false; 1769 mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord); 1770 } 1771 1772 // TODO: Make this private 1773 // Outside LatinIME, only used by the {@link InputTestsBase} test suite. 1774 @UsedForTesting 1775 void loadKeyboard() { 1776 // Since we are switching languages, the most urgent thing is to let the keyboard graphics 1777 // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on 1778 // the screen. Anything we do right now will delay this, so wait until the next frame 1779 // before we do the rest, like reopening dictionaries and updating suggestions. So we 1780 // post a message. 1781 mHandler.postReopenDictionaries(); 1782 loadSettings(); 1783 if (mKeyboardSwitcher.getMainKeyboardView() != null) { 1784 // Reload keyboard because the current language has been changed. 1785 mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); 1786 } 1787 } 1788 1789 private void hapticAndAudioFeedback(final int code, final int repeatCount) { 1790 final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1791 if (keyboardView != null && keyboardView.isInDraggingFinger()) { 1792 // No need to feedback while finger is dragging. 1793 return; 1794 } 1795 if (repeatCount > 0) { 1796 if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) { 1797 // No need to feedback when repeat delete key will have no effect. 1798 return; 1799 } 1800 // TODO: Use event time that the last feedback has been generated instead of relying on 1801 // a repeat count to thin out feedback. 1802 if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) { 1803 return; 1804 } 1805 } 1806 final AudioAndHapticFeedbackManager feedbackManager = 1807 AudioAndHapticFeedbackManager.getInstance(); 1808 if (repeatCount == 0) { 1809 // TODO: Reconsider how to perform haptic feedback when repeating key. 1810 feedbackManager.performHapticFeedback(keyboardView); 1811 } 1812 feedbackManager.performAudioFeedback(code); 1813 } 1814 1815 // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed; 1816 // release matching call is {@link #onReleaseKey(int,boolean)} below. 1817 @Override 1818 public void onPressKey(final int primaryCode, final int repeatCount, 1819 final boolean isSinglePointer) { 1820 mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer); 1821 hapticAndAudioFeedback(primaryCode, repeatCount); 1822 } 1823 1824 // Callback of the {@link KeyboardActionListener}. This is called when a key is released; 1825 // press matching call is {@link #onPressKey(int,int,boolean)} above. 1826 @Override 1827 public void onReleaseKey(final int primaryCode, final boolean withSliding) { 1828 mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); 1829 1830 // If accessibility is on, ensure the user receives keyboard state updates. 1831 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 1832 switch (primaryCode) { 1833 case Constants.CODE_SHIFT: 1834 AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); 1835 break; 1836 case Constants.CODE_SWITCH_ALPHA_SYMBOL: 1837 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); 1838 break; 1839 } 1840 } 1841 } 1842 1843 // Hooks for hardware keyboard 1844 @Override 1845 public boolean onKeyDown(final int keyCode, final KeyEvent event) { 1846 if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event); 1847 // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if 1848 // it doesn't know what to do with it and leave it to the application. For example, 1849 // hardware key events for adjusting the screen's brightness are passed as is. 1850 if (mInputLogic.mEventInterpreter.onHardwareKeyEvent(event)) { 1851 final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); 1852 mInputLogic.mCurrentlyPressedHardwareKeys.add(keyIdentifier); 1853 return true; 1854 } 1855 return super.onKeyDown(keyCode, event); 1856 } 1857 1858 @Override 1859 public boolean onKeyUp(final int keyCode, final KeyEvent event) { 1860 final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); 1861 if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { 1862 return true; 1863 } 1864 return super.onKeyUp(keyCode, event); 1865 } 1866 1867 // onKeyDown and onKeyUp are the main events we are interested in. There are two more events 1868 // related to handling of hardware key events that we may want to implement in the future: 1869 // boolean onKeyLongPress(final int keyCode, final KeyEvent event); 1870 // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event); 1871 1872 // receive ringer mode change and network state change. 1873 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 1874 @Override 1875 public void onReceive(final Context context, final Intent intent) { 1876 final String action = intent.getAction(); 1877 if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 1878 mSubtypeSwitcher.onNetworkStateChanged(intent); 1879 } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { 1880 AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged(); 1881 } 1882 } 1883 }; 1884 1885 private void launchSettings() { 1886 mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR); 1887 requestHideSelf(0); 1888 final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); 1889 if (mainKeyboardView != null) { 1890 mainKeyboardView.closing(); 1891 } 1892 launchSubActivity(SettingsActivity.class); 1893 } 1894 1895 public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) { 1896 // Put the text in the attached EditText into a safe, saved state before switching to a 1897 // new activity that will also use the soft keyboard. 1898 mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR); 1899 launchSubActivity(activityClass); 1900 } 1901 1902 private void launchSubActivity(final Class<? extends Activity> activityClass) { 1903 Intent intent = new Intent(); 1904 intent.setClass(LatinIME.this, activityClass); 1905 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 1906 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 1907 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 1908 startActivity(intent); 1909 } 1910 1911 private void showSubtypeSelectorAndSettings() { 1912 final CharSequence title = getString(R.string.english_ime_input_options); 1913 final CharSequence[] items = new CharSequence[] { 1914 // TODO: Should use new string "Select active input modes". 1915 getString(R.string.language_selection_title), 1916 getString(ApplicationUtils.getActivityTitleResId(this, SettingsActivity.class)), 1917 }; 1918 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 1919 @Override 1920 public void onClick(DialogInterface di, int position) { 1921 di.dismiss(); 1922 switch (position) { 1923 case 0: 1924 final Intent intent = IntentUtils.getInputLanguageSelectionIntent( 1925 mRichImm.getInputMethodIdOfThisIme(), 1926 Intent.FLAG_ACTIVITY_NEW_TASK 1927 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 1928 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 1929 startActivity(intent); 1930 break; 1931 case 1: 1932 launchSettings(); 1933 break; 1934 } 1935 } 1936 }; 1937 final AlertDialog.Builder builder = 1938 new AlertDialog.Builder(this).setItems(items, listener).setTitle(title); 1939 showOptionDialog(builder.create()); 1940 } 1941 1942 public void showOptionDialog(final AlertDialog dialog) { 1943 final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); 1944 if (windowToken == null) { 1945 return; 1946 } 1947 1948 dialog.setCancelable(true); 1949 dialog.setCanceledOnTouchOutside(true); 1950 1951 final Window window = dialog.getWindow(); 1952 final WindowManager.LayoutParams lp = window.getAttributes(); 1953 lp.token = windowToken; 1954 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 1955 window.setAttributes(lp); 1956 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 1957 1958 mOptionsDialog = dialog; 1959 dialog.show(); 1960 } 1961 1962 // TODO: can this be removed somehow without breaking the tests? 1963 @UsedForTesting 1964 /* package for test */ SuggestedWords getSuggestedWords() { 1965 // You may not use this method for anything else than debug 1966 return DEBUG ? mInputLogic.mSuggestedWords : null; 1967 } 1968 1969 // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. 1970 @UsedForTesting 1971 /* package for test */ boolean isCurrentlyWaitingForMainDictionary() { 1972 return mInputLogic.mSuggest.isCurrentlyWaitingForMainDictionary(); 1973 } 1974 1975 // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. 1976 @UsedForTesting 1977 /* package for test */ void replaceMainDictionaryForTest(final Locale locale) { 1978 mInputLogic.mSuggest.resetMainDict(this, locale, null); 1979 } 1980 1981 public void debugDumpStateAndCrashWithException(final String context) { 1982 final SettingsValues settingsValues = mSettings.getCurrent(); 1983 final StringBuilder s = new StringBuilder(settingsValues.toString()); 1984 s.append("\nAttributes : ").append(settingsValues.mInputAttributes) 1985 .append("\nContext : ").append(context); 1986 throw new RuntimeException(s.toString()); 1987 } 1988 1989 @Override 1990 protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) { 1991 super.dump(fd, fout, args); 1992 1993 final Printer p = new PrintWriterPrinter(fout); 1994 p.println("LatinIME state :"); 1995 p.println(" VersionCode = " + ApplicationUtils.getVersionCode(this)); 1996 p.println(" VersionName = " + ApplicationUtils.getVersionName(this)); 1997 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); 1998 final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; 1999 p.println(" Keyboard mode = " + keyboardMode); 2000 final SettingsValues settingsValues = mSettings.getCurrent(); 2001 p.println(" mIsSuggestionsRequested = " + settingsValues.isSuggestionsRequested()); 2002 p.println(" mCorrectionEnabled=" + settingsValues.mCorrectionEnabled); 2003 p.println(" isComposingWord=" + mInputLogic.mWordComposer.isComposingWord()); 2004 p.println(" mSoundOn=" + settingsValues.mSoundOn); 2005 p.println(" mVibrateOn=" + settingsValues.mVibrateOn); 2006 p.println(" mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn); 2007 p.println(" inputAttributes=" + settingsValues.mInputAttributes); 2008 // TODO: Dump all settings values 2009 } 2010} 2011