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