• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.inputmethod.latin;
18 
19 import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
20 import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
21 import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
22 
23 import android.app.Activity;
24 import android.app.AlertDialog;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.SharedPreferences;
31 import android.content.pm.PackageInfo;
32 import android.content.res.Configuration;
33 import android.content.res.Resources;
34 import android.graphics.Rect;
35 import android.inputmethodservice.InputMethodService;
36 import android.media.AudioManager;
37 import android.net.ConnectivityManager;
38 import android.os.Debug;
39 import android.os.Handler;
40 import android.os.HandlerThread;
41 import android.os.IBinder;
42 import android.os.Message;
43 import android.os.SystemClock;
44 import android.preference.PreferenceManager;
45 import android.text.InputType;
46 import android.text.SpannableString;
47 import android.text.TextUtils;
48 import android.text.style.SuggestionSpan;
49 import android.util.Log;
50 import android.util.PrintWriterPrinter;
51 import android.util.Printer;
52 import android.view.KeyCharacterMap;
53 import android.view.KeyEvent;
54 import android.view.View;
55 import android.view.ViewGroup.LayoutParams;
56 import android.view.Window;
57 import android.view.WindowManager;
58 import android.view.inputmethod.CompletionInfo;
59 import android.view.inputmethod.CorrectionInfo;
60 import android.view.inputmethod.EditorInfo;
61 import android.view.inputmethod.InputMethodSubtype;
62 
63 import com.android.inputmethod.accessibility.AccessibilityUtils;
64 import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
65 import com.android.inputmethod.annotations.UsedForTesting;
66 import com.android.inputmethod.compat.AppWorkaroundsUtils;
67 import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
68 import com.android.inputmethod.compat.SuggestionSpanUtils;
69 import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
70 import com.android.inputmethod.event.EventInterpreter;
71 import com.android.inputmethod.keyboard.KeyDetector;
72 import com.android.inputmethod.keyboard.Keyboard;
73 import com.android.inputmethod.keyboard.KeyboardActionListener;
74 import com.android.inputmethod.keyboard.KeyboardId;
75 import com.android.inputmethod.keyboard.KeyboardSwitcher;
76 import com.android.inputmethod.keyboard.MainKeyboardView;
77 import com.android.inputmethod.latin.RichInputConnection.Range;
78 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
79 import com.android.inputmethod.latin.Utils.Stats;
80 import com.android.inputmethod.latin.define.ProductionFlag;
81 import com.android.inputmethod.latin.suggestions.SuggestionStripView;
82 import com.android.inputmethod.research.ResearchLogger;
83 
84 import java.io.FileDescriptor;
85 import java.io.PrintWriter;
86 import java.util.ArrayList;
87 import java.util.Locale;
88 import java.util.TreeSet;
89 
90 /**
91  * Input method implementation for Qwerty'ish keyboard.
92  */
93 public class LatinIME extends InputMethodService implements KeyboardActionListener,
94         SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener,
95         Suggest.SuggestInitializationListener {
96     private static final String TAG = LatinIME.class.getSimpleName();
97     private static final boolean TRACE = false;
98     private static boolean DEBUG;
99 
100     private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
101 
102     // How many continuous deletes at which to start deleting at a higher speed.
103     private static final int DELETE_ACCELERATE_AT = 20;
104     // Key events coming any faster than this are long-presses.
105     private static final int QUICK_PRESS = 200;
106 
107     private static final int PENDING_IMS_CALLBACK_DURATION = 800;
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 static final int SPACE_STATE_NONE = 0;
116     // Double space: the state where the user pressed space twice quickly, which LatinIME
117     // resolved as period-space. Undoing this converts the period to a space.
118     private static final int SPACE_STATE_DOUBLE = 1;
119     // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
120     // have just been swapped. Undoing this swaps them back; the space is still considered weak.
121     private static final int SPACE_STATE_SWAP_PUNCTUATION = 2;
122     // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
123     // spaces happen when the user presses space, accepting the current suggestion (whether
124     // it's an auto-correction or not).
125     private static final int SPACE_STATE_WEAK = 3;
126     // Phantom space: a not-yet-inserted space that should get inserted on the next input,
127     // character provided it's not a separator. If it's a separator, the phantom space is dropped.
128     // Phantom spaces happen when a user chooses a word from the suggestion strip.
129     private static final int SPACE_STATE_PHANTOM = 4;
130 
131     // Current space state of the input method. This can be any of the above constants.
132     private int mSpaceState;
133 
134     private final Settings mSettings;
135 
136     private View mExtractArea;
137     private View mKeyPreviewBackingView;
138     private View mSuggestionsContainer;
139     private SuggestionStripView mSuggestionStripView;
140     // Never null
141     private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
142     @UsedForTesting Suggest mSuggest;
143     private CompletionInfo[] mApplicationSpecifiedCompletions;
144     private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils();
145 
146     private RichInputMethodManager mRichImm;
147     @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
148     private final SubtypeSwitcher mSubtypeSwitcher;
149     private final SubtypeState mSubtypeState = new SubtypeState();
150     // At start, create a default event interpreter that does nothing by passing it no decoder spec.
151     // The event interpreter should never be null.
152     private EventInterpreter mEventInterpreter = new EventInterpreter(this);
153 
154     private boolean mIsMainDictionaryAvailable;
155     private UserBinaryDictionary mUserDictionary;
156     private UserHistoryDictionary mUserHistoryDictionary;
157     private boolean mIsUserDictionaryAvailable;
158 
159     private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
160     private PositionalInfoForUserDictPendingAddition
161             mPositionalInfoForUserDictPendingAddition = null;
162     private final WordComposer mWordComposer = new WordComposer();
163     private final RichInputConnection mConnection = new RichInputConnection(this);
164     private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
165 
166     // Keep track of the last selection range to decide if we need to show word alternatives
167     private static final int NOT_A_CURSOR_POSITION = -1;
168     private int mLastSelectionStart = NOT_A_CURSOR_POSITION;
169     private int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
170 
171     // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
172     // "expect" it, it means the user actually moved the cursor.
173     private boolean mExpectingUpdateSelection;
174     private int mDeleteCount;
175     private long mLastKeyTime;
176     private TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
177 
178     // Member variables for remembering the current device orientation.
179     private int mDisplayOrientation;
180 
181     // Object for reacting to adding/removing a dictionary pack.
182     // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
183     // Service yet.
184     private BroadcastReceiver mDictionaryPackInstallReceiver =
185             ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
186                     ? null : new DictionaryPackInstallBroadcastReceiver(this);
187 
188     // Keeps track of most recently inserted text (multi-character key) for reverting
189     private String mEnteredText;
190 
191     // TODO: This boolean is persistent state and causes large side effects at unexpected times.
192     // Find a way to remove it for readability.
193     private boolean mIsAutoCorrectionIndicatorOn;
194 
195     private AlertDialog mOptionsDialog;
196 
197     private final boolean mIsHardwareAcceleratedDrawingEnabled;
198 
199     public final UIHandler mHandler = new UIHandler(this);
200 
201     public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
202         private static final int MSG_UPDATE_SHIFT_STATE = 0;
203         private static final int MSG_PENDING_IMS_CALLBACK = 1;
204         private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
205         private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
206         private static final int MSG_RESUME_SUGGESTIONS = 4;
207 
208         private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
209 
210         private int mDelayUpdateSuggestions;
211         private int mDelayUpdateShiftState;
212         private long mDoubleSpacePeriodTimeout;
213         private long mDoubleSpacePeriodTimerStart;
214 
UIHandler(final LatinIME outerInstance)215         public UIHandler(final LatinIME outerInstance) {
216             super(outerInstance);
217         }
218 
onCreate()219         public void onCreate() {
220             final Resources res = getOuterInstance().getResources();
221             mDelayUpdateSuggestions =
222                     res.getInteger(R.integer.config_delay_update_suggestions);
223             mDelayUpdateShiftState =
224                     res.getInteger(R.integer.config_delay_update_shift_state);
225             mDoubleSpacePeriodTimeout =
226                     res.getInteger(R.integer.config_double_space_period_timeout);
227         }
228 
229         @Override
handleMessage(final Message msg)230         public void handleMessage(final Message msg) {
231             final LatinIME latinIme = getOuterInstance();
232             final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
233             switch (msg.what) {
234             case MSG_UPDATE_SUGGESTION_STRIP:
235                 latinIme.updateSuggestionStrip();
236                 break;
237             case MSG_UPDATE_SHIFT_STATE:
238                 switcher.updateShiftState();
239                 break;
240             case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
241                 latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj,
242                         msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
243                 break;
244             case MSG_RESUME_SUGGESTIONS:
245                 latinIme.restartSuggestionsOnWordTouchedByCursor();
246                 break;
247             }
248         }
249 
postUpdateSuggestionStrip()250         public void postUpdateSuggestionStrip() {
251             sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
252         }
253 
postResumeSuggestions()254         public void postResumeSuggestions() {
255             removeMessages(MSG_RESUME_SUGGESTIONS);
256             sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
257         }
258 
cancelUpdateSuggestionStrip()259         public void cancelUpdateSuggestionStrip() {
260             removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
261         }
262 
hasPendingUpdateSuggestions()263         public boolean hasPendingUpdateSuggestions() {
264             return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
265         }
266 
postUpdateShiftState()267         public void postUpdateShiftState() {
268             removeMessages(MSG_UPDATE_SHIFT_STATE);
269             sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
270         }
271 
cancelUpdateShiftState()272         public void cancelUpdateShiftState() {
273             removeMessages(MSG_UPDATE_SHIFT_STATE);
274         }
275 
showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, final boolean dismissGestureFloatingPreviewText)276         public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
277                 final boolean dismissGestureFloatingPreviewText) {
278             removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
279             final int arg1 = dismissGestureFloatingPreviewText
280                     ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : 0;
281             obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords)
282                     .sendToTarget();
283         }
284 
startDoubleSpacePeriodTimer()285         public void startDoubleSpacePeriodTimer() {
286             mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis();
287         }
288 
cancelDoubleSpacePeriodTimer()289         public void cancelDoubleSpacePeriodTimer() {
290             mDoubleSpacePeriodTimerStart = 0;
291         }
292 
isAcceptingDoubleSpacePeriod()293         public boolean isAcceptingDoubleSpacePeriod() {
294             return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart
295                     < mDoubleSpacePeriodTimeout;
296         }
297 
298         // Working variables for the following methods.
299         private boolean mIsOrientationChanging;
300         private boolean mPendingSuccessiveImsCallback;
301         private boolean mHasPendingStartInput;
302         private boolean mHasPendingFinishInputView;
303         private boolean mHasPendingFinishInput;
304         private EditorInfo mAppliedEditorInfo;
305 
startOrientationChanging()306         public void startOrientationChanging() {
307             removeMessages(MSG_PENDING_IMS_CALLBACK);
308             resetPendingImsCallback();
309             mIsOrientationChanging = true;
310             final LatinIME latinIme = getOuterInstance();
311             if (latinIme.isInputViewShown()) {
312                 latinIme.mKeyboardSwitcher.saveKeyboardState();
313             }
314         }
315 
resetPendingImsCallback()316         private void resetPendingImsCallback() {
317             mHasPendingFinishInputView = false;
318             mHasPendingFinishInput = false;
319             mHasPendingStartInput = false;
320         }
321 
executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, boolean restarting)322         private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo,
323                 boolean restarting) {
324             if (mHasPendingFinishInputView)
325                 latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
326             if (mHasPendingFinishInput)
327                 latinIme.onFinishInputInternal();
328             if (mHasPendingStartInput)
329                 latinIme.onStartInputInternal(editorInfo, restarting);
330             resetPendingImsCallback();
331         }
332 
onStartInput(final EditorInfo editorInfo, final boolean restarting)333         public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
334             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
335                 // Typically this is the second onStartInput after orientation changed.
336                 mHasPendingStartInput = true;
337             } else {
338                 if (mIsOrientationChanging && restarting) {
339                     // This is the first onStartInput after orientation changed.
340                     mIsOrientationChanging = false;
341                     mPendingSuccessiveImsCallback = true;
342                 }
343                 final LatinIME latinIme = getOuterInstance();
344                 executePendingImsCallback(latinIme, editorInfo, restarting);
345                 latinIme.onStartInputInternal(editorInfo, restarting);
346             }
347         }
348 
onStartInputView(final EditorInfo editorInfo, final boolean restarting)349         public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
350             if (hasMessages(MSG_PENDING_IMS_CALLBACK)
351                     && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
352                 // Typically this is the second onStartInputView after orientation changed.
353                 resetPendingImsCallback();
354             } else {
355                 if (mPendingSuccessiveImsCallback) {
356                     // This is the first onStartInputView after orientation changed.
357                     mPendingSuccessiveImsCallback = false;
358                     resetPendingImsCallback();
359                     sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
360                             PENDING_IMS_CALLBACK_DURATION);
361                 }
362                 final LatinIME latinIme = getOuterInstance();
363                 executePendingImsCallback(latinIme, editorInfo, restarting);
364                 latinIme.onStartInputViewInternal(editorInfo, restarting);
365                 mAppliedEditorInfo = editorInfo;
366             }
367         }
368 
onFinishInputView(final boolean finishingInput)369         public void onFinishInputView(final boolean finishingInput) {
370             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
371                 // Typically this is the first onFinishInputView after orientation changed.
372                 mHasPendingFinishInputView = true;
373             } else {
374                 final LatinIME latinIme = getOuterInstance();
375                 latinIme.onFinishInputViewInternal(finishingInput);
376                 mAppliedEditorInfo = null;
377             }
378         }
379 
onFinishInput()380         public void onFinishInput() {
381             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
382                 // Typically this is the first onFinishInput after orientation changed.
383                 mHasPendingFinishInput = true;
384             } else {
385                 final LatinIME latinIme = getOuterInstance();
386                 executePendingImsCallback(latinIme, null, false);
387                 latinIme.onFinishInputInternal();
388             }
389         }
390     }
391 
392     static final class SubtypeState {
393         private InputMethodSubtype mLastActiveSubtype;
394         private boolean mCurrentSubtypeUsed;
395 
currentSubtypeUsed()396         public void currentSubtypeUsed() {
397             mCurrentSubtypeUsed = true;
398         }
399 
switchSubtype(final IBinder token, final RichInputMethodManager richImm)400         public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) {
401             final InputMethodSubtype currentSubtype = richImm.getInputMethodManager()
402                     .getCurrentInputMethodSubtype();
403             final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype;
404             final boolean currentSubtypeUsed = mCurrentSubtypeUsed;
405             if (currentSubtypeUsed) {
406                 mLastActiveSubtype = currentSubtype;
407                 mCurrentSubtypeUsed = false;
408             }
409             if (currentSubtypeUsed
410                     && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
411                     && !currentSubtype.equals(lastActiveSubtype)) {
412                 richImm.setInputMethodAndSubtype(token, lastActiveSubtype);
413                 return;
414             }
415             richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */);
416         }
417     }
418 
LatinIME()419     public LatinIME() {
420         super();
421         mSettings = Settings.getInstance();
422         mSubtypeSwitcher = SubtypeSwitcher.getInstance();
423         mKeyboardSwitcher = KeyboardSwitcher.getInstance();
424         mIsHardwareAcceleratedDrawingEnabled =
425                 InputMethodServiceCompatUtils.enableHardwareAcceleration(this);
426         Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled);
427     }
428 
429     @Override
onCreate()430     public void onCreate() {
431         Settings.init(this);
432         LatinImeLogger.init(this);
433         RichInputMethodManager.init(this);
434         mRichImm = RichInputMethodManager.getInstance();
435         SubtypeSwitcher.init(this);
436         KeyboardSwitcher.init(this);
437         AudioAndHapticFeedbackManager.init(this);
438         AccessibilityUtils.init(this);
439 
440         super.onCreate();
441 
442         mHandler.onCreate();
443         DEBUG = LatinImeLogger.sDBG;
444 
445         // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}.
446         loadSettings();
447         initSuggest();
448 
449         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
450             ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest);
451         }
452         mDisplayOrientation = getResources().getConfiguration().orientation;
453 
454         // Register to receive ringer mode change and network state change.
455         // Also receive installation and removal of a dictionary pack.
456         final IntentFilter filter = new IntentFilter();
457         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
458         filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
459         registerReceiver(mReceiver, filter);
460 
461         // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
462         // Service yet.
463         if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
464             final IntentFilter packageFilter = new IntentFilter();
465             packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
466             packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
467             packageFilter.addDataScheme(SCHEME_PACKAGE);
468             registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
469 
470             final IntentFilter newDictFilter = new IntentFilter();
471             newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
472             registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
473         }
474     }
475 
476     // Has to be package-visible for unit tests
477     @UsedForTesting
loadSettings()478     void loadSettings() {
479         final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
480         final InputAttributes inputAttributes =
481                 new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
482         mSettings.loadSettings(locale, inputAttributes);
483         // May need to reset the contacts dictionary depending on the user settings.
484         resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
485     }
486 
487     // Note that this method is called from a non-UI thread.
488     @Override
onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable)489     public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) {
490         mIsMainDictionaryAvailable = isMainDictionaryAvailable;
491         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
492         if (mainKeyboardView != null) {
493             mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
494         }
495     }
496 
initSuggest()497     private void initSuggest() {
498         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
499         final String localeStr = subtypeLocale.toString();
500 
501         final ContactsBinaryDictionary oldContactsDictionary;
502         if (mSuggest != null) {
503             oldContactsDictionary = mSuggest.getContactsDictionary();
504             mSuggest.close();
505         } else {
506             oldContactsDictionary = null;
507         }
508         mSuggest = new Suggest(this /* Context */, subtypeLocale,
509                 this /* SuggestInitializationListener */);
510         if (mSettings.getCurrent().mCorrectionEnabled) {
511             mSuggest.setAutoCorrectionThreshold(mSettings.getCurrent().mAutoCorrectionThreshold);
512         }
513 
514         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
515         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
516             ResearchLogger.getInstance().initSuggest(mSuggest);
517         }
518 
519         mUserDictionary = new UserBinaryDictionary(this, localeStr);
520         mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
521         mSuggest.setUserDictionary(mUserDictionary);
522 
523         resetContactsDictionary(oldContactsDictionary);
524 
525         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
526         mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, prefs);
527         mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
528     }
529 
530     /**
531      * Resets the contacts dictionary in mSuggest according to the user settings.
532      *
533      * This method takes an optional contacts dictionary to use when the locale hasn't changed
534      * since the contacts dictionary can be opened or closed as necessary depending on the settings.
535      *
536      * @param oldContactsDictionary an optional dictionary to use, or null
537      */
resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary)538     private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
539         final boolean shouldSetDictionary =
540                 (null != mSuggest && mSettings.getCurrent().mUseContactsDict);
541 
542         final ContactsBinaryDictionary dictionaryToUse;
543         if (!shouldSetDictionary) {
544             // Make sure the dictionary is closed. If it is already closed, this is a no-op,
545             // so it's safe to call it anyways.
546             if (null != oldContactsDictionary) oldContactsDictionary.close();
547             dictionaryToUse = null;
548         } else {
549             final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
550             if (null != oldContactsDictionary) {
551                 if (!oldContactsDictionary.mLocale.equals(locale)) {
552                     // If the locale has changed then recreate the contacts dictionary. This
553                     // allows locale dependent rules for handling bigram name predictions.
554                     oldContactsDictionary.close();
555                     dictionaryToUse = new ContactsBinaryDictionary(this, locale);
556                 } else {
557                     // Make sure the old contacts dictionary is opened. If it is already open,
558                     // this is a no-op, so it's safe to call it anyways.
559                     oldContactsDictionary.reopen(this);
560                     dictionaryToUse = oldContactsDictionary;
561                 }
562             } else {
563                 dictionaryToUse = new ContactsBinaryDictionary(this, locale);
564             }
565         }
566 
567         if (null != mSuggest) {
568             mSuggest.setContactsDictionary(dictionaryToUse);
569         }
570     }
571 
resetSuggestMainDict()572     /* package private */ void resetSuggestMainDict() {
573         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
574         mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
575         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
576     }
577 
578     @Override
onDestroy()579     public void onDestroy() {
580         if (mSuggest != null) {
581             mSuggest.close();
582             mSuggest = null;
583         }
584         mSettings.onDestroy();
585         unregisterReceiver(mReceiver);
586         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
587             ResearchLogger.getInstance().onDestroy();
588         }
589         // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
590         // Service yet.
591         if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
592             unregisterReceiver(mDictionaryPackInstallReceiver);
593         }
594         LatinImeLogger.commit();
595         LatinImeLogger.onDestroy();
596         super.onDestroy();
597     }
598 
599     @Override
onConfigurationChanged(final Configuration conf)600     public void onConfigurationChanged(final Configuration conf) {
601         // If orientation changed while predicting, commit the change
602         if (mDisplayOrientation != conf.orientation) {
603             mDisplayOrientation = conf.orientation;
604             mHandler.startOrientationChanging();
605             mConnection.beginBatchEdit();
606             commitTyped(LastComposedWord.NOT_A_SEPARATOR);
607             mConnection.finishComposingText();
608             mConnection.endBatchEdit();
609             if (isShowingOptionDialog()) {
610                 mOptionsDialog.dismiss();
611             }
612         }
613         super.onConfigurationChanged(conf);
614     }
615 
616     @Override
onCreateInputView()617     public View onCreateInputView() {
618         return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
619     }
620 
621     @Override
setInputView(final View view)622     public void setInputView(final View view) {
623         super.setInputView(view);
624         mExtractArea = getWindow().getWindow().getDecorView()
625                 .findViewById(android.R.id.extractArea);
626         mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
627         mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
628         mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
629         if (mSuggestionStripView != null)
630             mSuggestionStripView.setListener(this, view);
631         if (LatinImeLogger.sVISUALDEBUG) {
632             mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
633         }
634     }
635 
636     @Override
setCandidatesView(final View view)637     public void setCandidatesView(final View view) {
638         // To ensure that CandidatesView will never be set.
639         return;
640     }
641 
642     @Override
onStartInput(final EditorInfo editorInfo, final boolean restarting)643     public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
644         mHandler.onStartInput(editorInfo, restarting);
645     }
646 
647     @Override
onStartInputView(final EditorInfo editorInfo, final boolean restarting)648     public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
649         mHandler.onStartInputView(editorInfo, restarting);
650     }
651 
652     @Override
onFinishInputView(final boolean finishingInput)653     public void onFinishInputView(final boolean finishingInput) {
654         mHandler.onFinishInputView(finishingInput);
655     }
656 
657     @Override
onFinishInput()658     public void onFinishInput() {
659         mHandler.onFinishInput();
660     }
661 
662     @Override
onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype)663     public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) {
664         // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
665         // is not guaranteed. It may even be called at the same time on a different thread.
666         mSubtypeSwitcher.onSubtypeChanged(subtype);
667         loadKeyboard();
668     }
669 
onStartInputInternal(final EditorInfo editorInfo, final boolean restarting)670     private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
671         super.onStartInput(editorInfo, restarting);
672     }
673 
674     @SuppressWarnings("deprecation")
onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting)675     private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
676         super.onStartInputView(editorInfo, restarting);
677         final KeyboardSwitcher switcher = mKeyboardSwitcher;
678         final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
679         final SettingsValues currentSettings = mSettings.getCurrent();
680 
681         if (editorInfo == null) {
682             Log.e(TAG, "Null EditorInfo in onStartInputView()");
683             if (LatinImeLogger.sDBG) {
684                 throw new NullPointerException("Null EditorInfo in onStartInputView()");
685             }
686             return;
687         }
688         if (DEBUG) {
689             Log.d(TAG, "onStartInputView: editorInfo:"
690                     + String.format("inputType=0x%08x imeOptions=0x%08x",
691                             editorInfo.inputType, editorInfo.imeOptions));
692             Log.d(TAG, "All caps = "
693                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0)
694                     + ", sentence caps = "
695                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0)
696                     + ", word caps = "
697                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
698         }
699         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
700             final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
701             ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs);
702         }
703         if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
704             Log.w(TAG, "Deprecated private IME option specified: "
705                     + editorInfo.privateImeOptions);
706             Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead");
707         }
708         if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) {
709             Log.w(TAG, "Deprecated private IME option specified: "
710                     + editorInfo.privateImeOptions);
711             Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
712         }
713 
714         final PackageInfo packageInfo =
715                 TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName);
716         mAppWorkAroundsUtils.setPackageInfo(packageInfo);
717         if (null == packageInfo) {
718             new TargetPackageInfoGetterTask(this /* context */, this /* listener */)
719                     .execute(editorInfo.packageName);
720         }
721 
722         LatinImeLogger.onStartInputView(editorInfo);
723         // In landscape mode, this method gets called without the input view being created.
724         if (mainKeyboardView == null) {
725             return;
726         }
727 
728         // Forward this event to the accessibility utilities, if enabled.
729         final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
730         if (accessUtils.isTouchExplorationEnabled()) {
731             accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
732         }
733 
734         final boolean inputTypeChanged = !currentSettings.isSameInputType(editorInfo);
735         final boolean isDifferentTextField = !restarting || inputTypeChanged;
736         if (isDifferentTextField) {
737             mSubtypeSwitcher.updateParametersOnStartInputView();
738         }
739 
740         // The EditorInfo might have a flag that affects fullscreen mode.
741         // Note: This call should be done by InputMethodService?
742         updateFullscreenMode();
743         mApplicationSpecifiedCompletions = null;
744 
745         // The app calling setText() has the effect of clearing the composing
746         // span, so we should reset our state unconditionally, even if restarting is true.
747         mEnteredText = null;
748         resetComposingState(true /* alsoResetLastComposedWord */);
749         mDeleteCount = 0;
750         mSpaceState = SPACE_STATE_NONE;
751         mRecapitalizeStatus.deactivate();
752         mCurrentlyPressedHardwareKeys.clear();
753 
754         // Note: the following does a round-trip IPC on the main thread: be careful
755         final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
756         if (null != mSuggest && null != currentLocale && !currentLocale.equals(mSuggest.mLocale)) {
757             initSuggest();
758         }
759         if (mSuggestionStripView != null) {
760             // This will set the punctuation suggestions if next word suggestion is off;
761             // otherwise it will clear the suggestion strip.
762             setPunctuationSuggestions();
763         }
764         mSuggestedWords = SuggestedWords.EMPTY;
765 
766         mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart,
767                 false /* shouldFinishComposition */);
768 
769         if (isDifferentTextField) {
770             mainKeyboardView.closing();
771             loadSettings();
772 
773             if (mSuggest != null && currentSettings.mCorrectionEnabled) {
774                 mSuggest.setAutoCorrectionThreshold(currentSettings.mAutoCorrectionThreshold);
775             }
776 
777             switcher.loadKeyboard(editorInfo, currentSettings);
778         } else if (restarting) {
779             // TODO: Come up with a more comprehensive way to reset the keyboard layout when
780             // a keyboard layout set doesn't get reloaded in this method.
781             switcher.resetKeyboardStateToAlphabet();
782             // In apps like Talk, we come here when the text is sent and the field gets emptied and
783             // we need to re-evaluate the shift state, but not the whole layout which would be
784             // disruptive.
785             // Space state must be updated before calling updateShiftState
786             switcher.updateShiftState();
787         }
788         setSuggestionStripShownInternal(
789                 isSuggestionsStripVisible(), /* needsInputViewShown */ false);
790 
791         mLastSelectionStart = editorInfo.initialSelStart;
792         mLastSelectionEnd = editorInfo.initialSelEnd;
793 
794         mHandler.cancelUpdateSuggestionStrip();
795         mHandler.cancelDoubleSpacePeriodTimer();
796 
797         mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
798         mainKeyboardView.setKeyPreviewPopupEnabled(currentSettings.mKeyPreviewPopupOn,
799                 currentSettings.mKeyPreviewPopupDismissDelay);
800         mainKeyboardView.setSlidingKeyInputPreviewEnabled(
801                 currentSettings.mSlidingKeyInputPreviewEnabled);
802         mainKeyboardView.setGestureHandlingEnabledByUser(
803                 currentSettings.mGestureInputEnabled);
804         mainKeyboardView.setGesturePreviewMode(currentSettings.mGesturePreviewTrailEnabled,
805                 currentSettings.mGestureFloatingPreviewTextEnabled);
806 
807         // If we have a user dictionary addition in progress, we should check now if we should
808         // replace the previously committed string with the word that has actually been added
809         // to the user dictionary.
810         if (null != mPositionalInfoForUserDictPendingAddition
811                 && mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord(
812                         mConnection, editorInfo, mLastSelectionEnd, currentLocale)) {
813             mPositionalInfoForUserDictPendingAddition = null;
814         }
815         // If tryReplaceWithActualWord returns false, we don't know what word was
816         // added to the user dictionary yet, so we keep the data and defer processing. The word will
817         // be replaced when the user dictionary reports back with the actual word, which ends
818         // up calling #onWordAddedToUserDictionary() in this class.
819 
820         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
821     }
822 
823     // Callback for the TargetPackageInfoGetterTask
824     @Override
onTargetPackageInfoKnown(final PackageInfo info)825     public void onTargetPackageInfoKnown(final PackageInfo info) {
826         mAppWorkAroundsUtils.setPackageInfo(info);
827     }
828 
829     @Override
onWindowHidden()830     public void onWindowHidden() {
831         super.onWindowHidden();
832         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
833         if (mainKeyboardView != null) {
834             mainKeyboardView.closing();
835         }
836     }
837 
onFinishInputInternal()838     private void onFinishInputInternal() {
839         super.onFinishInput();
840 
841         LatinImeLogger.commit();
842         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
843         if (mainKeyboardView != null) {
844             mainKeyboardView.closing();
845         }
846     }
847 
onFinishInputViewInternal(final boolean finishingInput)848     private void onFinishInputViewInternal(final boolean finishingInput) {
849         super.onFinishInputView(finishingInput);
850         mKeyboardSwitcher.onFinishInputView();
851         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
852         if (mainKeyboardView != null) {
853             mainKeyboardView.cancelAllMessages();
854         }
855         // Remove pending messages related to update suggestions
856         mHandler.cancelUpdateSuggestionStrip();
857         resetComposingState(true /* alsoResetLastComposedWord */);
858         // Notify ResearchLogger
859         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
860             ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart,
861                     mLastSelectionEnd, getCurrentInputConnection());
862         }
863     }
864 
865     @Override
onUpdateSelection(final int oldSelStart, final int oldSelEnd, final int newSelStart, final int newSelEnd, final int composingSpanStart, final int composingSpanEnd)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=" + mLastSelectionStart
875                     + ", lse=" + mLastSelectionEnd
876                     + ", nss=" + newSelStart
877                     + ", nse=" + newSelEnd
878                     + ", cs=" + composingSpanStart
879                     + ", ce=" + composingSpanEnd);
880         }
881         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
882             final boolean expectingUpdateSelectionFromLogger =
883                     ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
884             ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
885                     oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
886                     composingSpanEnd, mExpectingUpdateSelection,
887                     expectingUpdateSelectionFromLogger, mConnection);
888             if (expectingUpdateSelectionFromLogger) {
889                 // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
890                 return;
891             }
892         }
893 
894         // TODO: refactor the following code to be less contrived.
895         // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means
896         // that the cursor is not at the end of the composing span, or there is a selection.
897         // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place
898         // as last time we were called (if there is a selection, it means the start hasn't
899         // changed, so it's the end that did).
900         final boolean selectionChanged = (newSelStart != composingSpanEnd
901                 || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart;
902         // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
903         // span in the view - we can use that to narrow down whether the cursor was moved
904         // by us or not. If we are composing a word but there is no composing span, then
905         // we know for sure the cursor moved while we were composing and we should reset
906         // the state.
907         final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
908         // If the keyboard is not visible, we don't need to do all the housekeeping work, as it
909         // will be reset when the keyboard shows up anyway.
910         // TODO: revisit this when LatinIME supports hardware keyboards.
911         // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
912         // TODO: find a better way to simulate actual execution.
913         if (isInputViewShown() && !mExpectingUpdateSelection
914                 && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) {
915             // TAKE CARE: there is a race condition when we enter this test even when the user
916             // did not explicitly move the cursor. This happens when typing fast, where two keys
917             // turn this flag on in succession and both onUpdateSelection() calls arrive after
918             // the second one - the first call successfully avoids this test, but the second one
919             // enters. For the moment we rely on noComposingSpan to further reduce the impact.
920 
921             // TODO: the following is probably better done in resetEntireInputState().
922             // it should only happen when the cursor moved, and the very purpose of the
923             // test below is to narrow down whether this happened or not. Likewise with
924             // the call to updateShiftState.
925             // We set this to NONE because after a cursor move, we don't want the space
926             // state-related special processing to kick in.
927             mSpaceState = SPACE_STATE_NONE;
928 
929             if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
930                 // If we are composing a word and moving the cursor, we would want to set a
931                 // suggestion span for recorrection to work correctly. Unfortunately, that
932                 // would involve the keyboard committing some new text, which would move the
933                 // cursor back to where it was. Latin IME could then fix the position of the cursor
934                 // again, but the asynchronous nature of the calls results in this wreaking havoc
935                 // with selection on double tap and the like.
936                 // Another option would be to send suggestions each time we set the composing
937                 // text, but that is probably too expensive to do, so we decided to leave things
938                 // as is.
939                 resetEntireInputState(newSelStart);
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             mRecapitalizeStatus.deactivate();
949             mKeyboardSwitcher.updateShiftState();
950         }
951         mExpectingUpdateSelection = false;
952 
953         // Make a note of the cursor position
954         mLastSelectionStart = newSelStart;
955         mLastSelectionEnd = newSelEnd;
956         mSubtypeState.currentSubtypeUsed();
957     }
958 
959     /**
960      * This is called when the user has clicked on the extracted text view,
961      * when running in fullscreen mode.  The default implementation hides
962      * the suggestions view when this happens, but only if the extracted text
963      * editor has a vertical scroll bar because its text doesn't fit.
964      * Here we override the behavior due to the possibility that a re-correction could
965      * cause the suggestions strip to disappear and re-appear.
966      */
967     @Override
onExtractedTextClicked()968     public void onExtractedTextClicked() {
969         if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
970 
971         super.onExtractedTextClicked();
972     }
973 
974     /**
975      * This is called when the user has performed a cursor movement in the
976      * extracted text view, when it is running in fullscreen mode.  The default
977      * implementation hides the suggestions view when a vertical movement
978      * happens, but only if the extracted text editor has a vertical scroll bar
979      * because its text doesn't fit.
980      * Here we override the behavior due to the possibility that a re-correction could
981      * cause the suggestions strip to disappear and re-appear.
982      */
983     @Override
onExtractedCursorMovement(final int dx, final int dy)984     public void onExtractedCursorMovement(final int dx, final int dy) {
985         if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
986 
987         super.onExtractedCursorMovement(dx, dy);
988     }
989 
990     @Override
hideWindow()991     public void hideWindow() {
992         LatinImeLogger.commit();
993         mKeyboardSwitcher.onHideWindow();
994 
995         if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
996             AccessibleKeyboardViewProxy.getInstance().onHideWindow();
997         }
998 
999         if (TRACE) Debug.stopMethodTracing();
1000         if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
1001             mOptionsDialog.dismiss();
1002             mOptionsDialog = null;
1003         }
1004         super.hideWindow();
1005     }
1006 
1007     @Override
onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions)1008     public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
1009         if (DEBUG) {
1010             Log.i(TAG, "Received completions:");
1011             if (applicationSpecifiedCompletions != null) {
1012                 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
1013                     Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
1014                 }
1015             }
1016         }
1017         if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return;
1018         if (applicationSpecifiedCompletions == null) {
1019             clearSuggestionStrip();
1020             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1021                 ResearchLogger.latinIME_onDisplayCompletions(null);
1022             }
1023             return;
1024         }
1025         mApplicationSpecifiedCompletions =
1026                 CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions);
1027 
1028         final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
1029                 SuggestedWords.getFromApplicationSpecifiedCompletions(
1030                         applicationSpecifiedCompletions);
1031         final SuggestedWords suggestedWords = new SuggestedWords(
1032                 applicationSuggestedWords,
1033                 false /* typedWordValid */,
1034                 false /* hasAutoCorrectionCandidate */,
1035                 false /* isPunctuationSuggestions */,
1036                 false /* isObsoleteSuggestions */,
1037                 false /* isPrediction */);
1038         // When in fullscreen mode, show completions generated by the application
1039         final boolean isAutoCorrection = false;
1040         setSuggestedWords(suggestedWords, isAutoCorrection);
1041         setAutoCorrectionIndicator(isAutoCorrection);
1042         setSuggestionStripShown(true);
1043         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1044             ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
1045         }
1046     }
1047 
setSuggestionStripShownInternal(final boolean shown, final boolean needsInputViewShown)1048     private void setSuggestionStripShownInternal(final boolean shown,
1049             final boolean needsInputViewShown) {
1050         // TODO: Modify this if we support suggestions with hard keyboard
1051         if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
1052             final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1053             final boolean inputViewShown = (mainKeyboardView != null)
1054                     ? mainKeyboardView.isShown() : false;
1055             final boolean shouldShowSuggestions = shown
1056                     && (needsInputViewShown ? inputViewShown : true);
1057             if (isFullscreenMode()) {
1058                 mSuggestionsContainer.setVisibility(
1059                         shouldShowSuggestions ? View.VISIBLE : View.GONE);
1060             } else {
1061                 mSuggestionsContainer.setVisibility(
1062                         shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
1063             }
1064         }
1065     }
1066 
setSuggestionStripShown(final boolean shown)1067     private void setSuggestionStripShown(final boolean shown) {
1068         setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
1069     }
1070 
getAdjustedBackingViewHeight()1071     private int getAdjustedBackingViewHeight() {
1072         final int currentHeight = mKeyPreviewBackingView.getHeight();
1073         if (currentHeight > 0) {
1074             return currentHeight;
1075         }
1076 
1077         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1078         if (mainKeyboardView == null) {
1079             return 0;
1080         }
1081         final int keyboardHeight = mainKeyboardView.getHeight();
1082         final int suggestionsHeight = mSuggestionsContainer.getHeight();
1083         final int displayHeight = getResources().getDisplayMetrics().heightPixels;
1084         final Rect rect = new Rect();
1085         mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
1086         final int notificationBarHeight = rect.top;
1087         final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
1088                 - keyboardHeight;
1089 
1090         final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
1091         params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight);
1092         mKeyPreviewBackingView.setLayoutParams(params);
1093         return params.height;
1094     }
1095 
1096     @Override
onComputeInsets(final InputMethodService.Insets outInsets)1097     public void onComputeInsets(final InputMethodService.Insets outInsets) {
1098         super.onComputeInsets(outInsets);
1099         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1100         if (mainKeyboardView == null || mSuggestionsContainer == null) {
1101             return;
1102         }
1103         final int adjustedBackingHeight = getAdjustedBackingViewHeight();
1104         final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
1105         final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
1106         // In fullscreen mode, the height of the extract area managed by InputMethodService should
1107         // be considered.
1108         // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
1109         final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
1110         final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0
1111                 : mSuggestionsContainer.getHeight();
1112         final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
1113         int visibleTopY = extraHeight;
1114         // Need to set touchable region only if input view is being shown
1115         if (mainKeyboardView.isShown()) {
1116             if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
1117                 visibleTopY -= suggestionsHeight;
1118             }
1119             final int touchY = mainKeyboardView.isShowingMoreKeysPanel() ? 0 : visibleTopY;
1120             final int touchWidth = mainKeyboardView.getWidth();
1121             final int touchHeight = mainKeyboardView.getHeight() + extraHeight
1122                     // Extend touchable region below the keyboard.
1123                     + EXTENDED_TOUCHABLE_REGION_HEIGHT;
1124             outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
1125             outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight);
1126         }
1127         outInsets.contentTopInsets = visibleTopY;
1128         outInsets.visibleTopInsets = visibleTopY;
1129     }
1130 
1131     @Override
onEvaluateFullscreenMode()1132     public boolean onEvaluateFullscreenMode() {
1133         // Reread resource value here, because this method is called by framework anytime as needed.
1134         final boolean isFullscreenModeAllowed =
1135                 Settings.readUseFullscreenMode(getResources());
1136         if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) {
1137             // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI
1138             // implies NO_FULLSCREEN. However, the framework mistakenly does.  i.e. NO_EXTRACT_UI
1139             // without NO_FULLSCREEN doesn't work as expected. Because of this we need this
1140             // hack for now.  Let's get rid of this once the framework gets fixed.
1141             final EditorInfo ei = getCurrentInputEditorInfo();
1142             return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0));
1143         } else {
1144             return false;
1145         }
1146     }
1147 
1148     @Override
updateFullscreenMode()1149     public void updateFullscreenMode() {
1150         super.updateFullscreenMode();
1151 
1152         if (mKeyPreviewBackingView == null) return;
1153         // In fullscreen mode, no need to have extra space to show the key preview.
1154         // If not, we should have extra space above the keyboard to show the key preview.
1155         mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
1156     }
1157 
1158     // This will reset the whole input state to the starting state. It will clear
1159     // the composing word, reset the last composed word, tell the inputconnection about it.
resetEntireInputState(final int newCursorPosition)1160     private void resetEntireInputState(final int newCursorPosition) {
1161         final boolean shouldFinishComposition = mWordComposer.isComposingWord();
1162         resetComposingState(true /* alsoResetLastComposedWord */);
1163         if (mSettings.getCurrent().mBigramPredictionEnabled) {
1164             clearSuggestionStrip();
1165         } else {
1166             setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false);
1167         }
1168         mConnection.resetCachesUponCursorMove(newCursorPosition, shouldFinishComposition);
1169     }
1170 
resetComposingState(final boolean alsoResetLastComposedWord)1171     private void resetComposingState(final boolean alsoResetLastComposedWord) {
1172         mWordComposer.reset();
1173         if (alsoResetLastComposedWord)
1174             mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
1175     }
1176 
commitTyped(final String separatorString)1177     private void commitTyped(final String separatorString) {
1178         if (!mWordComposer.isComposingWord()) return;
1179         final String typedWord = mWordComposer.getTypedWord();
1180         if (typedWord.length() > 0) {
1181             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1182                 ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
1183             }
1184             commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
1185                     separatorString);
1186         }
1187     }
1188 
1189     // Called from the KeyboardSwitcher which needs to know auto caps state to display
1190     // the right layout.
getCurrentAutoCapsState()1191     public int getCurrentAutoCapsState() {
1192         if (!mSettings.getCurrent().mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
1193 
1194         final EditorInfo ei = getCurrentInputEditorInfo();
1195         if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
1196         final int inputType = ei.inputType;
1197         // Warning: this depends on mSpaceState, which may not be the most current value. If
1198         // mSpaceState gets updated later, whoever called this may need to be told about it.
1199         return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(),
1200                 SPACE_STATE_PHANTOM == mSpaceState);
1201     }
1202 
getCurrentRecapitalizeState()1203     public int getCurrentRecapitalizeState() {
1204         if (!mRecapitalizeStatus.isActive()
1205                 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
1206             // Not recapitalizing at the moment
1207             return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
1208         }
1209         return mRecapitalizeStatus.getCurrentMode();
1210     }
1211 
1212     // Factor in auto-caps and manual caps and compute the current caps mode.
getActualCapsMode()1213     private int getActualCapsMode() {
1214         final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
1215         if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
1216         final int auto = getCurrentAutoCapsState();
1217         if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
1218             return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
1219         }
1220         if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED;
1221         return WordComposer.CAPS_MODE_OFF;
1222     }
1223 
swapSwapperAndSpace()1224     private void swapSwapperAndSpace() {
1225         final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
1226         // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
1227         if (lastTwo != null && lastTwo.length() == 2
1228                 && lastTwo.charAt(0) == Constants.CODE_SPACE) {
1229             mConnection.deleteSurroundingText(2, 0);
1230             final String text = lastTwo.charAt(1) + " ";
1231             mConnection.commitText(text, 1);
1232             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1233                 ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
1234             }
1235             mKeyboardSwitcher.updateShiftState();
1236         }
1237     }
1238 
maybeDoubleSpacePeriod()1239     private boolean maybeDoubleSpacePeriod() {
1240         if (!mSettings.getCurrent().mCorrectionEnabled) return false;
1241         if (!mSettings.getCurrent().mUseDoubleSpacePeriod) return false;
1242         if (!mHandler.isAcceptingDoubleSpacePeriod()) return false;
1243         final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
1244         if (lastThree != null && lastThree.length() == 3
1245                 && canBeFollowedByDoubleSpacePeriod(lastThree.charAt(0))
1246                 && lastThree.charAt(1) == Constants.CODE_SPACE
1247                 && lastThree.charAt(2) == Constants.CODE_SPACE) {
1248             mHandler.cancelDoubleSpacePeriodTimer();
1249             mConnection.deleteSurroundingText(2, 0);
1250             final String textToInsert = ". ";
1251             mConnection.commitText(textToInsert, 1);
1252             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1253                 ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
1254                         false /* isBatchMode */);
1255             }
1256             mKeyboardSwitcher.updateShiftState();
1257             return true;
1258         }
1259         return false;
1260     }
1261 
canBeFollowedByDoubleSpacePeriod(final int codePoint)1262     private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
1263         // TODO: Check again whether there really ain't a better way to check this.
1264         // TODO: This should probably be language-dependant...
1265         return Character.isLetterOrDigit(codePoint)
1266                 || codePoint == Constants.CODE_SINGLE_QUOTE
1267                 || codePoint == Constants.CODE_DOUBLE_QUOTE
1268                 || codePoint == Constants.CODE_CLOSING_PARENTHESIS
1269                 || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
1270                 || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
1271                 || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET;
1272     }
1273 
1274     // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
1275     // pressed.
1276     @Override
addWordToUserDictionary(final String word)1277     public void addWordToUserDictionary(final String word) {
1278         if (TextUtils.isEmpty(word)) {
1279             // Probably never supposed to happen, but just in case.
1280             mPositionalInfoForUserDictPendingAddition = null;
1281             return;
1282         }
1283         final String wordToEdit;
1284         if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) {
1285             wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
1286         } else {
1287             wordToEdit = word;
1288         }
1289         mUserDictionary.addWordToUserDictionary(wordToEdit);
1290     }
1291 
onWordAddedToUserDictionary(final String newSpelling)1292     public void onWordAddedToUserDictionary(final String newSpelling) {
1293         // If word was added but not by us, bail out
1294         if (null == mPositionalInfoForUserDictPendingAddition) return;
1295         if (mWordComposer.isComposingWord()) {
1296             // We are late... give up and return
1297             mPositionalInfoForUserDictPendingAddition = null;
1298             return;
1299         }
1300         mPositionalInfoForUserDictPendingAddition.setActualWordBeingAdded(newSpelling);
1301         if (mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord(
1302                 mConnection, getCurrentInputEditorInfo(), mLastSelectionEnd,
1303                 mSubtypeSwitcher.getCurrentSubtypeLocale())) {
1304             mPositionalInfoForUserDictPendingAddition = null;
1305         }
1306     }
1307 
isAlphabet(final int code)1308     private static boolean isAlphabet(final int code) {
1309         return Character.isLetter(code);
1310     }
1311 
onSettingsKeyPressed()1312     private void onSettingsKeyPressed() {
1313         if (isShowingOptionDialog()) return;
1314         showSubtypeSelectorAndSettings();
1315     }
1316 
1317     // Virtual codes representing custom requests.  These are used in onCustomRequest() below.
1318     public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
1319 
1320     @Override
onCustomRequest(final int requestCode)1321     public boolean onCustomRequest(final int requestCode) {
1322         if (isShowingOptionDialog()) return false;
1323         switch (requestCode) {
1324         case CODE_SHOW_INPUT_METHOD_PICKER:
1325             if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
1326                 mRichImm.getInputMethodManager().showInputMethodPicker();
1327                 return true;
1328             }
1329             return false;
1330         }
1331         return false;
1332     }
1333 
isShowingOptionDialog()1334     private boolean isShowingOptionDialog() {
1335         return mOptionsDialog != null && mOptionsDialog.isShowing();
1336     }
1337 
performEditorAction(final int actionId)1338     private void performEditorAction(final int actionId) {
1339         mConnection.performEditorAction(actionId);
1340     }
1341 
1342     // TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
handleLanguageSwitchKey()1343     private void handleLanguageSwitchKey() {
1344         final IBinder token = getWindow().getWindow().getAttributes().token;
1345         if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) {
1346             mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
1347             return;
1348         }
1349         mSubtypeState.switchSubtype(token, mRichImm);
1350     }
1351 
sendDownUpKeyEventForBackwardCompatibility(final int code)1352     private void sendDownUpKeyEventForBackwardCompatibility(final int code) {
1353         final long eventTime = SystemClock.uptimeMillis();
1354         mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
1355                 KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1356                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1357         mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
1358                 KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1359                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1360     }
1361 
sendKeyCodePoint(final int code)1362     private void sendKeyCodePoint(final int code) {
1363         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1364             ResearchLogger.latinIME_sendKeyCodePoint(code);
1365         }
1366         // TODO: Remove this special handling of digit letters.
1367         // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
1368         if (code >= '0' && code <= '9') {
1369             sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0);
1370             return;
1371         }
1372 
1373         if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) {
1374             // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
1375             // a hardware keyboard event on pressing enter or delete. This is bad for many
1376             // reasons (there are race conditions with commits) but some applications are
1377             // relying on this behavior so we continue to support it for older apps.
1378             sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER);
1379         } else {
1380             final String text = new String(new int[] { code }, 0, 1);
1381             mConnection.commitText(text, text.length());
1382         }
1383     }
1384 
1385     // Implementation of {@link KeyboardActionListener}.
1386     @Override
onCodeInput(final int primaryCode, final int x, final int y)1387     public void onCodeInput(final int primaryCode, final int x, final int y) {
1388         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1389             ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
1390         }
1391         final long when = SystemClock.uptimeMillis();
1392         if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
1393             mDeleteCount = 0;
1394         }
1395         mLastKeyTime = when;
1396         mConnection.beginBatchEdit();
1397         final KeyboardSwitcher switcher = mKeyboardSwitcher;
1398         // The space state depends only on the last character pressed and its own previous
1399         // state. Here, we revert the space state to neutral if the key is actually modifying
1400         // the input contents (any non-shift key), which is what we should do for
1401         // all inputs that do not result in a special state. Each character handling is then
1402         // free to override the state as they see fit.
1403         final int spaceState = mSpaceState;
1404         if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
1405 
1406         // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
1407         if (primaryCode != Constants.CODE_SPACE) {
1408             mHandler.cancelDoubleSpacePeriodTimer();
1409         }
1410 
1411         boolean didAutoCorrect = false;
1412         switch (primaryCode) {
1413         case Constants.CODE_DELETE:
1414             mSpaceState = SPACE_STATE_NONE;
1415             handleBackspace(spaceState);
1416             mDeleteCount++;
1417             mExpectingUpdateSelection = true;
1418             LatinImeLogger.logOnDelete(x, y);
1419             break;
1420         case Constants.CODE_SHIFT:
1421             // Note: calling back to the keyboard on Shift key is handled in onPressKey()
1422             // and onReleaseKey().
1423             final Keyboard currentKeyboard = switcher.getKeyboard();
1424             if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
1425                 // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
1426                 // alphabetic shift and shift while in symbol layout.
1427                 handleRecapitalize();
1428             }
1429             break;
1430         case Constants.CODE_SWITCH_ALPHA_SYMBOL:
1431             // Note: calling back to the keyboard on symbol key is handled in onPressKey()
1432             // and onReleaseKey().
1433             break;
1434         case Constants.CODE_SETTINGS:
1435             onSettingsKeyPressed();
1436             break;
1437         case Constants.CODE_SHORTCUT:
1438             mSubtypeSwitcher.switchToShortcutIME(this);
1439             break;
1440         case Constants.CODE_ACTION_NEXT:
1441             performEditorAction(EditorInfo.IME_ACTION_NEXT);
1442             break;
1443         case Constants.CODE_ACTION_PREVIOUS:
1444             performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
1445             break;
1446         case Constants.CODE_LANGUAGE_SWITCH:
1447             handleLanguageSwitchKey();
1448             break;
1449         case Constants.CODE_RESEARCH:
1450             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1451                 ResearchLogger.getInstance().onResearchKeySelected(this);
1452             }
1453             break;
1454         case Constants.CODE_ENTER:
1455             final EditorInfo editorInfo = getCurrentInputEditorInfo();
1456             final int imeOptionsActionId =
1457                     InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
1458             if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
1459                 // Either we have an actionLabel and we should performEditorAction with actionId
1460                 // regardless of its value.
1461                 performEditorAction(editorInfo.actionId);
1462             } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
1463                 // We didn't have an actionLabel, but we had another action to execute.
1464                 // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
1465                 // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
1466                 // means there should be an action and the app didn't bother to set a specific
1467                 // code for it - presumably it only handles one. It does not have to be treated
1468                 // in any specific way: anything that is not IME_ACTION_NONE should be sent to
1469                 // performEditorAction.
1470                 performEditorAction(imeOptionsActionId);
1471             } else {
1472                 // No action label, and the action from imeOptions is NONE: this is a regular
1473                 // enter key that should input a carriage return.
1474                 didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
1475             }
1476             break;
1477         case Constants.CODE_SHIFT_ENTER:
1478             didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
1479             break;
1480         default:
1481             didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState);
1482             break;
1483         }
1484         switcher.onCodeInput(primaryCode);
1485         // Reset after any single keystroke, except shift and symbol-shift
1486         if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT
1487                 && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
1488             mLastComposedWord.deactivate();
1489         if (Constants.CODE_DELETE != primaryCode) {
1490             mEnteredText = null;
1491         }
1492         mConnection.endBatchEdit();
1493     }
1494 
handleNonSpecialCharacter(final int primaryCode, final int x, final int y, final int spaceState)1495     private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y,
1496             final int spaceState) {
1497         mSpaceState = SPACE_STATE_NONE;
1498         final boolean didAutoCorrect;
1499         if (mSettings.getCurrent().isWordSeparator(primaryCode)) {
1500             didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
1501         } else {
1502             didAutoCorrect = false;
1503             if (SPACE_STATE_PHANTOM == spaceState) {
1504                 if (mSettings.isInternal()) {
1505                     if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
1506                         Stats.onAutoCorrection(
1507                                 "", mWordComposer.getTypedWord(), " ", mWordComposer);
1508                     }
1509                 }
1510                 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
1511                     // If we are in the middle of a recorrection, we need to commit the recorrection
1512                     // first so that we can insert the character at the current cursor position.
1513                     resetEntireInputState(mLastSelectionStart);
1514                 } else {
1515                     commitTyped(LastComposedWord.NOT_A_SEPARATOR);
1516                 }
1517             }
1518             final int keyX, keyY;
1519             final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
1520             if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
1521                 keyX = x;
1522                 keyY = y;
1523             } else {
1524                 keyX = Constants.NOT_A_COORDINATE;
1525                 keyY = Constants.NOT_A_COORDINATE;
1526             }
1527             handleCharacter(primaryCode, keyX, keyY, spaceState);
1528         }
1529         mExpectingUpdateSelection = true;
1530         return didAutoCorrect;
1531     }
1532 
1533     // Called from PointerTracker through the KeyboardActionListener interface
1534     @Override
onTextInput(final String rawText)1535     public void onTextInput(final String rawText) {
1536         mConnection.beginBatchEdit();
1537         if (mWordComposer.isComposingWord()) {
1538             commitCurrentAutoCorrection(rawText);
1539         } else {
1540             resetComposingState(true /* alsoResetLastComposedWord */);
1541         }
1542         mHandler.postUpdateSuggestionStrip();
1543         final String text = specificTldProcessingOnTextInput(rawText);
1544         if (SPACE_STATE_PHANTOM == mSpaceState) {
1545             promotePhantomSpace();
1546         }
1547         mConnection.commitText(text, 1);
1548         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1549             ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */);
1550         }
1551         mConnection.endBatchEdit();
1552         // Space state must be updated before calling updateShiftState
1553         mSpaceState = SPACE_STATE_NONE;
1554         mKeyboardSwitcher.updateShiftState();
1555         mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT);
1556         mEnteredText = text;
1557     }
1558 
1559     @Override
onStartBatchInput()1560     public void onStartBatchInput() {
1561         BatchInputUpdater.getInstance().onStartBatchInput(this);
1562         mHandler.cancelUpdateSuggestionStrip();
1563         mConnection.beginBatchEdit();
1564         if (mWordComposer.isComposingWord()) {
1565             if (mSettings.isInternal()) {
1566                 if (mWordComposer.isBatchMode()) {
1567                     Stats.onAutoCorrection("", mWordComposer.getTypedWord(), " ", mWordComposer);
1568                 }
1569             }
1570             final int wordComposerSize = mWordComposer.size();
1571             // Since isComposingWord() is true, the size is at least 1.
1572             final int lastChar = mWordComposer.getCodeBeforeCursor();
1573             if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
1574                 // If we are in the middle of a recorrection, we need to commit the recorrection
1575                 // first so that we can insert the batch input at the current cursor position.
1576                 resetEntireInputState(mLastSelectionStart);
1577             } else if (wordComposerSize <= 1) {
1578                 // We auto-correct the previous (typed, not gestured) string iff it's one character
1579                 // long. The reason for this is, even in the middle of gesture typing, you'll still
1580                 // tap one-letter words and you want them auto-corrected (typically, "i" in English
1581                 // should become "I"). However for any longer word, we assume that the reason for
1582                 // tapping probably is that the word you intend to type is not in the dictionary,
1583                 // so we do not attempt to correct, on the assumption that if that was a dictionary
1584                 // word, the user would probably have gestured instead.
1585                 commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
1586             } else {
1587                 commitTyped(LastComposedWord.NOT_A_SEPARATOR);
1588             }
1589             mExpectingUpdateSelection = true;
1590         }
1591         final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
1592         if (Character.isLetterOrDigit(codePointBeforeCursor)
1593                 || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) {
1594             mSpaceState = SPACE_STATE_PHANTOM;
1595         }
1596         mConnection.endBatchEdit();
1597         mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
1598     }
1599 
1600     private static final class BatchInputUpdater implements Handler.Callback {
1601         private final Handler mHandler;
1602         private LatinIME mLatinIme;
1603         private final Object mLock = new Object();
1604         private boolean mInBatchInput; // synchronized using {@link #mLock}.
1605 
BatchInputUpdater()1606         private BatchInputUpdater() {
1607             final HandlerThread handlerThread = new HandlerThread(
1608                     BatchInputUpdater.class.getSimpleName());
1609             handlerThread.start();
1610             mHandler = new Handler(handlerThread.getLooper(), this);
1611         }
1612 
1613         // Initialization-on-demand holder
1614         private static final class OnDemandInitializationHolder {
1615             public static final BatchInputUpdater sInstance = new BatchInputUpdater();
1616         }
1617 
getInstance()1618         public static BatchInputUpdater getInstance() {
1619             return OnDemandInitializationHolder.sInstance;
1620         }
1621 
1622         private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1;
1623 
1624         @Override
handleMessage(final Message msg)1625         public boolean handleMessage(final Message msg) {
1626             switch (msg.what) {
1627             case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
1628                 updateBatchInput((InputPointers)msg.obj);
1629                 break;
1630             }
1631             return true;
1632         }
1633 
1634         // Run in the UI thread.
onStartBatchInput(final LatinIME latinIme)1635         public void onStartBatchInput(final LatinIME latinIme) {
1636             synchronized (mLock) {
1637                 mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
1638                 mLatinIme = latinIme;
1639                 mInBatchInput = true;
1640             }
1641         }
1642 
1643         // Run in the Handler thread.
updateBatchInput(final InputPointers batchPointers)1644         private void updateBatchInput(final InputPointers batchPointers) {
1645             synchronized (mLock) {
1646                 if (!mInBatchInput) {
1647                     // Batch input has ended or canceled while the message was being delivered.
1648                     return;
1649                 }
1650                 final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
1651                 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
1652                         suggestedWords, false /* dismissGestureFloatingPreviewText */);
1653             }
1654         }
1655 
1656         // Run in the UI thread.
onUpdateBatchInput(final InputPointers batchPointers)1657         public void onUpdateBatchInput(final InputPointers batchPointers) {
1658             if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
1659                 return;
1660             }
1661             mHandler.obtainMessage(
1662                     MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers)
1663                     .sendToTarget();
1664         }
1665 
onCancelBatchInput()1666         public void onCancelBatchInput() {
1667             synchronized (mLock) {
1668                 mInBatchInput = false;
1669                 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
1670                         SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
1671             }
1672         }
1673 
1674         // Run in the UI thread.
onEndBatchInput(final InputPointers batchPointers)1675         public SuggestedWords onEndBatchInput(final InputPointers batchPointers) {
1676             synchronized (mLock) {
1677                 mInBatchInput = false;
1678                 final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
1679                 mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
1680                         suggestedWords, true /* dismissGestureFloatingPreviewText */);
1681                 return suggestedWords;
1682             }
1683         }
1684 
1685         // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
1686         // be synchronized.
getSuggestedWordsGestureLocked(final InputPointers batchPointers)1687         private SuggestedWords getSuggestedWordsGestureLocked(final InputPointers batchPointers) {
1688             mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
1689             final SuggestedWords suggestedWords =
1690                     mLatinIme.getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_GESTURE);
1691             final int suggestionCount = suggestedWords.size();
1692             if (suggestionCount <= 1) {
1693                 final String mostProbableSuggestion = (suggestionCount == 0) ? null
1694                         : suggestedWords.getWord(0);
1695                 return mLatinIme.getOlderSuggestions(mostProbableSuggestion);
1696             }
1697             return suggestedWords;
1698         }
1699     }
1700 
showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, final boolean dismissGestureFloatingPreviewText)1701     private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
1702             final boolean dismissGestureFloatingPreviewText) {
1703         showSuggestionStrip(suggestedWords, null);
1704         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
1705         mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
1706         if (dismissGestureFloatingPreviewText) {
1707             mainKeyboardView.dismissGestureFloatingPreviewText();
1708         }
1709     }
1710 
1711     @Override
onUpdateBatchInput(final InputPointers batchPointers)1712     public void onUpdateBatchInput(final InputPointers batchPointers) {
1713         BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers);
1714     }
1715 
1716     @Override
onEndBatchInput(final InputPointers batchPointers)1717     public void onEndBatchInput(final InputPointers batchPointers) {
1718         final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput(
1719                 batchPointers);
1720         final String batchInputText = suggestedWords.isEmpty()
1721                 ? null : suggestedWords.getWord(0);
1722         if (TextUtils.isEmpty(batchInputText)) {
1723             return;
1724         }
1725         mWordComposer.setBatchInputWord(batchInputText);
1726         mConnection.beginBatchEdit();
1727         if (SPACE_STATE_PHANTOM == mSpaceState) {
1728             promotePhantomSpace();
1729         }
1730         mConnection.setComposingText(batchInputText, 1);
1731         mExpectingUpdateSelection = true;
1732         mConnection.endBatchEdit();
1733         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1734             ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords);
1735         }
1736         // Space state must be updated before calling updateShiftState
1737         mSpaceState = SPACE_STATE_PHANTOM;
1738         mKeyboardSwitcher.updateShiftState();
1739     }
1740 
specificTldProcessingOnTextInput(final String text)1741     private String specificTldProcessingOnTextInput(final String text) {
1742         if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
1743                 || !Character.isLetter(text.charAt(1))) {
1744             // Not a tld: do nothing.
1745             return text;
1746         }
1747         // We have a TLD (or something that looks like this): make sure we don't add
1748         // a space even if currently in phantom mode.
1749         mSpaceState = SPACE_STATE_NONE;
1750         // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code
1751         final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
1752         if (lastOne != null && lastOne.length() == 1
1753                 && lastOne.charAt(0) == Constants.CODE_PERIOD) {
1754             return text.substring(1);
1755         } else {
1756             return text;
1757         }
1758     }
1759 
1760     // Called from PointerTracker through the KeyboardActionListener interface
1761     @Override
onFinishSlidingInput()1762     public void onFinishSlidingInput() {
1763         // User finished sliding input.
1764         mKeyboardSwitcher.onFinishSlidingInput();
1765     }
1766 
1767     // Called from PointerTracker through the KeyboardActionListener interface
1768     @Override
onCancelInput()1769     public void onCancelInput() {
1770         // User released a finger outside any key
1771         // Nothing to do so far.
1772     }
1773 
1774     @Override
onCancelBatchInput()1775     public void onCancelBatchInput() {
1776         BatchInputUpdater.getInstance().onCancelBatchInput();
1777     }
1778 
handleBackspace(final int spaceState)1779     private void handleBackspace(final int spaceState) {
1780         // In many cases, we may have to put the keyboard in auto-shift state again. However
1781         // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
1782         // during key repeat.
1783         mHandler.postUpdateShiftState();
1784 
1785         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
1786             // If we are in the middle of a recorrection, we need to commit the recorrection
1787             // first so that we can remove the character at the current cursor position.
1788             resetEntireInputState(mLastSelectionStart);
1789             // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
1790         }
1791         if (mWordComposer.isComposingWord()) {
1792             final int length = mWordComposer.size();
1793             if (length > 0) {
1794                 if (mWordComposer.isBatchMode()) {
1795                     if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1796                         final String word = mWordComposer.getTypedWord();
1797                         ResearchLogger.latinIME_handleBackspace_batch(word, 1);
1798                         ResearchLogger.getInstance().uncommitCurrentLogUnit(
1799                                 word, false /* dumpCurrentLogUnit */);
1800                     }
1801                     final String rejectedSuggestion = mWordComposer.getTypedWord();
1802                     mWordComposer.reset();
1803                     mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
1804                 } else {
1805                     mWordComposer.deleteLast();
1806                 }
1807                 mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1808                 mHandler.postUpdateSuggestionStrip();
1809             } else {
1810                 mConnection.deleteSurroundingText(1, 0);
1811             }
1812         } else {
1813             if (mLastComposedWord.canRevertCommit()) {
1814                 if (mSettings.isInternal()) {
1815                     Stats.onAutoCorrectionCancellation();
1816                 }
1817                 revertCommit();
1818                 return;
1819             }
1820             if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
1821                 // Cancel multi-character input: remove the text we just entered.
1822                 // This is triggered on backspace after a key that inputs multiple characters,
1823                 // like the smiley key or the .com key.
1824                 final int length = mEnteredText.length();
1825                 mConnection.deleteSurroundingText(length, 0);
1826                 mEnteredText = null;
1827                 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
1828                 // In addition we know that spaceState is false, and that we should not be
1829                 // reverting any autocorrect at this point. So we can safely return.
1830                 return;
1831             }
1832             if (SPACE_STATE_DOUBLE == spaceState) {
1833                 mHandler.cancelDoubleSpacePeriodTimer();
1834                 if (mConnection.revertDoubleSpacePeriod()) {
1835                     // No need to reset mSpaceState, it has already be done (that's why we
1836                     // receive it as a parameter)
1837                     return;
1838                 }
1839             } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1840                 if (mConnection.revertSwapPunctuation()) {
1841                     // Likewise
1842                     return;
1843                 }
1844             }
1845 
1846             // No cancelling of commit/double space/swap: we have a regular backspace.
1847             // We should backspace one char and restart suggestion if at the end of a word.
1848             if (mLastSelectionStart != mLastSelectionEnd) {
1849                 // If there is a selection, remove it.
1850                 final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
1851                 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
1852                 // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
1853                 // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
1854                 // but we want to set it right away to avoid it being used with the wrong values
1855                 // later (typically, in a subsequent press on backspace).
1856                 mLastSelectionEnd = mLastSelectionStart;
1857                 mConnection.deleteSurroundingText(numCharsDeleted, 0);
1858                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1859                     ResearchLogger.latinIME_handleBackspace(numCharsDeleted);
1860                 }
1861             } else {
1862                 // There is no selection, just delete one character.
1863                 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
1864                     // This should never happen.
1865                     Log.e(TAG, "Backspace when we don't know the selection position");
1866                 }
1867                 if (mAppWorkAroundsUtils.isBeforeJellyBean()) {
1868                     // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
1869                     // a hardware keyboard event on pressing enter or delete. This is bad for many
1870                     // reasons (there are race conditions with commits) but some applications are
1871                     // relying on this behavior so we continue to support it for older apps.
1872                     sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL);
1873                 } else {
1874                     mConnection.deleteSurroundingText(1, 0);
1875                 }
1876                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1877                     ResearchLogger.latinIME_handleBackspace(1);
1878                 }
1879                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
1880                     mConnection.deleteSurroundingText(1, 0);
1881                     if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1882                         ResearchLogger.latinIME_handleBackspace(1);
1883                     }
1884                 }
1885             }
1886             if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) {
1887                 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
1888             }
1889         }
1890     }
1891 
1892     /*
1893      * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
1894      */
maybeStripSpace(final int code, final int spaceState, final boolean isFromSuggestionStrip)1895     private boolean maybeStripSpace(final int code,
1896             final int spaceState, final boolean isFromSuggestionStrip) {
1897         if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
1898             mConnection.removeTrailingSpace();
1899             return false;
1900         }
1901         if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
1902                 && isFromSuggestionStrip) {
1903             if (mSettings.getCurrent().isUsuallyPrecededBySpace(code)) return false;
1904             if (mSettings.getCurrent().isUsuallyFollowedBySpace(code)) return true;
1905             mConnection.removeTrailingSpace();
1906         }
1907         return false;
1908     }
1909 
handleCharacter(final int primaryCode, final int x, final int y, final int spaceState)1910     private void handleCharacter(final int primaryCode, final int x,
1911             final int y, final int spaceState) {
1912         boolean isComposingWord = mWordComposer.isComposingWord();
1913 
1914         // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
1915         // See onStartBatchInput() to see how to do it.
1916         if (SPACE_STATE_PHANTOM == spaceState &&
1917                 !mSettings.getCurrent().isWordConnector(primaryCode)) {
1918             if (isComposingWord) {
1919                 // Sanity check
1920                 throw new RuntimeException("Should not be composing here");
1921             }
1922             promotePhantomSpace();
1923         }
1924 
1925         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
1926             // If we are in the middle of a recorrection, we need to commit the recorrection
1927             // first so that we can insert the character at the current cursor position.
1928             resetEntireInputState(mLastSelectionStart);
1929             isComposingWord = false;
1930         }
1931         // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
1932         // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
1933         // thread here.
1934         if (!isComposingWord && (isAlphabet(primaryCode)
1935                 || mSettings.getCurrent().isWordConnector(primaryCode))
1936                 && mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation) &&
1937                 !mConnection.isCursorTouchingWord(mSettings.getCurrent())) {
1938             // Reset entirely the composing state anyway, then start composing a new word unless
1939             // the character is a single quote. The idea here is, single quote is not a
1940             // separator and it should be treated as a normal character, except in the first
1941             // position where it should not start composing a word.
1942             isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode);
1943             // Here we don't need to reset the last composed word. It will be reset
1944             // when we commit this one, if we ever do; if on the other hand we backspace
1945             // it entirely and resume suggestions on the previous word, we'd like to still
1946             // have touch coordinates for it.
1947             resetComposingState(false /* alsoResetLastComposedWord */);
1948         }
1949         if (isComposingWord) {
1950             final int keyX, keyY;
1951             if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) {
1952                 final KeyDetector keyDetector =
1953                         mKeyboardSwitcher.getMainKeyboardView().getKeyDetector();
1954                 keyX = keyDetector.getTouchX(x);
1955                 keyY = keyDetector.getTouchY(y);
1956             } else {
1957                 keyX = x;
1958                 keyY = y;
1959             }
1960             mWordComposer.add(primaryCode, keyX, keyY);
1961             // If it's the first letter, make note of auto-caps state
1962             if (mWordComposer.size() == 1) {
1963                 mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
1964             }
1965             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1966         } else {
1967             final boolean swapWeakSpace = maybeStripSpace(primaryCode,
1968                     spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
1969 
1970             sendKeyCodePoint(primaryCode);
1971 
1972             if (swapWeakSpace) {
1973                 swapSwapperAndSpace();
1974                 mSpaceState = SPACE_STATE_WEAK;
1975             }
1976             // In case the "add to dictionary" hint was still displayed.
1977             if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
1978         }
1979         mHandler.postUpdateSuggestionStrip();
1980         if (mSettings.isInternal()) {
1981             Utils.Stats.onNonSeparator((char)primaryCode, x, y);
1982         }
1983     }
1984 
handleRecapitalize()1985     private void handleRecapitalize() {
1986         if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
1987         // If we have a recapitalize in progress, use it; otherwise, create a new one.
1988         if (!mRecapitalizeStatus.isActive()
1989                 || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
1990             final CharSequence selectedText =
1991                     mConnection.getSelectedText(0 /* flags, 0 for no styles */);
1992             if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
1993             mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
1994                     selectedText.toString(), mSettings.getCurrentLocale(),
1995                     mSettings.getWordSeparators());
1996             // We trim leading and trailing whitespace.
1997             mRecapitalizeStatus.trim();
1998             // Trimming the object may have changed the length of the string, and we need to
1999             // reposition the selection handles accordingly. As this result in an IPC call,
2000             // only do it if it's actually necessary, in other words if the recapitalize status
2001             // is not set at the same place as before.
2002             if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
2003                 mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
2004                 mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
2005                 mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
2006             }
2007         }
2008         mRecapitalizeStatus.rotate();
2009         final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
2010         mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
2011         mConnection.deleteSurroundingText(numCharsDeleted, 0);
2012         mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
2013         mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
2014         mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
2015         mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
2016         // Match the keyboard to the new state.
2017         mKeyboardSwitcher.updateShiftState();
2018     }
2019 
2020     // Returns true if we did an autocorrection, false otherwise.
handleSeparator(final int primaryCode, final int x, final int y, final int spaceState)2021     private boolean handleSeparator(final int primaryCode, final int x, final int y,
2022             final int spaceState) {
2023         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2024             ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
2025         }
2026         boolean didAutoCorrect = false;
2027         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
2028             // If we are in the middle of a recorrection, we need to commit the recorrection
2029             // first so that we can insert the separator at the current cursor position.
2030             resetEntireInputState(mLastSelectionStart);
2031         }
2032         if (mWordComposer.isComposingWord()) {
2033             if (mSettings.getCurrent().mCorrectionEnabled) {
2034                 // TODO: maybe cache Strings in an <String> sparse array or something
2035                 commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1));
2036                 didAutoCorrect = true;
2037             } else {
2038                 commitTyped(new String(new int[]{primaryCode}, 0, 1));
2039             }
2040         }
2041 
2042         final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
2043                 Constants.SUGGESTION_STRIP_COORDINATE == x);
2044 
2045         if (SPACE_STATE_PHANTOM == spaceState &&
2046                 mSettings.getCurrent().isUsuallyPrecededBySpace(primaryCode)) {
2047             promotePhantomSpace();
2048         }
2049         sendKeyCodePoint(primaryCode);
2050 
2051         if (Constants.CODE_SPACE == primaryCode) {
2052             if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) {
2053                 if (maybeDoubleSpacePeriod()) {
2054                     mSpaceState = SPACE_STATE_DOUBLE;
2055                 } else if (!isShowingPunctuationList()) {
2056                     mSpaceState = SPACE_STATE_WEAK;
2057                 }
2058             }
2059 
2060             mHandler.startDoubleSpacePeriodTimer();
2061             mHandler.postUpdateSuggestionStrip();
2062         } else {
2063             if (swapWeakSpace) {
2064                 swapSwapperAndSpace();
2065                 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
2066             } else if (SPACE_STATE_PHANTOM == spaceState
2067                     && mSettings.getCurrent().isUsuallyFollowedBySpace(primaryCode)) {
2068                 // If we are in phantom space state, and the user presses a separator, we want to
2069                 // stay in phantom space state so that the next keypress has a chance to add the
2070                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
2071                 // then insert a comma and go on to typing the next word, I want the space to be
2072                 // inserted automatically before the next word, the same way it is when I don't
2073                 // input the comma.
2074                 // The case is a little different if the separator is a space stripper. Such a
2075                 // separator does not normally need a space on the right (that's the difference
2076                 // between swappers and strippers), so we should not stay in phantom space state if
2077                 // the separator is a stripper. Hence the additional test above.
2078                 mSpaceState = SPACE_STATE_PHANTOM;
2079             }
2080 
2081             // Set punctuation right away. onUpdateSelection will fire but tests whether it is
2082             // already displayed or not, so it's okay.
2083             setPunctuationSuggestions();
2084         }
2085         if (mSettings.isInternal()) {
2086             Utils.Stats.onSeparator((char)primaryCode, x, y);
2087         }
2088 
2089         mKeyboardSwitcher.updateShiftState();
2090         return didAutoCorrect;
2091     }
2092 
getTextWithUnderline(final String text)2093     private CharSequence getTextWithUnderline(final String text) {
2094         return mIsAutoCorrectionIndicatorOn
2095                 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
2096                 : text;
2097     }
2098 
handleClose()2099     private void handleClose() {
2100         // TODO: Verify that words are logged properly when IME is closed.
2101         commitTyped(LastComposedWord.NOT_A_SEPARATOR);
2102         requestHideSelf(0);
2103         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
2104         if (mainKeyboardView != null) {
2105             mainKeyboardView.closing();
2106         }
2107     }
2108 
2109     // TODO: make this private
2110     // Outside LatinIME, only used by the test suite.
2111     @UsedForTesting
isShowingPunctuationList()2112     boolean isShowingPunctuationList() {
2113         if (mSuggestedWords == null) return false;
2114         return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords;
2115     }
2116 
isSuggestionsStripVisible()2117     private boolean isSuggestionsStripVisible() {
2118         if (mSuggestionStripView == null)
2119             return false;
2120         if (mSuggestionStripView.isShowingAddToDictionaryHint())
2121             return true;
2122         if (null == mSettings.getCurrent())
2123             return false;
2124         if (!mSettings.getCurrent().isSuggestionStripVisibleInOrientation(mDisplayOrientation))
2125             return false;
2126         if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn())
2127             return true;
2128         return mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation);
2129     }
2130 
clearSuggestionStrip()2131     private void clearSuggestionStrip() {
2132         setSuggestedWords(SuggestedWords.EMPTY, false);
2133         setAutoCorrectionIndicator(false);
2134     }
2135 
setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection)2136     private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) {
2137         mSuggestedWords = words;
2138         if (mSuggestionStripView != null) {
2139             mSuggestionStripView.setSuggestions(words);
2140             mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
2141         }
2142     }
2143 
setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator)2144     private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
2145         // Put a blue underline to a word in TextView which will be auto-corrected.
2146         if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
2147                 && mWordComposer.isComposingWord()) {
2148             mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
2149             final CharSequence textWithUnderline =
2150                     getTextWithUnderline(mWordComposer.getTypedWord());
2151             // TODO: when called from an updateSuggestionStrip() call that results from a posted
2152             // message, this is called outside any batch edit. Potentially, this may result in some
2153             // janky flickering of the screen, although the display speed makes it unlikely in
2154             // the practice.
2155             mConnection.setComposingText(textWithUnderline, 1);
2156         }
2157     }
2158 
updateSuggestionStrip()2159     private void updateSuggestionStrip() {
2160         mHandler.cancelUpdateSuggestionStrip();
2161 
2162         // Check if we have a suggestion engine attached.
2163         if (mSuggest == null
2164                 || !mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) {
2165             if (mWordComposer.isComposingWord()) {
2166                 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
2167                         + "requested!");
2168             }
2169             return;
2170         }
2171 
2172         if (!mWordComposer.isComposingWord() && !mSettings.getCurrent().mBigramPredictionEnabled) {
2173             setPunctuationSuggestions();
2174             return;
2175         }
2176 
2177         final SuggestedWords suggestedWords =
2178                 getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_TYPING);
2179         final String typedWord = mWordComposer.getTypedWord();
2180         showSuggestionStrip(suggestedWords, typedWord);
2181     }
2182 
getSuggestedWords(final int sessionId)2183     private SuggestedWords getSuggestedWords(final int sessionId) {
2184         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
2185         if (keyboard == null || mSuggest == null) {
2186             return SuggestedWords.EMPTY;
2187         }
2188         // Get the word on which we should search the bigrams. If we are composing a word, it's
2189         // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
2190         // should just skip whitespace if any, so 1.
2191         // TODO: this is slow (2-way IPC) - we should probably cache this instead.
2192         final String prevWord =
2193                 mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators,
2194                 mWordComposer.isComposingWord() ? 2 : 1);
2195         return mSuggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
2196                 mSettings.getBlockPotentiallyOffensive(),
2197                 mSettings.getCurrent().mCorrectionEnabled, sessionId);
2198     }
2199 
getSuggestedWordsOrOlderSuggestions(final int sessionId)2200     private SuggestedWords getSuggestedWordsOrOlderSuggestions(final int sessionId) {
2201         return maybeRetrieveOlderSuggestions(mWordComposer.getTypedWord(),
2202                 getSuggestedWords(sessionId));
2203     }
2204 
maybeRetrieveOlderSuggestions(final String typedWord, final SuggestedWords suggestedWords)2205     private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
2206             final SuggestedWords suggestedWords) {
2207         // TODO: consolidate this into getSuggestedWords
2208         // We update the suggestion strip only when we have some suggestions to show, i.e. when
2209         // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
2210         // replaced with the new one. However, when the word is a dictionary word, or when the
2211         // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
2212         // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
2213         // revert to suggestions - although it is unclear how we can come here if it's displayed.
2214         if (suggestedWords.size() > 1 || typedWord.length() <= 1
2215                 || suggestedWords.mTypedWordValid || null == mSuggestionStripView
2216                 || mSuggestionStripView.isShowingAddToDictionaryHint()) {
2217             return suggestedWords;
2218         } else {
2219             return getOlderSuggestions(typedWord);
2220         }
2221     }
2222 
getOlderSuggestions(final String typedWord)2223     private SuggestedWords getOlderSuggestions(final String typedWord) {
2224         SuggestedWords previousSuggestedWords = mSuggestedWords;
2225         if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) {
2226             previousSuggestedWords = SuggestedWords.EMPTY;
2227         }
2228         if (typedWord == null) {
2229             return previousSuggestedWords;
2230         }
2231         final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
2232                 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord,
2233                         previousSuggestedWords);
2234         return new SuggestedWords(typedWordAndPreviousSuggestions,
2235                 false /* typedWordValid */,
2236                 false /* hasAutoCorrectionCandidate */,
2237                 false /* isPunctuationSuggestions */,
2238                 true /* isObsoleteSuggestions */,
2239                 false /* isPrediction */);
2240     }
2241 
showSuggestionStrip(final SuggestedWords suggestedWords, final String typedWord)2242     private void showSuggestionStrip(final SuggestedWords suggestedWords, final String typedWord) {
2243         if (suggestedWords.isEmpty()) {
2244             clearSuggestionStrip();
2245             return;
2246         }
2247         final String autoCorrection;
2248         if (suggestedWords.mWillAutoCorrect) {
2249             autoCorrection = suggestedWords.getWord(1);
2250         } else {
2251             autoCorrection = typedWord;
2252         }
2253         mWordComposer.setAutoCorrection(autoCorrection);
2254         final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
2255         setSuggestedWords(suggestedWords, isAutoCorrection);
2256         setAutoCorrectionIndicator(isAutoCorrection);
2257         setSuggestionStripShown(isSuggestionsStripVisible());
2258     }
2259 
commitCurrentAutoCorrection(final String separatorString)2260     private void commitCurrentAutoCorrection(final String separatorString) {
2261         // Complete any pending suggestions query first
2262         if (mHandler.hasPendingUpdateSuggestions()) {
2263             updateSuggestionStrip();
2264         }
2265         final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
2266         final String typedWord = mWordComposer.getTypedWord();
2267         final String autoCorrection = (typedAutoCorrection != null)
2268                 ? typedAutoCorrection : typedWord;
2269         if (autoCorrection != null) {
2270             if (TextUtils.isEmpty(typedWord)) {
2271                 throw new RuntimeException("We have an auto-correction but the typed word "
2272                         + "is empty? Impossible! I must commit suicide.");
2273             }
2274             if (mSettings.isInternal()) {
2275                 Stats.onAutoCorrection(typedWord, autoCorrection, separatorString, mWordComposer);
2276             }
2277             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2278                 final SuggestedWords suggestedWords = mSuggestedWords;
2279                 ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
2280                         separatorString, mWordComposer.isBatchMode(), suggestedWords);
2281             }
2282             mExpectingUpdateSelection = true;
2283             commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
2284                     separatorString);
2285             if (!typedWord.equals(autoCorrection)) {
2286                 // This will make the correction flash for a short while as a visual clue
2287                 // to the user that auto-correction happened. It has no other effect; in particular
2288                 // note that this won't affect the text inside the text field AT ALL: it only makes
2289                 // the segment of text starting at the supplied index and running for the length
2290                 // of the auto-correction flash. At this moment, the "typedWord" argument is
2291                 // ignored by TextView.
2292                 mConnection.commitCorrection(
2293                         new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
2294                         typedWord, autoCorrection));
2295             }
2296         }
2297     }
2298 
2299     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
2300     // interface
2301     @Override
pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo)2302     public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) {
2303         final SuggestedWords suggestedWords = mSuggestedWords;
2304         final String suggestion = suggestionInfo.mWord;
2305         // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
2306         if (suggestion.length() == 1 && isShowingPunctuationList()) {
2307             // Word separators are suggested before the user inputs something.
2308             // So, LatinImeLogger logs "" as a user's input.
2309             LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords);
2310             // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
2311             final int primaryCode = suggestion.charAt(0);
2312             onCodeInput(primaryCode,
2313                     Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
2314             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2315                 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion,
2316                         false /* isBatchMode */, suggestedWords.mIsPrediction);
2317             }
2318             return;
2319         }
2320 
2321         mConnection.beginBatchEdit();
2322         if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
2323                 // In the batch input mode, a manually picked suggested word should just replace
2324                 // the current batch input text and there is no need for a phantom space.
2325                 && !mWordComposer.isBatchMode()) {
2326             final int firstChar = Character.codePointAt(suggestion, 0);
2327             if (!mSettings.getCurrent().isWordSeparator(firstChar)
2328                     || mSettings.getCurrent().isUsuallyPrecededBySpace(firstChar)) {
2329                 promotePhantomSpace();
2330             }
2331         }
2332 
2333         if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()
2334                 && mApplicationSpecifiedCompletions != null
2335                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
2336             mSuggestedWords = SuggestedWords.EMPTY;
2337             if (mSuggestionStripView != null) {
2338                 mSuggestionStripView.clear();
2339             }
2340             mKeyboardSwitcher.updateShiftState();
2341             resetComposingState(true /* alsoResetLastComposedWord */);
2342             final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
2343             mConnection.commitCompletion(completionInfo);
2344             mConnection.endBatchEdit();
2345             return;
2346         }
2347 
2348         // We need to log before we commit, because the word composer will store away the user
2349         // typed word.
2350         final String replacedWord = mWordComposer.getTypedWord();
2351         LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords);
2352         mExpectingUpdateSelection = true;
2353         commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
2354                 LastComposedWord.NOT_A_SEPARATOR);
2355         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2356             ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
2357                     mWordComposer.isBatchMode());
2358         }
2359         mConnection.endBatchEdit();
2360         // Don't allow cancellation of manual pick
2361         mLastComposedWord.deactivate();
2362         // Space state must be updated before calling updateShiftState
2363         mSpaceState = SPACE_STATE_PHANTOM;
2364         mKeyboardSwitcher.updateShiftState();
2365 
2366         // We should show the "Touch again to save" hint if the user pressed the first entry
2367         // AND it's in none of our current dictionaries (main, user or otherwise).
2368         // Please note that if mSuggest is null, it means that everything is off: suggestion
2369         // and correction, so we shouldn't try to show the hint
2370         final boolean showingAddToDictionaryHint =
2371                 SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind && mSuggest != null
2372                 // If the suggestion is not in the dictionary, the hint should be shown.
2373                 && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true);
2374 
2375         if (mSettings.isInternal()) {
2376             Stats.onSeparator((char)Constants.CODE_SPACE,
2377                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
2378         }
2379         if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
2380             mSuggestionStripView.showAddToDictionaryHint(
2381                     suggestion, mSettings.getCurrent().mHintToSaveText);
2382         } else {
2383             // If we're not showing the "Touch again to save", then update the suggestion strip.
2384             mHandler.postUpdateSuggestionStrip();
2385         }
2386     }
2387 
2388     /**
2389      * Commits the chosen word to the text field and saves it for later retrieval.
2390      */
commitChosenWord(final String chosenWord, final int commitType, final String separatorString)2391     private void commitChosenWord(final String chosenWord, final int commitType,
2392             final String separatorString) {
2393         final SuggestedWords suggestedWords = mSuggestedWords;
2394         mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
2395                 this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
2396         // Add the word to the user history dictionary
2397         final String prevWord = addToUserHistoryDictionary(chosenWord);
2398         // TODO: figure out here if this is an auto-correct or if the best word is actually
2399         // what user typed. Note: currently this is done much later in
2400         // LastComposedWord#didCommitTypedWord by string equality of the remembered
2401         // strings.
2402         mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString,
2403                 prevWord);
2404     }
2405 
setPunctuationSuggestions()2406     private void setPunctuationSuggestions() {
2407         if (mSettings.getCurrent().mBigramPredictionEnabled) {
2408             clearSuggestionStrip();
2409         } else {
2410             setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false);
2411         }
2412         setAutoCorrectionIndicator(false);
2413         setSuggestionStripShown(isSuggestionsStripVisible());
2414     }
2415 
addToUserHistoryDictionary(final String suggestion)2416     private String addToUserHistoryDictionary(final String suggestion) {
2417         if (TextUtils.isEmpty(suggestion)) return null;
2418         if (mSuggest == null) return null;
2419 
2420         // If correction is not enabled, we don't add words to the user history dictionary.
2421         // That's to avoid unintended additions in some sensitive fields, or fields that
2422         // expect to receive non-words.
2423         if (!mSettings.getCurrent().mCorrectionEnabled) return null;
2424 
2425         final Suggest suggest = mSuggest;
2426         final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
2427         if (suggest == null || userHistoryDictionary == null) {
2428             // Avoid concurrent issue
2429             return null;
2430         }
2431         final String prevWord
2432                 = mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators, 2);
2433         final String secondWord;
2434         if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
2435             secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
2436         } else {
2437             secondWord = suggestion;
2438         }
2439         // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
2440         // We don't add words with 0-frequency (assuming they would be profanity etc.).
2441         final int maxFreq = AutoCorrection.getMaxFrequency(
2442                 suggest.getUnigramDictionaries(), suggestion);
2443         if (maxFreq == 0) return null;
2444         userHistoryDictionary.addToUserHistory(prevWord, secondWord, maxFreq > 0);
2445         return prevWord;
2446     }
2447 
2448     /**
2449      * Check if the cursor is touching a word. If so, restart suggestions on this word, else
2450      * do nothing.
2451      */
restartSuggestionsOnWordTouchedByCursor()2452     private void restartSuggestionsOnWordTouchedByCursor() {
2453         // HACK: We may want to special-case some apps that exhibit bad behavior in case of
2454         // recorrection. This is a temporary, stopgap measure that will be removed later.
2455         // TODO: remove this.
2456         if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
2457         // If the cursor is not touching a word, or if there is a selection, return right away.
2458         if (mLastSelectionStart != mLastSelectionEnd) return;
2459         // If we don't know the cursor location, return.
2460         if (mLastSelectionStart < 0) return;
2461         if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return;
2462         final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(),
2463                 0 /* additionalPrecedingWordsCount */);
2464         if (null == range) return; // Happens if we don't have an input connection at all
2465         // If for some strange reason (editor bug or so) we measure the text before the cursor as
2466         // longer than what the entire text is supposed to be, the safe thing to do is bail out.
2467         if (range.mCharsBefore > mLastSelectionStart) return;
2468         final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
2469         final String typedWord = range.mWord.toString();
2470         if (range.mWord instanceof SpannableString) {
2471             final SpannableString spannableString = (SpannableString)range.mWord;
2472             int i = 0;
2473             for (Object object : spannableString.getSpans(0, spannableString.length(),
2474                     SuggestionSpan.class)) {
2475                 SuggestionSpan span = (SuggestionSpan)object;
2476                 for (String s : span.getSuggestions()) {
2477                     ++i;
2478                     if (!TextUtils.equals(s, typedWord)) {
2479                         suggestions.add(new SuggestedWordInfo(s,
2480                                 SuggestionStripView.MAX_SUGGESTIONS - i,
2481                                 SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED));
2482                     }
2483                 }
2484             }
2485         }
2486         mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
2487         mWordComposer.setCursorPositionWithinWord(range.mCharsBefore);
2488         mConnection.setComposingRegion(mLastSelectionStart - range.mCharsBefore,
2489                 mLastSelectionEnd + range.mCharsAfter);
2490         final SuggestedWords suggestedWords;
2491         if (suggestions.isEmpty()) {
2492             // We come here if there weren't any suggestion spans on this word. We will try to
2493             // compute suggestions for it instead.
2494             final SuggestedWords suggestedWordsIncludingTypedWord =
2495                     getSuggestedWords(Suggest.SESSION_TYPING);
2496             if (suggestedWordsIncludingTypedWord.size() > 1) {
2497                 // We were able to compute new suggestions for this word.
2498                 // Remove the typed word, since we don't want to display it in this case.
2499                 // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false.
2500                 suggestedWords =
2501                         suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord();
2502             } else {
2503                 // No saved suggestions, and we were unable to compute any good one either.
2504                 // Rather than displaying an empty suggestion strip, we'll display the original
2505                 // word alone in the middle.
2506                 // Since there is only one word, willAutoCorrect is false.
2507                 suggestedWords = suggestedWordsIncludingTypedWord;
2508             }
2509         } else {
2510             // We found suggestion spans in the word. We'll create the SuggestedWords out of
2511             // them, and make willAutoCorrect false.
2512             suggestedWords = new SuggestedWords(suggestions,
2513                     true /* typedWordValid */, false /* willAutoCorrect */,
2514                     false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
2515                     false /* isPrediction */);
2516         }
2517 
2518         // Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
2519         // We never want to auto-correct on a resumed suggestion. Please refer to the three
2520         // places above where suggestedWords is affected. We also need to reset
2521         // mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching the text to adapt it.
2522         // TODO: remove mIsAutoCorrectionIndicator on (see comment on definition)
2523         mIsAutoCorrectionIndicatorOn = false;
2524         showSuggestionStrip(suggestedWords, typedWord);
2525     }
2526 
2527     /**
2528      * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
2529      * word, else do nothing.
2530      */
restartSuggestionsOnWordBeforeCursorIfAtEndOfWord()2531     private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
2532         final CharSequence word =
2533                 mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
2534         if (null != word) {
2535             final String wordString = word.toString();
2536             restartSuggestionsOnWordBeforeCursor(wordString);
2537             // TODO: Handle the case where the user manually moves the cursor and then backs up over
2538             // a separator.  In that case, the current log unit should not be uncommitted.
2539             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2540                 ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
2541                         true /* dumpCurrentLogUnit */);
2542             }
2543         }
2544     }
2545 
restartSuggestionsOnWordBeforeCursor(final String word)2546     private void restartSuggestionsOnWordBeforeCursor(final String word) {
2547         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
2548         final int length = word.length();
2549         mConnection.deleteSurroundingText(length, 0);
2550         mConnection.setComposingText(word, 1);
2551         mHandler.postUpdateSuggestionStrip();
2552     }
2553 
revertCommit()2554     private void revertCommit() {
2555         final String previousWord = mLastComposedWord.mPrevWord;
2556         final String originallyTypedWord = mLastComposedWord.mTypedWord;
2557         final String committedWord = mLastComposedWord.mCommittedWord;
2558         final int cancelLength = committedWord.length();
2559         final int separatorLength = LastComposedWord.getSeparatorLength(
2560                 mLastComposedWord.mSeparatorString);
2561         // TODO: should we check our saved separator against the actual contents of the text view?
2562         final int deleteLength = cancelLength + separatorLength;
2563         if (DEBUG) {
2564             if (mWordComposer.isComposingWord()) {
2565                 throw new RuntimeException("revertCommit, but we are composing a word");
2566             }
2567             final CharSequence wordBeforeCursor =
2568                     mConnection.getTextBeforeCursor(deleteLength, 0)
2569                             .subSequence(0, cancelLength);
2570             if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
2571                 throw new RuntimeException("revertCommit check failed: we thought we were "
2572                         + "reverting \"" + committedWord
2573                         + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
2574             }
2575         }
2576         mConnection.deleteSurroundingText(deleteLength, 0);
2577         if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
2578             mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
2579         }
2580         mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1);
2581         if (mSettings.isInternal()) {
2582             Stats.onSeparator(mLastComposedWord.mSeparatorString,
2583                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
2584         }
2585         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2586             ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
2587                     mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
2588             ResearchLogger.getInstance().uncommitCurrentLogUnit(committedWord,
2589                     true /* dumpCurrentLogUnit */);
2590         }
2591         // Don't restart suggestion yet. We'll restart if the user deletes the
2592         // separator.
2593         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
2594         // We have a separator between the word and the cursor: we should show predictions.
2595         mHandler.postUpdateSuggestionStrip();
2596     }
2597 
2598     // This essentially inserts a space, and that's it.
promotePhantomSpace()2599     public void promotePhantomSpace() {
2600         if (mSettings.getCurrent().shouldInsertSpacesAutomatically()
2601                 && !mConnection.textBeforeCursorLooksLikeURL()) {
2602             sendKeyCodePoint(Constants.CODE_SPACE);
2603             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
2604                 ResearchLogger.latinIME_promotePhantomSpace();
2605             }
2606         }
2607     }
2608 
2609     // Used by the RingCharBuffer
isWordSeparator(final int code)2610     public boolean isWordSeparator(final int code) {
2611         return mSettings.getCurrent().isWordSeparator(code);
2612     }
2613 
2614     // TODO: Make this private
2615     // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
2616     @UsedForTesting
loadKeyboard()2617     void loadKeyboard() {
2618         // TODO: Why are we calling {@link #loadSettings()} and {@link #initSuggest()} in a
2619         // different order than in {@link #onStartInputView}?
2620         initSuggest();
2621         loadSettings();
2622         if (mKeyboardSwitcher.getMainKeyboardView() != null) {
2623             // Reload keyboard because the current language has been changed.
2624             mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
2625         }
2626         // Since we just changed languages, we should re-evaluate suggestions with whatever word
2627         // we are currently composing. If we are not composing anything, we may want to display
2628         // predictions or punctuation signs (which is done by the updateSuggestionStrip anyway).
2629         mHandler.postUpdateSuggestionStrip();
2630     }
2631 
2632     // Callback called by PointerTracker through the KeyboardActionListener. This is called when a
2633     // key is depressed; release matching call is onReleaseKey below.
2634     @Override
onPressKey(final int primaryCode, final boolean isSinglePointer)2635     public void onPressKey(final int primaryCode, final boolean isSinglePointer) {
2636         mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
2637     }
2638 
2639     // Callback by PointerTracker through the KeyboardActionListener. This is called when a key
2640     // is released; press matching call is onPressKey above.
2641     @Override
onReleaseKey(final int primaryCode, final boolean withSliding)2642     public void onReleaseKey(final int primaryCode, final boolean withSliding) {
2643         mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
2644 
2645         // If accessibility is on, ensure the user receives keyboard state updates.
2646         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
2647             switch (primaryCode) {
2648             case Constants.CODE_SHIFT:
2649                 AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
2650                 break;
2651             case Constants.CODE_SWITCH_ALPHA_SYMBOL:
2652                 AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
2653                 break;
2654             }
2655         }
2656 
2657         if (Constants.CODE_DELETE == primaryCode) {
2658             // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
2659             // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
2660             // pair-friendly way of deleting characters in InputConnection.
2661             // TODO: use getCodePointBeforeCursor instead to improve performance
2662             final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0);
2663             if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
2664                 mConnection.deleteSurroundingText(1, 0);
2665             }
2666         }
2667     }
2668 
2669     // Hooks for hardware keyboard
2670     @Override
onKeyDown(final int keyCode, final KeyEvent event)2671     public boolean onKeyDown(final int keyCode, final KeyEvent event) {
2672         if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event);
2673         // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if
2674         // it doesn't know what to do with it and leave it to the application. For example,
2675         // hardware key events for adjusting the screen's brightness are passed as is.
2676         if (mEventInterpreter.onHardwareKeyEvent(event)) {
2677             final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
2678             mCurrentlyPressedHardwareKeys.add(keyIdentifier);
2679             return true;
2680         }
2681         return super.onKeyDown(keyCode, event);
2682     }
2683 
2684     @Override
onKeyUp(final int keyCode, final KeyEvent event)2685     public boolean onKeyUp(final int keyCode, final KeyEvent event) {
2686         final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
2687         if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
2688             return true;
2689         }
2690         return super.onKeyUp(keyCode, event);
2691     }
2692 
2693     // onKeyDown and onKeyUp are the main events we are interested in. There are two more events
2694     // related to handling of hardware key events that we may want to implement in the future:
2695     // boolean onKeyLongPress(final int keyCode, final KeyEvent event);
2696     // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event);
2697 
2698     // receive ringer mode change and network state change.
2699     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2700         @Override
2701         public void onReceive(final Context context, final Intent intent) {
2702             final String action = intent.getAction();
2703             if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2704                 mSubtypeSwitcher.onNetworkStateChanged(intent);
2705             } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2706                 mKeyboardSwitcher.onRingerModeChanged();
2707             }
2708         }
2709     };
2710 
launchSettings()2711     private void launchSettings() {
2712         handleClose();
2713         launchSubActivity(SettingsActivity.class);
2714     }
2715 
launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass)2716     public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
2717         // Put the text in the attached EditText into a safe, saved state before switching to a
2718         // new activity that will also use the soft keyboard.
2719         commitTyped(LastComposedWord.NOT_A_SEPARATOR);
2720         launchSubActivity(activityClass);
2721     }
2722 
launchSubActivity(final Class<? extends Activity> activityClass)2723     private void launchSubActivity(final Class<? extends Activity> activityClass) {
2724         Intent intent = new Intent();
2725         intent.setClass(LatinIME.this, activityClass);
2726         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
2727                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2728                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2729         startActivity(intent);
2730     }
2731 
showSubtypeSelectorAndSettings()2732     private void showSubtypeSelectorAndSettings() {
2733         final CharSequence title = getString(R.string.english_ime_input_options);
2734         final CharSequence[] items = new CharSequence[] {
2735                 // TODO: Should use new string "Select active input modes".
2736                 getString(R.string.language_selection_title),
2737                 getString(Utils.getAcitivityTitleResId(this, SettingsActivity.class)),
2738         };
2739         final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2740             @Override
2741             public void onClick(DialogInterface di, int position) {
2742                 di.dismiss();
2743                 switch (position) {
2744                 case 0:
2745                     final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
2746                             mRichImm.getInputMethodIdOfThisIme(),
2747                             Intent.FLAG_ACTIVITY_NEW_TASK
2748                             | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2749                             | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2750                     startActivity(intent);
2751                     break;
2752                 case 1:
2753                     launchSettings();
2754                     break;
2755                 }
2756             }
2757         };
2758         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2759                 .setItems(items, listener)
2760                 .setTitle(title);
2761         showOptionDialog(builder.create());
2762     }
2763 
showOptionDialog(final AlertDialog dialog)2764     public void showOptionDialog(final AlertDialog dialog) {
2765         final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
2766         if (windowToken == null) {
2767             return;
2768         }
2769 
2770         dialog.setCancelable(true);
2771         dialog.setCanceledOnTouchOutside(true);
2772 
2773         final Window window = dialog.getWindow();
2774         final WindowManager.LayoutParams lp = window.getAttributes();
2775         lp.token = windowToken;
2776         lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
2777         window.setAttributes(lp);
2778         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
2779 
2780         mOptionsDialog = dialog;
2781         dialog.show();
2782     }
2783 
2784     // TODO: can this be removed somehow without breaking the tests?
2785     @UsedForTesting
getFirstSuggestedWord()2786     /* package for test */ String getFirstSuggestedWord() {
2787         return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null;
2788     }
2789 
debugDumpStateAndCrashWithException(final String context)2790     public void debugDumpStateAndCrashWithException(final String context) {
2791         final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
2792         s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
2793                 .append("\nContext : ").append(context);
2794         throw new RuntimeException(s.toString());
2795     }
2796 
2797     @Override
dump(final FileDescriptor fd, final PrintWriter fout, final String[] args)2798     protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) {
2799         super.dump(fd, fout, args);
2800 
2801         final Printer p = new PrintWriterPrinter(fout);
2802         p.println("LatinIME state :");
2803         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
2804         final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
2805         p.println("  Keyboard mode = " + keyboardMode);
2806         final SettingsValues settingsValues = mSettings.getCurrent();
2807         p.println("  mIsSuggestionsSuggestionsRequested = "
2808                 + settingsValues.isSuggestionsRequested(mDisplayOrientation));
2809         p.println("  mCorrectionEnabled=" + settingsValues.mCorrectionEnabled);
2810         p.println("  isComposingWord=" + mWordComposer.isComposingWord());
2811         p.println("  mSoundOn=" + settingsValues.mSoundOn);
2812         p.println("  mVibrateOn=" + settingsValues.mVibrateOn);
2813         p.println("  mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn);
2814         p.println("  inputAttributes=" + settingsValues.mInputAttributes);
2815     }
2816 }
2817