KeyboardSwitcher.java revision 34f18203960d34dca01c80355bae3549e09aaf88
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.Configuration;
22import android.content.res.Resources;
23import android.util.DisplayMetrics;
24import android.util.Log;
25import android.view.ContextThemeWrapper;
26import android.view.InflateException;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.inputmethod.EditorInfo;
30
31import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
32import com.android.inputmethod.keyboard.internal.KeyboardState;
33import com.android.inputmethod.latin.InputView;
34import com.android.inputmethod.latin.LatinIME;
35import com.android.inputmethod.latin.LatinImeLogger;
36import com.android.inputmethod.latin.LocaleUtils;
37import com.android.inputmethod.latin.R;
38import com.android.inputmethod.latin.Settings;
39import com.android.inputmethod.latin.SettingsValues;
40import com.android.inputmethod.latin.SubtypeSwitcher;
41import com.android.inputmethod.latin.Utils;
42
43import java.lang.ref.SoftReference;
44import java.util.HashMap;
45import java.util.Locale;
46
47public class KeyboardSwitcher implements KeyboardState.SwitchActions,
48        SharedPreferences.OnSharedPreferenceChangeListener {
49    private static final String TAG = KeyboardSwitcher.class.getSimpleName();
50    private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
51
52    public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916";
53    private static final int[] KEYBOARD_THEMES = {
54        R.style.KeyboardTheme,
55        R.style.KeyboardTheme_HighContrast,
56        R.style.KeyboardTheme_Stone,
57        R.style.KeyboardTheme_Stone_Bold,
58        R.style.KeyboardTheme_Gingerbread,
59        R.style.KeyboardTheme_IceCreamSandwich,
60    };
61
62    private SubtypeSwitcher mSubtypeSwitcher;
63    private SharedPreferences mPrefs;
64
65    private InputView mCurrentInputView;
66    private LatinKeyboardView mKeyboardView;
67    private LatinIME mInputMethodService;
68    private String mPackageName;
69    private Resources mResources;
70
71    private KeyboardState mState;
72
73    private KeyboardId mMainKeyboardId;
74    private KeyboardId mSymbolsKeyboardId;
75    private KeyboardId mSymbolsShiftedKeyboardId;
76
77    private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboardCache =
78            new HashMap<KeyboardId, SoftReference<LatinKeyboard>>();
79
80    /** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of
81     * what user actually typed. */
82    private boolean mIsAutoCorrectionActive;
83
84    private int mThemeIndex = -1;
85    private Context mThemeContext;
86
87    private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
88
89    public static KeyboardSwitcher getInstance() {
90        return sInstance;
91    }
92
93    private KeyboardSwitcher() {
94        // Intentional empty constructor for singleton.
95    }
96
97    public static void init(LatinIME ims, SharedPreferences prefs) {
98        sInstance.initInternal(ims, prefs);
99    }
100
101    private void initInternal(LatinIME ims, SharedPreferences prefs) {
102        mInputMethodService = ims;
103        mPackageName = ims.getPackageName();
104        mResources = ims.getResources();
105        mPrefs = prefs;
106        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
107        mState = new KeyboardState(this);
108        setContextThemeWrapper(ims, getKeyboardThemeIndex(ims, prefs));
109        prefs.registerOnSharedPreferenceChangeListener(this);
110    }
111
112    private static int getKeyboardThemeIndex(Context context, SharedPreferences prefs) {
113        final String defaultThemeId = context.getString(R.string.config_default_keyboard_theme_id);
114        final String themeId = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeId);
115        try {
116            final int themeIndex = Integer.valueOf(themeId);
117            if (themeIndex >= 0 && themeIndex < KEYBOARD_THEMES.length)
118                return themeIndex;
119        } catch (NumberFormatException e) {
120            // Format error, keyboard theme is default to 0.
121        }
122        Log.w(TAG, "Illegal keyboard theme in preference: " + themeId + ", default to 0");
123        return 0;
124    }
125
126    private void setContextThemeWrapper(Context context, int themeIndex) {
127        if (mThemeIndex != themeIndex) {
128            mThemeIndex = themeIndex;
129            mThemeContext = new ContextThemeWrapper(context, KEYBOARD_THEMES[themeIndex]);
130            mKeyboardCache.clear();
131        }
132    }
133
134    public void loadKeyboard(EditorInfo editorInfo, SettingsValues settingsValues) {
135        try {
136            mMainKeyboardId = getKeyboardId(editorInfo, false, false, settingsValues);
137            mSymbolsKeyboardId = getKeyboardId(editorInfo, true, false, settingsValues);
138            mSymbolsShiftedKeyboardId = getKeyboardId(editorInfo, true, true, settingsValues);
139            mState.onLoadKeyboard(mResources.getString(R.string.layout_switch_back_symbols),
140                    hasDistinctMultitouch());
141            // TODO: Should get rid of this special case handling for Phone Number layouts once we
142            // have separate layouts with unique KeyboardIds for alphabet and alphabet-shifted
143            // respectively.
144            if (mMainKeyboardId.isPhoneKeyboard()) {
145                mState.onToggleAlphabetAndSymbols();
146            }
147        } catch (RuntimeException e) {
148            Log.w(TAG, "loading keyboard failed: " + mMainKeyboardId, e);
149            LatinImeLogger.logOnException(mMainKeyboardId.toString(), e);
150        }
151    }
152
153    public void saveKeyboardState() {
154        if (isKeyboardAvailable()) {
155            mState.onSaveKeyboardState();
156        }
157    }
158
159    public void onFinishInputView() {
160        mIsAutoCorrectionActive = false;
161    }
162
163    public void onHideWindow() {
164        mIsAutoCorrectionActive = false;
165    }
166
167    private void setKeyboard(final Keyboard keyboard) {
168        final Keyboard oldKeyboard = mKeyboardView.getKeyboard();
169        mKeyboardView.setKeyboard(keyboard);
170        mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding);
171        mKeyboardView.setKeyPreviewPopupEnabled(
172                SettingsValues.isKeyPreviewPopupEnabled(mPrefs, mResources),
173                SettingsValues.getKeyPreviewPopupDismissDelay(mPrefs, mResources));
174        final boolean localeChanged = (oldKeyboard == null)
175                || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale);
176        mInputMethodService.mHandler.startDisplayLanguageOnSpacebar(localeChanged);
177        updateShiftState();
178    }
179
180    private LatinKeyboard getKeyboard(KeyboardId id) {
181        final SoftReference<LatinKeyboard> ref = mKeyboardCache.get(id);
182        LatinKeyboard keyboard = (ref == null) ? null : ref.get();
183        if (keyboard == null) {
184            final Locale savedLocale = LocaleUtils.setSystemLocale(mResources, id.mLocale);
185            try {
186                final LatinKeyboard.Builder builder = new LatinKeyboard.Builder(mThemeContext);
187                builder.load(id);
188                builder.setTouchPositionCorrectionEnabled(
189                        mSubtypeSwitcher.currentSubtypeContainsExtraValueKey(
190                                LatinIME.SUBTYPE_EXTRA_VALUE_SUPPORT_TOUCH_POSITION_CORRECTION));
191                keyboard = builder.build();
192            } finally {
193                LocaleUtils.setSystemLocale(mResources, savedLocale);
194            }
195            mKeyboardCache.put(id, new SoftReference<LatinKeyboard>(keyboard));
196
197            if (DEBUG_CACHE) {
198                Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": "
199                        + ((ref == null) ? "LOAD" : "GCed") + " id=" + id
200                        + " theme=" + Keyboard.themeName(keyboard.mThemeId));
201            }
202        } else if (DEBUG_CACHE) {
203            Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": HIT  id=" + id
204                    + " theme=" + Keyboard.themeName(keyboard.mThemeId));
205        }
206
207        keyboard.onAutoCorrectionStateChanged(mIsAutoCorrectionActive);
208        keyboard.setShiftLocked(false);
209        keyboard.setShifted(false);
210        // If the cached keyboard had been switched to another keyboard while the language was
211        // displayed on its spacebar, it might have had arbitrary text fade factor. In such case,
212        // we should reset the text fade factor. It is also applicable to shortcut key.
213        keyboard.setSpacebarTextFadeFactor(0.0f, null);
214        keyboard.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady(), null);
215        return keyboard;
216    }
217
218    private KeyboardId getKeyboardId(EditorInfo editorInfo, final boolean isSymbols,
219            final boolean isShift, SettingsValues settingsValues) {
220        final int mode = Utils.getKeyboardMode(editorInfo);
221        final int xmlId;
222        switch (mode) {
223        case KeyboardId.MODE_PHONE:
224            xmlId = (isSymbols && isShift) ? R.xml.kbd_phone_shift : R.xml.kbd_phone;
225            break;
226        case KeyboardId.MODE_NUMBER:
227            xmlId = R.xml.kbd_number;
228            break;
229        default:
230            if (isSymbols) {
231                xmlId = isShift ? R.xml.kbd_symbols_shift : R.xml.kbd_symbols;
232            } else {
233                xmlId = R.xml.kbd_qwerty;
234            }
235            break;
236        }
237
238        final boolean settingsKeyEnabled = settingsValues.isSettingsKeyEnabled();
239        @SuppressWarnings("deprecation")
240        final boolean noMicrophone = Utils.inPrivateImeOptions(
241                mPackageName, LatinIME.IME_OPTION_NO_MICROPHONE, editorInfo)
242                || Utils.inPrivateImeOptions(
243                        null, LatinIME.IME_OPTION_NO_MICROPHONE_COMPAT, editorInfo);
244        final boolean voiceKeyEnabled = settingsValues.isVoiceKeyEnabled(editorInfo)
245                && !noMicrophone;
246        final boolean voiceKeyOnMain = settingsValues.isVoiceKeyOnMain();
247        final boolean noSettingsKey = Utils.inPrivateImeOptions(
248                mPackageName, LatinIME.IME_OPTION_NO_SETTINGS_KEY, editorInfo);
249        final boolean hasSettingsKey = settingsKeyEnabled && !noSettingsKey;
250        final int f2KeyMode = getF2KeyMode(settingsKeyEnabled, noSettingsKey);
251        final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != voiceKeyOnMain);
252        final boolean forceAscii = Utils.inPrivateImeOptions(
253                mPackageName, LatinIME.IME_OPTION_FORCE_ASCII, editorInfo);
254        final boolean asciiCapable = mSubtypeSwitcher.currentSubtypeContainsExtraValueKey(
255                LatinIME.SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE);
256        final Locale locale = (forceAscii && !asciiCapable)
257                ? Locale.US : mSubtypeSwitcher.getInputLocale();
258        final Configuration conf = mResources.getConfiguration();
259        final DisplayMetrics dm = mResources.getDisplayMetrics();
260
261        return new KeyboardId(
262                mResources.getResourceEntryName(xmlId), xmlId, locale, conf.orientation,
263                dm.widthPixels, mode, editorInfo, hasSettingsKey, f2KeyMode, noSettingsKey,
264                voiceKeyEnabled, hasShortcutKey);
265    }
266
267    public boolean isAlphabetMode() {
268        final Keyboard keyboard = getLatinKeyboard();
269        return keyboard != null && keyboard.mId.isAlphabetKeyboard();
270    }
271
272    public boolean isInputViewShown() {
273        return mCurrentInputView != null && mCurrentInputView.isShown();
274    }
275
276    public boolean isShiftedOrShiftLocked() {
277        final Keyboard keyboard = getLatinKeyboard();
278        return keyboard != null && keyboard.isShiftedOrShiftLocked();
279    }
280
281    public boolean isManualTemporaryUpperCase() {
282        final Keyboard keyboard = getLatinKeyboard();
283        return keyboard != null && keyboard.isManualTemporaryUpperCase();
284    }
285
286    public boolean isKeyboardAvailable() {
287        if (mKeyboardView != null)
288            return mKeyboardView.getKeyboard() != null;
289        return false;
290    }
291
292    public LatinKeyboard getLatinKeyboard() {
293        if (mKeyboardView != null) {
294            final Keyboard keyboard = mKeyboardView.getKeyboard();
295            if (keyboard instanceof LatinKeyboard)
296                return (LatinKeyboard)keyboard;
297        }
298        return null;
299    }
300
301    // Implements {@link KeyboardState.SwitchActions}.
302    @Override
303    public void setShifted(int shiftMode) {
304        mInputMethodService.mHandler.cancelUpdateShiftState();
305        LatinKeyboard latinKeyboard = getLatinKeyboard();
306        if (latinKeyboard == null)
307            return;
308        if (shiftMode == AUTOMATIC_SHIFT) {
309            latinKeyboard.setAutomaticTemporaryUpperCase();
310        } else {
311            final boolean shifted = (shiftMode == MANUAL_SHIFT);
312            // On non-distinct multi touch panel device, we should also turn off the shift locked
313            // state when shift key is pressed to go to normal mode.
314            // On the other hand, on distinct multi touch panel device, turning off the shift
315            // locked state with shift key pressing is handled by onReleaseShift().
316            if (!hasDistinctMultitouch() && !shifted && mState.isShiftLocked()) {
317                latinKeyboard.setShiftLocked(false);
318            }
319            latinKeyboard.setShifted(shifted);
320        }
321        mKeyboardView.invalidateAllKeys();
322    }
323
324    // Implements {@link KeyboardState.SwitchActions}.
325    @Override
326    public void setShiftLocked(boolean shiftLocked) {
327        mInputMethodService.mHandler.cancelUpdateShiftState();
328        LatinKeyboard latinKeyboard = getLatinKeyboard();
329        if (latinKeyboard == null)
330            return;
331        latinKeyboard.setShiftLocked(shiftLocked);
332        mKeyboardView.invalidateAllKeys();
333        if (!shiftLocked) {
334            // To be able to turn off caps lock by "double tap" on shift key, we should ignore
335            // the second tap of the "double tap" from now for a while because we just have
336            // already turned off caps lock above.
337            mKeyboardView.startIgnoringDoubleTap();
338        }
339    }
340
341    /**
342     * Toggle keyboard shift state triggered by user touch event.
343     */
344    public void toggleShift() {
345        mState.onToggleShift();
346    }
347
348    /**
349     * Toggle caps lock state triggered by user touch event.
350     */
351    public void toggleCapsLock() {
352        mState.onToggleCapsLock();
353    }
354
355    /**
356     * Toggle between alphabet and symbols modes triggered by user touch event.
357     */
358    public void toggleAlphabetAndSymbols() {
359        mState.onToggleAlphabetAndSymbols();
360    }
361
362    /**
363     * Update keyboard shift state triggered by connected EditText status change.
364     */
365    public void updateShiftState() {
366        mState.onUpdateShiftState(mInputMethodService.getCurrentAutoCapsState());
367    }
368
369    public void onPressShift(boolean withSliding) {
370        mState.onPressShift(withSliding);
371    }
372
373    public void onReleaseShift(boolean withSliding) {
374        mState.onReleaseShift(withSliding);
375    }
376
377    public void onPressSymbol() {
378        mState.onPressSymbol();
379    }
380
381    public void onReleaseSymbol() {
382        mState.onReleaseSymbol();
383    }
384
385    public void onOtherKeyPressed() {
386        mState.onOtherKeyPressed();
387    }
388
389    public void onCancelInput() {
390        mState.onCancelInput(isSinglePointer());
391    }
392
393    // Implements {@link KeyboardState.SwitchActions}.
394    @Override
395    public void setSymbolsKeyboard() {
396        setKeyboard(getKeyboard(mSymbolsKeyboardId));
397    }
398
399    // Implements {@link KeyboardState.SwitchActions}.
400    @Override
401    public void setAlphabetKeyboard() {
402        setKeyboard(getKeyboard(mMainKeyboardId));
403    }
404
405    // Implements {@link KeyboardState.SwitchActions}.
406    @Override
407    public void setSymbolsShiftedKeyboard() {
408        final Keyboard keyboard = getKeyboard(mSymbolsShiftedKeyboardId);
409        setKeyboard(keyboard);
410        // TODO: Remove this logic once we introduce initial keyboard shift state attribute.
411        // Symbol shift keyboard may have a shift key that has a caps lock style indicator (a.k.a.
412        // sticky shift key). To show or dismiss the indicator, we need to call setShiftLocked()
413        // that takes care of the current keyboard having such shift key or not.
414        keyboard.setShiftLocked(keyboard.hasShiftLockKey());
415    }
416
417    public boolean isInMomentarySwitchState() {
418        return mState.isInMomentarySwitchState();
419    }
420
421    public boolean isVibrateAndSoundFeedbackRequired() {
422        return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
423    }
424
425    private boolean isSinglePointer() {
426        return mKeyboardView != null && mKeyboardView.getPointerCount() == 1;
427    }
428
429    public boolean hasDistinctMultitouch() {
430        return mKeyboardView != null && mKeyboardView.hasDistinctMultitouch();
431    }
432
433    /**
434     * Updates state machine to figure out when to automatically snap back to the previous mode.
435     */
436    public void onCodeInput(int code) {
437        mState.onCodeInput(code, isSinglePointer());
438    }
439
440    public LatinKeyboardView getKeyboardView() {
441        return mKeyboardView;
442    }
443
444    public View onCreateInputView() {
445        return createInputView(mThemeIndex, true);
446    }
447
448    private View createInputView(final int newThemeIndex, final boolean forceRecreate) {
449        if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate)
450            return mCurrentInputView;
451
452        if (mKeyboardView != null) {
453            mKeyboardView.closing();
454        }
455
456        final int oldThemeIndex = mThemeIndex;
457        Utils.GCUtils.getInstance().reset();
458        boolean tryGC = true;
459        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
460            try {
461                setContextThemeWrapper(mInputMethodService, newThemeIndex);
462                mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
463                        R.layout.input_view, null);
464                tryGC = false;
465            } catch (OutOfMemoryError e) {
466                Log.w(TAG, "load keyboard failed: " + e);
467                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
468                        oldThemeIndex + "," + newThemeIndex, e);
469            } catch (InflateException e) {
470                Log.w(TAG, "load keyboard failed: " + e);
471                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
472                        oldThemeIndex + "," + newThemeIndex, e);
473            }
474        }
475
476        mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
477        mKeyboardView.setKeyboardActionListener(mInputMethodService);
478
479        // This always needs to be set since the accessibility state can
480        // potentially change without the input view being re-created.
481        AccessibleKeyboardViewProxy.setView(mKeyboardView);
482
483        return mCurrentInputView;
484    }
485
486    private void postSetInputView(final View newInputView) {
487        final LatinIME latinIme = mInputMethodService;
488        latinIme.mHandler.post(new Runnable() {
489            @Override
490            public void run() {
491                if (newInputView != null) {
492                    latinIme.setInputView(newInputView);
493                }
494                latinIme.updateInputViewShown();
495            }
496        });
497    }
498
499    @Override
500    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
501        if (PREF_KEYBOARD_LAYOUT.equals(key)) {
502            final int themeIndex = getKeyboardThemeIndex(mInputMethodService, sharedPreferences);
503            postSetInputView(createInputView(themeIndex, false));
504        } else if (Settings.PREF_SHOW_SETTINGS_KEY.equals(key)) {
505            postSetInputView(createInputView(mThemeIndex, true));
506        }
507    }
508
509    public void onAutoCorrectionStateChanged(boolean isAutoCorrection) {
510        if (mIsAutoCorrectionActive != isAutoCorrection) {
511            mIsAutoCorrectionActive = isAutoCorrection;
512            final LatinKeyboard keyboard = getLatinKeyboard();
513            if (keyboard != null && keyboard.needsAutoCorrectionSpacebarLed()) {
514                final Key invalidatedKey = keyboard.onAutoCorrectionStateChanged(isAutoCorrection);
515                final LatinKeyboardView keyboardView = getKeyboardView();
516                if (keyboardView != null)
517                    keyboardView.invalidateKey(invalidatedKey);
518            }
519        }
520    }
521
522    private static int getF2KeyMode(boolean settingsKeyEnabled, boolean noSettingsKey) {
523        if (noSettingsKey) {
524            // Never shows the Settings key
525            return KeyboardId.F2KEY_MODE_SHORTCUT_IME;
526        }
527
528        if (settingsKeyEnabled) {
529            return KeyboardId.F2KEY_MODE_SETTINGS;
530        } else {
531            // It should be alright to fall back to the Settings key on 7-inch layouts
532            // even when the Settings key is not explicitly enabled.
533            return KeyboardId.F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS;
534        }
535    }
536}
537