SubtypeSwitcher.java revision 9313bef894cef4be2f5821be1d812b30f1451894
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import com.android.inputmethod.keyboard.KeyboardSwitcher;
20import com.android.inputmethod.voice.SettingsUtil;
21import com.android.inputmethod.voice.VoiceIMEConnector;
22import com.android.inputmethod.voice.VoiceInput;
23
24import android.content.Context;
25import android.content.SharedPreferences;
26import android.content.pm.PackageManager;
27import android.content.res.Configuration;
28import android.content.res.Resources;
29import android.graphics.drawable.Drawable;
30import android.os.IBinder;
31import android.text.TextUtils;
32import android.util.Log;
33import android.view.inputmethod.InputMethodInfo;
34import android.view.inputmethod.InputMethodManager;
35import android.view.inputmethod.InputMethodSubtype;
36
37import java.util.ArrayList;
38import java.util.Arrays;
39import java.util.List;
40import java.util.Locale;
41import java.util.Map;
42
43public class SubtypeSwitcher {
44    private static boolean DBG = LatinImeLogger.sDBG;
45    private static final String TAG = "SubtypeSwitcher";
46
47    private static final char LOCALE_SEPARATER = '_';
48    private static final String KEYBOARD_MODE = "keyboard";
49    private static final String VOICE_MODE = "voice";
50    private final TextUtils.SimpleStringSplitter mLocaleSplitter =
51            new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER);
52
53    private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
54    private /* final */ LatinIME mService;
55    private /* final */ SharedPreferences mPrefs;
56    private /* final */ InputMethodManager mImm;
57    private /* final */ Resources mResources;
58    private final ArrayList<InputMethodSubtype> mEnabledKeyboardSubtypesOfCurrentInputMethod =
59            new ArrayList<InputMethodSubtype>();
60    private final ArrayList<String> mEnabledLanguagesOfCurrentInputMethod = new ArrayList<String>();
61
62    private boolean mConfigUseSpacebarLanguageSwitcher;
63
64    /*-----------------------------------------------------------*/
65    // Variants which should be changed only by reload functions.
66    private boolean mNeedsToDisplayLanguage;
67    private boolean mIsSystemLanguageSameAsInputLanguage;
68    private InputMethodInfo mShortcutInfo;
69    private InputMethodSubtype mShortcutSubtype;
70    private List<InputMethodSubtype> mAllEnabledSubtypesOfCurrentInputMethod;
71    private Locale mSystemLocale;
72    private Locale mInputLocale;
73    private String mInputLocaleStr;
74    private String mMode;
75    private VoiceInput mVoiceInput;
76    /*-----------------------------------------------------------*/
77
78    public static SubtypeSwitcher getInstance() {
79        return sInstance;
80    }
81
82    public static void init(LatinIME service, SharedPreferences prefs) {
83        sInstance.mPrefs = prefs;
84        sInstance.resetParams(service);
85        sInstance.updateAllParameters();
86
87        SubtypeLocale.init(service);
88    }
89
90    private SubtypeSwitcher() {
91        // Intentional empty constructor for singleton.
92    }
93
94    private void resetParams(LatinIME service) {
95        mService = service;
96        mResources = service.getResources();
97        mImm = (InputMethodManager) service.getSystemService(Context.INPUT_METHOD_SERVICE);
98        mEnabledKeyboardSubtypesOfCurrentInputMethod.clear();
99        mEnabledLanguagesOfCurrentInputMethod.clear();
100        mSystemLocale = null;
101        mInputLocale = null;
102        mInputLocaleStr = null;
103        // Mode is initialized to KEYBOARD_MODE, in case that LatinIME can't obtain currentSubtype
104        mMode = KEYBOARD_MODE;
105        mAllEnabledSubtypesOfCurrentInputMethod = null;
106        // TODO: Voice input should be created here
107        mVoiceInput = null;
108        mConfigUseSpacebarLanguageSwitcher = mResources.getBoolean(
109                R.bool.config_use_spacebar_language_switcher);
110        if (mConfigUseSpacebarLanguageSwitcher)
111            initLanguageSwitcher(service);
112    }
113
114    // Update all parameters stored in SubtypeSwitcher.
115    // Only configuration changed event is allowed to call this because this is heavy.
116    private void updateAllParameters() {
117        mSystemLocale = mResources.getConfiguration().locale;
118        updateSubtype(mImm.getCurrentInputMethodSubtype());
119        updateParametersOnStartInputView();
120    }
121
122    // Update parameters which are changed outside LatinIME. This parameters affect UI so they
123    // should be updated every time onStartInputview.
124    public void updateParametersOnStartInputView() {
125        if (mConfigUseSpacebarLanguageSwitcher) {
126            updateForSpacebarLanguageSwitch();
127        } else {
128            updateEnabledSubtypes();
129        }
130        updateShortcutIME();
131    }
132
133    // Reload enabledSubtypes from the framework.
134    private void updateEnabledSubtypes() {
135        boolean foundCurrentSubtypeBecameDisabled = true;
136        mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList(
137                null, true);
138        mEnabledLanguagesOfCurrentInputMethod.clear();
139        mEnabledKeyboardSubtypesOfCurrentInputMethod.clear();
140        for (InputMethodSubtype ims: mAllEnabledSubtypesOfCurrentInputMethod) {
141            final String locale = ims.getLocale();
142            final String mode = ims.getMode();
143            mLocaleSplitter.setString(locale);
144            if (mLocaleSplitter.hasNext()) {
145                mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next());
146            }
147            if (locale.equals(mInputLocaleStr) && mode.equals(mMode)) {
148                foundCurrentSubtypeBecameDisabled = false;
149            }
150            if (KEYBOARD_MODE.equals(ims.getMode())) {
151                mEnabledKeyboardSubtypesOfCurrentInputMethod.add(ims);
152            }
153        }
154        mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1
155                && mIsSystemLanguageSameAsInputLanguage);
156        if (foundCurrentSubtypeBecameDisabled) {
157            if (DBG) {
158                Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + mMode);
159                Log.w(TAG, "Last subtype was disabled. Update to the current one.");
160            }
161            updateSubtype(mImm.getCurrentInputMethodSubtype());
162        }
163    }
164
165    private void updateShortcutIME() {
166        if (DBG) {
167            Log.d(TAG, "Update shortcut IME from : "
168                    + (mShortcutInfo == null ? "<null>" : mShortcutInfo.getId()) + ", "
169                    + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale()
170                            + ", " + mShortcutSubtype.getMode())));
171        }
172        // TODO: Update an icon for shortcut IME
173        Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
174                mImm.getShortcutInputMethodsAndSubtypes();
175        for (InputMethodInfo imi: shortcuts.keySet()) {
176            List<InputMethodSubtype> subtypes = shortcuts.get(imi);
177            // TODO: Returns the first found IMI for now. Should handle all shortcuts as
178            // appropriate.
179            mShortcutInfo = imi;
180            // TODO: Pick up the first found subtype for now. Should handle all subtypes
181            // as appropriate.
182            mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
183            break;
184        }
185        if (DBG) {
186            Log.d(TAG, "Update shortcut IME to : "
187                    + (mShortcutInfo == null ? "<null>" : mShortcutInfo.getId()) + ", "
188                    + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale()
189                            + ", " + mShortcutSubtype.getMode())));
190        }
191    }
192
193    // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function.
194    public void updateSubtype(InputMethodSubtype newSubtype) {
195        final String newLocale;
196        final String newMode;
197        if (newSubtype == null) {
198            // Normally, newSubtype shouldn't be null. But just in case newSubtype was null,
199            // fallback to the default locale and mode.
200            Log.w(TAG, "Couldn't get the current subtype.");
201            newLocale = "en_US";
202            newMode = KEYBOARD_MODE;
203        } else {
204            newLocale = newSubtype.getLocale();
205            newMode = newSubtype.getMode();
206        }
207        if (DBG) {
208            Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode
209                    + ", from: " + mInputLocaleStr + ", " + mMode);
210        }
211        boolean languageChanged = false;
212        if (!newLocale.equals(mInputLocaleStr)) {
213            if (mInputLocaleStr != null) {
214                languageChanged = true;
215            }
216            updateInputLocale(newLocale);
217        }
218        boolean modeChanged = false;
219        String oldMode = mMode;
220        if (!newMode.equals(mMode)) {
221            if (mMode != null) {
222                modeChanged = true;
223            }
224            mMode = newMode;
225        }
226
227        // If the old mode is voice input, we need to reset or cancel its status.
228        // We cancel its status when we change mode, while we reset otherwise.
229        if (isKeyboardMode()) {
230            if (modeChanged) {
231                if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) {
232                    mVoiceInput.cancel();
233                }
234            }
235            if (modeChanged || languageChanged) {
236                updateShortcutIME();
237                mService.onRefreshKeyboard();
238            }
239        } else if (isVoiceMode() && mVoiceInput != null) {
240            if (VOICE_MODE.equals(oldMode)) {
241                mVoiceInput.reset();
242            }
243            // If needsToShowWarningDialog is true, voice input need to show warning before
244            // show recognition view.
245            if (languageChanged || modeChanged
246                    || VoiceIMEConnector.getInstance().needsToShowWarningDialog()) {
247                triggerVoiceIME();
248            }
249        } else {
250            Log.w(TAG, "Unknown subtype mode: " + mMode);
251            if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) {
252                // We need to reset the voice input to release the resources and to reset its status
253                // as it is not the current input mode.
254                mVoiceInput.reset();
255            }
256        }
257    }
258
259    // Update the current input locale from Locale string.
260    private void updateInputLocale(String inputLocaleStr) {
261        // example: inputLocaleStr = "en_US" "en" ""
262        // "en_US" --> language: en  & country: US
263        // "en" --> language: en
264        // "" --> the system locale
265        mLocaleSplitter.setString(inputLocaleStr);
266        if (mLocaleSplitter.hasNext()) {
267            String language = mLocaleSplitter.next();
268            if (mLocaleSplitter.hasNext()) {
269                mInputLocale = new Locale(language, mLocaleSplitter.next());
270            } else {
271                mInputLocale = new Locale(language);
272            }
273            mInputLocaleStr = inputLocaleStr;
274        } else {
275            mInputLocale = mSystemLocale;
276            String country = mSystemLocale.getCountry();
277            mInputLocaleStr = mSystemLocale.getLanguage()
278                    + (TextUtils.isEmpty(country) ? "" : "_" + mSystemLocale.getLanguage());
279        }
280        mIsSystemLanguageSameAsInputLanguage = getSystemLocale().getLanguage().equalsIgnoreCase(
281                getInputLocale().getLanguage());
282        mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1
283                && mIsSystemLanguageSameAsInputLanguage);
284    }
285
286    ////////////////////////////
287    // Shortcut IME functions //
288    ////////////////////////////
289
290    public void switchToShortcutIME() {
291        final IBinder token = mService.getWindow().getWindow().getAttributes().token;
292        if (token == null || mShortcutInfo == null) {
293            return;
294        }
295        final String imiId = mShortcutInfo.getId();
296        final InputMethodSubtype subtype = mShortcutSubtype;
297        new Thread("SwitchToShortcutIME") {
298            @Override
299            public void run() {
300                mImm.setInputMethodAndSubtype(token, imiId, subtype);
301            }
302        }.start();
303    }
304
305    public Drawable getShortcutIcon() {
306        return getSubtypeIcon(mShortcutInfo, mShortcutSubtype);
307    }
308
309    private Drawable getSubtypeIcon(InputMethodInfo imi, InputMethodSubtype subtype) {
310        final PackageManager pm = mService.getPackageManager();
311        if (imi != null) {
312            final String imiPackageName = imi.getPackageName();
313            if (DBG) {
314                Log.d(TAG, "Update icons of IME: " + imiPackageName + ","
315                        + subtype.getLocale() + "," + subtype.getMode());
316            }
317            if (subtype != null) {
318                return pm.getDrawable(imiPackageName, subtype.getIconResId(),
319                        imi.getServiceInfo().applicationInfo);
320            } else if (imi.getSubtypeCount() > 0 && imi.getSubtypeAt(0) != null) {
321                return pm.getDrawable(imiPackageName,
322                        imi.getSubtypeAt(0).getIconResId(),
323                        imi.getServiceInfo().applicationInfo);
324            } else {
325                try {
326                    return pm.getApplicationInfo(imiPackageName, 0).loadIcon(pm);
327                } catch (PackageManager.NameNotFoundException e) {
328                    Log.w(TAG, "IME can't be found: " + imiPackageName);
329                }
330            }
331        }
332        return null;
333    }
334
335    //////////////////////////////////
336    // Language Switching functions //
337    //////////////////////////////////
338
339    public int getEnabledKeyboardLocaleCount() {
340        if (mConfigUseSpacebarLanguageSwitcher) {
341            return mLanguageSwitcher.getLocaleCount();
342        } else {
343            return mEnabledKeyboardSubtypesOfCurrentInputMethod.size();
344        }
345    }
346
347    public boolean useSpacebarLanguageSwitcher() {
348        return mConfigUseSpacebarLanguageSwitcher;
349    }
350
351    public boolean needsToDisplayLanguage() {
352        return mNeedsToDisplayLanguage;
353    }
354
355    public Locale getInputLocale() {
356        if (mConfigUseSpacebarLanguageSwitcher) {
357            return mLanguageSwitcher.getInputLocale();
358        } else {
359            return mInputLocale;
360        }
361    }
362
363    public String getInputLocaleStr() {
364        if (mConfigUseSpacebarLanguageSwitcher) {
365            String inputLanguage = null;
366            inputLanguage = mLanguageSwitcher.getInputLanguage();
367            // Should return system locale if there is no Language available.
368            if (inputLanguage == null) {
369                inputLanguage = getSystemLocale().getLanguage();
370            }
371            return inputLanguage;
372        } else {
373            return mInputLocaleStr;
374        }
375    }
376
377    public String[] getEnabledLanguages() {
378        if (mConfigUseSpacebarLanguageSwitcher) {
379            return mLanguageSwitcher.getEnabledLanguages();
380        } else {
381            return mEnabledLanguagesOfCurrentInputMethod.toArray(
382                    new String[mEnabledLanguagesOfCurrentInputMethod.size()]);
383        }
384    }
385
386    public Locale getSystemLocale() {
387        if (mConfigUseSpacebarLanguageSwitcher) {
388            return mLanguageSwitcher.getSystemLocale();
389        } else {
390            return mSystemLocale;
391        }
392    }
393
394    public boolean isSystemLanguageSameAsInputLanguage() {
395        if (mConfigUseSpacebarLanguageSwitcher) {
396            return getSystemLocale().getLanguage().equalsIgnoreCase(
397                    getInputLocaleStr().substring(0, 2));
398        } else {
399            return mIsSystemLanguageSameAsInputLanguage;
400        }
401    }
402
403    public void onConfigurationChanged(Configuration conf) {
404        final Locale systemLocale = conf.locale;
405        // If system configuration was changed, update all parameters.
406        if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) {
407            if (mConfigUseSpacebarLanguageSwitcher) {
408                // If the system locale changes and is different from the saved
409                // locale (mSystemLocale), then reload the input locale list from the
410                // latin ime settings (shared prefs) and reset the input locale
411                // to the first one.
412                mLanguageSwitcher.loadLocales(mPrefs);
413                mLanguageSwitcher.setSystemLocale(systemLocale);
414            } else {
415                updateAllParameters();
416            }
417        }
418    }
419
420    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
421        if (mConfigUseSpacebarLanguageSwitcher) {
422            if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) {
423                mLanguageSwitcher.loadLocales(sharedPreferences);
424            }
425        }
426    }
427
428    /**
429     * Change system locale for this application
430     * @param newLocale
431     * @return oldLocale
432     */
433    public Locale changeSystemLocale(Locale newLocale) {
434        Configuration conf = mResources.getConfiguration();
435        Locale oldLocale = conf.locale;
436        conf.locale = newLocale;
437        mResources.updateConfiguration(conf, mResources.getDisplayMetrics());
438        return oldLocale;
439    }
440
441    public boolean isKeyboardMode() {
442        return KEYBOARD_MODE.equals(mMode);
443    }
444
445
446    ///////////////////////////
447    // Voice Input functions //
448    ///////////////////////////
449
450    public boolean setVoiceInput(VoiceInput vi) {
451        if (mVoiceInput == null && vi != null) {
452            mVoiceInput = vi;
453            if (isVoiceMode()) {
454                if (DBG) {
455                    Log.d(TAG, "Set and call voice input.: " + getInputLocaleStr());
456                }
457                triggerVoiceIME();
458                return true;
459            }
460        }
461        return false;
462    }
463
464    public boolean isVoiceMode() {
465        return VOICE_MODE.equals(mMode);
466    }
467
468    private void triggerVoiceIME() {
469        if (!mService.isInputViewShown()) return;
470        VoiceIMEConnector.getInstance().startListening(false,
471                KeyboardSwitcher.getInstance().getInputView().getWindowToken());
472    }
473
474    //////////////////////////////////////
475    // Spacebar Language Switch support //
476    //////////////////////////////////////
477
478    private LanguageSwitcher mLanguageSwitcher;
479
480    public static String getFullDisplayName(Locale locale, boolean returnsNameInThisLocale) {
481        if (returnsNameInThisLocale) {
482            return toTitleCase(SubtypeLocale.getFullDisplayName(locale));
483        } else {
484            return toTitleCase(locale.getDisplayName());
485        }
486    }
487
488    public static String getDisplayLanguage(Locale locale) {
489        return toTitleCase(locale.getDisplayLanguage(locale));
490    }
491
492    public static String getShortDisplayLanguage(Locale locale) {
493        return toTitleCase(locale.getLanguage());
494    }
495
496    private static String toTitleCase(String s) {
497        if (s.length() == 0) {
498            return s;
499        }
500        return Character.toUpperCase(s.charAt(0)) + s.substring(1);
501    }
502
503    private void updateForSpacebarLanguageSwitch() {
504        // We need to update mNeedsToDisplayLanguage in onStartInputView because
505        // getEnabledKeyboardLocaleCount could have been changed.
506        mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1
507                && getSystemLocale().getLanguage().equalsIgnoreCase(
508                        getInputLocale().getLanguage()));
509    }
510
511    public String getInputLanguageName() {
512        return getDisplayLanguage(getInputLocale());
513    }
514
515    public String getNextInputLanguageName() {
516        if (mConfigUseSpacebarLanguageSwitcher) {
517            return getDisplayLanguage(mLanguageSwitcher.getNextInputLocale());
518        } else {
519            return "";
520        }
521    }
522
523    public String getPreviousInputLanguageName() {
524        if (mConfigUseSpacebarLanguageSwitcher) {
525            return getDisplayLanguage(mLanguageSwitcher.getPrevInputLocale());
526        } else {
527            return "";
528        }
529    }
530
531    // A list of locales which are supported by default for voice input, unless we get a
532    // different list from Gservices.
533    private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES =
534            "en " +
535            "en_US " +
536            "en_GB " +
537            "en_AU " +
538            "en_CA " +
539            "en_IE " +
540            "en_IN " +
541            "en_NZ " +
542            "en_SG " +
543            "en_ZA ";
544
545    public boolean isVoiceSupported(String locale) {
546        // Get the current list of supported locales and check the current locale against that
547        // list. We cache this value so as not to check it every time the user starts a voice
548        // input. Because this method is called by onStartInputView, this should mean that as
549        // long as the locale doesn't change while the user is keeping the IME open, the
550        // value should never be stale.
551        String supportedLocalesString = SettingsUtil.getSettingsString(
552                mService.getContentResolver(),
553                SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES,
554                DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES);
555        List<String> voiceInputSupportedLocales = Arrays.asList(
556                supportedLocalesString.split("\\s+"));
557        return voiceInputSupportedLocales.contains(locale);
558    }
559
560    public void loadSettings() {
561        if (mConfigUseSpacebarLanguageSwitcher) {
562            mLanguageSwitcher.loadLocales(mPrefs);
563        }
564    }
565
566    public void toggleLanguage(boolean reset, boolean next) {
567        if (mConfigUseSpacebarLanguageSwitcher) {
568            if (reset) {
569                mLanguageSwitcher.reset();
570            } else {
571                if (next) {
572                    mLanguageSwitcher.next();
573                } else {
574                    mLanguageSwitcher.prev();
575                }
576            }
577            mLanguageSwitcher.persist(mPrefs);
578        }
579    }
580
581    private void initLanguageSwitcher(LatinIME service) {
582        final Configuration conf = service.getResources().getConfiguration();
583        mLanguageSwitcher = new LanguageSwitcher(service);
584        mLanguageSwitcher.loadLocales(mPrefs);
585        mLanguageSwitcher.setSystemLocale(conf.locale);
586    }
587}
588