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