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