KeyboardSwitcher.java revision 9116bf18f9c83084f9d451e2e709eff32db27d36
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.Key;
32import com.android.inputmethod.keyboard.internal.ModifierKeyState;
33import com.android.inputmethod.keyboard.internal.ShiftKeyState;
34import com.android.inputmethod.latin.LatinIME;
35import com.android.inputmethod.latin.LatinImeLogger;
36import com.android.inputmethod.latin.R;
37import com.android.inputmethod.latin.Settings;
38import com.android.inputmethod.latin.SubtypeSwitcher;
39import com.android.inputmethod.latin.Utils;
40
41import java.lang.ref.SoftReference;
42import java.util.HashMap;
43import java.util.Locale;
44
45public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener {
46    private static final String TAG = KeyboardSwitcher.class.getSimpleName();
47    private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
48    public static final boolean DEBUG_STATE = false;
49
50    public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20100902";
51    private static final int[] KEYBOARD_THEMES = {
52        R.style.KeyboardTheme,
53        R.style.KeyboardTheme_HighContrast,
54        R.style.KeyboardTheme_Stone,
55        R.style.KeyboardTheme_Stone_Bold,
56        R.style.KeyboardTheme_Gingerbread,
57        R.style.KeyboardTheme_IceCreamSandwich,
58    };
59
60    private SubtypeSwitcher mSubtypeSwitcher;
61    private SharedPreferences mPrefs;
62
63    private View mCurrentInputView;
64    private LatinKeyboardView mKeyboardView;
65    private LatinIME mInputMethodService;
66
67    // TODO: Combine these key state objects with auto mode switch state.
68    private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
69    private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
70
71    private KeyboardId mSymbolsId;
72    private KeyboardId mSymbolsShiftedId;
73
74    private KeyboardId mCurrentId;
75    private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboardCache =
76            new HashMap<KeyboardId, SoftReference<LatinKeyboard>>();
77
78    private EditorInfo mAttribute;
79    private boolean mIsSymbols;
80    /** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of
81     * what user actually typed. */
82    private boolean mIsAutoCorrectionActive;
83    private boolean mVoiceKeyEnabled;
84    private boolean mVoiceButtonOnPrimary;
85
86    // TODO: Encapsulate these state handling to separate class and combine with ShiftKeyState
87    // and ModifierKeyState.
88    private static final int SWITCH_STATE_ALPHA = 0;
89    private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
90    private static final int SWITCH_STATE_SYMBOL = 2;
91    // The following states are used only on the distinct multi-touch panel devices.
92    private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
93    private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
94    private static final int SWITCH_STATE_CHORDING_ALPHA = 5;
95    private static final int SWITCH_STATE_CHORDING_SYMBOL = 6;
96    private int mSwitchState = SWITCH_STATE_ALPHA;
97
98    // Indicates whether or not we have the settings key in option of settings
99    private boolean mSettingsKeyEnabledInSettings;
100    private static final int SETTINGS_KEY_MODE_AUTO = R.string.settings_key_mode_auto;
101    private static final int SETTINGS_KEY_MODE_ALWAYS_SHOW =
102            R.string.settings_key_mode_always_show;
103    // NOTE: No need to have SETTINGS_KEY_MODE_ALWAYS_HIDE here because it's not being referred to
104    // in the source code now.
105    // Default is SETTINGS_KEY_MODE_AUTO.
106    private static final int DEFAULT_SETTINGS_KEY_MODE = SETTINGS_KEY_MODE_AUTO;
107
108    private int mThemeIndex = -1;
109    private Context mThemeContext;
110    private int mKeyboardWidth;
111
112    private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
113
114    public static KeyboardSwitcher getInstance() {
115        return sInstance;
116    }
117
118    private KeyboardSwitcher() {
119        // Intentional empty constructor for singleton.
120    }
121
122    public static void init(LatinIME ims, SharedPreferences prefs) {
123        sInstance.mInputMethodService = ims;
124        sInstance.mPrefs = prefs;
125        sInstance.mSubtypeSwitcher = SubtypeSwitcher.getInstance();
126        sInstance.setContextThemeWrapper(ims, getKeyboardThemeIndex(ims, prefs));
127        prefs.registerOnSharedPreferenceChangeListener(sInstance);
128    }
129
130    private static int getKeyboardThemeIndex(Context context, SharedPreferences prefs) {
131        final String defaultThemeId = context.getString(R.string.config_default_keyboard_theme_id);
132        final String themeId = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeId);
133        try {
134            final int themeIndex = Integer.valueOf(themeId);
135            if (themeIndex >= 0 && themeIndex < KEYBOARD_THEMES.length)
136                return themeIndex;
137        } catch (NumberFormatException e) {
138            // Format error, keyboard theme is default to 0.
139        }
140        Log.w(TAG, "Illegal keyboard theme in preference: " + themeId + ", default to 0");
141        return 0;
142    }
143
144    private void setContextThemeWrapper(Context context, int themeIndex) {
145        if (mThemeIndex != themeIndex) {
146            mThemeIndex = themeIndex;
147            mThemeContext = new ContextThemeWrapper(context, KEYBOARD_THEMES[themeIndex]);
148            mKeyboardCache.clear();
149        }
150    }
151
152    public void loadKeyboard(EditorInfo attribute, boolean voiceKeyEnabled,
153            boolean voiceButtonOnPrimary) {
154        mSwitchState = SWITCH_STATE_ALPHA;
155        try {
156            loadKeyboardInternal(attribute, voiceKeyEnabled, voiceButtonOnPrimary, false);
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 Resources res = mInputMethodService.getResources();
289        final int orientation = res.getConfiguration().orientation;
290        if (mKeyboardWidth == 0)
291            mKeyboardWidth = res.getDisplayMetrics().widthPixels;
292        final Locale locale = mSubtypeSwitcher.getInputLocale();
293        return new KeyboardId(
294                res.getResourceEntryName(xmlId), xmlId, locale, orientation, mKeyboardWidth,
295                mode, attribute, hasSettingsKey, mVoiceKeyEnabled, hasVoiceKey, enableShiftLock);
296    }
297
298    private KeyboardId makeSiblingKeyboardId(KeyboardId base, int alphabet, int phone) {
299        final int xmlId = base.mMode == KeyboardId.MODE_PHONE ? phone : alphabet;
300        final String xmlName = mInputMethodService.getResources().getResourceEntryName(xmlId);
301        return base.cloneWithNewLayout(xmlName, xmlId);
302    }
303
304    public int getKeyboardMode() {
305        return mCurrentId != null ? mCurrentId.mMode : KeyboardId.MODE_TEXT;
306    }
307
308    public boolean isAlphabetMode() {
309        return mCurrentId != null && mCurrentId.isAlphabetKeyboard();
310    }
311
312    public boolean isInputViewShown() {
313        return mCurrentInputView != null && mCurrentInputView.isShown();
314    }
315
316    public boolean isKeyboardAvailable() {
317        if (mKeyboardView != null)
318            return mKeyboardView.getKeyboard() != null;
319        return false;
320    }
321
322    public LatinKeyboard getLatinKeyboard() {
323        if (mKeyboardView != null) {
324            final Keyboard keyboard = mKeyboardView.getKeyboard();
325            if (keyboard instanceof LatinKeyboard)
326                return (LatinKeyboard)keyboard;
327        }
328        return null;
329    }
330
331    public boolean isShiftedOrShiftLocked() {
332        LatinKeyboard latinKeyboard = getLatinKeyboard();
333        if (latinKeyboard != null)
334            return latinKeyboard.isShiftedOrShiftLocked();
335        return false;
336    }
337
338    public boolean isShiftLocked() {
339        LatinKeyboard latinKeyboard = getLatinKeyboard();
340        if (latinKeyboard != null)
341            return latinKeyboard.isShiftLocked();
342        return false;
343    }
344
345    public boolean isAutomaticTemporaryUpperCase() {
346        LatinKeyboard latinKeyboard = getLatinKeyboard();
347        if (latinKeyboard != null)
348            return latinKeyboard.isAutomaticTemporaryUpperCase();
349        return false;
350    }
351
352    public boolean isManualTemporaryUpperCase() {
353        LatinKeyboard latinKeyboard = getLatinKeyboard();
354        if (latinKeyboard != null)
355            return latinKeyboard.isManualTemporaryUpperCase();
356        return false;
357    }
358
359    private boolean isManualTemporaryUpperCaseFromAuto() {
360        LatinKeyboard latinKeyboard = getLatinKeyboard();
361        if (latinKeyboard != null)
362            return latinKeyboard.isManualTemporaryUpperCaseFromAuto();
363        return false;
364    }
365
366    private void setManualTemporaryUpperCase(boolean shifted) {
367        LatinKeyboard latinKeyboard = getLatinKeyboard();
368        if (latinKeyboard != null) {
369            // On non-distinct multi touch panel device, we should also turn off the shift locked
370            // state when shift key is pressed to go to normal mode.
371            // On the other hand, on distinct multi touch panel device, turning off the shift locked
372            // state with shift key pressing is handled by onReleaseShift().
373            if (!hasDistinctMultitouch() && !shifted && latinKeyboard.isShiftLocked()) {
374                latinKeyboard.setShiftLocked(false);
375            }
376            if (latinKeyboard.setShifted(shifted)) {
377                mKeyboardView.invalidateAllKeys();
378            }
379        }
380    }
381
382    private void setShiftLocked(boolean shiftLocked) {
383        LatinKeyboard latinKeyboard = getLatinKeyboard();
384        if (latinKeyboard != null && latinKeyboard.setShiftLocked(shiftLocked)) {
385            mKeyboardView.invalidateAllKeys();
386        }
387    }
388
389    /**
390     * Toggle keyboard shift state triggered by user touch event.
391     */
392    public void toggleShift() {
393        mInputMethodService.mHandler.cancelUpdateShiftState();
394        if (DEBUG_STATE)
395            Log.d(TAG, "toggleShift:"
396                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
397                    + " shiftKeyState=" + mShiftKeyState);
398        if (isAlphabetMode()) {
399            setManualTemporaryUpperCase(!isShiftedOrShiftLocked());
400        } else {
401            toggleShiftInSymbol();
402        }
403    }
404
405    public void toggleCapsLock() {
406        mInputMethodService.mHandler.cancelUpdateShiftState();
407        if (DEBUG_STATE)
408            Log.d(TAG, "toggleCapsLock:"
409                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
410                    + " shiftKeyState=" + mShiftKeyState);
411        if (isAlphabetMode()) {
412            if (isShiftLocked()) {
413                // Shift key is long pressed while caps lock state, we will toggle back to normal
414                // state. And mark as if shift key is released.
415                setShiftLocked(false);
416                mShiftKeyState.onRelease();
417            } else {
418                setShiftLocked(true);
419            }
420        }
421    }
422
423    private void setAutomaticTemporaryUpperCase() {
424        LatinKeyboard latinKeyboard = getLatinKeyboard();
425        if (latinKeyboard != null) {
426            latinKeyboard.setAutomaticTemporaryUpperCase();
427            mKeyboardView.invalidateAllKeys();
428        }
429    }
430
431    /**
432     * Update keyboard shift state triggered by connected EditText status change.
433     */
434    public void updateShiftState() {
435        final ShiftKeyState shiftKeyState = mShiftKeyState;
436        if (DEBUG_STATE)
437            Log.d(TAG, "updateShiftState:"
438                    + " autoCaps=" + mInputMethodService.getCurrentAutoCapsState()
439                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
440                    + " shiftKeyState=" + shiftKeyState);
441        if (isAlphabetMode()) {
442            if (!isShiftLocked() && !shiftKeyState.isIgnoring()) {
443                if (shiftKeyState.isReleasing() && mInputMethodService.getCurrentAutoCapsState()) {
444                    // Only when shift key is releasing, automatic temporary upper case will be set.
445                    setAutomaticTemporaryUpperCase();
446                } else {
447                    setManualTemporaryUpperCase(shiftKeyState.isMomentary());
448                }
449            }
450        } else {
451            // In symbol keyboard mode, we should clear shift key state because only alphabet
452            // keyboard has shift key.
453            shiftKeyState.onRelease();
454        }
455    }
456
457    public void changeKeyboardMode() {
458        if (DEBUG_STATE)
459            Log.d(TAG, "changeKeyboardMode:"
460                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
461                    + " shiftKeyState=" + mShiftKeyState);
462        toggleKeyboardMode();
463        if (isShiftLocked() && isAlphabetMode())
464            setShiftLocked(true);
465        updateShiftState();
466    }
467
468    public void onPressShift(boolean withSliding) {
469        if (!isKeyboardAvailable())
470            return;
471        ShiftKeyState shiftKeyState = mShiftKeyState;
472        if (DEBUG_STATE)
473            Log.d(TAG, "onPressShift:"
474                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
475                    + " shiftKeyState=" + shiftKeyState + " sliding=" + withSliding);
476        if (isAlphabetMode()) {
477            if (isShiftLocked()) {
478                // Shift key is pressed while caps lock state, we will treat this state as shifted
479                // caps lock state and mark as if shift key pressed while normal state.
480                shiftKeyState.onPress();
481                setManualTemporaryUpperCase(true);
482            } else if (isAutomaticTemporaryUpperCase()) {
483                // Shift key is pressed while automatic temporary upper case, we have to move to
484                // manual temporary upper case.
485                shiftKeyState.onPress();
486                setManualTemporaryUpperCase(true);
487            } else if (isShiftedOrShiftLocked()) {
488                // In manual upper case state, we just record shift key has been pressing while
489                // shifted state.
490                shiftKeyState.onPressOnShifted();
491            } else {
492                // In base layout, chording or manual temporary upper case mode is started.
493                shiftKeyState.onPress();
494                toggleShift();
495            }
496        } else {
497            // In symbol mode, just toggle symbol and symbol more keyboard.
498            shiftKeyState.onPress();
499            toggleShift();
500            mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
501        }
502    }
503
504    public void onReleaseShift(boolean withSliding) {
505        if (!isKeyboardAvailable())
506            return;
507        ShiftKeyState shiftKeyState = mShiftKeyState;
508        if (DEBUG_STATE)
509            Log.d(TAG, "onReleaseShift:"
510                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
511                    + " shiftKeyState=" + shiftKeyState + " sliding=" + withSliding);
512        if (isAlphabetMode()) {
513            if (shiftKeyState.isMomentary()) {
514                // After chording input while normal state.
515                toggleShift();
516            } else if (isShiftLocked() && !shiftKeyState.isIgnoring() && !withSliding) {
517                // Shift has been pressed without chording while caps lock state.
518                toggleCapsLock();
519                // To be able to turn off caps lock by "double tap" on shift key, we should ignore
520                // the second tap of the "double tap" from now for a while because we just have
521                // already turned off caps lock above.
522                mKeyboardView.startIgnoringDoubleTap();
523            } else if (isShiftedOrShiftLocked() && shiftKeyState.isPressingOnShifted()
524                    && !withSliding) {
525                // Shift has been pressed without chording while shifted state.
526                toggleShift();
527            } else if (isManualTemporaryUpperCaseFromAuto() && shiftKeyState.isPressing()
528                    && !withSliding) {
529                // Shift has been pressed without chording while manual temporary upper case
530                // transited from automatic temporary upper case.
531                toggleShift();
532            }
533        } else {
534            // In symbol mode, snap back to the previous keyboard mode if the user chords the shift
535            // key and another key, then releases the shift key.
536            if (mSwitchState == SWITCH_STATE_CHORDING_SYMBOL) {
537                toggleShift();
538            }
539        }
540        shiftKeyState.onRelease();
541    }
542
543    public void onPressSymbol() {
544        if (DEBUG_STATE)
545            Log.d(TAG, "onPressSymbol:"
546                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
547                    + " symbolKeyState=" + mSymbolKeyState);
548        changeKeyboardMode();
549        mSymbolKeyState.onPress();
550        mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
551    }
552
553    public void onReleaseSymbol() {
554        if (DEBUG_STATE)
555            Log.d(TAG, "onReleaseSymbol:"
556                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
557                    + " symbolKeyState=" + mSymbolKeyState);
558        // Snap back to the previous keyboard mode if the user chords the mode change key and
559        // another key, then releases the mode change key.
560        if (mSwitchState == SWITCH_STATE_CHORDING_ALPHA) {
561            changeKeyboardMode();
562        }
563        mSymbolKeyState.onRelease();
564    }
565
566    public void onOtherKeyPressed() {
567        if (DEBUG_STATE)
568            Log.d(TAG, "onOtherKeyPressed:"
569                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
570                    + " shiftKeyState=" + mShiftKeyState
571                    + " symbolKeyState=" + mSymbolKeyState);
572        mShiftKeyState.onOtherKeyPressed();
573        mSymbolKeyState.onOtherKeyPressed();
574    }
575
576    public void onCancelInput() {
577        // Snap back to the previous keyboard mode if the user cancels sliding input.
578        if (getPointerCount() == 1) {
579            if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
580                changeKeyboardMode();
581            } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
582                toggleShift();
583            }
584        }
585    }
586
587    private void toggleShiftInSymbol() {
588        if (isAlphabetMode())
589            return;
590        final LatinKeyboard keyboard;
591        if (mCurrentId.equals(mSymbolsId) || !mCurrentId.equals(mSymbolsShiftedId)) {
592            keyboard = getKeyboard(mSymbolsShiftedId);
593            // Symbol shifted keyboard has an ALT key that has a caps lock style indicator. To
594            // enable the indicator, we need to call setShiftLocked(true).
595            keyboard.setShiftLocked(true);
596        } else {
597            keyboard = getKeyboard(mSymbolsId);
598            // Symbol keyboard has an ALT key that has a caps lock style indicator. To disable the
599            // indicator, we need to call setShiftLocked(false).
600            keyboard.setShiftLocked(false);
601        }
602        setKeyboard(keyboard);
603    }
604
605    public boolean isInMomentarySwitchState() {
606        return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
607                || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
608    }
609
610    public boolean isVibrateAndSoundFeedbackRequired() {
611        return mKeyboardView == null || !mKeyboardView.isInSlidingKeyInput();
612    }
613
614    private int getPointerCount() {
615        return mKeyboardView == null ? 0 : mKeyboardView.getPointerCount();
616    }
617
618    private void toggleKeyboardMode() {
619        loadKeyboardInternal(mAttribute, mVoiceKeyEnabled, mVoiceButtonOnPrimary, !mIsSymbols);
620        if (mIsSymbols) {
621            mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
622        } else {
623            mSwitchState = SWITCH_STATE_ALPHA;
624        }
625    }
626
627    public boolean hasDistinctMultitouch() {
628        return mKeyboardView != null && mKeyboardView.hasDistinctMultitouch();
629    }
630
631    private static boolean isSpaceCharacter(int c) {
632        return c == Keyboard.CODE_SPACE || c == Keyboard.CODE_ENTER;
633    }
634
635    private static boolean isQuoteCharacter(int c) {
636        // Apostrophe, quotation mark.
637        if (c == Keyboard.CODE_SINGLE_QUOTE || c == Keyboard.CODE_DOUBLE_QUOTE)
638            return true;
639        // \u2018: Left single quotation mark
640        // \u2019: Right single quotation mark
641        // \u201a: Single low-9 quotation mark
642        // \u201b: Single high-reversed-9 quotation mark
643        // \u201c: Left double quotation mark
644        // \u201d: Right double quotation mark
645        // \u201e: Double low-9 quotation mark
646        // \u201f: Double high-reversed-9 quotation mark
647        if (c >= '\u2018' && c <= '\u201f')
648            return true;
649        // \u00ab: Left-pointing double angle quotation mark
650        // \u00bb: Right-pointing double angle quotation mark
651        if (c == '\u00ab' || c == '\u00bb')
652            return true;
653        return false;
654    }
655
656    /**
657     * Updates state machine to figure out when to automatically snap back to the previous mode.
658     */
659    public void onKey(int code) {
660        if (DEBUG_STATE)
661            Log.d(TAG, "onKey: code=" + code + " switchState=" + mSwitchState
662                    + " pointers=" + getPointerCount());
663        switch (mSwitchState) {
664        case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
665            // Only distinct multi touch devices can be in this state.
666            // On non-distinct multi touch devices, mode change key is handled by
667            // {@link LatinIME#onCodeInput}, not by {@link LatinIME#onPress} and
668            // {@link LatinIME#onRelease}. So, on such devices, {@link #mSwitchState} starts
669            // from {@link #SWITCH_STATE_SYMBOL_BEGIN}, or {@link #SWITCH_STATE_ALPHA}, not from
670            // {@link #SWITCH_STATE_MOMENTARY}.
671            if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
672                // Detected only the mode change key has been pressed, and then released.
673                if (mIsSymbols) {
674                    mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
675                } else {
676                    mSwitchState = SWITCH_STATE_ALPHA;
677                }
678            } else if (getPointerCount() == 1) {
679                // Snap back to the previous keyboard mode if the user pressed the mode change key
680                // and slid to other key, then released the finger.
681                // If the user cancels the sliding input, snapping back to the previous keyboard
682                // mode is handled by {@link #onCancelInput}.
683                changeKeyboardMode();
684            } else {
685                // Chording input is being started. The keyboard mode will be snapped back to the
686                // previous mode in {@link onReleaseSymbol} when the mode change key is released.
687                mSwitchState = SWITCH_STATE_CHORDING_ALPHA;
688            }
689            break;
690        case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
691            if (code == Keyboard.CODE_SHIFT) {
692                // Detected only the shift key has been pressed on symbol layout, and then released.
693                mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
694            } else if (getPointerCount() == 1) {
695                // Snap back to the previous keyboard mode if the user pressed the shift key on
696                // symbol mode and slid to other key, then released the finger.
697                toggleShift();
698                mSwitchState = SWITCH_STATE_SYMBOL;
699            } else {
700                // Chording input is being started. The keyboard mode will be snapped back to the
701                // previous mode in {@link onReleaseShift} when the shift key is released.
702                mSwitchState = SWITCH_STATE_CHORDING_SYMBOL;
703            }
704            break;
705        case SWITCH_STATE_SYMBOL_BEGIN:
706            if (!isSpaceCharacter(code) && code >= 0) {
707                mSwitchState = SWITCH_STATE_SYMBOL;
708            }
709            // Snap back to alpha keyboard mode immediately if user types a quote character.
710            if (isQuoteCharacter(code)) {
711                changeKeyboardMode();
712            }
713            break;
714        case SWITCH_STATE_SYMBOL:
715        case SWITCH_STATE_CHORDING_SYMBOL:
716            // Snap back to alpha keyboard mode if user types one or more non-space/enter
717            // characters followed by a space/enter or a quote character.
718            if (isSpaceCharacter(code) || isQuoteCharacter(code)) {
719                changeKeyboardMode();
720            }
721            break;
722        }
723    }
724
725    public LatinKeyboardView getKeyboardView() {
726        return mKeyboardView;
727    }
728
729    public View onCreateInputView() {
730        return createInputView(mThemeIndex, true);
731    }
732
733    private View createInputView(final int newThemeIndex, final boolean forceRecreate) {
734        if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate)
735            return mCurrentInputView;
736
737        if (mKeyboardView != null) {
738            mKeyboardView.closing();
739        }
740
741        final int oldThemeIndex = mThemeIndex;
742        Utils.GCUtils.getInstance().reset();
743        boolean tryGC = true;
744        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
745            try {
746                setContextThemeWrapper(mInputMethodService, newThemeIndex);
747                mCurrentInputView = LayoutInflater.from(mThemeContext).inflate(
748                        R.layout.input_view, null);
749                tryGC = false;
750            } catch (OutOfMemoryError e) {
751                Log.w(TAG, "load keyboard failed: " + e);
752                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
753                        oldThemeIndex + "," + newThemeIndex, e);
754            } catch (InflateException e) {
755                Log.w(TAG, "load keyboard failed: " + e);
756                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
757                        oldThemeIndex + "," + newThemeIndex, e);
758            }
759        }
760
761        mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
762        mKeyboardView.setOnKeyboardActionListener(mInputMethodService);
763
764        // This always needs to be set since the accessibility state can
765        // potentially change without the input view being re-created.
766        AccessibleKeyboardViewProxy.setView(mKeyboardView);
767
768        return mCurrentInputView;
769    }
770
771    private void postSetInputView(final View newInputView) {
772        mInputMethodService.mHandler.post(new Runnable() {
773            @Override
774            public void run() {
775                if (newInputView != null) {
776                    mInputMethodService.setInputView(newInputView);
777                }
778                mInputMethodService.updateInputViewShown();
779            }
780        });
781    }
782
783    @Override
784    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
785        if (PREF_KEYBOARD_LAYOUT.equals(key)) {
786            final int layoutId = getKeyboardThemeIndex(mInputMethodService, sharedPreferences);
787            postSetInputView(createInputView(layoutId, false));
788        } else if (Settings.PREF_SETTINGS_KEY.equals(key)) {
789            mSettingsKeyEnabledInSettings = getSettingsKeyMode(sharedPreferences,
790                    mInputMethodService);
791            postSetInputView(createInputView(mThemeIndex, true));
792        }
793    }
794
795    public void onAutoCorrectionStateChanged(boolean isAutoCorrection) {
796        if (mIsAutoCorrectionActive != isAutoCorrection) {
797            mIsAutoCorrectionActive = isAutoCorrection;
798            final LatinKeyboard keyboard = getLatinKeyboard();
799            if (keyboard != null && keyboard.needsAutoCorrectionSpacebarLed()) {
800                final Key invalidatedKey = keyboard.onAutoCorrectionStateChanged(isAutoCorrection);
801                final LatinKeyboardView keyboardView = getKeyboardView();
802                if (keyboardView != null)
803                    keyboardView.invalidateKey(invalidatedKey);
804            }
805        }
806    }
807
808    private static boolean getSettingsKeyMode(SharedPreferences prefs, Context context) {
809        Resources resources = context.getResources();
810        final boolean showSettingsKeyOption = resources.getBoolean(
811                R.bool.config_enable_show_settings_key_option);
812        if (showSettingsKeyOption) {
813            final String settingsKeyMode = prefs.getString(Settings.PREF_SETTINGS_KEY,
814                    resources.getString(DEFAULT_SETTINGS_KEY_MODE));
815            // We show the settings key when 1) SETTINGS_KEY_MODE_ALWAYS_SHOW or
816            // 2) SETTINGS_KEY_MODE_AUTO and there are two or more enabled IMEs on the system
817            if (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW))
818                    || (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_AUTO))
819                            && Utils.hasMultipleEnabledIMEsOrSubtypes(
820                                    (InputMethodManagerCompatWrapper.getInstance(context))))) {
821                return true;
822            }
823            return false;
824        }
825        // If the show settings key option is disabled, we always try showing the settings key.
826        return true;
827    }
828}
829