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