KeyboardSwitcher.java revision 854302282de2a2fde999c059097c5a4d2f0dba79
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.keyboard.internal.KeyboardState;
31import com.android.inputmethod.latin.InputView;
32import com.android.inputmethod.latin.LatinIME;
33import com.android.inputmethod.latin.LatinImeLogger;
34import com.android.inputmethod.latin.R;
35import com.android.inputmethod.latin.Settings;
36import com.android.inputmethod.latin.SettingsValues;
37import com.android.inputmethod.latin.SubtypeSwitcher;
38import com.android.inputmethod.latin.Utils;
39
40public class KeyboardSwitcher implements KeyboardState.SwitchActions,
41        SharedPreferences.OnSharedPreferenceChangeListener {
42    private static final String TAG = KeyboardSwitcher.class.getSimpleName();
43
44    public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916";
45    private static final int[] KEYBOARD_THEMES = {
46        R.style.KeyboardTheme,
47        R.style.KeyboardTheme_HighContrast,
48        R.style.KeyboardTheme_Stone,
49        R.style.KeyboardTheme_Stone_Bold,
50        R.style.KeyboardTheme_Gingerbread,
51        R.style.KeyboardTheme_IceCreamSandwich,
52    };
53
54    private SubtypeSwitcher mSubtypeSwitcher;
55    private SharedPreferences mPrefs;
56
57    private InputView mCurrentInputView;
58    private LatinKeyboardView mKeyboardView;
59    private LatinIME mInputMethodService;
60    private Resources mResources;
61
62    private KeyboardState mState;
63
64    private KeyboardSet mKeyboardSet;
65
66    /** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of
67     * what user actually typed. */
68    private boolean mIsAutoCorrectionActive;
69
70    private int mThemeIndex = -1;
71    private Context mThemeContext;
72
73    private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
74
75    public static KeyboardSwitcher getInstance() {
76        return sInstance;
77    }
78
79    private KeyboardSwitcher() {
80        // Intentional empty constructor for singleton.
81    }
82
83    public static void init(LatinIME ims, SharedPreferences prefs) {
84        sInstance.initInternal(ims, prefs);
85    }
86
87    private void initInternal(LatinIME ims, SharedPreferences prefs) {
88        mInputMethodService = ims;
89        mResources = ims.getResources();
90        mPrefs = prefs;
91        mSubtypeSwitcher = SubtypeSwitcher.getInstance();
92        mState = new KeyboardState(this);
93        setContextThemeWrapper(ims, getKeyboardThemeIndex(ims, prefs));
94        prefs.registerOnSharedPreferenceChangeListener(this);
95    }
96
97    private static int getKeyboardThemeIndex(Context context, SharedPreferences prefs) {
98        final String defaultThemeId = context.getString(R.string.config_default_keyboard_theme_id);
99        final String themeId = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeId);
100        try {
101            final int themeIndex = Integer.valueOf(themeId);
102            if (themeIndex >= 0 && themeIndex < KEYBOARD_THEMES.length)
103                return themeIndex;
104        } catch (NumberFormatException e) {
105            // Format error, keyboard theme is default to 0.
106        }
107        Log.w(TAG, "Illegal keyboard theme in preference: " + themeId + ", default to 0");
108        return 0;
109    }
110
111    private void setContextThemeWrapper(Context context, int themeIndex) {
112        if (mThemeIndex != themeIndex) {
113            mThemeIndex = themeIndex;
114            mThemeContext = new ContextThemeWrapper(context, KEYBOARD_THEMES[themeIndex]);
115            KeyboardSet.clearKeyboardCache();
116        }
117    }
118
119    public void loadKeyboard(EditorInfo editorInfo, SettingsValues settingsValues) {
120        mKeyboardSet = new KeyboardSet.Builder(mThemeContext, editorInfo, settingsValues)
121                .build();
122        final KeyboardId mainKeyboardId = mKeyboardSet.getMainKeyboardId();
123        try {
124            mState.onLoadKeyboard(mResources.getString(R.string.layout_switch_back_symbols),
125                    hasDistinctMultitouch());
126        } catch (RuntimeException e) {
127            Log.w(TAG, "loading keyboard failed: " + mainKeyboardId, e);
128            LatinImeLogger.logOnException(mainKeyboardId.toString(), e);
129            return;
130        }
131        // TODO: Should get rid of this special case handling for Phone Number layouts once we
132        // have separate layouts with unique KeyboardIds for alphabet and alphabet-shifted
133        // respectively.
134        if (mainKeyboardId.isPhoneKeyboard()) {
135            mState.onToggleAlphabetAndSymbols();
136        }
137    }
138
139    public void saveKeyboardState() {
140        if (isKeyboardAvailable()) {
141            mState.onSaveKeyboardState();
142        }
143    }
144
145    public void onFinishInputView() {
146        mIsAutoCorrectionActive = false;
147    }
148
149    public void onHideWindow() {
150        mIsAutoCorrectionActive = false;
151    }
152
153    private void setKeyboard(final Keyboard keyboard) {
154        final Keyboard oldKeyboard = mKeyboardView.getKeyboard();
155        mKeyboardView.setKeyboard(keyboard);
156        mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding);
157        mKeyboardView.setKeyPreviewPopupEnabled(
158                SettingsValues.isKeyPreviewPopupEnabled(mPrefs, mResources),
159                SettingsValues.getKeyPreviewPopupDismissDelay(mPrefs, mResources));
160        final boolean localeChanged = (oldKeyboard == null)
161                || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale);
162        if (keyboard instanceof LatinKeyboard) {
163            final LatinKeyboard latinKeyboard = (LatinKeyboard)keyboard;
164            latinKeyboard.updateAutoCorrectionState(mIsAutoCorrectionActive);
165            // If the cached keyboard had been switched to another keyboard while the language was
166            // displayed on its spacebar, it might have had arbitrary text fade factor. In such
167            // case, we should reset the text fade factor. It is also applicable to shortcut key.
168            latinKeyboard.updateSpacebarLanguage(0.0f,
169                    Utils.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */),
170                    mSubtypeSwitcher.needsToDisplayLanguage(latinKeyboard.mId.mLocale));
171            latinKeyboard.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady());
172        }
173        updateShiftState();
174        mInputMethodService.mHandler.startDisplayLanguageOnSpacebar(localeChanged);
175    }
176
177    public boolean isAlphabetMode() {
178        final Keyboard keyboard = getLatinKeyboard();
179        return keyboard != null && keyboard.mId.isAlphabetKeyboard();
180    }
181
182    public boolean isInputViewShown() {
183        return mCurrentInputView != null && mCurrentInputView.isShown();
184    }
185
186    public boolean isShiftedOrShiftLocked() {
187        final Keyboard keyboard = getLatinKeyboard();
188        return keyboard != null && keyboard.isShiftedOrShiftLocked();
189    }
190
191    public boolean isManualTemporaryUpperCase() {
192        final Keyboard keyboard = getLatinKeyboard();
193        return keyboard != null && keyboard.isManualTemporaryUpperCase();
194    }
195
196    public boolean isKeyboardAvailable() {
197        if (mKeyboardView != null)
198            return mKeyboardView.getKeyboard() != null;
199        return false;
200    }
201
202    public LatinKeyboard getLatinKeyboard() {
203        if (mKeyboardView != null) {
204            final Keyboard keyboard = mKeyboardView.getKeyboard();
205            if (keyboard instanceof LatinKeyboard)
206                return (LatinKeyboard)keyboard;
207        }
208        return null;
209    }
210
211    // Implements {@link KeyboardState.SwitchActions}.
212    @Override
213    public void setShifted(int shiftMode) {
214        mInputMethodService.mHandler.cancelUpdateShiftState();
215        LatinKeyboard latinKeyboard = getLatinKeyboard();
216        if (latinKeyboard == null)
217            return;
218        if (shiftMode == AUTOMATIC_SHIFT) {
219            latinKeyboard.setAutomaticTemporaryUpperCase();
220        } else {
221            final boolean shifted = (shiftMode == MANUAL_SHIFT);
222            // On non-distinct multi touch panel device, we should also turn off the shift locked
223            // state when shift key is pressed to go to normal mode.
224            // On the other hand, on distinct multi touch panel device, turning off the shift
225            // locked state with shift key pressing is handled by onReleaseShift().
226            if (!hasDistinctMultitouch() && !shifted && mState.isShiftLocked()) {
227                latinKeyboard.setShiftLocked(false);
228            }
229            latinKeyboard.setShifted(shifted);
230        }
231        mKeyboardView.invalidateAllKeys();
232    }
233
234    // Implements {@link KeyboardState.SwitchActions}.
235    @Override
236    public void setShiftLocked(boolean shiftLocked) {
237        mInputMethodService.mHandler.cancelUpdateShiftState();
238        LatinKeyboard latinKeyboard = getLatinKeyboard();
239        if (latinKeyboard == null)
240            return;
241        latinKeyboard.setShiftLocked(shiftLocked);
242        mKeyboardView.invalidateAllKeys();
243        if (!shiftLocked) {
244            // To be able to turn off caps lock by "double tap" on shift key, we should ignore
245            // the second tap of the "double tap" from now for a while because we just have
246            // already turned off caps lock above.
247            mKeyboardView.startIgnoringDoubleTap();
248        }
249    }
250
251    /**
252     * Toggle keyboard shift state triggered by user touch event.
253     */
254    public void toggleShift() {
255        mState.onToggleShift();
256    }
257
258    /**
259     * Toggle caps lock state triggered by user touch event.
260     */
261    public void toggleCapsLock() {
262        mState.onToggleCapsLock();
263    }
264
265    /**
266     * Toggle between alphabet and symbols modes triggered by user touch event.
267     */
268    public void toggleAlphabetAndSymbols() {
269        mState.onToggleAlphabetAndSymbols();
270    }
271
272    /**
273     * Update keyboard shift state triggered by connected EditText status change.
274     */
275    public void updateShiftState() {
276        mState.onUpdateShiftState(mInputMethodService.getCurrentAutoCapsState());
277    }
278
279    public void onPressShift(boolean withSliding) {
280        mState.onPressShift(withSliding);
281    }
282
283    public void onReleaseShift(boolean withSliding) {
284        mState.onReleaseShift(withSliding);
285    }
286
287    public void onPressSymbol() {
288        mState.onPressSymbol();
289    }
290
291    public void onReleaseSymbol() {
292        mState.onReleaseSymbol();
293    }
294
295    public void onOtherKeyPressed() {
296        mState.onOtherKeyPressed();
297    }
298
299    public void onCancelInput() {
300        mState.onCancelInput(isSinglePointer());
301    }
302
303    // Implements {@link KeyboardState.SwitchActions}.
304    @Override
305    public void setSymbolsKeyboard() {
306        setKeyboard(mKeyboardSet.getSymbolsKeyboard());
307    }
308
309    // Implements {@link KeyboardState.SwitchActions}.
310    @Override
311    public void setAlphabetKeyboard() {
312        setKeyboard(mKeyboardSet.getMainKeyboard());
313    }
314
315    // Implements {@link KeyboardState.SwitchActions}.
316    @Override
317    public void setSymbolsShiftedKeyboard() {
318        setKeyboard(mKeyboardSet.getSymbolsShiftedKeyboard());
319    }
320
321    public boolean isInMomentarySwitchState() {
322        return mState.isInMomentarySwitchState();
323    }
324
325    public boolean isVibrateAndSoundFeedbackRequired() {
326        return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
327    }
328
329    private boolean isSinglePointer() {
330        return mKeyboardView != null && mKeyboardView.getPointerCount() == 1;
331    }
332
333    public boolean hasDistinctMultitouch() {
334        return mKeyboardView != null && mKeyboardView.hasDistinctMultitouch();
335    }
336
337    /**
338     * Updates state machine to figure out when to automatically snap back to the previous mode.
339     */
340    public void onCodeInput(int code) {
341        mState.onCodeInput(code, isSinglePointer());
342    }
343
344    public LatinKeyboardView getKeyboardView() {
345        return mKeyboardView;
346    }
347
348    public View onCreateInputView() {
349        return createInputView(mThemeIndex, true);
350    }
351
352    private View createInputView(final int newThemeIndex, final boolean forceRecreate) {
353        if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate)
354            return mCurrentInputView;
355
356        if (mKeyboardView != null) {
357            mKeyboardView.closing();
358        }
359
360        final int oldThemeIndex = mThemeIndex;
361        Utils.GCUtils.getInstance().reset();
362        boolean tryGC = true;
363        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
364            try {
365                setContextThemeWrapper(mInputMethodService, newThemeIndex);
366                mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
367                        R.layout.input_view, null);
368                tryGC = false;
369            } catch (OutOfMemoryError e) {
370                Log.w(TAG, "load keyboard failed: " + e);
371                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
372                        oldThemeIndex + "," + newThemeIndex, e);
373            } catch (InflateException e) {
374                Log.w(TAG, "load keyboard failed: " + e);
375                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
376                        oldThemeIndex + "," + newThemeIndex, e);
377            }
378        }
379
380        mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
381        mKeyboardView.setKeyboardActionListener(mInputMethodService);
382
383        // This always needs to be set since the accessibility state can
384        // potentially change without the input view being re-created.
385        AccessibleKeyboardViewProxy.setView(mKeyboardView);
386
387        return mCurrentInputView;
388    }
389
390    private void postSetInputView(final View newInputView) {
391        final LatinIME latinIme = mInputMethodService;
392        latinIme.mHandler.post(new Runnable() {
393            @Override
394            public void run() {
395                if (newInputView != null) {
396                    latinIme.setInputView(newInputView);
397                }
398                latinIme.updateInputViewShown();
399            }
400        });
401    }
402
403    @Override
404    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
405        if (PREF_KEYBOARD_LAYOUT.equals(key)) {
406            final int themeIndex = getKeyboardThemeIndex(mInputMethodService, sharedPreferences);
407            postSetInputView(createInputView(themeIndex, false));
408        } else if (Settings.PREF_SHOW_SETTINGS_KEY.equals(key)) {
409            postSetInputView(createInputView(mThemeIndex, true));
410        }
411    }
412
413    public void onNetworkStateChanged() {
414        final LatinKeyboard keyboard = getLatinKeyboard();
415        if (keyboard == null) return;
416        final Key updatedKey = keyboard.updateShortcutKey(
417                SubtypeSwitcher.getInstance().isShortcutImeReady());
418        if (updatedKey != null && mKeyboardView != null) {
419            mKeyboardView.invalidateKey(updatedKey);
420        }
421    }
422
423    public void onAutoCorrectionStateChanged(boolean isAutoCorrection) {
424        if (mIsAutoCorrectionActive != isAutoCorrection) {
425            mIsAutoCorrectionActive = isAutoCorrection;
426            final LatinKeyboard keyboard = getLatinKeyboard();
427            if (keyboard != null && keyboard.needsAutoCorrectionSpacebarLed()) {
428                final Key invalidatedKey = keyboard.updateAutoCorrectionState(isAutoCorrection);
429                final LatinKeyboardView keyboardView = getKeyboardView();
430                if (keyboardView != null)
431                    keyboardView.invalidateKey(invalidatedKey);
432            }
433        }
434    }
435
436    private static String themeName(int themeId) {
437        // This should be aligned with theme-*.xml resource files' themeId attribute.
438        switch (themeId) {
439        case 0: return "Basic";
440        case 1: return "BasicHighContrast";
441        case 5: return "IceCreamSandwich";
442        case 6: return "Stone";
443        case 7: return "StoneBold";
444        case 8: return "GingerBread";
445        default: return null;
446        }
447    }
448}
449