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