KeyboardSwitcher.java revision ec52fb6dbb0794d9872bb3e83ea166c6996acf4e
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 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        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(mCurrentId);
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(KeyboardId id) {
246        return id.equals(mMainKeyboardId) ? 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        LatinKeyboard latinKeyboard = getLatinKeyboard();
378        if (latinKeyboard != null)
379            return latinKeyboard.isShiftedOrShiftLocked();
380        return false;
381    }
382
383    public boolean isShiftLocked() {
384        LatinKeyboard latinKeyboard = getLatinKeyboard();
385        if (latinKeyboard != null)
386            return latinKeyboard.isShiftLocked();
387        return false;
388    }
389
390    private boolean isShiftLockShifted() {
391        LatinKeyboard latinKeyboard = getLatinKeyboard();
392        if (latinKeyboard != null)
393            return latinKeyboard.isShiftLockShifted();
394        return false;
395    }
396
397    private boolean isAutomaticTemporaryUpperCase() {
398        LatinKeyboard latinKeyboard = getLatinKeyboard();
399        if (latinKeyboard != null)
400            return latinKeyboard.isAutomaticTemporaryUpperCase();
401        return false;
402    }
403
404    public boolean isManualTemporaryUpperCase() {
405        LatinKeyboard latinKeyboard = getLatinKeyboard();
406        if (latinKeyboard != null)
407            return latinKeyboard.isManualTemporaryUpperCase();
408        return false;
409    }
410
411    private boolean isManualTemporaryUpperCaseFromAuto() {
412        LatinKeyboard latinKeyboard = getLatinKeyboard();
413        if (latinKeyboard != null)
414            return latinKeyboard.isManualTemporaryUpperCaseFromAuto();
415        return false;
416    }
417
418    private void setShift(int shiftMode) {
419        LatinKeyboard latinKeyboard = getLatinKeyboard();
420        if (latinKeyboard == null)
421            return;
422        if (shiftMode == AUTOMATIC_SHIFT) {
423            latinKeyboard.setAutomaticTemporaryUpperCase();
424            mKeyboardView.invalidateAllKeys();
425        } else {
426            final boolean shifted = (shiftMode == MANUAL_SHIFT);
427            // On non-distinct multi touch panel device, we should also turn off the shift locked
428            // state when shift key is pressed to go to normal mode.
429            // On the other hand, on distinct multi touch panel device, turning off the shift
430            // locked state with shift key pressing is handled by onReleaseShift().
431            if (!hasDistinctMultitouch() && !shifted && latinKeyboard.isShiftLocked()) {
432                latinKeyboard.setShiftLocked(false);
433            }
434            if (latinKeyboard.setShifted(shifted)) {
435                mKeyboardView.invalidateAllKeys();
436            }
437        }
438    }
439
440    private void setShiftLocked(boolean shiftLocked) {
441        LatinKeyboard latinKeyboard = getLatinKeyboard();
442        if (latinKeyboard != null && latinKeyboard.setShiftLocked(shiftLocked)) {
443            mKeyboardView.invalidateAllKeys();
444        }
445    }
446
447    /**
448     * Toggle keyboard shift state triggered by user touch event.
449     */
450    public void toggleShift() {
451        mInputMethodService.mHandler.cancelUpdateShiftState();
452        if (DEBUG_STATE) {
453            Log.d(TAG, "toggleShift:"
454                    + " keyboard=" + getLatinKeyboard().mShiftState
455                    + " state=" + mState);
456        }
457        if (isAlphabetMode()) {
458            setShift(isShiftedOrShiftLocked() ? UNSHIFT : MANUAL_SHIFT);
459        } else {
460            toggleShiftInSymbol();
461        }
462    }
463
464    public void toggleCapsLock() {
465        mInputMethodService.mHandler.cancelUpdateShiftState();
466        if (DEBUG_STATE) {
467            Log.d(TAG, "toggleCapsLock:"
468                    + " keyboard=" + getLatinKeyboard().mShiftState
469                    + " state=" + mState);
470        }
471        if (isAlphabetMode()) {
472            if (isShiftLocked()) {
473                // Shift key is long pressed while caps lock state, we will toggle back to normal
474                // state. And mark as if shift key is released.
475                setShiftLocked(false);
476                mState.onToggleCapsLock();
477            } else {
478                setShiftLocked(true);
479            }
480        }
481    }
482
483    public void changeKeyboardMode() {
484        if (DEBUG_STATE) {
485            Log.d(TAG, "changeKeyboardMode:"
486                    + " keyboard=" + getLatinKeyboard().mShiftState
487                    + " state=" + mState);
488        }
489        toggleKeyboardMode();
490        if (isShiftLocked() && isAlphabetMode()) {
491            setShiftLocked(true);
492        }
493        updateShiftState();
494    }
495
496    private void startIgnoringDoubleTap() {
497        if (mKeyboardView != null) {
498            mKeyboardView.startIgnoringDoubleTap();
499        }
500    }
501
502    /**
503     * Update keyboard shift state triggered by connected EditText status change.
504     */
505    public void updateShiftState() {
506        final boolean isAlphabetMode = isAlphabetMode();
507        final boolean isShiftLocked = isShiftLocked();
508        if (DEBUG_STATE) {
509            Log.d(TAG, "updateShiftState:"
510                    + " autoCaps=" + mInputMethodService.getCurrentAutoCapsState()
511                    + " keyboard=" + getLatinKeyboard().mShiftState
512                    + " isAlphabetMode=" + isAlphabetMode
513                    + " isShiftLocked=" + isShiftLocked
514                    + " state=" + mState);
515        }
516        if (isAlphabetMode) {
517            if (!isShiftLocked && !mState.isShiftKeyIgnoring()) {
518                if (mState.isShiftKeyReleasing() && mInputMethodService.getCurrentAutoCapsState()) {
519                    // Only when shift key is releasing, automatic temporary upper case will be set.
520                    setShift(AUTOMATIC_SHIFT);
521                } else {
522                    setShift(mState.isShiftKeyMomentary() ? MANUAL_SHIFT : UNSHIFT);
523                }
524            }
525        }
526        mState.onUpdateShiftState(isAlphabetMode);
527    }
528
529    public void onPressShift(boolean withSliding) {
530        if (!isKeyboardAvailable())
531            return;
532        if (DEBUG_STATE) {
533            Log.d(TAG, "onPressShift:"
534                    + " keyboard=" + getLatinKeyboard().mShiftState
535                    + " state=" + mState + " sliding=" + withSliding);
536        }
537        final boolean isAlphabetMode = isAlphabetMode();
538        final boolean isShiftLocked = isShiftLocked();
539        final boolean isAutomaticTemporaryUpperCase = isAutomaticTemporaryUpperCase();
540        final boolean isShiftedOrShiftLocked = isShiftedOrShiftLocked();
541        if (isAlphabetMode) {
542            if (isShiftLocked) {
543                // Shift key is pressed while caps lock state, we will treat this state as shifted
544                // caps lock state and mark as if shift key pressed while normal state.
545                setShift(MANUAL_SHIFT);
546            } else if (isAutomaticTemporaryUpperCase) {
547                // Shift key is pressed while automatic temporary upper case, we have to move to
548                // manual temporary upper case.
549                setShift(MANUAL_SHIFT);
550            } else if (isShiftedOrShiftLocked) {
551                // In manual upper case state, we just record shift key has been pressing while
552                // shifted state.
553            } else {
554                // In base layout, chording or manual temporary upper case mode is started.
555                toggleShift();
556            }
557        } else {
558            // In symbol mode, just toggle symbol and symbol more keyboard.
559            toggleShift();
560            mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
561        }
562        mState.onPressShift(isAlphabetMode, isShiftLocked, isAutomaticTemporaryUpperCase,
563                isShiftedOrShiftLocked);
564    }
565
566    public void onReleaseShift(boolean withSliding) {
567        if (!isKeyboardAvailable())
568            return;
569        if (DEBUG_STATE) {
570            Log.d(TAG, "onReleaseShift:"
571                    + " keyboard=" + getLatinKeyboard().mShiftState
572                    + " state=" + mState + " sliding=" + withSliding);
573        }
574        final boolean isAlphabetMode = isAlphabetMode();
575        final boolean isShiftLocked = isShiftLocked();
576        final boolean isShiftLockShifted = isShiftLockShifted();
577        final boolean isShiftedOrShiftLocked = isShiftedOrShiftLocked();
578        final boolean isManualTemporaryUpperCaseFromAuto = isManualTemporaryUpperCaseFromAuto();
579        if (isAlphabetMode) {
580            if (mState.isShiftKeyMomentary()) {
581                // After chording input while normal state.
582                toggleShift();
583            } else if (isShiftLocked && !isShiftLockShifted && (mState.isShiftKeyPressing()
584                    || mState.isShiftKeyPressingOnShifted()) && !withSliding) {
585                // Shift has been long pressed, ignore this release.
586            } else if (isShiftLocked && !mState.isShiftKeyIgnoring() && !withSliding) {
587                // Shift has been pressed without chording while caps lock state.
588                toggleCapsLock();
589                // To be able to turn off caps lock by "double tap" on shift key, we should ignore
590                // the second tap of the "double tap" from now for a while because we just have
591                // already turned off caps lock above.
592                startIgnoringDoubleTap();
593            } else if (isShiftedOrShiftLocked && mState.isShiftKeyPressingOnShifted()
594                    && !withSliding) {
595                // Shift has been pressed without chording while shifted state.
596                toggleShift();
597            } else if (isManualTemporaryUpperCaseFromAuto && mState.isShiftKeyPressing()
598                    && !withSliding) {
599                // Shift has been pressed without chording while manual temporary upper case
600                // transited from automatic temporary upper case.
601                toggleShift();
602            }
603        } else {
604            // In symbol mode, snap back to the previous keyboard mode if the user chords the shift
605            // key and another key, then releases the shift key.
606            if (mSwitchState == SWITCH_STATE_CHORDING_SYMBOL) {
607                toggleShift();
608            }
609        }
610        mState.onReleaseShift();
611    }
612
613    public void onPressSymbol() {
614        if (DEBUG_STATE) {
615            Log.d(TAG, "onPressSymbol:"
616                    + " keyboard=" + getLatinKeyboard().mShiftState
617                    + " state=" + mState);
618        }
619        changeKeyboardMode();
620        mState.onPressSymbol();
621        mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
622    }
623
624    public void onReleaseSymbol() {
625        if (DEBUG_STATE) {
626            Log.d(TAG, "onReleaseSymbol:"
627                    + " keyboard=" + getLatinKeyboard().mShiftState
628                    + " state=" + mState);
629        }
630        // Snap back to the previous keyboard mode if the user chords the mode change key and
631        // another key, then releases the mode change key.
632        if (mSwitchState == SWITCH_STATE_CHORDING_ALPHA) {
633            changeKeyboardMode();
634        }
635        mState.onReleaseSymbol();
636    }
637
638    public void onOtherKeyPressed() {
639        if (DEBUG_STATE) {
640            Log.d(TAG, "onOtherKeyPressed:"
641                    + " keyboard=" + getLatinKeyboard().mShiftState
642                    + " state=" + mState);
643        }
644        mState.onOtherKeyPressed();
645    }
646
647    public void onCancelInput() {
648        // Snap back to the previous keyboard mode if the user cancels sliding input.
649        if (isSinglePointer()) {
650            if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
651                changeKeyboardMode();
652            } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
653                toggleShift();
654            }
655        }
656    }
657
658    private boolean mPrevMainKeyboardWasShiftLocked;
659
660    private void toggleKeyboardMode() {
661        if (mCurrentId.equals(mMainKeyboardId)) {
662            mPrevMainKeyboardWasShiftLocked = isShiftLocked();
663            setKeyboard(getKeyboard(mSymbolsKeyboardId));
664        } else {
665            setKeyboard(getKeyboard(mMainKeyboardId));
666            setShiftLocked(mPrevMainKeyboardWasShiftLocked);
667            mPrevMainKeyboardWasShiftLocked = false;
668        }
669    }
670
671    private void toggleShiftInSymbol() {
672        if (isAlphabetMode())
673            return;
674        final LatinKeyboard keyboard;
675        if (mCurrentId.equals(mSymbolsKeyboardId)
676                || !mCurrentId.equals(mSymbolsShiftedKeyboardId)) {
677            keyboard = getKeyboard(mSymbolsShiftedKeyboardId);
678        } else {
679            keyboard = getKeyboard(mSymbolsKeyboardId);
680        }
681        setKeyboard(keyboard);
682    }
683
684    public boolean isInMomentarySwitchState() {
685        return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
686                || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
687    }
688
689    public boolean isVibrateAndSoundFeedbackRequired() {
690        return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
691    }
692
693    private boolean isSinglePointer() {
694        return mKeyboardView != null && mKeyboardView.getPointerCount() == 1;
695    }
696
697    public boolean hasDistinctMultitouch() {
698        return mKeyboardView != null && mKeyboardView.hasDistinctMultitouch();
699    }
700
701    private static boolean isSpaceCharacter(int c) {
702        return c == Keyboard.CODE_SPACE || c == Keyboard.CODE_ENTER;
703    }
704
705    private static boolean isLayoutSwitchBackCharacter(int c) {
706        if (TextUtils.isEmpty(mLayoutSwitchBackSymbols)) return false;
707        if (mLayoutSwitchBackSymbols.indexOf(c) >= 0) return true;
708        return false;
709    }
710
711    /**
712     * Updates state machine to figure out when to automatically snap back to the previous mode.
713     */
714    public void onKey(int code) {
715        if (DEBUG_STATE) {
716            Log.d(TAG, "onKey: code=" + code + " switchState=" + mSwitchState
717                    + " isSinglePointer=" + isSinglePointer());
718        }
719        switch (mSwitchState) {
720        case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
721            // Only distinct multi touch devices can be in this state.
722            // On non-distinct multi touch devices, mode change key is handled by
723            // {@link LatinIME#onCodeInput}, not by {@link LatinIME#onPress} and
724            // {@link LatinIME#onRelease}. So, on such devices, {@link #mSwitchState} starts
725            // from {@link #SWITCH_STATE_SYMBOL_BEGIN}, or {@link #SWITCH_STATE_ALPHA}, not from
726            // {@link #SWITCH_STATE_MOMENTARY}.
727            if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
728                // Detected only the mode change key has been pressed, and then released.
729                if (mCurrentId.equals(mMainKeyboardId)) {
730                    mSwitchState = SWITCH_STATE_ALPHA;
731                } else {
732                    mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
733                }
734            } else if (isSinglePointer()) {
735                // Snap back to the previous keyboard mode if the user pressed the mode change key
736                // and slid to other key, then released the finger.
737                // If the user cancels the sliding input, snapping back to the previous keyboard
738                // mode is handled by {@link #onCancelInput}.
739                changeKeyboardMode();
740            } else {
741                // Chording input is being started. The keyboard mode will be snapped back to the
742                // previous mode in {@link onReleaseSymbol} when the mode change key is released.
743                mSwitchState = SWITCH_STATE_CHORDING_ALPHA;
744            }
745            break;
746        case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
747            if (code == Keyboard.CODE_SHIFT) {
748                // Detected only the shift key has been pressed on symbol layout, and then released.
749                mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
750            } else if (isSinglePointer()) {
751                // Snap back to the previous keyboard mode if the user pressed the shift key on
752                // symbol mode and slid to other key, then released the finger.
753                toggleShift();
754                mSwitchState = SWITCH_STATE_SYMBOL;
755            } else {
756                // Chording input is being started. The keyboard mode will be snapped back to the
757                // previous mode in {@link onReleaseShift} when the shift key is released.
758                mSwitchState = SWITCH_STATE_CHORDING_SYMBOL;
759            }
760            break;
761        case SWITCH_STATE_SYMBOL_BEGIN:
762            if (!isSpaceCharacter(code) && code >= 0) {
763                mSwitchState = SWITCH_STATE_SYMBOL;
764            }
765            // Snap back to alpha keyboard mode immediately if user types a quote character.
766            if (isLayoutSwitchBackCharacter(code)) {
767                changeKeyboardMode();
768            }
769            break;
770        case SWITCH_STATE_SYMBOL:
771        case SWITCH_STATE_CHORDING_SYMBOL:
772            // Snap back to alpha keyboard mode if user types one or more non-space/enter
773            // characters followed by a space/enter or a quote character.
774            if (isSpaceCharacter(code) || isLayoutSwitchBackCharacter(code)) {
775                changeKeyboardMode();
776            }
777            break;
778        }
779    }
780
781    public LatinKeyboardView getKeyboardView() {
782        return mKeyboardView;
783    }
784
785    public View onCreateInputView() {
786        return createInputView(mThemeIndex, true);
787    }
788
789    private View createInputView(final int newThemeIndex, final boolean forceRecreate) {
790        if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate)
791            return mCurrentInputView;
792
793        if (mKeyboardView != null) {
794            mKeyboardView.closing();
795        }
796
797        final int oldThemeIndex = mThemeIndex;
798        Utils.GCUtils.getInstance().reset();
799        boolean tryGC = true;
800        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
801            try {
802                setContextThemeWrapper(mInputMethodService, newThemeIndex);
803                mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
804                        R.layout.input_view, null);
805                tryGC = false;
806            } catch (OutOfMemoryError e) {
807                Log.w(TAG, "load keyboard failed: " + e);
808                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
809                        oldThemeIndex + "," + newThemeIndex, e);
810            } catch (InflateException e) {
811                Log.w(TAG, "load keyboard failed: " + e);
812                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
813                        oldThemeIndex + "," + newThemeIndex, e);
814            }
815        }
816
817        mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
818        mKeyboardView.setKeyboardActionListener(mInputMethodService);
819
820        // This always needs to be set since the accessibility state can
821        // potentially change without the input view being re-created.
822        AccessibleKeyboardViewProxy.setView(mKeyboardView);
823
824        return mCurrentInputView;
825    }
826
827    private void postSetInputView(final View newInputView) {
828        mInputMethodService.mHandler.post(new Runnable() {
829            @Override
830            public void run() {
831                if (newInputView != null) {
832                    mInputMethodService.setInputView(newInputView);
833                }
834                mInputMethodService.updateInputViewShown();
835            }
836        });
837    }
838
839    @Override
840    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
841        if (PREF_KEYBOARD_LAYOUT.equals(key)) {
842            final int themeIndex = getKeyboardThemeIndex(mInputMethodService, sharedPreferences);
843            postSetInputView(createInputView(themeIndex, false));
844        } else if (Settings.PREF_SHOW_SETTINGS_KEY.equals(key)) {
845            postSetInputView(createInputView(mThemeIndex, true));
846        }
847    }
848
849    public void onAutoCorrectionStateChanged(boolean isAutoCorrection) {
850        if (mIsAutoCorrectionActive != isAutoCorrection) {
851            mIsAutoCorrectionActive = isAutoCorrection;
852            final LatinKeyboard keyboard = getLatinKeyboard();
853            if (keyboard != null && keyboard.needsAutoCorrectionSpacebarLed()) {
854                final Key invalidatedKey = keyboard.onAutoCorrectionStateChanged(isAutoCorrection);
855                final LatinKeyboardView keyboardView = getKeyboardView();
856                if (keyboardView != null)
857                    keyboardView.invalidateKey(invalidatedKey);
858            }
859        }
860    }
861
862    private static int getF2KeyMode(boolean settingsKeyEnabled, boolean noSettingsKey) {
863        if (noSettingsKey) {
864            // Never shows the Settings key
865            return KeyboardId.F2KEY_MODE_SHORTCUT_IME;
866        }
867
868        if (settingsKeyEnabled) {
869            return KeyboardId.F2KEY_MODE_SETTINGS;
870        } else {
871            // It should be alright to fall back to the Settings key on 7-inch layouts
872            // even when the Settings key is not explicitly enabled.
873            return KeyboardId.F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS;
874        }
875    }
876}
877