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