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