KeyboardSwitcher.java revision 6b7100fecaaaf0e8e42c4d2ccebac165e89e79bf
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
17package com.android.inputmethod.keyboard;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.text.TextUtils;
24import android.util.DisplayMetrics;
25import android.util.Log;
26import android.view.ContextThemeWrapper;
27import android.view.InflateException;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.inputmethod.EditorInfo;
31
32import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
33import com.android.inputmethod.keyboard.internal.KeyboardState;
34import com.android.inputmethod.latin.InputView;
35import com.android.inputmethod.latin.LatinIME;
36import com.android.inputmethod.latin.LatinImeLogger;
37import com.android.inputmethod.latin.LocaleUtils;
38import com.android.inputmethod.latin.R;
39import com.android.inputmethod.latin.Settings;
40import com.android.inputmethod.latin.SubtypeSwitcher;
41import com.android.inputmethod.latin.Utils;
42
43import java.lang.ref.SoftReference;
44import java.util.HashMap;
45import java.util.Locale;
46
47public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener {
48    private static final String TAG = KeyboardSwitcher.class.getSimpleName();
49    private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
50    public static final boolean DEBUG_STATE = false;
51
52    public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916";
53    private static final int[] KEYBOARD_THEMES = {
54        R.style.KeyboardTheme,
55        R.style.KeyboardTheme_HighContrast,
56        R.style.KeyboardTheme_Stone,
57        R.style.KeyboardTheme_Stone_Bold,
58        R.style.KeyboardTheme_Gingerbread,
59        R.style.KeyboardTheme_IceCreamSandwich,
60    };
61
62    private SubtypeSwitcher mSubtypeSwitcher;
63    private SharedPreferences mPrefs;
64
65    private InputView mCurrentInputView;
66    private LatinKeyboardView mKeyboardView;
67    private LatinIME mInputMethodService;
68    private String mPackageName;
69    private Resources mResources;
70
71    private KeyboardState mState;
72    private static final int UNSHIFT = 0;
73    private static final int MANUAL_SHIFT = 1;
74    private static final int AUTOMATIC_SHIFT = 2;
75
76    private KeyboardId mMainKeyboardId;
77    private KeyboardId mSymbolsKeyboardId;
78    private KeyboardId mSymbolsShiftedKeyboardId;
79
80    private KeyboardId mCurrentId;
81    private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboardCache =
82            new HashMap<KeyboardId, SoftReference<LatinKeyboard>>();
83
84    private KeyboardLayoutState mSavedKeyboardState = new KeyboardLayoutState();
85
86    /** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of
87     * what user actually typed. */
88    private boolean mIsAutoCorrectionActive;
89
90    // TODO: Encapsulate these state handling to separate class and combine with ShiftKeyState
91    // and ModifierKeyState into KeyboardState.
92    private static final int SWITCH_STATE_ALPHA = 0;
93    private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
94    private static final int SWITCH_STATE_SYMBOL = 2;
95    // The following states are used only on the distinct multi-touch panel devices.
96    private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
97    private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
98    private static final int SWITCH_STATE_CHORDING_ALPHA = 5;
99    private static final int SWITCH_STATE_CHORDING_SYMBOL = 6;
100    private int mSwitchState = SWITCH_STATE_ALPHA;
101
102    private String mLayoutSwitchBackSymbols;
103
104    private int mThemeIndex = -1;
105    private Context mThemeContext;
106
107    private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
108
109    // TODO: Move this to KeyboardState.
110    private class KeyboardLayoutState {
111        private boolean mIsValid;
112        private boolean mIsAlphabetMode;
113        private boolean mIsShiftLocked;
114        private boolean mIsShifted;
115
116        public void save() {
117            mIsAlphabetMode = isAlphabetMode();
118            if (mIsAlphabetMode) {
119                mIsShiftLocked = mState.isShiftLocked();
120                mIsShifted = !mIsShiftLocked && mState.isShiftedOrShiftLocked();
121            } else {
122                mIsShiftLocked = false;
123                mIsShifted = isSymbolShifted();
124            }
125            mIsValid = true;
126        }
127
128        public void restore() {
129            if (!mIsValid || mIsAlphabetMode) {
130                setAlphabetKeyboard();
131            } else {
132                if (mIsShifted) {
133                    setSymbolsShiftedKeyboard();
134                } else {
135                    setSymbolsKeyboard();
136                }
137            }
138
139            if (!mIsValid) return;
140            mIsValid = false;
141
142            if (mIsAlphabetMode) {
143                setShiftLocked(mIsShiftLocked);
144                if (!mIsShiftLocked) {
145                    setShifted(mIsShifted ? MANUAL_SHIFT : UNSHIFT);
146                }
147            }
148        }
149    }
150
151    public static KeyboardSwitcher getInstance() {
152        return sInstance;
153    }
154
155    private KeyboardSwitcher() {
156        // Intentional empty constructor for singleton.
157    }
158
159    public static void init(LatinIME ims, SharedPreferences prefs) {
160        sInstance.initInternal(ims, prefs);
161    }
162
163    private void initInternal(LatinIME ims, SharedPreferences prefs) {
164        mInputMethodService = ims;
165        mPackageName = ims.getPackageName();
166        mResources = ims.getResources();
167        mPrefs = prefs;
168        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
169        mState = new KeyboardState();
170        setContextThemeWrapper(ims, getKeyboardThemeIndex(ims, prefs));
171        prefs.registerOnSharedPreferenceChangeListener(this);
172    }
173
174    private static int getKeyboardThemeIndex(Context context, SharedPreferences prefs) {
175        final String defaultThemeId = context.getString(R.string.config_default_keyboard_theme_id);
176        final String themeId = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeId);
177        try {
178            final int themeIndex = Integer.valueOf(themeId);
179            if (themeIndex >= 0 && themeIndex < KEYBOARD_THEMES.length)
180                return themeIndex;
181        } catch (NumberFormatException e) {
182            // Format error, keyboard theme is default to 0.
183        }
184        Log.w(TAG, "Illegal keyboard theme in preference: " + themeId + ", default to 0");
185        return 0;
186    }
187
188    private void setContextThemeWrapper(Context context, int themeIndex) {
189        if (mThemeIndex != themeIndex) {
190            mThemeIndex = themeIndex;
191            mThemeContext = new ContextThemeWrapper(context, KEYBOARD_THEMES[themeIndex]);
192            mKeyboardCache.clear();
193        }
194    }
195
196    public void loadKeyboard(EditorInfo editorInfo, Settings.Values settingsValues) {
197        try {
198            mMainKeyboardId = getKeyboardId(editorInfo, false, false, settingsValues);
199            mSymbolsKeyboardId = getKeyboardId(editorInfo, true, false, settingsValues);
200            mSymbolsShiftedKeyboardId = getKeyboardId(editorInfo, true, true, settingsValues);
201            mState.onLoadKeyboard();
202            mLayoutSwitchBackSymbols = mResources.getString(R.string.layout_switch_back_symbols);
203            mSavedKeyboardState.restore();
204        } catch (RuntimeException e) {
205            Log.w(TAG, "loading keyboard failed: " + mMainKeyboardId, e);
206            LatinImeLogger.logOnException(mMainKeyboardId.toString(), e);
207        }
208    }
209
210    public void saveKeyboardState() {
211        if (mCurrentId != null) {
212            mSavedKeyboardState.save();
213        }
214    }
215
216    public void onFinishInputView() {
217        mIsAutoCorrectionActive = false;
218    }
219
220    public void onHideWindow() {
221        mIsAutoCorrectionActive = false;
222    }
223
224    private void setKeyboard(final Keyboard keyboard) {
225        final Keyboard oldKeyboard = mKeyboardView.getKeyboard();
226        mKeyboardView.setKeyboard(keyboard);
227        mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding);
228        mCurrentId = keyboard.mId;
229        mSwitchState = getSwitchState();
230        updateShiftLockState(keyboard);
231        mKeyboardView.setKeyPreviewPopupEnabled(
232                Settings.Values.isKeyPreviewPopupEnabled(mPrefs, mResources),
233                Settings.Values.getKeyPreviewPopupDismissDelay(mPrefs, mResources));
234        final boolean localeChanged = (oldKeyboard == null)
235                || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale);
236        mInputMethodService.mHandler.startDisplayLanguageOnSpacebar(localeChanged);
237        updateShiftState();
238    }
239
240    private int getSwitchState() {
241        return isAlphabetMode() ? SWITCH_STATE_ALPHA : SWITCH_STATE_SYMBOL_BEGIN;
242    }
243
244    private void updateShiftLockState(Keyboard keyboard) {
245        if (mCurrentId.equals(mSymbolsShiftedKeyboardId)) {
246            // Symbol keyboard may have an ALT key that has a caps lock style indicator (a.k.a.
247            // sticky shift key). To show or dismiss the indicator, we need to call setShiftLocked()
248            // that takes care of the current keyboard having such ALT key or not.
249            keyboard.setShiftLocked(keyboard.hasShiftLockKey());
250        } else if (mCurrentId.equals(mSymbolsKeyboardId)) {
251            // Symbol keyboard has an ALT key that has a caps lock style indicator. To disable the
252            // indicator, we need to call setShiftLocked(false).
253            keyboard.setShiftLocked(false);
254        }
255    }
256
257    private LatinKeyboard getKeyboard(KeyboardId id) {
258        final SoftReference<LatinKeyboard> ref = mKeyboardCache.get(id);
259        LatinKeyboard keyboard = (ref == null) ? null : ref.get();
260        if (keyboard == null) {
261            final Locale savedLocale = LocaleUtils.setSystemLocale(mResources, id.mLocale);
262            try {
263                final LatinKeyboard.Builder builder = new LatinKeyboard.Builder(mThemeContext);
264                builder.load(id);
265                builder.setTouchPositionCorrectionEnabled(
266                        mSubtypeSwitcher.currentSubtypeContainsExtraValueKey(
267                                LatinIME.SUBTYPE_EXTRA_VALUE_SUPPORT_TOUCH_POSITION_CORRECTION));
268                keyboard = builder.build();
269            } finally {
270                LocaleUtils.setSystemLocale(mResources, savedLocale);
271            }
272            mKeyboardCache.put(id, new SoftReference<LatinKeyboard>(keyboard));
273
274            if (DEBUG_CACHE) {
275                Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": "
276                        + ((ref == null) ? "LOAD" : "GCed") + " id=" + id
277                        + " theme=" + Keyboard.themeName(keyboard.mThemeId));
278            }
279        } else if (DEBUG_CACHE) {
280            Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": HIT  id=" + id
281                    + " theme=" + Keyboard.themeName(keyboard.mThemeId));
282        }
283
284        keyboard.onAutoCorrectionStateChanged(mIsAutoCorrectionActive);
285        keyboard.setShiftLocked(false);
286        keyboard.setShifted(false);
287        // If the cached keyboard had been switched to another keyboard while the language was
288        // displayed on its spacebar, it might have had arbitrary text fade factor. In such case,
289        // we should reset the text fade factor. It is also applicable to shortcut key.
290        keyboard.setSpacebarTextFadeFactor(0.0f, null);
291        keyboard.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady(), null);
292        return keyboard;
293    }
294
295    private KeyboardId getKeyboardId(EditorInfo editorInfo, final boolean isSymbols,
296            final boolean isShift, Settings.Values settingsValues) {
297        final int mode = Utils.getKeyboardMode(editorInfo);
298        final int xmlId;
299        switch (mode) {
300        case KeyboardId.MODE_PHONE:
301            xmlId = (isSymbols && isShift) ? R.xml.kbd_phone_shift : R.xml.kbd_phone;
302            break;
303        case KeyboardId.MODE_NUMBER:
304            xmlId = R.xml.kbd_number;
305            break;
306        default:
307            if (isSymbols) {
308                xmlId = isShift ? R.xml.kbd_symbols_shift : R.xml.kbd_symbols;
309            } else {
310                xmlId = R.xml.kbd_qwerty;
311            }
312            break;
313        }
314
315        final boolean settingsKeyEnabled = settingsValues.isSettingsKeyEnabled();
316        @SuppressWarnings("deprecation")
317        final boolean noMicrophone = Utils.inPrivateImeOptions(
318                mPackageName, LatinIME.IME_OPTION_NO_MICROPHONE, editorInfo)
319                || Utils.inPrivateImeOptions(
320                        null, LatinIME.IME_OPTION_NO_MICROPHONE_COMPAT, editorInfo);
321        final boolean voiceKeyEnabled = settingsValues.isVoiceKeyEnabled(editorInfo)
322                && !noMicrophone;
323        final boolean voiceKeyOnMain = settingsValues.isVoiceKeyOnMain();
324        final boolean noSettingsKey = Utils.inPrivateImeOptions(
325                mPackageName, LatinIME.IME_OPTION_NO_SETTINGS_KEY, editorInfo);
326        final boolean hasSettingsKey = settingsKeyEnabled && !noSettingsKey;
327        final int f2KeyMode = getF2KeyMode(settingsKeyEnabled, noSettingsKey);
328        final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != voiceKeyOnMain);
329        final boolean forceAscii = Utils.inPrivateImeOptions(
330                mPackageName, LatinIME.IME_OPTION_FORCE_ASCII, editorInfo);
331        final boolean asciiCapable = mSubtypeSwitcher.currentSubtypeContainsExtraValueKey(
332                LatinIME.SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE);
333        final Locale locale = (forceAscii && !asciiCapable)
334                ? Locale.US : mSubtypeSwitcher.getInputLocale();
335        final Configuration conf = mResources.getConfiguration();
336        final DisplayMetrics dm = mResources.getDisplayMetrics();
337
338        return new KeyboardId(
339                mResources.getResourceEntryName(xmlId), xmlId, locale, conf.orientation,
340                dm.widthPixels, mode, editorInfo, hasSettingsKey, f2KeyMode, noSettingsKey,
341                voiceKeyEnabled, hasShortcutKey);
342    }
343
344    public int getKeyboardMode() {
345        return mCurrentId != null ? mCurrentId.mMode : KeyboardId.MODE_TEXT;
346    }
347
348    public boolean isAlphabetMode() {
349        return mCurrentId != null && mCurrentId.isAlphabetKeyboard();
350    }
351
352    public boolean isInputViewShown() {
353        return mCurrentInputView != null && mCurrentInputView.isShown();
354    }
355
356    public boolean isKeyboardAvailable() {
357        if (mKeyboardView != null)
358            return mKeyboardView.getKeyboard() != null;
359        return false;
360    }
361
362    public LatinKeyboard getLatinKeyboard() {
363        if (mKeyboardView != null) {
364            final Keyboard keyboard = mKeyboardView.getKeyboard();
365            if (keyboard instanceof LatinKeyboard)
366                return (LatinKeyboard)keyboard;
367        }
368        return null;
369    }
370
371    public boolean isShiftedOrShiftLocked() {
372        return mState.isShiftedOrShiftLocked();
373    }
374
375    public boolean isManualTemporaryUpperCase() {
376        return mState.isManualTemporaryUpperCase();
377    }
378
379    private void setShifted(int shiftMode) {
380        mInputMethodService.mHandler.cancelUpdateShiftState();
381        LatinKeyboard latinKeyboard = getLatinKeyboard();
382        if (latinKeyboard == null)
383            return;
384        if (shiftMode == AUTOMATIC_SHIFT) {
385            mState.setAutomaticTemporaryUpperCase();
386            latinKeyboard.setAutomaticTemporaryUpperCase();
387        } else {
388            final boolean shifted = (shiftMode == MANUAL_SHIFT);
389            // On non-distinct multi touch panel device, we should also turn off the shift locked
390            // state when shift key is pressed to go to normal mode.
391            // On the other hand, on distinct multi touch panel device, turning off the shift
392            // locked state with shift key pressing is handled by onReleaseShift().
393            if (!hasDistinctMultitouch() && !shifted && latinKeyboard.isShiftLocked()) {
394                mState.setShiftLocked(false);
395                latinKeyboard.setShiftLocked(false);
396            }
397            mState.setShifted(shifted);
398            latinKeyboard.setShifted(shifted);
399        }
400        mKeyboardView.invalidateAllKeys();
401    }
402
403    private void setShiftLocked(boolean shiftLocked) {
404        mInputMethodService.mHandler.cancelUpdateShiftState();
405        LatinKeyboard latinKeyboard = getLatinKeyboard();
406        if (latinKeyboard == null)
407            return;
408        mState.setShiftLocked(shiftLocked);
409        latinKeyboard.setShiftLocked(shiftLocked);
410        mKeyboardView.invalidateAllKeys();
411        if (!shiftLocked) {
412            // To be able to turn off caps lock by "double tap" on shift key, we should ignore
413            // the second tap of the "double tap" from now for a while because we just have
414            // already turned off caps lock above.
415            mKeyboardView.startIgnoringDoubleTap();
416        }
417    }
418
419    /**
420     * Toggle keyboard shift state triggered by user touch event.
421     */
422    public void toggleShift() {
423        if (DEBUG_STATE) {
424            Log.d(TAG, "toggleShift: " + mState);
425        }
426        if (isAlphabetMode()) {
427            setShifted(mState.isShiftedOrShiftLocked() ? UNSHIFT : MANUAL_SHIFT);
428        } else {
429            toggleShiftInSymbols();
430        }
431    }
432
433    public void toggleCapsLock() {
434        if (DEBUG_STATE) {
435            Log.d(TAG, "toggleCapsLock: " + mState);
436        }
437        if (isAlphabetMode()) {
438            if (mState.isShiftLocked()) {
439                setShiftLocked(false);
440                // TODO: Remove this.
441                // Shift key is long pressed while caps lock state, we will toggle back to normal
442                // state. And mark as if shift key is released.
443                mState.onReleaseCapsLock();
444            } else {
445                setShiftLocked(true);
446            }
447        }
448    }
449
450    public void toggleKeyboardMode() {
451        if (DEBUG_STATE) {
452            Log.d(TAG, "toggleKeyboard: " + mState);
453        }
454        toggleAlphabetAndSymbols();
455    }
456
457    /**
458     * Update keyboard shift state triggered by connected EditText status change.
459     */
460    public void updateShiftState() {
461        if (DEBUG_STATE) {
462            Log.d(TAG, "updateShiftState: " + mState
463                    + " autoCaps=" + mInputMethodService.getCurrentAutoCapsState());
464        }
465        final boolean isAlphabetMode = isAlphabetMode();
466        final boolean isShiftLocked = mState.isShiftLocked();
467        if (isAlphabetMode) {
468            if (!isShiftLocked && !mState.isShiftKeyIgnoring()) {
469                if (mState.isShiftKeyReleasing() && mInputMethodService.getCurrentAutoCapsState()) {
470                    // Only when shift key is releasing, automatic temporary upper case will be set.
471                    setShifted(AUTOMATIC_SHIFT);
472                } else {
473                    setShifted(mState.isShiftKeyMomentary() ? MANUAL_SHIFT : UNSHIFT);
474                }
475            }
476        }
477        mState.onUpdateShiftState(isAlphabetMode);
478    }
479
480    public void onPressShift(boolean withSliding) {
481        if (!isKeyboardAvailable())
482            return;
483        if (DEBUG_STATE) {
484            Log.d(TAG, "onPressShift: " + mState + " sliding=" + withSliding);
485        }
486        final boolean isAlphabetMode = isAlphabetMode();
487        final boolean isShiftLocked = mState.isShiftLocked();
488        final boolean isAutomaticTemporaryUpperCase = mState.isAutomaticTemporaryUpperCase();
489        final boolean isShiftedOrShiftLocked = mState.isShiftedOrShiftLocked();
490        if (isAlphabetMode) {
491            if (isShiftLocked) {
492                // Shift key is pressed while caps lock state, we will treat this state as shifted
493                // caps lock state and mark as if shift key pressed while normal state.
494                setShifted(MANUAL_SHIFT);
495            } else if (isAutomaticTemporaryUpperCase) {
496                // Shift key is pressed while automatic temporary upper case, we have to move to
497                // manual temporary upper case.
498                setShifted(MANUAL_SHIFT);
499            } else if (isShiftedOrShiftLocked) {
500                // In manual upper case state, we just record shift key has been pressing while
501                // shifted state.
502            } else {
503                // In base layout, chording or manual temporary upper case mode is started.
504                setShifted(MANUAL_SHIFT);
505            }
506        } else {
507            // In symbol mode, just toggle symbol and symbol more keyboard.
508            toggleShiftInSymbols();
509            mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
510        }
511        mState.onPressShift(isAlphabetMode, isShiftLocked, isAutomaticTemporaryUpperCase,
512                isShiftedOrShiftLocked);
513    }
514
515    public void onReleaseShift(boolean withSliding) {
516        if (!isKeyboardAvailable())
517            return;
518        if (DEBUG_STATE) {
519            Log.d(TAG, "onReleaseShift: " + mState + " sliding=" + withSliding);
520        }
521        final boolean isAlphabetMode = isAlphabetMode();
522        final boolean isShiftLocked = mState.isShiftLocked();
523        final boolean isShiftLockShifted = mState.isShiftLockShifted();
524        final boolean isShiftedOrShiftLocked = mState.isShiftedOrShiftLocked();
525        final boolean isManualTemporaryUpperCaseFromAuto =
526                mState.isManualTemporaryUpperCaseFromAuto();
527        if (isAlphabetMode) {
528            if (mState.isShiftKeyMomentary()) {
529                // After chording input while normal state.
530                setShifted(UNSHIFT);
531            } else if (isShiftLocked && !isShiftLockShifted && (mState.isShiftKeyPressing()
532                    || mState.isShiftKeyPressingOnShifted()) && !withSliding) {
533                // Shift has been long pressed, ignore this release.
534            } else if (isShiftLocked && !mState.isShiftKeyIgnoring() && !withSliding) {
535                // Shift has been pressed without chording while caps lock state.
536                setShiftLocked(false);
537            } else if (isShiftedOrShiftLocked && mState.isShiftKeyPressingOnShifted()
538                    && !withSliding) {
539                // Shift has been pressed without chording while shifted state.
540                setShifted(UNSHIFT);
541            } else if (isManualTemporaryUpperCaseFromAuto && mState.isShiftKeyPressing()
542                    && !withSliding) {
543                // Shift has been pressed without chording while manual temporary upper case
544                // transited from automatic temporary upper case.
545                setShifted(UNSHIFT);
546            }
547        } else {
548            // In symbol mode, snap back to the previous keyboard mode if the user chords the shift
549            // key and another key, then releases the shift key.
550            if (mSwitchState == SWITCH_STATE_CHORDING_SYMBOL) {
551                toggleShiftInSymbols();
552            }
553        }
554        mState.onReleaseShift();
555    }
556
557    public void onPressSymbol() {
558        if (DEBUG_STATE) {
559            Log.d(TAG, "onPressSymbol: " + mState);
560        }
561        toggleAlphabetAndSymbols();
562        mState.onPressSymbol();
563        mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
564    }
565
566    public void onReleaseSymbol() {
567        if (DEBUG_STATE) {
568            Log.d(TAG, "onReleaseSymbol: " + mState);
569            }
570        // Snap back to the previous keyboard mode if the user chords the mode change key and
571        // another key, then releases the mode change key.
572        if (mSwitchState == SWITCH_STATE_CHORDING_ALPHA) {
573            toggleAlphabetAndSymbols();
574        }
575        mState.onReleaseSymbol();
576    }
577
578    public void onOtherKeyPressed() {
579        if (DEBUG_STATE) {
580            Log.d(TAG, "onOtherKeyPressed: " + mState);
581        }
582        mState.onOtherKeyPressed();
583    }
584
585    public void onCancelInput() {
586        // Snap back to the previous keyboard mode if the user cancels sliding input.
587        if (isSinglePointer()) {
588            if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
589                toggleAlphabetAndSymbols();
590            } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
591                toggleShiftInSymbols();
592            }
593        }
594    }
595
596    private boolean mPrevMainKeyboardWasShiftLocked;
597
598    private void setSymbolsKeyboard() {
599        mPrevMainKeyboardWasShiftLocked = mState.isShiftLocked();
600        setKeyboard(getKeyboard(mSymbolsKeyboardId));
601    }
602
603    private void setAlphabetKeyboard() {
604        setKeyboard(getKeyboard(mMainKeyboardId));
605        setShiftLocked(mPrevMainKeyboardWasShiftLocked);
606        mPrevMainKeyboardWasShiftLocked = false;
607    }
608
609    private void toggleAlphabetAndSymbols() {
610        if (isAlphabetMode()) {
611            setSymbolsKeyboard();
612        } else {
613            setAlphabetKeyboard();
614        }
615    }
616
617    private boolean isSymbolShifted() {
618        return mCurrentId != null && mCurrentId.equals(mSymbolsShiftedKeyboardId);
619    }
620
621    private void setSymbolsShiftedKeyboard() {
622        setKeyboard(getKeyboard(mSymbolsShiftedKeyboardId));
623    }
624
625    private void toggleShiftInSymbols() {
626        if (isSymbolShifted()) {
627            setSymbolsKeyboard();
628        } else {
629            setSymbolsShiftedKeyboard();
630        }
631    }
632
633    public boolean isInMomentarySwitchState() {
634        return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
635                || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
636    }
637
638    public boolean isVibrateAndSoundFeedbackRequired() {
639        return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
640    }
641
642    private boolean isSinglePointer() {
643        return mKeyboardView != null && mKeyboardView.getPointerCount() == 1;
644    }
645
646    public boolean hasDistinctMultitouch() {
647        return mKeyboardView != null && mKeyboardView.hasDistinctMultitouch();
648    }
649
650    private static boolean isSpaceCharacter(int c) {
651        return c == Keyboard.CODE_SPACE || c == Keyboard.CODE_ENTER;
652    }
653
654    private boolean isLayoutSwitchBackCharacter(int c) {
655        if (TextUtils.isEmpty(mLayoutSwitchBackSymbols)) return false;
656        if (mLayoutSwitchBackSymbols.indexOf(c) >= 0) return true;
657        return false;
658    }
659
660    /**
661     * Updates state machine to figure out when to automatically snap back to the previous mode.
662     */
663    public void onKey(int code) {
664        if (DEBUG_STATE) {
665            Log.d(TAG, "onKey: code=" + code + " switchState=" + mSwitchState
666                    + " isSinglePointer=" + isSinglePointer());
667        }
668        switch (mSwitchState) {
669        case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
670            // Only distinct multi touch devices can be in this state.
671            // On non-distinct multi touch devices, mode change key is handled by
672            // {@link LatinIME#onCodeInput}, not by {@link LatinIME#onPress} and
673            // {@link LatinIME#onRelease}. So, on such devices, {@link #mSwitchState} starts
674            // from {@link #SWITCH_STATE_SYMBOL_BEGIN}, or {@link #SWITCH_STATE_ALPHA}, not from
675            // {@link #SWITCH_STATE_MOMENTARY}.
676            if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
677                // Detected only the mode change key has been pressed, and then released.
678                if (mCurrentId.equals(mMainKeyboardId)) {
679                    mSwitchState = SWITCH_STATE_ALPHA;
680                } else {
681                    mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
682                }
683            } else if (isSinglePointer()) {
684                // Snap back to the previous keyboard mode if the user pressed the mode change key
685                // and slid to other key, then released the finger.
686                // If the user cancels the sliding input, snapping back to the previous keyboard
687                // mode is handled by {@link #onCancelInput}.
688                toggleAlphabetAndSymbols();
689            } else {
690                // Chording input is being started. The keyboard mode will be snapped back to the
691                // previous mode in {@link onReleaseSymbol} when the mode change key is released.
692                mSwitchState = SWITCH_STATE_CHORDING_ALPHA;
693            }
694            break;
695        case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
696            if (code == Keyboard.CODE_SHIFT) {
697                // Detected only the shift key has been pressed on symbol layout, and then released.
698                mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
699            } else if (isSinglePointer()) {
700                // Snap back to the previous keyboard mode if the user pressed the shift key on
701                // symbol mode and slid to other key, then released the finger.
702                toggleShiftInSymbols();
703                mSwitchState = SWITCH_STATE_SYMBOL;
704            } else {
705                // Chording input is being started. The keyboard mode will be snapped back to the
706                // previous mode in {@link onReleaseShift} when the shift key is released.
707                mSwitchState = SWITCH_STATE_CHORDING_SYMBOL;
708            }
709            break;
710        case SWITCH_STATE_SYMBOL_BEGIN:
711            if (!isSpaceCharacter(code) && code >= 0) {
712                mSwitchState = SWITCH_STATE_SYMBOL;
713            }
714            // Snap back to alpha keyboard mode immediately if user types a quote character.
715            if (isLayoutSwitchBackCharacter(code)) {
716                setAlphabetKeyboard();
717            }
718            break;
719        case SWITCH_STATE_SYMBOL:
720        case SWITCH_STATE_CHORDING_SYMBOL:
721            // Snap back to alpha keyboard mode if user types one or more non-space/enter
722            // characters followed by a space/enter or a quote character.
723            if (isSpaceCharacter(code) || isLayoutSwitchBackCharacter(code)) {
724                setAlphabetKeyboard();
725            }
726            break;
727        }
728    }
729
730    public LatinKeyboardView getKeyboardView() {
731        return mKeyboardView;
732    }
733
734    public View onCreateInputView() {
735        return createInputView(mThemeIndex, true);
736    }
737
738    private View createInputView(final int newThemeIndex, final boolean forceRecreate) {
739        if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate)
740            return mCurrentInputView;
741
742        if (mKeyboardView != null) {
743            mKeyboardView.closing();
744        }
745
746        final int oldThemeIndex = mThemeIndex;
747        Utils.GCUtils.getInstance().reset();
748        boolean tryGC = true;
749        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
750            try {
751                setContextThemeWrapper(mInputMethodService, newThemeIndex);
752                mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
753                        R.layout.input_view, null);
754                tryGC = false;
755            } catch (OutOfMemoryError e) {
756                Log.w(TAG, "load keyboard failed: " + e);
757                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
758                        oldThemeIndex + "," + newThemeIndex, e);
759            } catch (InflateException e) {
760                Log.w(TAG, "load keyboard failed: " + e);
761                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
762                        oldThemeIndex + "," + newThemeIndex, e);
763            }
764        }
765
766        mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
767        mKeyboardView.setKeyboardActionListener(mInputMethodService);
768
769        // This always needs to be set since the accessibility state can
770        // potentially change without the input view being re-created.
771        AccessibleKeyboardViewProxy.setView(mKeyboardView);
772
773        return mCurrentInputView;
774    }
775
776    private void postSetInputView(final View newInputView) {
777        mInputMethodService.mHandler.post(new Runnable() {
778            @Override
779            public void run() {
780                if (newInputView != null) {
781                    mInputMethodService.setInputView(newInputView);
782                }
783                mInputMethodService.updateInputViewShown();
784            }
785        });
786    }
787
788    @Override
789    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
790        if (PREF_KEYBOARD_LAYOUT.equals(key)) {
791            final int themeIndex = getKeyboardThemeIndex(mInputMethodService, sharedPreferences);
792            postSetInputView(createInputView(themeIndex, false));
793        } else if (Settings.PREF_SHOW_SETTINGS_KEY.equals(key)) {
794            postSetInputView(createInputView(mThemeIndex, true));
795        }
796    }
797
798    public void onAutoCorrectionStateChanged(boolean isAutoCorrection) {
799        if (mIsAutoCorrectionActive != isAutoCorrection) {
800            mIsAutoCorrectionActive = isAutoCorrection;
801            final LatinKeyboard keyboard = getLatinKeyboard();
802            if (keyboard != null && keyboard.needsAutoCorrectionSpacebarLed()) {
803                final Key invalidatedKey = keyboard.onAutoCorrectionStateChanged(isAutoCorrection);
804                final LatinKeyboardView keyboardView = getKeyboardView();
805                if (keyboardView != null)
806                    keyboardView.invalidateKey(invalidatedKey);
807            }
808        }
809    }
810
811    private static int getF2KeyMode(boolean settingsKeyEnabled, boolean noSettingsKey) {
812        if (noSettingsKey) {
813            // Never shows the Settings key
814            return KeyboardId.F2KEY_MODE_SHORTCUT_IME;
815        }
816
817        if (settingsKeyEnabled) {
818            return KeyboardId.F2KEY_MODE_SETTINGS;
819        } else {
820            // It should be alright to fall back to the Settings key on 7-inch layouts
821            // even when the Settings key is not explicitly enabled.
822            return KeyboardId.F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS;
823        }
824    }
825}
826