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