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