• 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"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.inputmethod.latin;
18 
19 import android.app.AlertDialog;
20 import android.content.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.SharedPreferences;
26 import android.content.res.Configuration;
27 import android.content.res.Resources;
28 import android.inputmethodservice.InputMethodService;
29 import android.media.AudioManager;
30 import android.net.ConnectivityManager;
31 import android.os.Build;
32 import android.os.Debug;
33 import android.os.Message;
34 import android.os.SystemClock;
35 import android.preference.PreferenceActivity;
36 import android.preference.PreferenceManager;
37 import android.text.InputType;
38 import android.text.TextUtils;
39 import android.util.Log;
40 import android.util.PrintWriterPrinter;
41 import android.util.Printer;
42 import android.view.HapticFeedbackConstants;
43 import android.view.KeyEvent;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.view.ViewParent;
47 import android.view.inputmethod.CompletionInfo;
48 import android.view.inputmethod.EditorInfo;
49 import android.view.inputmethod.ExtractedText;
50 import android.view.inputmethod.InputConnection;
51 
52 import com.android.inputmethod.accessibility.AccessibilityUtils;
53 import com.android.inputmethod.compat.CompatUtils;
54 import com.android.inputmethod.compat.EditorInfoCompatUtils;
55 import com.android.inputmethod.compat.InputConnectionCompatUtils;
56 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
57 import com.android.inputmethod.compat.InputMethodServiceCompatWrapper;
58 import com.android.inputmethod.compat.InputTypeCompatUtils;
59 import com.android.inputmethod.compat.SuggestionSpanUtils;
60 import com.android.inputmethod.compat.VibratorCompatWrapper;
61 import com.android.inputmethod.deprecated.LanguageSwitcherProxy;
62 import com.android.inputmethod.deprecated.VoiceProxy;
63 import com.android.inputmethod.keyboard.Key;
64 import com.android.inputmethod.keyboard.Keyboard;
65 import com.android.inputmethod.keyboard.KeyboardActionListener;
66 import com.android.inputmethod.keyboard.KeyboardSwitcher;
67 import com.android.inputmethod.keyboard.KeyboardView;
68 import com.android.inputmethod.keyboard.LatinKeyboard;
69 import com.android.inputmethod.keyboard.LatinKeyboardView;
70 
71 import java.io.FileDescriptor;
72 import java.io.PrintWriter;
73 import java.util.Locale;
74 
75 /**
76  * Input method implementation for Qwerty'ish keyboard.
77  */
78 public class LatinIME extends InputMethodServiceCompatWrapper implements KeyboardActionListener,
79         SuggestionsView.Listener {
80     private static final String TAG = LatinIME.class.getSimpleName();
81     private static final boolean PERF_DEBUG = false;
82     private static final boolean TRACE = false;
83     private static boolean DEBUG;
84 
85     /**
86      * The private IME option used to indicate that no microphone should be
87      * shown for a given text field. For instance, this is specified by the
88      * search dialog when the dialog is already showing a voice search button.
89      *
90      * @deprecated Use {@link LatinIME#IME_OPTION_NO_MICROPHONE} with package name prefixed.
91      */
92     @SuppressWarnings("dep-ann")
93     public static final String IME_OPTION_NO_MICROPHONE_COMPAT = "nm";
94 
95     /**
96      * The private IME option used to indicate that no microphone should be
97      * shown for a given text field. For instance, this is specified by the
98      * search dialog when the dialog is already showing a voice search button.
99      */
100     public static final String IME_OPTION_NO_MICROPHONE = "noMicrophoneKey";
101 
102     /**
103      * The private IME option used to indicate that no settings key should be
104      * shown for a given text field.
105      */
106     public static final String IME_OPTION_NO_SETTINGS_KEY = "noSettingsKey";
107 
108     /**
109      * The private IME option used to indicate that the given text field needs
110      * ASCII code points input.
111      */
112     public static final String IME_OPTION_FORCE_ASCII = "forceAscii";
113 
114     /**
115      * The subtype extra value used to indicate that the subtype keyboard layout is capable for
116      * typing ASCII characters.
117      */
118     public static final String SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE = "AsciiCapable";
119 
120     /**
121      * The subtype extra value used to indicate that the subtype keyboard layout supports touch
122      * position correction.
123      */
124     public static final String SUBTYPE_EXTRA_VALUE_SUPPORT_TOUCH_POSITION_CORRECTION =
125             "SupportTouchPositionCorrection";
126     /**
127      * The subtype extra value used to indicate that the subtype keyboard layout should be loaded
128      * from the specified locale.
129      */
130     public static final String SUBTYPE_EXTRA_VALUE_KEYBOARD_LOCALE = "KeyboardLocale";
131 
132     private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
133 
134     // How many continuous deletes at which to start deleting at a higher speed.
135     private static final int DELETE_ACCELERATE_AT = 20;
136     // Key events coming any faster than this are long-presses.
137     private static final int QUICK_PRESS = 200;
138 
139     private static final int PENDING_IMS_CALLBACK_DURATION = 800;
140 
141     /**
142      * The name of the scheme used by the Package Manager to warn of a new package installation,
143      * replacement or removal.
144      */
145     private static final String SCHEME_PACKAGE = "package";
146 
147     private int mSuggestionVisibility;
148     private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
149             = R.string.prefs_suggestion_visibility_show_value;
150     private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
151             = R.string.prefs_suggestion_visibility_show_only_portrait_value;
152     private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
153             = R.string.prefs_suggestion_visibility_hide_value;
154 
155     private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
156         SUGGESTION_VISIBILILTY_SHOW_VALUE,
157         SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
158         SUGGESTION_VISIBILILTY_HIDE_VALUE
159     };
160 
161     private Settings.Values mSettingsValues;
162 
163     private View mExtractArea;
164     private View mKeyPreviewBackingView;
165     private View mSuggestionsContainer;
166     private SuggestionsView mSuggestionsView;
167     private Suggest mSuggest;
168     private CompletionInfo[] mApplicationSpecifiedCompletions;
169 
170     private InputMethodManagerCompatWrapper mImm;
171     private Resources mResources;
172     private SharedPreferences mPrefs;
173     private String mInputMethodId;
174     private KeyboardSwitcher mKeyboardSwitcher;
175     private SubtypeSwitcher mSubtypeSwitcher;
176     private VoiceProxy mVoiceProxy;
177 
178     private UserDictionary mUserDictionary;
179     private UserBigramDictionary mUserBigramDictionary;
180     private UserUnigramDictionary mUserUnigramDictionary;
181     private boolean mIsUserDictionaryAvaliable;
182 
183     // TODO: Create an inner class to group options and pseudo-options to improve readability.
184     // These variables are initialized according to the {@link EditorInfo#inputType}.
185     private boolean mShouldInsertMagicSpace;
186     private boolean mInputTypeNoAutoCorrect;
187     private boolean mIsSettingsSuggestionStripOn;
188     private boolean mApplicationSpecifiedCompletionOn;
189 
190     private final StringBuilder mComposingStringBuilder = new StringBuilder();
191     private WordComposer mWordComposer = new WordComposer();
192     private CharSequence mBestWord;
193     private boolean mHasUncommittedTypedChars;
194     // Magic space: a space that should disappear on space/apostrophe insertion, move after the
195     // punctuation on punctuation insertion, and become a real space on alpha char insertion.
196     private boolean mJustAddedMagicSpace; // This indicates whether the last char is a magic space.
197     // This indicates whether the last keypress resulted in processing of double space replacement
198     // with period-space.
199     private boolean mJustReplacedDoubleSpace;
200 
201     private int mCorrectionMode;
202     private int mCommittedLength;
203     // Keep track of the last selection range to decide if we need to show word alternatives
204     private int mLastSelectionStart;
205     private int mLastSelectionEnd;
206 
207     // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
208     // "expect" it, it means the user actually moved the cursor.
209     private boolean mExpectingUpdateSelection;
210     private int mDeleteCount;
211     private long mLastKeyTime;
212 
213     private AudioManager mAudioManager;
214     private float mFxVolume = -1.0f; // default volume
215     private boolean mSilentModeOn; // System-wide current configuration
216 
217     private VibratorCompatWrapper mVibrator;
218     private long mKeypressVibrationDuration = -1;
219 
220     // TODO: Move this flag to VoiceProxy
221     private boolean mConfigurationChanging;
222 
223     // Member variables for remembering the current device orientation.
224     private int mDisplayOrientation;
225 
226     // Object for reacting to adding/removing a dictionary pack.
227     private BroadcastReceiver mDictionaryPackInstallReceiver =
228             new DictionaryPackInstallBroadcastReceiver(this);
229 
230     // Keeps track of most recently inserted text (multi-character key) for reverting
231     private CharSequence mEnteredText;
232 
233     private final ComposingStateManager mComposingStateManager =
234             ComposingStateManager.getInstance();
235 
236     public final UIHandler mHandler = new UIHandler(this);
237 
238     public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
239         private static final int MSG_UPDATE_SUGGESTIONS = 0;
240         private static final int MSG_UPDATE_SHIFT_STATE = 1;
241         private static final int MSG_VOICE_RESULTS = 2;
242         private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 3;
243         private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 4;
244         private static final int MSG_SPACE_TYPED = 5;
245         private static final int MSG_SET_BIGRAM_PREDICTIONS = 6;
246         private static final int MSG_PENDING_IMS_CALLBACK = 7;
247 
UIHandler(LatinIME outerInstance)248         public UIHandler(LatinIME outerInstance) {
249             super(outerInstance);
250         }
251 
252         @Override
handleMessage(Message msg)253         public void handleMessage(Message msg) {
254             final LatinIME latinIme = getOuterInstance();
255             final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
256             final LatinKeyboardView inputView = switcher.getKeyboardView();
257             switch (msg.what) {
258             case MSG_UPDATE_SUGGESTIONS:
259                 latinIme.updateSuggestions();
260                 break;
261             case MSG_UPDATE_SHIFT_STATE:
262                 switcher.updateShiftState();
263                 break;
264             case MSG_SET_BIGRAM_PREDICTIONS:
265                 latinIme.updateBigramPredictions();
266                 break;
267             case MSG_VOICE_RESULTS:
268                 latinIme.mVoiceProxy.handleVoiceResults(latinIme.preferCapitalization()
269                         || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked()));
270                 break;
271             case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR:
272                 if (inputView != null) {
273                     inputView.setSpacebarTextFadeFactor(
274                             (1.0f + latinIme.mSettingsValues.
275                                     mFinalFadeoutFactorOfLanguageOnSpacebar) / 2,
276                             (LatinKeyboard)msg.obj);
277                 }
278                 sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj),
279                         latinIme.mSettingsValues.mDurationOfFadeoutLanguageOnSpacebar);
280                 break;
281             case MSG_DISMISS_LANGUAGE_ON_SPACEBAR:
282                 if (inputView != null) {
283                     inputView.setSpacebarTextFadeFactor(
284                             latinIme.mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar,
285                             (LatinKeyboard)msg.obj);
286                 }
287                 break;
288             }
289         }
290 
postUpdateSuggestions()291         public void postUpdateSuggestions() {
292             removeMessages(MSG_UPDATE_SUGGESTIONS);
293             sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS),
294                     getOuterInstance().mSettingsValues.mDelayUpdateSuggestions);
295         }
296 
cancelUpdateSuggestions()297         public void cancelUpdateSuggestions() {
298             removeMessages(MSG_UPDATE_SUGGESTIONS);
299         }
300 
hasPendingUpdateSuggestions()301         public boolean hasPendingUpdateSuggestions() {
302             return hasMessages(MSG_UPDATE_SUGGESTIONS);
303         }
304 
postUpdateShiftKeyState()305         public void postUpdateShiftKeyState() {
306             removeMessages(MSG_UPDATE_SHIFT_STATE);
307             sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE),
308                     getOuterInstance().mSettingsValues.mDelayUpdateShiftState);
309         }
310 
cancelUpdateShiftState()311         public void cancelUpdateShiftState() {
312             removeMessages(MSG_UPDATE_SHIFT_STATE);
313         }
314 
postUpdateBigramPredictions()315         public void postUpdateBigramPredictions() {
316             removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
317             sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS),
318                     getOuterInstance().mSettingsValues.mDelayUpdateSuggestions);
319         }
320 
cancelUpdateBigramPredictions()321         public void cancelUpdateBigramPredictions() {
322             removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
323         }
324 
updateVoiceResults()325         public void updateVoiceResults() {
326             sendMessage(obtainMessage(MSG_VOICE_RESULTS));
327         }
328 
startDisplayLanguageOnSpacebar(boolean localeChanged)329         public void startDisplayLanguageOnSpacebar(boolean localeChanged) {
330             final LatinIME latinIme = getOuterInstance();
331             removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR);
332             removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR);
333             final LatinKeyboardView inputView = latinIme.mKeyboardSwitcher.getKeyboardView();
334             if (inputView != null) {
335                 final LatinKeyboard keyboard = latinIme.mKeyboardSwitcher.getLatinKeyboard();
336                 // The language is always displayed when the delay is negative.
337                 final boolean needsToDisplayLanguage = localeChanged
338                         || latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar < 0;
339                 // The language is never displayed when the delay is zero.
340                 if (latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar != 0) {
341                     inputView.setSpacebarTextFadeFactor(needsToDisplayLanguage ? 1.0f
342                             : latinIme.mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar,
343                             keyboard);
344                 }
345                 // The fadeout animation will start when the delay is positive.
346                 if (localeChanged
347                         && latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar > 0) {
348                     sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard),
349                             latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar);
350                 }
351             }
352         }
353 
354         public void startDoubleSpacesTimer() {
355             removeMessages(MSG_SPACE_TYPED);
356             sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED),
357                     getOuterInstance().mSettingsValues.mDoubleSpacesTurnIntoPeriodTimeout);
358         }
359 
360         public void cancelDoubleSpacesTimer() {
361             removeMessages(MSG_SPACE_TYPED);
362         }
363 
364         public boolean isAcceptingDoubleSpaces() {
365             return hasMessages(MSG_SPACE_TYPED);
366         }
367 
368         // Working variables for the following methods.
369         private boolean mIsOrientationChanging;
370         private boolean mPendingSuccesiveImsCallback;
371         private boolean mHasPendingStartInput;
372         private boolean mHasPendingFinishInputView;
373         private boolean mHasPendingFinishInput;
374 
startOrientationChanging()375         public void startOrientationChanging() {
376             mIsOrientationChanging = true;
377             final LatinIME latinIme = getOuterInstance();
378             latinIme.mKeyboardSwitcher.saveKeyboardState();
379         }
380 
resetPendingImsCallback()381         private void resetPendingImsCallback() {
382             mHasPendingFinishInputView = false;
383             mHasPendingFinishInput = false;
384             mHasPendingStartInput = false;
385         }
386 
executePendingImsCallback(LatinIME latinIme, EditorInfo attribute, boolean restarting)387         private void executePendingImsCallback(LatinIME latinIme, EditorInfo attribute,
388                 boolean restarting) {
389             if (mHasPendingFinishInputView)
390                 latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
391             if (mHasPendingFinishInput)
392                 latinIme.onFinishInputInternal();
393             if (mHasPendingStartInput)
394                 latinIme.onStartInputInternal(attribute, restarting);
395             resetPendingImsCallback();
396         }
397 
onStartInput(EditorInfo attribute, boolean restarting)398         public void onStartInput(EditorInfo attribute, boolean restarting) {
399             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
400                 // Typically this is the second onStartInput after orientation changed.
401                 mHasPendingStartInput = true;
402             } else {
403                 if (mIsOrientationChanging && restarting) {
404                     // This is the first onStartInput after orientation changed.
405                     mIsOrientationChanging = false;
406                     mPendingSuccesiveImsCallback = true;
407                 }
408                 final LatinIME latinIme = getOuterInstance();
409                 executePendingImsCallback(latinIme, attribute, restarting);
410                 latinIme.onStartInputInternal(attribute, restarting);
411             }
412         }
413 
onStartInputView(EditorInfo attribute, boolean restarting)414         public void onStartInputView(EditorInfo attribute, boolean restarting) {
415              if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
416                  // Typically this is the second onStartInputView after orientation changed.
417                  resetPendingImsCallback();
418              } else {
419                  if (mPendingSuccesiveImsCallback) {
420                      // This is the first onStartInputView after orientation changed.
421                      mPendingSuccesiveImsCallback = false;
422                      resetPendingImsCallback();
423                      sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
424                              PENDING_IMS_CALLBACK_DURATION);
425                  }
426                  final LatinIME latinIme = getOuterInstance();
427                  executePendingImsCallback(latinIme, attribute, restarting);
428                  latinIme.onStartInputViewInternal(attribute, restarting);
429              }
430         }
431 
onFinishInputView(boolean finishingInput)432         public void onFinishInputView(boolean finishingInput) {
433             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
434                 // Typically this is the first onFinishInputView after orientation changed.
435                 mHasPendingFinishInputView = true;
436             } else {
437                 final LatinIME latinIme = getOuterInstance();
438                 latinIme.onFinishInputViewInternal(finishingInput);
439             }
440         }
441 
onFinishInput()442         public void onFinishInput() {
443             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
444                 // Typically this is the first onFinishInput after orientation changed.
445                 mHasPendingFinishInput = true;
446             } else {
447                 final LatinIME latinIme = getOuterInstance();
448                 executePendingImsCallback(latinIme, null, false);
449                 latinIme.onFinishInputInternal();
450             }
451         }
452     }
453 
454     @Override
onCreate()455     public void onCreate() {
456         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
457         mPrefs = prefs;
458         LatinImeLogger.init(this, prefs);
459         LanguageSwitcherProxy.init(this, prefs);
460         InputMethodManagerCompatWrapper.init(this);
461         SubtypeSwitcher.init(this);
462         KeyboardSwitcher.init(this, prefs);
463         AccessibilityUtils.init(this, prefs);
464 
465         super.onCreate();
466 
467         mImm = InputMethodManagerCompatWrapper.getInstance();
468         mInputMethodId = Utils.getInputMethodId(mImm, getPackageName());
469         mSubtypeSwitcher = SubtypeSwitcher.getInstance();
470         mKeyboardSwitcher = KeyboardSwitcher.getInstance();
471         mVibrator = VibratorCompatWrapper.getInstance(this);
472         DEBUG = LatinImeLogger.sDBG;
473 
474         final Resources res = getResources();
475         mResources = res;
476 
477         loadSettings();
478 
479         Utils.GCUtils.getInstance().reset();
480         boolean tryGC = true;
481         for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
482             try {
483                 initSuggest();
484                 tryGC = false;
485             } catch (OutOfMemoryError e) {
486                 tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
487             }
488         }
489 
490         mDisplayOrientation = res.getConfiguration().orientation;
491 
492         // Register to receive ringer mode change and network state change.
493         // Also receive installation and removal of a dictionary pack.
494         final IntentFilter filter = new IntentFilter();
495         filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
496         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
497         registerReceiver(mReceiver, filter);
498         mVoiceProxy = VoiceProxy.init(this, prefs, mHandler);
499 
500         final IntentFilter packageFilter = new IntentFilter();
501         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
502         packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
503         packageFilter.addDataScheme(SCHEME_PACKAGE);
504         registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
505 
506         final IntentFilter newDictFilter = new IntentFilter();
507         newDictFilter.addAction(
508                 DictionaryPackInstallBroadcastReceiver.NEW_DICTIONARY_INTENT_ACTION);
509         registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
510     }
511 
512     // Has to be package-visible for unit tests
loadSettings()513     /* package */ void loadSettings() {
514         if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
515         if (null == mSubtypeSwitcher) mSubtypeSwitcher = SubtypeSwitcher.getInstance();
516         mSettingsValues = new Settings.Values(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr());
517         resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
518         updateSoundEffectVolume();
519         updateKeypressVibrationDuration();
520     }
521 
initSuggest()522     private void initSuggest() {
523         final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
524         final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr);
525 
526         final Resources res = mResources;
527         final Locale savedLocale = LocaleUtils.setSystemLocale(res, keyboardLocale);
528         final ContactsDictionary oldContactsDictionary;
529         if (mSuggest != null) {
530             oldContactsDictionary = mSuggest.getContactsDictionary();
531             mSuggest.close();
532         } else {
533             oldContactsDictionary = null;
534         }
535 
536         int mainDicResId = Utils.getMainDictionaryResourceId(res);
537         mSuggest = new Suggest(this, mainDicResId, keyboardLocale);
538         if (mSettingsValues.mAutoCorrectEnabled) {
539             mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
540         }
541 
542         mUserDictionary = new UserDictionary(this, localeStr);
543         mSuggest.setUserDictionary(mUserDictionary);
544         mIsUserDictionaryAvaliable = mUserDictionary.isEnabled();
545 
546         resetContactsDictionary(oldContactsDictionary);
547 
548         mUserUnigramDictionary
549                 = new UserUnigramDictionary(this, this, localeStr, Suggest.DIC_USER_UNIGRAM);
550         mSuggest.setUserUnigramDictionary(mUserUnigramDictionary);
551 
552         mUserBigramDictionary
553                 = new UserBigramDictionary(this, this, localeStr, Suggest.DIC_USER_BIGRAM);
554         mSuggest.setUserBigramDictionary(mUserBigramDictionary);
555 
556         updateCorrectionMode();
557 
558         LocaleUtils.setSystemLocale(res, savedLocale);
559     }
560 
561     /**
562      * Resets the contacts dictionary in mSuggest according to the user settings.
563      *
564      * This method takes an optional contacts dictionary to use. Since the contacts dictionary
565      * does not depend on the locale, it can be reused across different instances of Suggest.
566      * The dictionary will also be opened or closed as necessary depending on the settings.
567      *
568      * @param oldContactsDictionary an optional dictionary to use, or null
569      */
resetContactsDictionary(final ContactsDictionary oldContactsDictionary)570     private void resetContactsDictionary(final ContactsDictionary oldContactsDictionary) {
571         final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict);
572 
573         final ContactsDictionary dictionaryToUse;
574         if (!shouldSetDictionary) {
575             // Make sure the dictionary is closed. If it is already closed, this is a no-op,
576             // so it's safe to call it anyways.
577             if (null != oldContactsDictionary) oldContactsDictionary.close();
578             dictionaryToUse = null;
579         } else if (null != oldContactsDictionary) {
580             // Make sure the old contacts dictionary is opened. If it is already open, this is a
581             // no-op, so it's safe to call it anyways.
582             oldContactsDictionary.reopen(this);
583             dictionaryToUse = oldContactsDictionary;
584         } else {
585             dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
586         }
587 
588         if (null != mSuggest) {
589             mSuggest.setContactsDictionary(dictionaryToUse);
590         }
591     }
592 
resetSuggestMainDict()593     /* package private */ void resetSuggestMainDict() {
594         final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
595         final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr);
596         int mainDicResId = Utils.getMainDictionaryResourceId(mResources);
597         mSuggest.resetMainDict(this, mainDicResId, keyboardLocale);
598     }
599 
600     @Override
onDestroy()601     public void onDestroy() {
602         if (mSuggest != null) {
603             mSuggest.close();
604             mSuggest = null;
605         }
606         unregisterReceiver(mReceiver);
607         unregisterReceiver(mDictionaryPackInstallReceiver);
608         mVoiceProxy.destroy();
609         LatinImeLogger.commit();
610         LatinImeLogger.onDestroy();
611         super.onDestroy();
612     }
613 
614     @Override
onConfigurationChanged(Configuration conf)615     public void onConfigurationChanged(Configuration conf) {
616         mSubtypeSwitcher.onConfigurationChanged(conf);
617         mComposingStateManager.onFinishComposingText();
618         // If orientation changed while predicting, commit the change
619         if (mDisplayOrientation != conf.orientation) {
620             mDisplayOrientation = conf.orientation;
621             mHandler.startOrientationChanging();
622             final InputConnection ic = getCurrentInputConnection();
623             commitTyped(ic);
624             if (ic != null) ic.finishComposingText(); // For voice input
625             if (isShowingOptionDialog())
626                 mOptionsDialog.dismiss();
627         }
628 
629         mConfigurationChanging = true;
630         super.onConfigurationChanged(conf);
631         mVoiceProxy.onConfigurationChanged(conf);
632         mConfigurationChanging = false;
633 
634         // This will work only when the subtype is not supported.
635         LanguageSwitcherProxy.onConfigurationChanged(conf);
636     }
637 
638     @Override
onCreateInputView()639     public View onCreateInputView() {
640         return mKeyboardSwitcher.onCreateInputView();
641     }
642 
643     @Override
setInputView(View view)644     public void setInputView(View view) {
645         super.setInputView(view);
646         mExtractArea = getWindow().getWindow().getDecorView()
647                 .findViewById(android.R.id.extractArea);
648         mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
649         mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
650         mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view);
651         if (mSuggestionsView != null)
652             mSuggestionsView.setListener(this, view);
653         if (LatinImeLogger.sVISUALDEBUG) {
654             mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
655         }
656     }
657 
658     @Override
setCandidatesView(View view)659     public void setCandidatesView(View view) {
660         // To ensure that CandidatesView will never be set.
661         return;
662     }
663 
664     @Override
onStartInput(EditorInfo attribute, boolean restarting)665     public void onStartInput(EditorInfo attribute, boolean restarting) {
666         mHandler.onStartInput(attribute, restarting);
667     }
668 
669     @Override
onStartInputView(EditorInfo attribute, boolean restarting)670     public void onStartInputView(EditorInfo attribute, boolean restarting) {
671         mHandler.onStartInputView(attribute, restarting);
672     }
673 
674     @Override
onFinishInputView(boolean finishingInput)675     public void onFinishInputView(boolean finishingInput) {
676         mHandler.onFinishInputView(finishingInput);
677     }
678 
679     @Override
onFinishInput()680     public void onFinishInput() {
681         mHandler.onFinishInput();
682     }
683 
onStartInputInternal(EditorInfo attribute, boolean restarting)684     private void onStartInputInternal(EditorInfo attribute, boolean restarting) {
685         super.onStartInput(attribute, restarting);
686     }
687 
onStartInputViewInternal(EditorInfo attribute, boolean restarting)688     private void onStartInputViewInternal(EditorInfo attribute, boolean restarting) {
689         super.onStartInputView(attribute, restarting);
690         final KeyboardSwitcher switcher = mKeyboardSwitcher;
691         LatinKeyboardView inputView = switcher.getKeyboardView();
692 
693         if (DEBUG) {
694             Log.d(TAG, "onStartInputView: attribute:" + ((attribute == null) ? "none"
695                     : String.format("inputType=0x%08x imeOptions=0x%08x",
696                             attribute.inputType, attribute.imeOptions)));
697         }
698         // In landscape mode, this method gets called without the input view being created.
699         if (inputView == null) {
700             return;
701         }
702 
703         // Forward this event to the accessibility utilities, if enabled.
704         final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
705         if (accessUtils.isTouchExplorationEnabled()) {
706             accessUtils.onStartInputViewInternal(attribute, restarting);
707         }
708 
709         mSubtypeSwitcher.updateParametersOnStartInputView();
710 
711         TextEntryState.reset();
712 
713         // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to
714         // know now whether this is a password text field, because we need to know now whether we
715         // want to enable the voice button.
716         final VoiceProxy voiceIme = mVoiceProxy;
717         final int inputType = (attribute != null) ? attribute.inputType : 0;
718         voiceIme.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(inputType)
719                 || InputTypeCompatUtils.isVisiblePasswordInputType(inputType));
720 
721         initializeInputAttributes(attribute);
722 
723         inputView.closing();
724         mEnteredText = null;
725         mComposingStringBuilder.setLength(0);
726         mHasUncommittedTypedChars = false;
727         mDeleteCount = 0;
728         mJustAddedMagicSpace = false;
729         mJustReplacedDoubleSpace = false;
730 
731         loadSettings();
732         updateCorrectionMode();
733         updateSuggestionVisibility(mPrefs, mResources);
734 
735         if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
736             mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
737          }
738         mVoiceProxy.loadSettings(attribute, mPrefs);
739         // This will work only when the subtype is not supported.
740         LanguageSwitcherProxy.loadSettings();
741 
742         if (mSubtypeSwitcher.isKeyboardMode()) {
743             switcher.loadKeyboard(attribute, mSettingsValues);
744         }
745 
746         if (mSuggestionsView != null)
747             mSuggestionsView.clear();
748         // The EditorInfo might have a flag that affects fullscreen mode.
749         updateFullscreenMode();
750         setSuggestionStripShownInternal(
751                 isSuggestionsStripVisible(), /* needsInputViewShown */ false);
752         // Delay updating suggestions because keyboard input view may not be shown at this point.
753         mHandler.postUpdateSuggestions();
754 
755         inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
756                 mSettingsValues.mKeyPreviewPopupDismissDelay);
757         inputView.setProximityCorrectionEnabled(true);
758 
759         voiceIme.onStartInputView(inputView.getWindowToken());
760 
761         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
762     }
763 
initializeInputAttributes(EditorInfo attribute)764     private void initializeInputAttributes(EditorInfo attribute) {
765         if (attribute == null)
766             return;
767         final int inputType = attribute.inputType;
768         if (inputType == InputType.TYPE_NULL) {
769             // TODO: We should honor TYPE_NULL specification.
770             Log.i(TAG, "InputType.TYPE_NULL is specified");
771         }
772         final int inputClass = inputType & InputType.TYPE_MASK_CLASS;
773         final int variation = inputType & InputType.TYPE_MASK_VARIATION;
774         if (inputClass == 0) {
775             Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x imeOptions=0x%08x",
776                     inputType, attribute.imeOptions));
777         }
778 
779         mShouldInsertMagicSpace = false;
780         mInputTypeNoAutoCorrect = false;
781         mIsSettingsSuggestionStripOn = false;
782         mApplicationSpecifiedCompletionOn = false;
783         mApplicationSpecifiedCompletions = null;
784 
785         if (inputClass == InputType.TYPE_CLASS_TEXT) {
786             mIsSettingsSuggestionStripOn = true;
787             // Make sure that passwords are not displayed in {@link SuggestionsView}.
788             if (InputTypeCompatUtils.isPasswordInputType(inputType)
789                     || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)) {
790                 mIsSettingsSuggestionStripOn = false;
791             }
792             if (InputTypeCompatUtils.isEmailVariation(variation)
793                     || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) {
794                 mShouldInsertMagicSpace = false;
795             } else {
796                 mShouldInsertMagicSpace = true;
797             }
798             if (InputTypeCompatUtils.isEmailVariation(variation)) {
799                 mIsSettingsSuggestionStripOn = false;
800             } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
801                 mIsSettingsSuggestionStripOn = false;
802             } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
803                 mIsSettingsSuggestionStripOn = false;
804             } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) {
805                 // If it's a browser edit field and auto correct is not ON explicitly, then
806                 // disable auto correction, but keep suggestions on.
807                 if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) {
808                     mInputTypeNoAutoCorrect = true;
809                 }
810             }
811 
812             // If NO_SUGGESTIONS is set, don't do prediction.
813             if ((inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
814                 mIsSettingsSuggestionStripOn = false;
815                 mInputTypeNoAutoCorrect = true;
816             }
817             // If it's not multiline and the autoCorrect flag is not set, then don't correct
818             if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0
819                     && (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
820                 mInputTypeNoAutoCorrect = true;
821             }
822             if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
823                 mIsSettingsSuggestionStripOn = false;
824                 mApplicationSpecifiedCompletionOn = isFullscreenMode();
825             }
826         }
827     }
828 
829     @Override
onWindowHidden()830     public void onWindowHidden() {
831         super.onWindowHidden();
832         KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
833         if (inputView != null) inputView.closing();
834     }
835 
onFinishInputInternal()836     private void onFinishInputInternal() {
837         super.onFinishInput();
838 
839         LatinImeLogger.commit();
840 
841         mVoiceProxy.flushVoiceInputLogs(mConfigurationChanging);
842 
843         KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
844         if (inputView != null) inputView.closing();
845         if (mUserUnigramDictionary != null) mUserUnigramDictionary.flushPendingWrites();
846         if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites();
847     }
848 
onFinishInputViewInternal(boolean finishingInput)849     private void onFinishInputViewInternal(boolean finishingInput) {
850         super.onFinishInputView(finishingInput);
851         mKeyboardSwitcher.onFinishInputView();
852         KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
853         if (inputView != null) inputView.cancelAllMessages();
854         // Remove pending messages related to update suggestions
855         mHandler.cancelUpdateSuggestions();
856     }
857 
858     @Override
onUpdateExtractedText(int token, ExtractedText text)859     public void onUpdateExtractedText(int token, ExtractedText text) {
860         super.onUpdateExtractedText(token, text);
861         mVoiceProxy.showPunctuationHintIfNecessary();
862     }
863 
864     @Override
onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)865     public void onUpdateSelection(int oldSelStart, int oldSelEnd,
866             int newSelStart, int newSelEnd,
867             int candidatesStart, int candidatesEnd) {
868         super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
869                 candidatesStart, candidatesEnd);
870 
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=" + candidatesStart
879                     + ", ce=" + candidatesEnd);
880         }
881 
882         mVoiceProxy.setCursorAndSelection(newSelEnd, newSelStart);
883 
884         // If the current selection in the text view changes, we should
885         // clear whatever candidate text we have.
886         final boolean selectionChanged = (newSelStart != candidatesEnd
887                 || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart;
888         final boolean candidatesCleared = candidatesStart == -1 && candidatesEnd == -1;
889         if (!mExpectingUpdateSelection) {
890             if (((mComposingStringBuilder.length() > 0 && mHasUncommittedTypedChars)
891                     || mVoiceProxy.isVoiceInputHighlighted())
892                     && (selectionChanged || candidatesCleared)) {
893                 mComposingStringBuilder.setLength(0);
894                 mHasUncommittedTypedChars = false;
895                 TextEntryState.reset();
896                 updateSuggestions();
897                 final InputConnection ic = getCurrentInputConnection();
898                 if (ic != null) {
899                     ic.finishComposingText();
900                 }
901                 mComposingStateManager.onFinishComposingText();
902                 mVoiceProxy.setVoiceInputHighlighted(false);
903             } else if (!mHasUncommittedTypedChars) {
904                 TextEntryState.reset();
905                 updateSuggestions();
906             }
907             mJustAddedMagicSpace = false; // The user moved the cursor.
908             mJustReplacedDoubleSpace = false;
909         }
910         mExpectingUpdateSelection = false;
911         mHandler.postUpdateShiftKeyState();
912 
913         // Make a note of the cursor position
914         mLastSelectionStart = newSelStart;
915         mLastSelectionEnd = newSelEnd;
916     }
917 
setLastSelection(int start, int end)918     public void setLastSelection(int start, int end) {
919         mLastSelectionStart = start;
920         mLastSelectionEnd = end;
921     }
922 
923     /**
924      * This is called when the user has clicked on the extracted text view,
925      * when running in fullscreen mode.  The default implementation hides
926      * the suggestions view when this happens, but only if the extracted text
927      * editor has a vertical scroll bar because its text doesn't fit.
928      * Here we override the behavior due to the possibility that a re-correction could
929      * cause the suggestions strip to disappear and re-appear.
930      */
931     @Override
onExtractedTextClicked()932     public void onExtractedTextClicked() {
933         if (isSuggestionsRequested()) return;
934 
935         super.onExtractedTextClicked();
936     }
937 
938     /**
939      * This is called when the user has performed a cursor movement in the
940      * extracted text view, when it is running in fullscreen mode.  The default
941      * implementation hides the suggestions view when a vertical movement
942      * happens, but only if the extracted text editor has a vertical scroll bar
943      * because its text doesn't fit.
944      * Here we override the behavior due to the possibility that a re-correction could
945      * cause the suggestions strip to disappear and re-appear.
946      */
947     @Override
onExtractedCursorMovement(int dx, int dy)948     public void onExtractedCursorMovement(int dx, int dy) {
949         if (isSuggestionsRequested()) return;
950 
951         super.onExtractedCursorMovement(dx, dy);
952     }
953 
954     @Override
hideWindow()955     public void hideWindow() {
956         LatinImeLogger.commit();
957         mKeyboardSwitcher.onHideWindow();
958 
959         if (TRACE) Debug.stopMethodTracing();
960         if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
961             mOptionsDialog.dismiss();
962             mOptionsDialog = null;
963         }
964         mVoiceProxy.hideVoiceWindow(mConfigurationChanging);
965         super.hideWindow();
966     }
967 
968     @Override
onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions)969     public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
970         if (DEBUG) {
971             Log.i(TAG, "Received completions:");
972             if (applicationSpecifiedCompletions != null) {
973                 for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
974                     Log.i(TAG, "  #" + i + ": " + applicationSpecifiedCompletions[i]);
975                 }
976             }
977         }
978         if (mApplicationSpecifiedCompletionOn) {
979             mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
980             if (applicationSpecifiedCompletions == null) {
981                 clearSuggestions();
982                 return;
983             }
984 
985             SuggestedWords.Builder builder = new SuggestedWords.Builder()
986                     .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions)
987                     .setTypedWordValid(false)
988                     .setHasMinimalSuggestion(false);
989             // When in fullscreen mode, show completions generated by the application
990             setSuggestions(builder.build());
991             mBestWord = null;
992             setSuggestionStripShown(true);
993         }
994     }
995 
setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown)996     private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
997         // TODO: Modify this if we support suggestions with hard keyboard
998         if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
999             final boolean shouldShowSuggestions = shown
1000                     && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true);
1001             if (isFullscreenMode()) {
1002                 mSuggestionsContainer.setVisibility(
1003                         shouldShowSuggestions ? View.VISIBLE : View.GONE);
1004             } else {
1005                 mSuggestionsContainer.setVisibility(
1006                         shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
1007             }
1008         }
1009     }
1010 
setSuggestionStripShown(boolean shown)1011     private void setSuggestionStripShown(boolean shown) {
1012         setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
1013     }
1014 
1015     @Override
onComputeInsets(InputMethodService.Insets outInsets)1016     public void onComputeInsets(InputMethodService.Insets outInsets) {
1017         super.onComputeInsets(outInsets);
1018         final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1019         if (inputView == null || mSuggestionsContainer == null)
1020             return;
1021         // In fullscreen mode, the height of the extract area managed by InputMethodService should
1022         // be considered.
1023         // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
1024         final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
1025         final int backingHeight = (mKeyPreviewBackingView.getVisibility() == View.GONE) ? 0
1026                 : mKeyPreviewBackingView.getHeight();
1027         final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0
1028                 : mSuggestionsContainer.getHeight();
1029         final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
1030         int touchY = extraHeight;
1031         // Need to set touchable region only if input view is being shown
1032         if (mKeyboardSwitcher.isInputViewShown()) {
1033             if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
1034                 touchY -= suggestionsHeight;
1035             }
1036             final int touchWidth = inputView.getWidth();
1037             final int touchHeight = inputView.getHeight() + extraHeight
1038                     // Extend touchable region below the keyboard.
1039                     + EXTENDED_TOUCHABLE_REGION_HEIGHT;
1040             if (DEBUG) {
1041                 Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth
1042                         + " height=" + touchHeight);
1043             }
1044             setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight);
1045         }
1046         outInsets.contentTopInsets = touchY;
1047         outInsets.visibleTopInsets = touchY;
1048     }
1049 
1050     @Override
onEvaluateFullscreenMode()1051     public boolean onEvaluateFullscreenMode() {
1052         return super.onEvaluateFullscreenMode()
1053                 && mResources.getBoolean(R.bool.config_use_fullscreen_mode);
1054     }
1055 
1056     @Override
updateFullscreenMode()1057     public void updateFullscreenMode() {
1058         super.updateFullscreenMode();
1059 
1060         if (mKeyPreviewBackingView == null) return;
1061         // In extract mode, no need to have extra space to show the key preview.
1062         // If not, we should have extra space above the keyboard to show the key preview.
1063         mKeyPreviewBackingView.setVisibility(isExtractViewShown() ? View.GONE : View.VISIBLE);
1064     }
1065 
1066     @Override
onKeyDown(int keyCode, KeyEvent event)1067     public boolean onKeyDown(int keyCode, KeyEvent event) {
1068         switch (keyCode) {
1069         case KeyEvent.KEYCODE_BACK:
1070             if (event.getRepeatCount() == 0) {
1071                 if (mSuggestionsView != null && mSuggestionsView.handleBack()) {
1072                     return true;
1073                 }
1074                 final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
1075                 if (keyboardView != null && keyboardView.handleBack()) {
1076                     return true;
1077                 }
1078             }
1079             break;
1080         }
1081         return super.onKeyDown(keyCode, event);
1082     }
1083 
1084     @Override
onKeyUp(int keyCode, KeyEvent event)1085     public boolean onKeyUp(int keyCode, KeyEvent event) {
1086         switch (keyCode) {
1087         case KeyEvent.KEYCODE_DPAD_DOWN:
1088         case KeyEvent.KEYCODE_DPAD_UP:
1089         case KeyEvent.KEYCODE_DPAD_LEFT:
1090         case KeyEvent.KEYCODE_DPAD_RIGHT:
1091             // Enable shift key and DPAD to do selections
1092             if (mKeyboardSwitcher.isInputViewShown()
1093                     && mKeyboardSwitcher.isShiftedOrShiftLocked()) {
1094                 KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
1095                         event.getAction(), event.getKeyCode(), event.getRepeatCount(),
1096                         event.getDeviceId(), event.getScanCode(),
1097                         KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
1098                 final InputConnection ic = getCurrentInputConnection();
1099                 if (ic != null)
1100                     ic.sendKeyEvent(newEvent);
1101                 return true;
1102             }
1103             break;
1104         }
1105         return super.onKeyUp(keyCode, event);
1106     }
1107 
commitTyped(final InputConnection ic)1108     public void commitTyped(final InputConnection ic) {
1109         if (!mHasUncommittedTypedChars) return;
1110         mHasUncommittedTypedChars = false;
1111         if (mComposingStringBuilder.length() > 0) {
1112             if (ic != null) {
1113                 ic.commitText(mComposingStringBuilder, 1);
1114             }
1115             mCommittedLength = mComposingStringBuilder.length();
1116             TextEntryState.acceptedTyped(mComposingStringBuilder);
1117             addToUserUnigramAndBigramDictionaries(mComposingStringBuilder,
1118                     UserUnigramDictionary.FREQUENCY_FOR_TYPED);
1119         }
1120         updateSuggestions();
1121     }
1122 
getCurrentAutoCapsState()1123     public boolean getCurrentAutoCapsState() {
1124         final InputConnection ic = getCurrentInputConnection();
1125         EditorInfo ei = getCurrentInputEditorInfo();
1126         if (mSettingsValues.mAutoCap && ic != null && ei != null
1127                 && ei.inputType != InputType.TYPE_NULL) {
1128             return ic.getCursorCapsMode(ei.inputType) != 0;
1129         }
1130         return false;
1131     }
1132 
swapSwapperAndSpace()1133     private void swapSwapperAndSpace() {
1134         final InputConnection ic = getCurrentInputConnection();
1135         if (ic == null) return;
1136         CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
1137         // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
1138         if (lastTwo != null && lastTwo.length() == 2
1139                 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
1140             ic.beginBatchEdit();
1141             ic.deleteSurroundingText(2, 0);
1142             ic.commitText(lastTwo.charAt(1) + " ", 1);
1143             ic.endBatchEdit();
1144             mKeyboardSwitcher.updateShiftState();
1145         }
1146     }
1147 
maybeDoubleSpace()1148     private void maybeDoubleSpace() {
1149         if (mCorrectionMode == Suggest.CORRECTION_NONE) return;
1150         final InputConnection ic = getCurrentInputConnection();
1151         if (ic == null) return;
1152         final CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
1153         if (lastThree != null && lastThree.length() == 3
1154                 && Utils.canBeFollowedByPeriod(lastThree.charAt(0))
1155                 && lastThree.charAt(1) == Keyboard.CODE_SPACE
1156                 && lastThree.charAt(2) == Keyboard.CODE_SPACE
1157                 && mHandler.isAcceptingDoubleSpaces()) {
1158             mHandler.cancelDoubleSpacesTimer();
1159             ic.beginBatchEdit();
1160             ic.deleteSurroundingText(2, 0);
1161             ic.commitText(". ", 1);
1162             ic.endBatchEdit();
1163             mKeyboardSwitcher.updateShiftState();
1164             mJustReplacedDoubleSpace = true;
1165         } else {
1166             mHandler.startDoubleSpacesTimer();
1167         }
1168     }
1169 
1170     // "ic" must not null
maybeRemovePreviousPeriod(final InputConnection ic, CharSequence text)1171     private void maybeRemovePreviousPeriod(final InputConnection ic, CharSequence text) {
1172         // When the text's first character is '.', remove the previous period
1173         // if there is one.
1174         CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1175         if (lastOne != null && lastOne.length() == 1
1176                 && lastOne.charAt(0) == Keyboard.CODE_PERIOD
1177                 && text.charAt(0) == Keyboard.CODE_PERIOD) {
1178             ic.deleteSurroundingText(1, 0);
1179         }
1180     }
1181 
removeTrailingSpace()1182     private void removeTrailingSpace() {
1183         final InputConnection ic = getCurrentInputConnection();
1184         if (ic == null) return;
1185 
1186         CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
1187         if (lastOne != null && lastOne.length() == 1
1188                 && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
1189             ic.deleteSurroundingText(1, 0);
1190         }
1191     }
1192 
1193     @Override
addWordToDictionary(String word)1194     public boolean addWordToDictionary(String word) {
1195         mUserDictionary.addWord(word, 128);
1196         // Suggestion strip should be updated after the operation of adding word to the
1197         // user dictionary
1198         mHandler.postUpdateSuggestions();
1199         return true;
1200     }
1201 
isAlphabet(int code)1202     private boolean isAlphabet(int code) {
1203         if (Character.isLetter(code)) {
1204             return true;
1205         } else {
1206             return false;
1207         }
1208     }
1209 
onSettingsKeyPressed()1210     private void onSettingsKeyPressed() {
1211         if (isShowingOptionDialog()) return;
1212         if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
1213             showSubtypeSelectorAndSettings();
1214         } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm, false /* exclude aux subtypes */)) {
1215             showOptionsMenu();
1216         } else {
1217             launchSettings();
1218         }
1219     }
1220 
1221     // Virtual codes representing custom requests.  These are used in onCustomRequest() below.
1222     public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
1223 
1224     @Override
onCustomRequest(int requestCode)1225     public boolean onCustomRequest(int requestCode) {
1226         if (isShowingOptionDialog()) return false;
1227         switch (requestCode) {
1228         case CODE_SHOW_INPUT_METHOD_PICKER:
1229             if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm, true /* include aux subtypes */)) {
1230                 mImm.showInputMethodPicker();
1231                 return true;
1232             }
1233             return false;
1234         }
1235         return false;
1236     }
1237 
isShowingOptionDialog()1238     private boolean isShowingOptionDialog() {
1239         return mOptionsDialog != null && mOptionsDialog.isShowing();
1240     }
1241 
1242     // Implementation of {@link KeyboardActionListener}.
1243     @Override
onCodeInput(int primaryCode, int[] keyCodes, int x, int y)1244     public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
1245         long when = SystemClock.uptimeMillis();
1246         if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
1247             mDeleteCount = 0;
1248         }
1249         mLastKeyTime = when;
1250         KeyboardSwitcher switcher = mKeyboardSwitcher;
1251         final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
1252         final boolean lastStateOfJustReplacedDoubleSpace = mJustReplacedDoubleSpace;
1253         mJustReplacedDoubleSpace = false;
1254         switch (primaryCode) {
1255         case Keyboard.CODE_DELETE:
1256             handleBackspace(lastStateOfJustReplacedDoubleSpace);
1257             mDeleteCount++;
1258             mExpectingUpdateSelection = true;
1259             LatinImeLogger.logOnDelete();
1260             break;
1261         case Keyboard.CODE_SHIFT:
1262             // Shift key is handled in onPress() when device has distinct multi-touch panel.
1263             if (!distinctMultiTouch)
1264                 switcher.toggleShift();
1265             break;
1266         case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
1267             // Symbol key is handled in onPress() when device has distinct multi-touch panel.
1268             if (!distinctMultiTouch)
1269                 switcher.changeKeyboardMode();
1270             break;
1271         case Keyboard.CODE_CANCEL:
1272             if (!isShowingOptionDialog()) {
1273                 handleClose();
1274             }
1275             break;
1276         case Keyboard.CODE_SETTINGS:
1277             onSettingsKeyPressed();
1278             break;
1279         case Keyboard.CODE_CAPSLOCK:
1280             switcher.toggleCapsLock();
1281             //$FALL-THROUGH$
1282         case Keyboard.CODE_HAPTIC_AND_AUDIO_FEEDBACK_ONLY:
1283             // Dummy code for haptic and audio feedbacks.
1284             vibrate();
1285             playKeyClick(primaryCode);
1286             break;
1287         case Keyboard.CODE_SHORTCUT:
1288             mSubtypeSwitcher.switchToShortcutIME();
1289             break;
1290         case Keyboard.CODE_TAB:
1291             handleTab();
1292             // There are two cases for tab. Either we send a "next" event, that may change the
1293             // focus but will never move the cursor. Or, we send a real tab keycode, which some
1294             // applications may accept or ignore, and we don't know whether this will move the
1295             // cursor or not. So actually, we don't really know.
1296             // So to go with the safer option, we'd rather behave as if the user moved the
1297             // cursor when they didn't than the opposite. We also expect that most applications
1298             // will actually use tab only for focus movement.
1299             // To sum it up: do not update mExpectingUpdateSelection here.
1300             break;
1301         default:
1302             if (mSettingsValues.isWordSeparator(primaryCode)) {
1303                 handleSeparator(primaryCode, x, y);
1304             } else {
1305                 handleCharacter(primaryCode, keyCodes, x, y);
1306             }
1307             mExpectingUpdateSelection = true;
1308             break;
1309         }
1310         switcher.onKey(primaryCode);
1311         // Reset after any single keystroke
1312         mEnteredText = null;
1313     }
1314 
1315     @Override
onTextInput(CharSequence text)1316     public void onTextInput(CharSequence text) {
1317         mVoiceProxy.commitVoiceInput();
1318         final InputConnection ic = getCurrentInputConnection();
1319         if (ic == null) return;
1320         ic.beginBatchEdit();
1321         commitTyped(ic);
1322         maybeRemovePreviousPeriod(ic, text);
1323         ic.commitText(text, 1);
1324         ic.endBatchEdit();
1325         mKeyboardSwitcher.updateShiftState();
1326         mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY);
1327         mJustAddedMagicSpace = false;
1328         mEnteredText = text;
1329     }
1330 
1331     @Override
onCancelInput()1332     public void onCancelInput() {
1333         // User released a finger outside any key
1334         mKeyboardSwitcher.onCancelInput();
1335     }
1336 
handleBackspace(boolean justReplacedDoubleSpace)1337     private void handleBackspace(boolean justReplacedDoubleSpace) {
1338         if (mVoiceProxy.logAndRevertVoiceInput()) return;
1339 
1340         final InputConnection ic = getCurrentInputConnection();
1341         if (ic == null) return;
1342         ic.beginBatchEdit();
1343 
1344         mVoiceProxy.handleBackspace();
1345 
1346         final boolean deleteChar = !mHasUncommittedTypedChars;
1347         if (mHasUncommittedTypedChars) {
1348             final int length = mComposingStringBuilder.length();
1349             if (length > 0) {
1350                 mComposingStringBuilder.delete(length - 1, length);
1351                 mWordComposer.deleteLast();
1352                 final CharSequence textWithUnderline =
1353                         mComposingStateManager.isAutoCorrectionIndicatorOn()
1354                                 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(
1355                                             this, mComposingStringBuilder)
1356                                 : mComposingStringBuilder;
1357                 ic.setComposingText(textWithUnderline, 1);
1358                 if (mComposingStringBuilder.length() == 0) {
1359                     mHasUncommittedTypedChars = false;
1360                 }
1361                 if (1 == length) {
1362                     // 1 == length means we are about to erase the last character of the word,
1363                     // so we can show bigrams.
1364                     mHandler.postUpdateBigramPredictions();
1365                 } else {
1366                     // length > 1, so we still have letters to deduce a suggestion from.
1367                     mHandler.postUpdateSuggestions();
1368                 }
1369             } else {
1370                 ic.deleteSurroundingText(1, 0);
1371             }
1372         }
1373         mHandler.postUpdateShiftKeyState();
1374 
1375         TextEntryState.backspace();
1376         if (TextEntryState.isUndoCommit()) {
1377             revertLastWord(ic);
1378             ic.endBatchEdit();
1379             return;
1380         }
1381         if (justReplacedDoubleSpace) {
1382             if (revertDoubleSpace(ic)) {
1383                 ic.endBatchEdit();
1384                 return;
1385             }
1386         }
1387 
1388         if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
1389             ic.deleteSurroundingText(mEnteredText.length(), 0);
1390         } else if (deleteChar) {
1391             if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
1392                 // Go back to the suggestion mode if the user canceled the
1393                 // "Touch again to save".
1394                 // NOTE: In gerenal, we don't revert the word when backspacing
1395                 // from a manual suggestion pick.  We deliberately chose a
1396                 // different behavior only in the case of picking the first
1397                 // suggestion (typed word).  It's intentional to have made this
1398                 // inconsistent with backspacing after selecting other suggestions.
1399                 revertLastWord(ic);
1400             } else {
1401                 sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1402                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
1403                     sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
1404                 }
1405             }
1406         }
1407         ic.endBatchEdit();
1408     }
1409 
handleTab()1410     private void handleTab() {
1411         final int imeOptions = getCurrentInputEditorInfo().imeOptions;
1412         if (!EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
1413                 && !EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)) {
1414             sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
1415             return;
1416         }
1417 
1418         final InputConnection ic = getCurrentInputConnection();
1419         if (ic == null)
1420             return;
1421 
1422         // True if keyboard is in either chording shift or manual temporary upper case mode.
1423         final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase();
1424         if (EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
1425                 && !isManualTemporaryUpperCase) {
1426             EditorInfoCompatUtils.performEditorActionNext(ic);
1427         } else if (EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)
1428                 && isManualTemporaryUpperCase) {
1429             EditorInfoCompatUtils.performEditorActionPrevious(ic);
1430         }
1431     }
1432 
handleCharacter(int primaryCode, int[] keyCodes, int x, int y)1433     private void handleCharacter(int primaryCode, int[] keyCodes, int x, int y) {
1434         mVoiceProxy.handleCharacter();
1435 
1436         if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceStripper(primaryCode)) {
1437             removeTrailingSpace();
1438         }
1439 
1440         int code = primaryCode;
1441         if ((isAlphabet(code) || mSettingsValues.isSymbolExcludedFromWordSeparators(code))
1442                 && isSuggestionsRequested() && !isCursorTouchingWord()) {
1443             if (!mHasUncommittedTypedChars) {
1444                 mHasUncommittedTypedChars = true;
1445                 mComposingStringBuilder.setLength(0);
1446                 mWordComposer.reset();
1447                 clearSuggestions();
1448                 mComposingStateManager.onFinishComposingText();
1449             }
1450         }
1451         final KeyboardSwitcher switcher = mKeyboardSwitcher;
1452         if (switcher.isShiftedOrShiftLocked()) {
1453             if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
1454                     || keyCodes[0] > Character.MAX_CODE_POINT) {
1455                 return;
1456             }
1457             code = keyCodes[0];
1458             if (switcher.isAlphabetMode() && Character.isLowerCase(code)) {
1459                 // In some locales, such as Turkish, Character.toUpperCase() may return a wrong
1460                 // character because it doesn't take care of locale.
1461                 final String upperCaseString = new String(new int[] {code}, 0, 1)
1462                         .toUpperCase(mSubtypeSwitcher.getInputLocale());
1463                 if (upperCaseString.codePointCount(0, upperCaseString.length()) == 1) {
1464                     code = upperCaseString.codePointAt(0);
1465                 } else {
1466                     // Some keys, such as [eszett], have upper case as multi-characters.
1467                     onTextInput(upperCaseString);
1468                     return;
1469                 }
1470             }
1471         }
1472         if (mHasUncommittedTypedChars) {
1473             mComposingStringBuilder.append((char) code);
1474             mWordComposer.add(code, keyCodes, x, y);
1475             final InputConnection ic = getCurrentInputConnection();
1476             if (ic != null) {
1477                 // If it's the first letter, make note of auto-caps state
1478                 if (mWordComposer.size() == 1) {
1479                     mWordComposer.setAutoCapitalized(getCurrentAutoCapsState());
1480                     mComposingStateManager.onStartComposingText();
1481                 }
1482                 final CharSequence textWithUnderline =
1483                         mComposingStateManager.isAutoCorrectionIndicatorOn()
1484                                 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(
1485                                         this, mComposingStringBuilder)
1486                                 : mComposingStringBuilder;
1487                 ic.setComposingText(textWithUnderline, 1);
1488             }
1489             mHandler.postUpdateSuggestions();
1490         } else {
1491             sendKeyChar((char)code);
1492         }
1493         if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
1494             swapSwapperAndSpace();
1495         } else {
1496             mJustAddedMagicSpace = false;
1497         }
1498 
1499         switcher.updateShiftState();
1500         if (LatinIME.PERF_DEBUG) measureCps();
1501         TextEntryState.typedCharacter((char) code, mSettingsValues.isWordSeparator(code), x, y);
1502     }
1503 
handleSeparator(int primaryCode, int x, int y)1504     private void handleSeparator(int primaryCode, int x, int y) {
1505         mVoiceProxy.handleSeparator();
1506         mComposingStateManager.onFinishComposingText();
1507 
1508         // Should dismiss the "Touch again to save" message when handling separator
1509         if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
1510             mHandler.cancelUpdateBigramPredictions();
1511             mHandler.postUpdateSuggestions();
1512         }
1513 
1514         boolean pickedDefault = false;
1515         // Handle separator
1516         final InputConnection ic = getCurrentInputConnection();
1517         if (ic != null) {
1518             ic.beginBatchEdit();
1519         }
1520         if (mHasUncommittedTypedChars) {
1521             // In certain languages where single quote is a separator, it's better
1522             // not to auto correct, but accept the typed word. For instance,
1523             // in Italian dov' should not be expanded to dove' because the elision
1524             // requires the last vowel to be removed.
1525             final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
1526                     && !mInputTypeNoAutoCorrect;
1527             if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
1528                 pickedDefault = pickDefaultSuggestion(primaryCode);
1529             } else {
1530                 commitTyped(ic);
1531             }
1532         }
1533 
1534         if (mJustAddedMagicSpace) {
1535             if (mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
1536                 sendKeyChar((char)primaryCode);
1537                 swapSwapperAndSpace();
1538             } else {
1539                 if (mSettingsValues.isMagicSpaceStripper(primaryCode)) removeTrailingSpace();
1540                 sendKeyChar((char)primaryCode);
1541                 mJustAddedMagicSpace = false;
1542             }
1543         } else {
1544             sendKeyChar((char)primaryCode);
1545         }
1546 
1547         if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) {
1548             maybeDoubleSpace();
1549         }
1550 
1551         TextEntryState.typedCharacter((char) primaryCode, true, x, y);
1552 
1553         if (pickedDefault) {
1554             CharSequence typedWord = mWordComposer.getTypedWord();
1555             TextEntryState.backToAcceptedDefault(typedWord);
1556             if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) {
1557                 InputConnectionCompatUtils.commitCorrection(
1558                         ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord);
1559             }
1560         }
1561         if (Keyboard.CODE_SPACE == primaryCode) {
1562             if (!isCursorTouchingWord()) {
1563                 mHandler.cancelUpdateSuggestions();
1564                 mHandler.postUpdateBigramPredictions();
1565             }
1566         } else {
1567             // Set punctuation right away. onUpdateSelection will fire but tests whether it is
1568             // already displayed or not, so it's okay.
1569             setPunctuationSuggestions();
1570         }
1571         mKeyboardSwitcher.updateShiftState();
1572         if (ic != null) {
1573             ic.endBatchEdit();
1574         }
1575     }
1576 
handleClose()1577     private void handleClose() {
1578         commitTyped(getCurrentInputConnection());
1579         mVoiceProxy.handleClose();
1580         requestHideSelf(0);
1581         LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
1582         if (inputView != null)
1583             inputView.closing();
1584     }
1585 
isSuggestionsRequested()1586     public boolean isSuggestionsRequested() {
1587         return mIsSettingsSuggestionStripOn
1588                 && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
1589     }
1590 
isShowingPunctuationList()1591     public boolean isShowingPunctuationList() {
1592         return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
1593     }
1594 
isShowingSuggestionsStrip()1595     public boolean isShowingSuggestionsStrip() {
1596         return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
1597                 || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
1598                         && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
1599     }
1600 
isSuggestionsStripVisible()1601     public boolean isSuggestionsStripVisible() {
1602         if (mSuggestionsView == null)
1603             return false;
1604         if (mSuggestionsView.isShowingAddToDictionaryHint() || TextEntryState.isRecorrecting())
1605             return true;
1606         if (!isShowingSuggestionsStrip())
1607             return false;
1608         if (mApplicationSpecifiedCompletionOn)
1609             return true;
1610         return isSuggestionsRequested();
1611     }
1612 
switchToKeyboardView()1613     public void switchToKeyboardView() {
1614         if (DEBUG) {
1615             Log.d(TAG, "Switch to keyboard view.");
1616         }
1617         View v = mKeyboardSwitcher.getKeyboardView();
1618         if (v != null) {
1619             // Confirms that the keyboard view doesn't have parent view.
1620             ViewParent p = v.getParent();
1621             if (p != null && p instanceof ViewGroup) {
1622                 ((ViewGroup) p).removeView(v);
1623             }
1624             setInputView(v);
1625         }
1626         setSuggestionStripShown(isSuggestionsStripVisible());
1627         updateInputViewShown();
1628         mHandler.postUpdateSuggestions();
1629     }
1630 
clearSuggestions()1631     public void clearSuggestions() {
1632         setSuggestions(SuggestedWords.EMPTY);
1633     }
1634 
setSuggestions(SuggestedWords words)1635     public void setSuggestions(SuggestedWords words) {
1636         if (mSuggestionsView != null) {
1637             mSuggestionsView.setSuggestions(words);
1638             mKeyboardSwitcher.onAutoCorrectionStateChanged(
1639                     words.hasWordAboveAutoCorrectionScoreThreshold());
1640         }
1641 
1642         // Put a blue underline to a word in TextView which will be auto-corrected.
1643         final InputConnection ic = getCurrentInputConnection();
1644         if (ic != null) {
1645             final boolean oldAutoCorrectionIndicator =
1646                     mComposingStateManager.isAutoCorrectionIndicatorOn();
1647             final boolean newAutoCorrectionIndicator = Utils.willAutoCorrect(words);
1648             if (oldAutoCorrectionIndicator != newAutoCorrectionIndicator) {
1649                 if (LatinImeLogger.sDBG) {
1650                     Log.d(TAG, "Flip the indicator. " + oldAutoCorrectionIndicator
1651                             + " -> " + newAutoCorrectionIndicator);
1652                 }
1653                 final CharSequence textWithUnderline = newAutoCorrectionIndicator
1654                         ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(
1655                                 this, mComposingStringBuilder)
1656                         : mComposingStringBuilder;
1657                 if (!TextUtils.isEmpty(textWithUnderline)) {
1658                     ic.setComposingText(textWithUnderline, 1);
1659                 }
1660                 mComposingStateManager.setAutoCorrectionIndicatorOn(newAutoCorrectionIndicator);
1661             }
1662         }
1663     }
1664 
updateSuggestions()1665     public void updateSuggestions() {
1666         // Check if we have a suggestion engine attached.
1667         if ((mSuggest == null || !isSuggestionsRequested())
1668                 && !mVoiceProxy.isVoiceInputHighlighted()) {
1669             return;
1670         }
1671 
1672         mHandler.cancelUpdateSuggestions();
1673         mHandler.cancelUpdateBigramPredictions();
1674 
1675         if (!mHasUncommittedTypedChars) {
1676             setPunctuationSuggestions();
1677             return;
1678         }
1679 
1680         final WordComposer wordComposer = mWordComposer;
1681         // TODO: May need a better way of retrieving previous word
1682         final InputConnection ic = getCurrentInputConnection();
1683         final CharSequence prevWord;
1684         if (null == ic) {
1685             prevWord = null;
1686         } else {
1687             prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
1688         }
1689         // getSuggestedWordBuilder handles gracefully a null value of prevWord
1690         final SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
1691                 wordComposer, prevWord, mKeyboardSwitcher.getLatinKeyboard().getProximityInfo());
1692 
1693         boolean autoCorrectionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection();
1694         final CharSequence typedWord = wordComposer.getTypedWord();
1695         // Here, we want to promote a whitelisted word if exists.
1696         // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid"
1697         // but still autocorrected from - in the case the whitelist only capitalizes the word.
1698         // The whitelist should be case-insensitive, so it's not possible to be consistent with
1699         // a boolean flag. Right now this is handled with a slight hack in
1700         // WhitelistDictionary#shouldForciblyAutoCorrectFrom.
1701         final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected(
1702                 mSuggest.getUnigramDictionaries(), typedWord, preferCapitalization());
1703         if (mCorrectionMode == Suggest.CORRECTION_FULL
1704                 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
1705             autoCorrectionAvailable |= (!allowsToBeAutoCorrected);
1706         }
1707         // Don't auto-correct words with multiple capital letter
1708         autoCorrectionAvailable &= !wordComposer.isMostlyCaps();
1709         autoCorrectionAvailable &= !TextEntryState.isRecorrecting();
1710 
1711         // Basically, we update the suggestion strip only when suggestion count > 1.  However,
1712         // there is an exception: We update the suggestion strip whenever typed word's length
1713         // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
1714         // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
1715         // need to clear the previous state when the user starts typing a word (i.e. typed word's
1716         // length == 1).
1717         if (typedWord != null) {
1718             if (builder.size() > 1 || typedWord.length() == 1 || (!allowsToBeAutoCorrected)
1719                     || mSuggestionsView.isShowingAddToDictionaryHint()) {
1720                 builder.setTypedWordValid(!allowsToBeAutoCorrected).setHasMinimalSuggestion(
1721                         autoCorrectionAvailable);
1722             } else {
1723                 SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
1724                 if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
1725                     if (builder.size() == 0) {
1726                         return;
1727                     }
1728                     previousSuggestions = SuggestedWords.EMPTY;
1729                 }
1730                 builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions);
1731             }
1732         }
1733         showSuggestions(builder.build(), typedWord);
1734     }
1735 
showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord)1736     public void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) {
1737         final boolean shouldBlockAutoCorrectionBySafetyNet =
1738                 Utils.shouldBlockAutoCorrectionBySafetyNet(suggestedWords, mSuggest);
1739         if (shouldBlockAutoCorrectionBySafetyNet) {
1740             suggestedWords.setShouldBlockAutoCorrection();
1741         }
1742         setSuggestions(suggestedWords);
1743         if (suggestedWords.size() > 0) {
1744             if (shouldBlockAutoCorrectionBySafetyNet) {
1745                 mBestWord = typedWord;
1746             } else if (suggestedWords.hasAutoCorrectionWord()) {
1747                 mBestWord = suggestedWords.getWord(1);
1748             } else {
1749                 mBestWord = typedWord;
1750             }
1751         } else {
1752             mBestWord = null;
1753         }
1754         setSuggestionStripShown(isSuggestionsStripVisible());
1755     }
1756 
pickDefaultSuggestion(int separatorCode)1757     private boolean pickDefaultSuggestion(int separatorCode) {
1758         // Complete any pending suggestions query first
1759         if (mHandler.hasPendingUpdateSuggestions()) {
1760             mHandler.cancelUpdateSuggestions();
1761             updateSuggestions();
1762         }
1763         if (mBestWord != null && mBestWord.length() > 0) {
1764             TextEntryState.acceptedDefault(mWordComposer.getTypedWord(), mBestWord, separatorCode);
1765             mExpectingUpdateSelection = true;
1766             commitBestWord(mBestWord);
1767             // Add the word to the user unigram dictionary if it's not a known word
1768             addToUserUnigramAndBigramDictionaries(mBestWord,
1769                     UserUnigramDictionary.FREQUENCY_FOR_TYPED);
1770             return true;
1771         }
1772         return false;
1773     }
1774 
1775     @Override
pickSuggestionManually(int index, CharSequence suggestion)1776     public void pickSuggestionManually(int index, CharSequence suggestion) {
1777         mComposingStateManager.onFinishComposingText();
1778         SuggestedWords suggestions = mSuggestionsView.getSuggestions();
1779         mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion,
1780                 mSettingsValues.mWordSeparators);
1781 
1782         final boolean recorrecting = TextEntryState.isRecorrecting();
1783         final InputConnection ic = getCurrentInputConnection();
1784         if (ic != null) {
1785             ic.beginBatchEdit();
1786         }
1787         if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null
1788                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
1789             if (ic != null) {
1790                 final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
1791                 ic.commitCompletion(completionInfo);
1792             }
1793             mCommittedLength = suggestion.length();
1794             if (mSuggestionsView != null) {
1795                 mSuggestionsView.clear();
1796             }
1797             mKeyboardSwitcher.updateShiftState();
1798             if (ic != null) {
1799                 ic.endBatchEdit();
1800             }
1801             return;
1802         }
1803 
1804         // If this is a punctuation, apply it through the normal key press
1805         if (suggestion.length() == 1 && (mSettingsValues.isWordSeparator(suggestion.charAt(0))
1806                 || mSettingsValues.isSuggestedPunctuation(suggestion.charAt(0)))) {
1807             // Word separators are suggested before the user inputs something.
1808             // So, LatinImeLogger logs "" as a user's input.
1809             LatinImeLogger.logOnManualSuggestion(
1810                     "", suggestion.toString(), index, suggestions.mWords);
1811             // Find out whether the previous character is a space. If it is, as a special case
1812             // for punctuation entered through the suggestion strip, it should be considered
1813             // a magic space even if it was a normal space. This is meant to help in case the user
1814             // pressed space on purpose of displaying the suggestion strip punctuation.
1815             final int rawPrimaryCode = suggestion.charAt(0);
1816             // Maybe apply the "bidi mirrored" conversions for parentheses
1817             final LatinKeyboard keyboard = mKeyboardSwitcher.getLatinKeyboard();
1818             final int primaryCode = Key.getRtlParenthesisCode(
1819                     rawPrimaryCode, keyboard.mIsRtlKeyboard);
1820 
1821             final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : "";
1822             final int toLeft = (ic == null || TextUtils.isEmpty(beforeText))
1823                     ? 0 : beforeText.charAt(0);
1824             final boolean oldMagicSpace = mJustAddedMagicSpace;
1825             if (Keyboard.CODE_SPACE == toLeft) mJustAddedMagicSpace = true;
1826             onCodeInput(primaryCode, new int[] { primaryCode },
1827                     KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
1828                     KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
1829             mJustAddedMagicSpace = oldMagicSpace;
1830             if (ic != null) {
1831                 ic.endBatchEdit();
1832             }
1833             return;
1834         }
1835         if (!mHasUncommittedTypedChars) {
1836             // If we are not composing a word, then it was a suggestion inferred from
1837             // context - no user input. We should reset the word composer.
1838             mWordComposer.reset();
1839         }
1840         mExpectingUpdateSelection = true;
1841         commitBestWord(suggestion);
1842         // Add the word to the auto dictionary if it's not a known word
1843         if (index == 0) {
1844             addToUserUnigramAndBigramDictionaries(suggestion,
1845                     UserUnigramDictionary.FREQUENCY_FOR_PICKED);
1846         } else {
1847             addToOnlyBigramDictionary(suggestion, 1);
1848         }
1849         LatinImeLogger.logOnManualSuggestion(mComposingStringBuilder.toString(),
1850                 suggestion.toString(), index, suggestions.mWords);
1851         TextEntryState.acceptedSuggestion(mComposingStringBuilder.toString(), suggestion);
1852         // Follow it with a space
1853         if (mShouldInsertMagicSpace && !recorrecting) {
1854             sendMagicSpace();
1855         }
1856 
1857         // We should show the "Touch again to save" hint if the user pressed the first entry
1858         // AND either:
1859         // - There is no dictionary (we know that because we tried to load it => null != mSuggest
1860         //   AND mSuggest.hasMainDictionary() is false)
1861         // - There is a dictionary and the word is not in it
1862         // Please note that if mSuggest is null, it means that everything is off: suggestion
1863         // and correction, so we shouldn't try to show the hint
1864         // We used to look at mCorrectionMode here, but showing the hint should have nothing
1865         // to do with the autocorrection setting.
1866         final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
1867                 // If there is no dictionary the hint should be shown.
1868                 && (!mSuggest.hasMainDictionary()
1869                         // If "suggestion" is not in the dictionary, the hint should be shown.
1870                         || !AutoCorrection.isValidWord(
1871                                 mSuggest.getUnigramDictionaries(), suggestion, true));
1872 
1873         if (!recorrecting) {
1874             // Fool the state watcher so that a subsequent backspace will not do a revert, unless
1875             // we just did a correction, in which case we need to stay in
1876             // TextEntryState.State.PICKED_SUGGESTION state.
1877             TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true,
1878                     WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
1879         }
1880         if (!showingAddToDictionaryHint) {
1881             // If we're not showing the "Touch again to save", then show corrections again.
1882             // In case the cursor position doesn't change, make sure we show the suggestions again.
1883             updateBigramPredictions();
1884             // Updating the predictions right away may be slow and feel unresponsive on slower
1885             // terminals. On the other hand if we just postUpdateBigramPredictions() it will
1886             // take a noticeable delay to update them which may feel uneasy.
1887         }
1888         if (showingAddToDictionaryHint) {
1889             if (mIsUserDictionaryAvaliable) {
1890                 mSuggestionsView.showAddToDictionaryHint(suggestion);
1891             } else {
1892                 mHandler.postUpdateSuggestions();
1893             }
1894         }
1895         if (ic != null) {
1896             ic.endBatchEdit();
1897         }
1898     }
1899 
1900     /**
1901      * Commits the chosen word to the text field and saves it for later retrieval.
1902      */
commitBestWord(CharSequence bestWord)1903     private void commitBestWord(CharSequence bestWord) {
1904         final KeyboardSwitcher switcher = mKeyboardSwitcher;
1905         if (!switcher.isKeyboardAvailable())
1906             return;
1907         final InputConnection ic = getCurrentInputConnection();
1908         if (ic != null) {
1909             mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators);
1910             if (mSettingsValues.mEnableSuggestionSpanInsertion) {
1911                 final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
1912                 ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
1913                         this, bestWord, suggestedWords), 1);
1914             } else {
1915                 ic.commitText(bestWord, 1);
1916             }
1917         }
1918         mHasUncommittedTypedChars = false;
1919         mCommittedLength = bestWord.length();
1920     }
1921 
1922     private static final WordComposer sEmptyWordComposer = new WordComposer();
updateBigramPredictions()1923     public void updateBigramPredictions() {
1924         if (mSuggest == null || !isSuggestionsRequested())
1925             return;
1926 
1927         if (!mSettingsValues.mBigramPredictionEnabled) {
1928             setPunctuationSuggestions();
1929             return;
1930         }
1931 
1932         final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
1933                 mSettingsValues.mWordSeparators);
1934         SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(sEmptyWordComposer,
1935                 prevWord, mKeyboardSwitcher.getLatinKeyboard().getProximityInfo());
1936 
1937         if (builder.size() > 0) {
1938             // Explicitly supply an empty typed word (the no-second-arg version of
1939             // showSuggestions will retrieve the word near the cursor, we don't want that here)
1940             showSuggestions(builder.build(), "");
1941         } else {
1942             if (!isShowingPunctuationList()) setPunctuationSuggestions();
1943         }
1944     }
1945 
setPunctuationSuggestions()1946     public void setPunctuationSuggestions() {
1947         setSuggestions(mSettingsValues.mSuggestPuncList);
1948         setSuggestionStripShown(isSuggestionsStripVisible());
1949     }
1950 
addToUserUnigramAndBigramDictionaries(CharSequence suggestion, int frequencyDelta)1951     private void addToUserUnigramAndBigramDictionaries(CharSequence suggestion,
1952             int frequencyDelta) {
1953         checkAddToDictionary(suggestion, frequencyDelta, false);
1954     }
1955 
addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta)1956     private void addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta) {
1957         checkAddToDictionary(suggestion, frequencyDelta, true);
1958     }
1959 
1960     /**
1961      * Adds to the UserBigramDictionary and/or UserUnigramDictionary
1962      * @param selectedANotTypedWord true if it should be added to bigram dictionary if possible
1963      */
checkAddToDictionary(CharSequence suggestion, int frequencyDelta, boolean selectedANotTypedWord)1964     private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta,
1965             boolean selectedANotTypedWord) {
1966         if (suggestion == null || suggestion.length() < 1) return;
1967 
1968         // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
1969         // adding words in situations where the user or application really didn't
1970         // want corrections enabled or learned.
1971         if (!(mCorrectionMode == Suggest.CORRECTION_FULL
1972                 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
1973             return;
1974         }
1975 
1976         if (null != mSuggest && null != mUserUnigramDictionary) {
1977             final boolean selectedATypedWordAndItsInUserUnigramDic =
1978                     !selectedANotTypedWord && mUserUnigramDictionary.isValidWord(suggestion);
1979             final boolean isValidWord = AutoCorrection.isValidWord(
1980                     mSuggest.getUnigramDictionaries(), suggestion, true);
1981             final boolean needsToAddToUserUnigramDictionary =
1982                     selectedATypedWordAndItsInUserUnigramDic || !isValidWord;
1983             if (needsToAddToUserUnigramDictionary) {
1984                 mUserUnigramDictionary.addWord(suggestion.toString(), frequencyDelta);
1985             }
1986         }
1987 
1988         if (mUserBigramDictionary != null) {
1989             // We don't want to register as bigrams words separated by a separator.
1990             // For example "I will, and you too" : we don't want the pair ("will" "and") to be
1991             // a bigram.
1992             final InputConnection ic = getCurrentInputConnection();
1993             if (null != ic) {
1994                 final CharSequence prevWord =
1995                         EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
1996                 if (!TextUtils.isEmpty(prevWord)) {
1997                     mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString());
1998                 }
1999             }
2000         }
2001     }
2002 
isCursorTouchingWord()2003     public boolean isCursorTouchingWord() {
2004         final InputConnection ic = getCurrentInputConnection();
2005         if (ic == null) return false;
2006         CharSequence toLeft = ic.getTextBeforeCursor(1, 0);
2007         CharSequence toRight = ic.getTextAfterCursor(1, 0);
2008         if (!TextUtils.isEmpty(toLeft)
2009                 && !mSettingsValues.isWordSeparator(toLeft.charAt(0))
2010                 && !mSettingsValues.isSuggestedPunctuation(toLeft.charAt(0))) {
2011             return true;
2012         }
2013         if (!TextUtils.isEmpty(toRight)
2014                 && !mSettingsValues.isWordSeparator(toRight.charAt(0))
2015                 && !mSettingsValues.isSuggestedPunctuation(toRight.charAt(0))) {
2016             return true;
2017         }
2018         return false;
2019     }
2020 
2021     // "ic" must not null
sameAsTextBeforeCursor(final InputConnection ic, CharSequence text)2022     private boolean sameAsTextBeforeCursor(final InputConnection ic, CharSequence text) {
2023         CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
2024         return TextUtils.equals(text, beforeText);
2025     }
2026 
2027     // "ic" must not null
revertLastWord(final InputConnection ic)2028     private void revertLastWord(final InputConnection ic) {
2029         if (mHasUncommittedTypedChars || mComposingStringBuilder.length() <= 0) {
2030             sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
2031             return;
2032         }
2033 
2034         final CharSequence separator = ic.getTextBeforeCursor(1, 0);
2035         ic.deleteSurroundingText(1, 0);
2036         final CharSequence textToTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0);
2037         ic.deleteSurroundingText(mCommittedLength, 0);
2038 
2039         // Re-insert "separator" only when the deleted character was word separator and the
2040         // composing text wasn't equal to the auto-corrected text which can be found before
2041         // the cursor.
2042         if (!TextUtils.isEmpty(separator)
2043                 && mSettingsValues.isWordSeparator(separator.charAt(0))
2044                 && !TextUtils.equals(mComposingStringBuilder, textToTheLeft)) {
2045             ic.commitText(mComposingStringBuilder, 1);
2046             TextEntryState.acceptedTyped(mComposingStringBuilder);
2047             ic.commitText(separator, 1);
2048             TextEntryState.typedCharacter(separator.charAt(0), true,
2049                     WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
2050             // Clear composing text
2051             mComposingStringBuilder.setLength(0);
2052         } else {
2053             mHasUncommittedTypedChars = true;
2054             ic.setComposingText(mComposingStringBuilder, 1);
2055             TextEntryState.backspace();
2056         }
2057         mHandler.cancelUpdateBigramPredictions();
2058         mHandler.postUpdateSuggestions();
2059     }
2060 
2061     // "ic" must not null
revertDoubleSpace(final InputConnection ic)2062     private boolean revertDoubleSpace(final InputConnection ic) {
2063         mHandler.cancelDoubleSpacesTimer();
2064         // Here we test whether we indeed have a period and a space before us. This should not
2065         // be needed, but it's there just in case something went wrong.
2066         final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
2067         if (!". ".equals(textBeforeCursor))
2068             return false;
2069         ic.beginBatchEdit();
2070         ic.deleteSurroundingText(2, 0);
2071         ic.commitText("  ", 1);
2072         ic.endBatchEdit();
2073         return true;
2074     }
2075 
isWordSeparator(int code)2076     public boolean isWordSeparator(int code) {
2077         return mSettingsValues.isWordSeparator(code);
2078     }
2079 
sendMagicSpace()2080     private void sendMagicSpace() {
2081         sendKeyChar((char)Keyboard.CODE_SPACE);
2082         mJustAddedMagicSpace = true;
2083         mKeyboardSwitcher.updateShiftState();
2084     }
2085 
preferCapitalization()2086     public boolean preferCapitalization() {
2087         return mWordComposer.isFirstCharCapitalized();
2088     }
2089 
2090     // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
2091     // according to new language or mode.
onRefreshKeyboard()2092     public void onRefreshKeyboard() {
2093         if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
2094             // Before Honeycomb, Voice IME is in LatinIME and it changes the current input view,
2095             // so that we need to re-create the keyboard input view here.
2096             setInputView(mKeyboardSwitcher.onCreateInputView());
2097         }
2098         // Reload keyboard because the current language has been changed.
2099         mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
2100         initSuggest();
2101         loadSettings();
2102     }
2103 
2104     @Override
onPress(int primaryCode, boolean withSliding)2105     public void onPress(int primaryCode, boolean withSliding) {
2106         final KeyboardSwitcher switcher = mKeyboardSwitcher;
2107         if (switcher.isVibrateAndSoundFeedbackRequired()) {
2108             vibrate();
2109             playKeyClick(primaryCode);
2110         }
2111         final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
2112         if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
2113             switcher.onPressShift(withSliding);
2114         } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
2115             switcher.onPressSymbol();
2116         } else {
2117             switcher.onOtherKeyPressed();
2118         }
2119     }
2120 
2121     @Override
onRelease(int primaryCode, boolean withSliding)2122     public void onRelease(int primaryCode, boolean withSliding) {
2123         KeyboardSwitcher switcher = mKeyboardSwitcher;
2124         // Reset any drag flags in the keyboard
2125         final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
2126         if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
2127             switcher.onReleaseShift(withSliding);
2128         } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
2129             switcher.onReleaseSymbol();
2130         }
2131     }
2132 
2133 
2134     // receive ringer mode change and network state change.
2135     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
2136         @Override
2137         public void onReceive(Context context, Intent intent) {
2138             final String action = intent.getAction();
2139             if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
2140                 updateRingerMode();
2141             } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
2142                 mSubtypeSwitcher.onNetworkStateChanged(intent);
2143             }
2144         }
2145     };
2146 
2147     // update sound effect volume
updateSoundEffectVolume()2148     private void updateSoundEffectVolume() {
2149         final String[] volumePerHardwareList = mResources.getStringArray(R.array.keypress_volumes);
2150         final String hardwarePrefix = Build.HARDWARE + ",";
2151         for (final String element : volumePerHardwareList) {
2152             if (element.startsWith(hardwarePrefix)) {
2153                 mFxVolume = Float.parseFloat(element.substring(element.lastIndexOf(',') + 1));
2154                 break;
2155             }
2156         }
2157     }
2158 
2159     // update flags for silent mode
updateRingerMode()2160     private void updateRingerMode() {
2161         if (mAudioManager == null) {
2162             mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
2163             if (mAudioManager == null) return;
2164         }
2165         mSilentModeOn = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
2166     }
2167 
updateKeypressVibrationDuration()2168     private void updateKeypressVibrationDuration() {
2169         mKeypressVibrationDuration = Utils.getCurrentVibrationDuration(mPrefs, mResources);
2170     }
2171 
playKeyClick(int primaryCode)2172     private void playKeyClick(int primaryCode) {
2173         // if mAudioManager is null, we don't have the ringer state yet
2174         // mAudioManager will be set by updateRingerMode
2175         if (mAudioManager == null) {
2176             if (mKeyboardSwitcher.getKeyboardView() != null) {
2177                 updateRingerMode();
2178             }
2179         }
2180         if (isSoundOn()) {
2181             final int sound;
2182             switch (primaryCode) {
2183             case Keyboard.CODE_DELETE:
2184                 sound = AudioManager.FX_KEYPRESS_DELETE;
2185                 break;
2186             case Keyboard.CODE_ENTER:
2187                 sound = AudioManager.FX_KEYPRESS_RETURN;
2188                 break;
2189             case Keyboard.CODE_SPACE:
2190                 sound = AudioManager.FX_KEYPRESS_SPACEBAR;
2191                 break;
2192             default:
2193                 sound = AudioManager.FX_KEYPRESS_STANDARD;
2194                 break;
2195             }
2196             mAudioManager.playSoundEffect(sound, mFxVolume);
2197         }
2198     }
2199 
vibrate()2200     public void vibrate() {
2201         if (!mSettingsValues.mVibrateOn) {
2202             return;
2203         }
2204         if (mKeypressVibrationDuration < 0) {
2205             // Go ahead with the system default
2206             LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
2207             if (inputView != null) {
2208                 inputView.performHapticFeedback(
2209                         HapticFeedbackConstants.KEYBOARD_TAP,
2210                         HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
2211             }
2212         } else if (mVibrator != null) {
2213             mVibrator.vibrate(mKeypressVibrationDuration);
2214         }
2215     }
2216 
getCurrentWord()2217     public WordComposer getCurrentWord() {
2218         return mWordComposer;
2219     }
2220 
isSoundOn()2221     boolean isSoundOn() {
2222         return mSettingsValues.mSoundOn && !mSilentModeOn;
2223     }
2224 
updateCorrectionMode()2225     private void updateCorrectionMode() {
2226         // TODO: cleanup messy flags
2227         final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
2228                 && !mInputTypeNoAutoCorrect;
2229         mCorrectionMode = (shouldAutoCorrect && mSettingsValues.mAutoCorrectEnabled)
2230                 ? Suggest.CORRECTION_FULL
2231                 : (shouldAutoCorrect ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE);
2232         mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect
2233                 && mSettingsValues.mAutoCorrectEnabled)
2234                 ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
2235         if (mSuggest != null) {
2236             mSuggest.setCorrectionMode(mCorrectionMode);
2237         }
2238     }
2239 
updateSuggestionVisibility(final SharedPreferences prefs, final Resources res)2240     private void updateSuggestionVisibility(final SharedPreferences prefs, final Resources res) {
2241         final String suggestionVisiblityStr = prefs.getString(
2242                 Settings.PREF_SHOW_SUGGESTIONS_SETTING,
2243                 res.getString(R.string.prefs_suggestion_visibility_default_value));
2244         for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
2245             if (suggestionVisiblityStr.equals(res.getString(visibility))) {
2246                 mSuggestionVisibility = visibility;
2247                 break;
2248             }
2249         }
2250     }
2251 
launchSettings()2252     protected void launchSettings() {
2253         launchSettingsClass(Settings.class);
2254     }
2255 
launchDebugSettings()2256     public void launchDebugSettings() {
2257         launchSettingsClass(DebugSettings.class);
2258     }
2259 
launchSettingsClass(Class<? extends PreferenceActivity> settingsClass)2260     protected void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
2261         handleClose();
2262         Intent intent = new Intent();
2263         intent.setClass(LatinIME.this, settingsClass);
2264         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2265         startActivity(intent);
2266     }
2267 
showSubtypeSelectorAndSettings()2268     private void showSubtypeSelectorAndSettings() {
2269         final CharSequence title = getString(R.string.english_ime_input_options);
2270         final CharSequence[] items = new CharSequence[] {
2271                 // TODO: Should use new string "Select active input modes".
2272                 getString(R.string.language_selection_title),
2273                 getString(R.string.english_ime_settings),
2274         };
2275         final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2276             @Override
2277             public void onClick(DialogInterface di, int position) {
2278                 di.dismiss();
2279                 switch (position) {
2280                 case 0:
2281                     Intent intent = CompatUtils.getInputLanguageSelectionIntent(
2282                             mInputMethodId, Intent.FLAG_ACTIVITY_NEW_TASK
2283                             | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
2284                             | Intent.FLAG_ACTIVITY_CLEAR_TOP);
2285                     startActivity(intent);
2286                     break;
2287                 case 1:
2288                     launchSettings();
2289                     break;
2290                 }
2291             }
2292         };
2293         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2294                 .setItems(items, listener)
2295                 .setTitle(title);
2296         showOptionDialogInternal(builder.create());
2297     }
2298 
showOptionsMenu()2299     private void showOptionsMenu() {
2300         final CharSequence title = getString(R.string.english_ime_input_options);
2301         final CharSequence[] items = new CharSequence[] {
2302                 getString(R.string.selectInputMethod),
2303                 getString(R.string.english_ime_settings),
2304         };
2305         final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
2306             @Override
2307             public void onClick(DialogInterface di, int position) {
2308                 di.dismiss();
2309                 switch (position) {
2310                 case 0:
2311                     mImm.showInputMethodPicker();
2312                     break;
2313                 case 1:
2314                     launchSettings();
2315                     break;
2316                 }
2317             }
2318         };
2319         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
2320                 .setItems(items, listener)
2321                 .setTitle(title);
2322         showOptionDialogInternal(builder.create());
2323     }
2324 
2325     @Override
dump(FileDescriptor fd, PrintWriter fout, String[] args)2326     protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
2327         super.dump(fd, fout, args);
2328 
2329         final Printer p = new PrintWriterPrinter(fout);
2330         p.println("LatinIME state :");
2331         p.println("  Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode());
2332         p.println("  mComposingStringBuilder=" + mComposingStringBuilder.toString());
2333         p.println("  mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn);
2334         p.println("  mCorrectionMode=" + mCorrectionMode);
2335         p.println("  mHasUncommittedTypedChars=" + mHasUncommittedTypedChars);
2336         p.println("  mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
2337         p.println("  mShouldInsertMagicSpace=" + mShouldInsertMagicSpace);
2338         p.println("  mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn);
2339         p.println("  TextEntryState.state=" + TextEntryState.getState());
2340         p.println("  mSoundOn=" + mSettingsValues.mSoundOn);
2341         p.println("  mVibrateOn=" + mSettingsValues.mVibrateOn);
2342         p.println("  mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
2343     }
2344 
2345     // Characters per second measurement
2346 
2347     private long mLastCpsTime;
2348     private static final int CPS_BUFFER_SIZE = 16;
2349     private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE];
2350     private int mCpsIndex;
2351 
measureCps()2352     private void measureCps() {
2353         long now = System.currentTimeMillis();
2354         if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial
2355         mCpsIntervals[mCpsIndex] = now - mLastCpsTime;
2356         mLastCpsTime = now;
2357         mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE;
2358         long total = 0;
2359         for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i];
2360         System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total));
2361     }
2362 }
2363