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