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