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