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