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